모던 자바 인 액션 - 8장 컬렉션 API 개선

Modern Java In Action 정리 - 컬렉션 API 개선

  • 컬렉션 팩토리 사용하기
  • 리스트 및 집합과 사용할 새로운 관용패턴 배우기
  • 맵과 사용할 새로운 관용 패턴 배우기

모던 자바 인 액션 책을 보고 정리한 글입니다.

컬렉션 팩토리

  • 자바 9에서는 작은 컬렉션 객체를 쉽게 만들수 있는 몇 가지 방법을 제공한다.
    1
    2
    3
    4
    List<String> friends = new ArrayList<>();
    friends.add("Raphael");
    friends.add("Olivia");
    friends.add("Thibaut");
    
  • 위처럼 세 문자열을 저장하는데 많은 코드가 소요된다 따라서 아래와 같이 사용하여 코드를 줄일 수 있다.
    1
    List<String> friends = Arrays.asList("Raphael", "Olivia", "Thibaut");
    
  • 고정된 크기의 리스트를 만들었으니 새로운 요소를 추가하거나 요소를 삭제한다면 UnsupportedOperationException 이 발생한다. 요소를 갱신하는것은 정상적으로 작동한다.
  • Java 9 부터는 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 팩토리 메서드를 제공한다.

리스트 팩토리

  • List.of 팩토리 메서드를 활용하여 간단하게 리스트를 만들수 있게 되었다.
    1
    List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
    
  • of 메소드를 사용하더라도 요소를 추가하거나 삭제하면 여전히 UnsupportedOperationException 에러가 발생한다.
  • 때로는 컬렉션이 의도치 않게 변경되는것을 막는것이 더 효과적일수 있다.
  • Arrays.asList와 of의 차이점은 가변인수를 사용하지 않는다는것이다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      static <E> List<E> of() {
          return ImmutableCollections.emptyList();
      }
    
      static <E> List<E> of(E e1) {
          return new List12(e1);
      }
      static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) {
          return new ListN(new Object[]{e1, e2, e3, e4, e5, e6, e7, e8, e9, e10});
      }
    
  • 가변인수를 사용한다면 그만큼의 배열을 리스트로 만들고 추후 객체 반환까지 해야하는 비용이 발생하지만 리스트 팩토리 메서드를 활용해 불필요한 비용을 줄일수 있게 되었다.

집합 팩토리

  • List.of와 비슷하게 변경 불가능한 집합을 만들수 있다.
    1
    Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
    

맵 팩토리

  • 맵을 만드는것은 리스트나 Set을 만드는것에 비해 조금 복잡하지만 Java 9 에서 두가지 방법으로 맵을 만들수 있도록 제공하고 있다.
1
2
3
4
5
6
7
// Key - Value 를 번갈아 가며 생성
Map<String, Integer> friendsMap = Map.of("Raphael", 30, "Olivia", 30, "Thibaut", 30);

// Map의 인수가 많을경우 entry를 활용해서 만드는것이 좋다.
Map<String, Integer> friendsMapEntry = Map.ofEntries(entry("Raphael", 30),
                entry("Olivia", 30),
                entry("Thibaut", 30));

리스트와 집합 처리

  • 자바 8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.
  • removeIf : Predicate를 만족하는 요소를 제거한다. List나 Set을 구현하거나 구현을 상속받은 클래스에서 이용할 수 있다
  • replaceAll : 리스트에서 이용할수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꾼다.
  • sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.
  • 위 메서드들은 호출한 컬렉션 자체를 변경하게 된다. 일반적으로 새로운 결과를 만드는 스트림과는 달리 기존 컬렉션에 변화를 주는 기능이 추가된 이유는 무엇일까 ?

removeIf

1
2
3
4
5
for (Transaction transaction : transactions) {
  if (True) {
    transactions.remove(transaction);
  }
}
  • 위 코드는 ConcurrentModificationException 에러를 발생시킨다. for - each 루프는 내부적으로 iterator 객체를 사용하므로 아래와 같이 해석된다.
    1
    2
    3
    4
    5
    6
    for (Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext();) {
    Transaction transaction = iterator.next();
    if (true) {
      transactions.remove(transaction);
    }
    }
    
  • 두 개의 개별 객체가 컬렉션을 관리하는 것에 주목.
  • Iterator 객체 : next(), hasNext()를 사용해 소스를 질의 한다.
  • Collection 객체 : remove()를 호출하여 요소를 삭제
  • 결과적으로 반복자의 상태는 컬렉션의 상태와 서로 동기화되지 않고 있다. 아래와 같이 Iterator 객체를 명시적으로 삭제하여 문제를 해결할 수 있다.
    1
    2
    3
    4
    5
    6
    for (Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext();) {
    Transaction transaction = iterator.next();
    if (true) {
      iterator.remove();
    }
    }
    
  • 코드가 조금 복잡해졌지만 위 코드 패턴은 Java8의 removeIf 메서드로 변경할 수 있다. 위에서 말한것 처럼 removeIf는 Predicate를 인수로 받는다.
    1
    transactions.removeIf(transaction -> true);
    

replaceAll 메서드

  • List 인터페이스의 replaceAll 메서드를 이용하여 리스트의 각 요소를 새로운 요소로 변경할 수 있다. 일반적으로 스트림 Api 를 사용한다면 아래와 같이 사용가능하다.
    1
    2
    3
    referenceCodes.stream()
                .map(Strings::toUpperCase))
                .collect(toList());
    
  • 위 처럼 스트림을 활용한다면 새로운 컬렉션이 생성되고 기존 컬렉션은 변경되지 않는다. 기존 컬렉션을 변경하기 위해 아래와 같이 사용할 수 있다.
    1
    2
    3
    4
    5
    for (ListIterator<String> iterator = referenceCodes.listIterator(); iterator.hasNext();) {
    iterator.hasNext();
    String code = iterator.next();
    iterator.set(code.toUpperCase());
    }
    
  • 코드가 복잡해지고 컬렉션 객체를 Iterator와 혼용하여 사용한다면 반복과 컬렉션 변경이 동시에 이루어 지며 쉽게 문제를 일으킨다.
  • Java8 에서 제공하는 기능을 사용하도록 하자
    1
    referenceCodes.replaceAll(code -> code.toUpperCase());
    

맵 처리

forEach 메서드

  • 맵에서 Key, Value을 반복하며 확인하려면 Map.Entry<K, V>의 반복자를 활용해야 한다.
  • Java 8 부터 Map 인터페이스는 BiConsumer (Key, Value를 인수로 받는다.)를 인수로 받는 forEach 메서드가 추가되어 간편하게 구현할 수 있다.
    1
    2
    Map<String, Integer> ageOfFriends = new HashMap<>();
    ageOfFriends.forEach((friend, age) -> print(friend + "is" + age)))
    

정렬 메서드

  • 다음 두 개의 새로운 유틸리티를 활용하여 맵의 항목을 key 또는 Value를 기준으로 정렬할 수 있다.
  • Entry.comparingByValue
  • Entry.comparingByKey
1
2
3
4
5
Map<String, String> favouriteMovies = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"), entry("Olivia", "James Bond"));

favouriteMovies.entrySet()
               .stream()
               .sorted(Entry.comparingByKey());

getOrDefault

  • 기존엔 맵에 존재하지 않는 키로 값을 찾으려할때 NPE가 발생할 가능성이 있어 Null 체크가 필요했다.
  • 현재는 getOrDefault 라는 메소드가 지원되니 안전하게 문제를 해결할 수 있습니다.
1
2
3
4
Map<String, String> favouriteMovies = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"), entry("Olivia", "James Bond"));

favouriteMovies.getOrDefault("Olivia", "Matrix"); // James Bond 출력
favouriteMovies.getOrDefault("Thibaut", "Matrix"); // Matrix 출력
  • 키가 존재하더라도 값이 Nulㅣ인 경우에는 여전히 Null 을 반환하게 되니 주의하자.

계산 패턴

  • 맵에 키가 존재하는지 여부에 따라 어떤 동작을 실행하고 결과를 저장해야 하는 상황이 필요할 때가 있다.
  • 다음 세가지 메소드가 이러한 상황에서 도움이 된다.
  • computeIfAbsent : 제공된 키에 해당하는 값이 존재하지 않으면(값이 없거나 Null) 제공된 키로 새로운 Value를 맵에 추가한다.
  • computeIfPresent : 제공된 키가 존재하면 새로운 Value로 맵에 추가한다.
  • compute : 제공된 키로 새 Value를 추가한다.

삭제 패턴

  • 제공된 키에 해당하는 맵 항목을 제거하는 remove는 존재하지만 특정 key에 특정 value일때만 제거 할 수 있도록 오버라이드된 메소드가 존재한다.
  • remove(key, value);

교체 패턴

  • 맵의 항목을 변경하는데 사용할 수 있는 두 개의 메서드가 추가되었다.
  • replaceAll : BiFunction을 적용한 결과로 각 항목의 값을 교체한다. 이전에 알려준 List.replaceAll과 비슷한 기능이다.
  • replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을때만 변경하도록 하는 오버로드된 버전도 있다.
    1
    2
    Map<String, String> favouriteMovies = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"), entry("Olivia", "James Bond"));
    favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
    

Merge

  • 두 그룹의 연락처를 포함하는 두 맵을 합친다고 가정하자. 일반적으로 putAll을 사용할 수 있다.
    1
    2
    3
    4
    Map<String, String> family = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"));
    Map<String, String> friends = Map.ofEntries(entry("Olivia", "James Bond"));
    Map<String, String> everyone = new HashMap<>(family);
    everyone.putAll(friends);
    
  • 중복된 키가 존재하지 않는다면 위 코드는 정상적으로 동작할 것이다.
  • 만약 중복된 키가 존재하고 값을 유연하게 다루기 위해 merge 메서드를 활용할 수 있다.
  • merge 메서드는 중복된 메서드를 어떤 방식으로 합칠지 결정하는 BiFunction 을 인수로 받는다.
    1
    2
    3
    4
    Map<String, String> family = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"));
    Map<String, String> friends = Map.ofEntries(entry("Olivia", "James Bond"), entry("Cristina", "James bond"));
    Map<String, String> everyone = new HashMap<>(family);
    everyone.merge(k, v, (movie1, movie2) -> movie1 + movie2));
    
  • 지정된 키와 연관된 값이 없거나 값이 Null 일경우 merge는 키를 널이 아닌 값과 연결한다.
  • merge는 연결된 값을 주어진 매핑함수의 결과 값으로 대치하거나 결과가 Null일 경우 항목을 제거한다.

ConcurrentHashMap

  • ConcurrentHashMap은 동시성 친화적이며 최신기술을 반영한 HashMap 버전이다.
  • ConcurrentHashMap은 스트림에서 봤던것과 비슷한 종류의 세 가지 연산을 지원한다.
    1. forEach : 각 (key, value) 쌍에 주어진 액션을 실행
    2. reduce : 모든 (key, value) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
    3. search : 널이 아닌 값을 반환할 때 까지 각 (key, value)쌍에 함수를 적용
  • 다음처럼 키에 함수 받기, 값, Map.Entry, (key, value)인수를 이용한 네 가지 연산 형태를 지원
    1. key, value로 연산 (forEach, reduce, search)
    2. key로 연산(forEachKey, reduceKeys, searchKeys)
    3. value로 연산(forEachValue, reduceValues, searchValues)
    4. Map.Entry 객체로 연산(forEachEntry, reduceEntries, searchEntries)
  • 이들 연산은 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행한다는 점을 주목하자, 따라서 이들 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다.
  • ConcurrentHashMap은 맵의 매핑 개수를 확인하는 mappingCount 메서드를 제공한다.
  • 기존 size 메서드 대신 새 코드에서는 int를 반환하는 mappingCount 메서드를 사용하는것이 좋다.
  • ConcurrentHashMap을 집합 뷰로 반환한느 keySet이라는 새로운 메서드도 제공한다.
  • 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다.
  • newKeySet이라는 새 메서드를 이용해 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다.