조컴퓨터

CHAPTER 8 - 컬렉션 API 개선 본문

책읽기/모던 자바 인 액션

CHAPTER 8 - 컬렉션 API 개선

챠오위 2022. 6. 4. 01:51

PART 3 - 스트림과 람다를 이용한 효과적 프로그래밍

CHAPTER 8 - 컬렉션 API 개선

 

8.1 컬렉션 팩토리

자바에서는 적은 요소를 포함하는 리스트를 만들 수 있는데, 이를 팩토리 메서드화 한 것이 `Arrays.asList()` 이다. 고정 기의 리스트를 만들었으므로 요소를 갱신할 수는 있지만 새 요소를 추가하거나 요소를 삭제할 수는 없다. 예를 들어, 요소를 추가하는 작업시 `UnsupportedOperationException` 이 발생한다.

 

리스트의 경우에는 팩토리 메서드가 있지만, 집합의 경우에는 팩토리 메서드가 없으므로 다른 방법이 필요하다. 리스트를 인수로 받는 HashSet 생성자를 사용하거나 다음과 같은 스트림 API 를 사용하는 방법이 있다.

Set<string> alphabet = new HashSet<>(Arrays.asList("A", "B", "C");
Set<String> number = Stream.of("1", "2", "3").collect(Collectors.toset());

하지만 두 방법 모두 매끄럽지 못하며 내부적으로 불필요한 객체 할당을 필요로 한다. 그리고 그 결과는 변환할 수 있는 집합이다.

 

자바 9에서 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 팩토리 메서드를 제공한다.

8.1.1 리스트 팩토리 `List.of`

요소 추가시 `java.lang.UnsupportedOperationException` 을 발생시켜 컬렉션이 의도치 않게 변하는 것을 막아준다. 하지만 요소 자체가 변하는 것을 막을 수 없다. 

8.1.2 집합 팩토리 `Set.of`

중복된 요소를 제공해 집합을 만들려고 하면 요소가 중복되어 있다는 설명과 함께 `IllegalArgumentException` 이 발생한다. 집합은 오직 고유의 요소만 포함할 수 있다.

8.1.3 맵 팩토리 `Map.of`, `Map.ofEntries`

  • `Map.of` : 키와 값을 번갈아 제공하는 방법으로 맵을 만들 수 있다.
  • `Map.ofEntries` : `Map.Entry <K, V>` 객체를 인수로 받아 맵을 만들 수 있다.

 

8.2 리스트와 집합 처리

자바 8 에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.

  • removeIf : Predicate 를 만족하는 요소를 제거한다. List 나 Set 을 구현하거나 그 구현을 상속받은 모든 클래스에서 이용할 수 있다.
  • replaceAll : 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꾼다.
  • sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.

 

8.3 맵 처리

자바 8 에서는 Map 인터페이스에 몇 가지 디폴트 메서드를 추가했다. (13장에서 자세히 다룬다)

8.3.1 forEach 메서드

자바 8 에서부터 Map 인터페이스는 BiConsumer (키와 값을 인수로 받음) 를 인수로 받는 forEach 메서드를 지원한다.

8.3.2 정렬 메서드

다음 두 개의 유틸리티로 맵의 항목을 값 또는 키를 기준으로 정렬할 수 있다.

  • Entry.comparingByValue
  • Entry.comparingByKey

8.3.3 getOrDefault 메서드

찾으려는 키가 존재하지 않으면 널이 반환되므로 NullPointerException 을 방지하기 위해 요청 결과가 널인지 확인해야 한다. 이때 getOrDefault 메서드가 이 문제를 해결할 수 있다. 이 메서드는 첫 번째 인수로 키를, 두 번째 인수로 기본값을 받으며 맵에 키가 존재하지 않으면 두 번째 인수로 받은 기본값을 반환한다. 단, 키가 존재하더라도 값이 널인 상황에서는 getOrDefault 가 널을 반환할 수 있다. 

8.3.4 계산 패턴

  • computeIfAbsent : 제공된 키에 해당하는 값이 없으면 (값이 없거나 널), 키를 이용해 새 값을 계산하고 맵에 추가한다.
  • computeIfPresent : 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다.
  • compute : 제공된 키로 새 값을 계산하고 맵에 저장한다.

8.3.5 삭제 패턴

제공된 키에 해당하는 맵 항목을 제거하는 remove 메서드와 더불어, 자바 8에서는 키가 특정한 값과 연관되어 있을 때만 항목을 제거하는 오버로드 버전 메서드를 제공한다.

8.3.6 교체 패턴

맵의 항목을 바꾸는 데 사용할 수 있는 두 개의 메서드가 맵에 추가되었다.

  • replaceAll : BiFunction 을 적용한 결과로 각 항목의 값을 교체한다. List 의 replaceAll 과 비슷한 동작을 수행한다.
  • Replace : 키가 있으면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 떄만 값을 교체하는 오버로드 버전도 있다.

8.3.7 합침

두 개의 맵에서 값을 합칠 때 putAll 을 사용할 수 있다. 중복된 키가 없으면 잘 동작한다. 하지만 값을 좀 더 유연하게 합쳐야 한다면 merge 메서드를 이용할 수 있다. 이 메서드는 중복된 키를 어떻게 합칠지 결정하는 BiFunction 을 인수로 받는다.

 

8.4 개선된 ConcurrentHashMap

8.4.1 리듀스와 검색

  • forEach : 각 (키, 값) 쌍에 주어진 액션을 실행
  • reduce : 모든 (키, 값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
  • search : 널이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용

또한 이들 연산에 threshold 를 지정해야 한다. 맵의 크기가 주어진 기준값보다 작으면 순차적으로 연산을 실행한다. 기준값을 1 로 지정하면 공통 스레드 풀을 이용해 병렬성을 극대화한다. 

8.4.2 계수

맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다. 기존의 size 메서드 대신 새 코드에서는 int 를 반환하는 mappingCount 메서드를 사용하는 것이 좋다. 그래야 매핑의 개수가 int 의 범위를 넘어서는 상황에 대처할 수 있기 때문이다.

8.4.3 집합뷰

ConcurrentHashMap 을 집합 뷰로 반환하는 keySet 이라는 새 메서드를 제공한다. 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다. newKeySet 이라는 새 메서드를 이용해 ConcurrentHashMap 으로 유지되는 집합을 만들 수도 있다.