본문 바로가기
Book/모던 자바 인 액션

[모던 자바 인 액션] Chapter 08. 컬렉션 API 개선 (2)

by ghan2 2024. 11. 9.

 

 

8.3 맵 처리

자바 8에서는 Map 인터페이스에 몇 가지 디폴트 메서드를 추가했다. 디폴트는 뒤에서 자세히 다루고 여기서는 디폴트 메서드가 기본적인 구현을 인터페이스에 제공하는 기능 정도로 생각해두자. 

 

8.3.1 forEach 메서드

자바 8서부터 Map 인터페이스는 BiConsumer(키와 값을 인수로 받음)를 인수로 받는 forEach 메서드를 지원하므로 코드를 좀 더 간단하게 구현할 수 있다.

ageOfFriends.forEach((friend, age) -> System.out.println(friend + " is " + age + " years old"));

 

8.3.2 정렬 메서드

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

  • Entry.comparingByValue
  • Entry.comparingByKey
favouriteMovies
	.entrySet()
    .stream()
    .sorted(Entry.comparingByKey())
    .forEachOrdered(System.out::println);

 

8.3.3 getOrDefault 메서드

기존에는 찾으려는 키가 존재하지 않으면 널이 반환되므로 NullPointerException을 방지하려면 요청 결과가 널인지 확인해야 한다. getOrDefault 메서드를 이용하면 쉽게 이 문제를 해결할 수 있다. 

Map<String, String> favouriteMovies = 
				Map.ofEntries(entry("Raphael", "Star Wars"),
                			  entry("Olivia", "James Bond"));
                              
System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix")); //James Bond
System.out.println(favouriteMovies.getOrDefault("Thibaut", "Matrix")); //Matrix

 

자바 8에서는 키의 값이 존재하는지 여부를 확인할 수 있는 더 복잡한 패턴도 제공한다. 이를 다음 8.3.4절에서 살펴보자 !

 

8.3.4 계산 패턴

맵에 키가 존재하는지 여부에 따라 어떤 동작을 실행하고 결과를 저장해야 하는 상황이 필요한 때가 있다. 

예를 들어, 키를 이용해 값비싼 동작을 실행해서 얻은 결과를 캐시하려 한다. 키가 존재하면 결과를 다시 계산할 필요가 없다. 

다음 세 가지 연산이 이런 상황에서 도움을 준다.

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

 

8.3.5 삭제 패턴

  • remove : favouriteMovies.remove(key, value);

 

8.3.6 교체 패턴

  • replaceAll : BiFunction을 적용한 결과로 각 항목의 값을 교체한다. List의 repalceAll과 비슷하다.
  • Replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전도 있다.
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());

 

 

8.3.7 합침

  • putAll : 중복된 키가 없는 경우 잘 동작.
  • merge : 중복된 키가 있는 경우 사용
Map<string, String> everyone = new HashMap<>(family);

friends.forEach((k, v) -> everyone.merge(k, v, (movie1, movie2) -> movie1 + " & " + movie2));

 

merge를 이용해서 초기화 검사를 구현할 수도 있다. 

영화를 몇 회 시청했는지 기록하는 맵이 있다고 하자.

moviesToCount.merge(movieName, 1L, (count, increment) -> count + 1L);

 

 

8.4 개선된 ConcurrentHashMap

ConcurrentHashMap 클래스는 동시성 친화적이면서 최신 기술을 반영한 HashMap버전이다. 

내부 자료 구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용한다. 따라서 동기화된 해시테이블 버전에 비해 일기 쓰기 연산 성능이 월등하다.

 

8.4.1. 리듀스와 검색

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

이들 연산은 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행한다는 점에 주목하자. 따라서 이들 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다.

 

8.4.2 계수

ConcurrentHashMap 클래스는 맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다.

기존의 size 메서드 대신 새 코드에서는 long을 반환하는 mappingCount 메서드를 사용하는 것이 좋다.

 

8.4.3 집합뷰

집합 뷰로 반환하는 keySet 이라는 메서드를 제공한다. 

 

8.5 마치며

  • 자바 9는 적은 원소를 포함하며 바꿀 수 없는 리스트, 집합, 맵을 쉽게 만들 수 있도록 List.of, Set.of, Map.of, Map.ofEntries 등의 컬렉션 팩토리를 지원한다.
  • 이들 컬렉션 팩토리가 반환한 객체는 만들어진 다음 바꿀 수 없다. 
  • List 인터페이스는 removeIf, replaceAll, sort 세 가지 디폴트 메서드를 지원한다.
  • Set 인터페이스는 removeIf 디폴트 메서드를 지원한다.
  • Map 인터페이스는 자주 사용하는 패턴과 버그를 방지할 수 있도록 다양한 디폴트 메서드를 제공한다.
  • ConcurrentHashMap은 Map 에서 상속받은 새 디폴트 메서드를 지원함과 동시에 스레드 안전성도 제공한다.