[도서][모던 자바 인 액션] 컬렉션 API 개선

Junseo Kim·2021년 3월 6일
0

컬렉션 팩토리

기존에는 적은 요소를 포함하는 List나 Set을 만들때 아래와 같이 생성하였다.

List<String> friends = Arrays.asList("air", "picka");
Set<String> friends = new HashSet<>(Arrays.asList("air", "picka"));

Arrays.asList는 고정 크기의 리스트이므로 갱신은 가능하지만, 요소의 추가나 삭제가 불가능하다.

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

리스트 팩토리

List.of() 메서드를 사용해서 간단하게 리스트를 만들 수 있다.

List<String> friends = List.of("air", "picka");

List.of 메서드는 변경할 수 없는 리스트이다. 갱신이나 추가 삭제 모두 불가능하며 시도하면 UnsupportedOperationException이 발생한다.

따라서 컬렉션이 의도치 않게 변하는 것을 막을 수 있지만, 요소 자체가 변하는 것은 막을 수 없다. 또 null은 요소로 올 수 없다.

집합 팩토리

Set.of() 메서드를 사용해서 간단하게 set을 만들 수 있다.

Set<String> friends = Set.of("air", "picka");

하지만 인수로 중복되는 값을 넘길 경우 IllegalArgumentException이 발생한다. set은 중복을 허용하지 않기 때문이다.

맵 팩토리

Map.of() 메서드를 사용해서 map을 만들 수 있다. Map은 키와 값을 같이 가지고 있으므로 List나 Set보다는 조금 복잡하다. 2가지 방법이 존재한다.

// 1. 열 개 이하의 키와 값 쌍을 가진 맵을 만드는 경우
Map<String, Integer> ageOfFriends = Map.of("air", 26, "calvin", 25);
// 2. 열 개 이상의 키와 값 쌍을 가진 맵을 만드는 경우
Map<String, Integer> ageOfFriends = Map.ofEntries(
        Map.entry("air", 26),
        Map.entry("calvin", 25)
);

Map.ofEntries는 Map.Entry<K, V> 객체를 인수로 받는다.

of 팩토리 메서드의 가변인수

내부적으로 가변 인수는 추가 배열을 할당해서 리스트로 감싼다. 따라서 배열을 할당하고 초기화하며 나중에 가비지 컬렉션을 하는 비용을 지불해야 한다. 따라서 List.of, Set.of, Map.of는 10개까지 인수로 받는 메서드가 오버로딩 되어있고 11개부터는 가변인수를 사용한다.

데이터 처리 형식을 설정하거나 데이터를 변환할 필요가 없다면 스트림 대신 팩토리 메서드를 이용한다.

리스트와 집합 처리

새로운 결과를 만드는 stream과 달리 아래에서 살펴볼 removeIf, replaceAll는 호출한 컬렉션 자체를 바꾼다.

removeIf

List, Set에서 이용가능. 프레디케이트를 만족하는 요소를 제거.

removeIf가 추가된 이유를 살펴보자. 아래의 코드는 ConcurrentModificationException을 발생시킨다. for-each 구문은 내부적으로 Iterator 객체를 사용하기 때문에 Iterator 객체, Collection 객체 총 2개의 객체가 컬렉션을 관리한다. 따라서 반복자의 상태가 컬렉션의 상태와 서로 동기화되지 않게된다.

for (Transaction transaction : transactions) {
    if (Character.isDigit(transaction.getReferencecode().charAt(0))) {
        transactions.remove(transaction);
    }
}

이를 해결해보면 아래와 같이 할 수 있는데 코드가 복잡해졌다.

for (Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext(); ) {
    Transaction transaction = iterator.next();
    if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
        iterator.remove();
    }
}

이를 removeIf를 사용해서 간결하게 바꿀 수 있다.

transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0)));

replaceAll

List에서 이용가능. UnaryOperator 함수를 이용해 요소를 바꾼다.

List의 요소를 바꿀때도 Iterator 객체, 컬렉션 변경을 혼용해서 사용하면 문제가 일어날 수 있다. 이것을 replaceAll로 쉽게 해결할 수 있다.

referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));

맵 처리

자바 8부터 Map 인터페이스에 몇 가지 디폴트 메서드가 추가됐다.

forEach

키와 값을 인수로 받는 Biconsumer을 인수로 받는 forEach 메서드를 사용해서 키와 값을 반복하면서 확인할 수 있다.

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

정렬메서드

스트림의 sorted에 Entry.comparingByValueEntry.comparingByKey를 넘겨주어 정렬할 수 있다.

  • Entry.comparingByValue: value 기준으로 정렬
  • Entry.comparingByKey: key 기준으로 정렬
favoriteMovies.entrySet().stream()
    .sorted(Entry.comparingByKey())
    .forEachOrdered(System.out::println);

getOrDefault 메서드

기존에는 Map에 찾으려는 key가 없는 경우 null이 반환되었다. 이 경우 찾으려는 key가 없으면 기본값을 반환해줄 수 있다. 첫 번째 인수로 key를 두 번째 인수로 기본값을 받아, key가 존재하지 않을경우 기본값을 반환한다.

Map<String, String> favoriteMovies = 
    Map.ofEntries(
        entry("Raphael", "Star Wars"),
        entry("Olivia", "James Bond")
    );

key가 존재하더라도 저장된 값이 null이면 null을 반환한다.

계산 패턴

Map에 key가 존재하는지 여부에 따라 어떤 동작을 실행하고 결과를 저장해야하는 상황일 때 사용하는 패턴이다.

  • computeIfAbsent: 제공된 key에 해당하는 값이 없거나 null이면, key를 이용해 새 값을 계산하고 맵에 추가한다. 정보를 캐시할 때 사용가능하다.

  • computeIfPresent: 제공된 key가 존재하면 새 값을 계산하고 맵에 추가한다.

  • compute: 제공된 key로 새 값을 계산하고 맵에 저장한다.

삭제 패턴

자바 8부터 key가 특정한 value와 연관되었을 때만 항목을 제거하는 오버로드 버전 메서드가 있다.

favoriteMovies.remove("Raphael", "Jack Reacher 2"); // map의 Raphael(key)의 value가 Jack Reacher 2(value)인 경우만 삭제

교체 패턴

map의 항목을 바꾸는 데 사용할 수 있는 메서드

  • replaceAll: BiFunction을 적용한 결과로 각 항목의 값을 교체한다.
  • replace: key가 존재하면 map의 value를 바꾼다. key가 특정 value로 매핑되었을 때만 value를 교체하는 버전의 메서드도 있다.
favoriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());

favoriteMovies.replace("Raphael", "harry potter"); 

favoriteMovies.replace("Olivia", "james bond", "coco");

합침

두 개의 map에서 값을 합치거나 바꿔야할 때 사용. merge메서드로 가능하다.

기존에 두 map을 합칠 때 아래와 같은 방법을 사용했다.

Map<String, String> family = Map.ofEntries(
        Map.entry("Teo", "Star Wars"),
        Map.entry("Cristina", "James Bond")
);
Map<String, String> friends = Map.ofEntries(
        Map.entry("Raphael", "Star Wars")
);
        
Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends);

중복된 key가 없다면 문제없이 동작한다.

merge메서드는 중복된 key를 어떻게 합칠지 결정하는 BiFunction을 인수로 받아 중복된 key가 존재하는 map을 합칠 수 있게 해준다.

Map<String, String> family = Map.ofEntries(
        Map.entry("Teo", "Star Wars"),
        Map.entry("Cristina", "James Bond")
);
Map<String, String> friends = Map.ofEntries(
        Map.entry("Raphael", "Star Wars"),
        Map.entry("Cristina", "Matrix")
);

Map<String, String> everyone = new HashMap<>(family);
friends.forEach((key, value) ->
        everyone.merge(key, value, (movie1, movie2) -> movie1 + " & " + movie2));

개선된 ConcurrentHashMap

ConcurrentHashMap 클래스는 동시성 친화적이며 최신 기술을 반영한 HashMap 버전이다. 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용한다.

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

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

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

int, long, double 등 기본값에는 특화연산(reduceValuesToInt 등)이 존재하므로 박싱작업이 필요없다.

key, value로 연산

  • forEach
  • reduce
  • search

key로 연산

  • forEachKey
  • reduceKeys
  • searchKeys

value로 연산

  • forEachValue
  • reduceValues
  • searchValues

Map.Entry 객체로 연산

  • forEachEntry
  • reduceEntries
  • searchEntries

계수

map의 매핑 개수를 반환하는 mappingCount 메서드를 제공.

집합뷰

ConcurrentHashMap을 set으로 반환하는 KeySet 메서드 제공. map을 바꾸면 set도 바뀌고 set을 바꾸면 map도 영향을 받는다.

ConcurrentHashMap을.newKeySet()으로 ConcurrentHashMap으로 유지되는 set을 만들수도있다.

0개의 댓글