Java Collections 마스터하기

대부분의 개발자는 기본적으로 ArrayList나 HashSet을 사용합니다. 간단한 작업에는 문제가 없지만, 속도나 확장성이 필요한 경우에는 한계가 있습니다.

예전에 일반 ArrayList를 사용하여 게임 리더보드를 만든 적이 있습니다. 점수가 바뀔 때마다 정렬을 수행했더니 UI가 계속 멈추더군요. 언어를 활용하는 대신 언어와 싸우고 있었던 셈입니다.

잘못된 도구 사용을 멈추세요. 더 빠르고 깔끔한 코드를 작성하려면 다음 세 가지 특화된 컬렉션을 사용하세요.

  1. Enum 상수를 위한 EnumSet

Enum에 HashSet을 사용하면 성능 손실을 감수해야 합니다. 모든 삽입 작업 시 Enum이 객체로 박싱(boxing)되어 불필요한 오버헤드가 발생합니다.

EnumSet은 비트 벡터(bit vector)를 사용합니다. 단일 CPU 명령어를 사용하여 체크를 수행합니다.

Before:

Set<Ability> abilities = new HashSet<>();
abilities.add(Ability.FIRE);

After:

EnumSet<Ability> abilities = EnumSet.of(Ability.FIRE);
  1. 범위 쿼리를 위한 NavigableSet

정렬된 리스트를 수동으로 루프 돌며 범위를 찾는 방식은 느리고 오류가 발생하기 쉽습니다. 특히 'off-by-one' 버그가 자주 발생하곤 합니다.

NavigableSet은 데이터를 자동으로 정렬된 상태로 유지합니다. 부분 집합(subset)에 대해 O(log n)의 조회 성능을 제공합니다.

Before:

Collections.sort(scores);
List<Integer> topTen = scores.subList(size - 10, size);

After:

NavigableSet<Integer> scores = new TreeSet<>(Comparator.reverseOrder());
scores.add(1542);
NavigableSet<Integer> topTen = scores.headSet(scores.first(), true).stream().limit(10).collect(Collectors.toCollection(TreeSet::new));
  1. 읽기 작업이 많은 리스트를 위한 CopyOnWriteArrayList

ArrayList에 synchronized 블록을 사용하면 모든 읽기 작업의 속도가 저하됩니다. 또한 한 스레드가 쓰는 동안 다른 스레드가 읽으면 ConcurrentModificationException이 발생할 수 있습니다.

CopyOnWriteArrayList는 쓰기 작업이 발생할 때마다 배열의 새로운 복사본을 생성합니다. 읽기 작업은 배열의 스냅샷을 참조합니다.

Before:

List<String> log = Collections.synchronizedList(new ArrayList<>());
// 쓰기 작업이 동시에 발생하면 반복 중 오류가 발생할 수 있습니다.

After:

CopyOnWriteArrayList<String> log = new CopyOnWriteArrayList<>();
// 반복 작업이 안전하며 오류가 발생하지 않습니다.

늘 쓰던 두 가지 컬렉션만 고집하지 마세요. 데이터 패턴에 맞는 도구를 선택하세요.

출처: https://dev.to/timevolt/the-java-collections-force-mastering-the-hidden-gems-like-a-jedi-4438