[모던 자바인 액션] chpt.8 컬렉션 API 개선

sameul__choi·2022년 5월 4일
0

[모던 자바인 액션]

목록 보기
11/11
post-thumbnail
  1. 컬렉션 팩토리 사용하기
  2. 리스트 및 집합과 사용할 새로운 관용 패턴 배우기
  3. 맵과 사용할 새로운 관용 패턴 배우기

컬렉션 API가 없었다면 개발자는 힘들었을거다. 거의 모든 자바 애플리케이션에서 컬렉션을 사용한다. 지금까지 컬렉션과 스트림 API를 이용하여 데이터 처리 쿼리를 어떻게 효율적으로 하는 지 살펴봤다. 하지만 여전히 쉽지 않고 에러를 유발하는 여러 단점이 있다.

8장에서는 자바8,9에 추가된 편리한 컬렉션 API에 대해서 배운다. 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 자바 9에 새로 추가된 컬렉션 팩토리를 살펴보고, 개선사항으로 리스트와 집합에서 요소를 삭제하거나 바꾸는 관용 패턴을 적용하는 방법을 배워본다.

8.1 컬렉션 팩토리

List<String> friends = new ArrayList<>;
friends.add("raphael");
friends.add("eddy");
friends.add("thibaut");

문자열 세개를 저장하는데도 많은 코드가 필요하다.

List<String> friends = Arrays.asList("raphael", "eddy", "thibaut");

고정 크기의 리스트를 만들었으므로 요소를 갱신할 순 있지만, 새 요소를 추가하거나 요소를 삭제할 순 없다.
요소를 추가하려면 UnsupportedOperationException이 발생한다. 내부적으로 고정된 크기의 변환할 수 있는 배열로 구현되었기 때문이다.

Set<String> friends = new HashSet<>(Arrays.asList("raphael","eddy","thibaut"));

Set<String> friends = Stream.of("raphael","eedy","thibaut").collect(toSet());

리스트를 인수로 받는 HashSet 생성자를 사용할 수 있겠지만, 내부적으로 불필요한 객체 할당을 필요로하며 매끄럽지 못하다. 그리고 결과는 변환할 수 있는 집합이라는 사실도 있다.

그렇다면 map은 어떨까 ? 자바 9에서는 작은 집합, 리스트, 맵을 쉽게 만들 수 있는 팩토리 메서드를 제공한다.

자바 9에서 제공되는 팩토리 메서드

  • List.of : 변경할 수 없는 불변 리스트를 만듦

  • Set.of : 변경할 수 없는 불변 집합을 만듦, 중복된 요소를 제공하여 집합 생성하면 IllegalArgumentException 발생

  • Map.of : 키와 값을 번갈아 제공하는 방법으로 맵을 만들 수 있음

  • Map.ofEntries : Map.Entry<K,V> 객체를 인수로 받아 맵을 만들 수 있다. 엔틑리 생성은 Map.entry 팩토리 메서드를 이용하여 전달.

8.2 리스트와 집합 처리

자바 8에 다음 메서드들이 추가 되었다.

  • removeIf : 프레디케이트를 만족하는 요소를 제거, List나 Set을 구현하거나 그 구현을 상속받은 모든 클래스에서 이용가능
transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0)));
  • replaceAll : 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용하여 요소를 바꾼다.
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));
  • sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.

위 메서드가 나온 이유

이들은 새로운 결과를 만드는 스트림 동작과는 달리 기존 컬렉션을 바꾼다. 왜 추가되었나 ? 컬렉션을 바꾸는 동작은 복잡하고 에러를 유발하기 때문이다.

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

for-each 루프는 내부적으로 Iterator 객체를 사용하므로 두개의 개별 객체가 컬렉션을 관리하게 된다.

  • Iterator 객체, next(), hasNext()를 이용하여 소스를 질의한다.
  • Collection 객체 자체, remove()를 호출해 요소를 삭제한다.

결과적으로 반복자의 상태는 컬렉션의 상태와 서로 동기화 되지 않는다. Iterator 객체를 명시적으로 사용하고 객체의 remove() 메서드를 호출함으로 이 문제를 해결할 순 있지만, 코드가 복잡해진다.

8.3 map 처리

8.3.1 forEach 메서드

맵에서 키와 값을 반복하여 확인하는 것은 번거롭다. 때문에 아래와 같이 반복자를 이용해 맵의 항목 집합을 반복한다.

for(Map.Entry<String, Integer> entry: ageOfFriends.entrySet()) {
	String friend = entry.getKey();
    Integer age = entry.getKey();
    System.out.println(friend + age);

자바 8에서부터 Map 인터페이스는 BiConsumer (키와 값을 인수로 받는다.)를 인수로 받는 forEach메서드를 지원하므로 코드를 조금 더 간단하게 만들 수 있다.

ageOfFriends.forEach((friend, age) -> System.out.println(friend + age));

8.3.2 정렬 메서드

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

  • Entry.comparingByValue
  • Entry.comparingByKey
favouriteMusics
	.entrySet()
    .stream()
    .sorted(Entry.comparingByKey())
    // 사람의 이름을 알파벳 순으로 스트림 요소 처리
    .forEachOrdered(System.out::println);

8.3.3 getOrDefault 메서드

요청한 키가 맵에 존재하지 않을 때, 이를 어떻게 처리하냐도 흔히 발생하는 문제인데, 새로 추가된 getOrDefault 메서드를 이용하면 쉽게 해결할 수 있다.

기존에 찾으려는 키가 존재하지 않으면 널이 반환되므로 NPE를 발생하는데 이를 방지하려면 요청 결과가 null인지 확인해야한다. 기본값을 반환하는 방식으로 이 문제를 해결한다.

8.3.4 계산 패턴

맵에 키가 존재하는 지 여부에 따라 어떤 동작을 실행하고 결과를 저장해야 하는 상황(값비싼 동작 후 얻은 결과를 캐시하려고 할 때)이라면 다음의 세가지 연산을 사용하면 도움이 된다.

  • computeIfAbsent : 제공된 키에 해당하는 값이 없거나 null이라면, 키를 이용해 새로운 값을 계산하고 맵에 추가한다.

  • computeIfPresent : 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다. 계산한 값이 null이라면 맵에 추가하지 않으면 오히려 존재하던 key 또한 제거한다.

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

8.3.5 삭제 패턴

default boolean remove(Object key, Object value)

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

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형으로 반환하지만 long 형으로 반환하는 mappingCount를 사용할 때 매핑의 개수가 int의 범위를 넘어서는 상황에 대하여 대처할 수 있을 것이다.

8.4.3 집합 뷰

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

0개의 댓글