스트림과 람다를 이용한 효과적 프로그래밍
컬렉션 팩토리
자바에서 적은 요소를 포함하는 리스트를 어떻게 만들까?
다음 방법은 세 문자열을 저장하는데도 많은 코드가 필요하다.
다음처럼 Arrays.asList() 팩토리 메서드를 이용해 코드를 줄일 수 있지만, 고정 크기의 리스트이기 때문에 요소를 추가하려하면 Unsupported OperationException이 발생한다.
Unsupported OperationException 예외 발생
내부적으로 고정된 크기의 변환할 수 있는 배열로 구현되었기 때문에 이와같은 일이 일어난다.
리스트를 인수로 받는 HashSet 생성자를 사용하거나, 스트림 API로 해결할 수 있다.
하지만 두 방법 모두 매끄럽지 못하며 불필요한 객체 할당을 필요로 한다.
8.1.1 리스트 팩토리
List.of 팩토리 메서드를 이용하면 간단하게 리스트를 만들 수 있다.
하지만 마찬가지로 변경할 수 없는 리스트이기 때문에 add()나 set() 메서드를 사용할 수 없다.
8.1.2 집합 팩토리
List.of와 비슷한 방법으로 바꿀 수 없는 집합을 만들 수 있다.
8.1.3 맵 팩토리
Map.of 팩토리 메서드에 키와 값을 번갈아 제공하는 방법으로 맵을 만들 수 있다.
10개 이상의 키와 값 쌍을 가진 맵을 만들때는 Map.Entry<K, V> 객체를 인수로 받는 Map.ofEntries 팩토리 메서드를 이용하는 것이 좋다.
8.2 리스트와 집합 처리
자바 8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.
removeIf : 프레디케이트를 만족하는 요소를 제거한다.
replaceAll : UnaryOperator 함수를 이용해 요소를 바꾼다. 리스트에서 사용할 수 있다.
sort : 리스트를 정렬한다.
이 메서드는 새로운 결과를 만드는 것이 아니라 호출한 기존 컬렉션 자체를 바꾼다.
8.2.1 removeIf 메서드
다음은 숫자로 시작되는 참조 코드를 가진 트랜잭션을 삭제하는 코드다.
코드를 실행해보면 ConcurrentModificationException을 일으킨다.
forEach 루프는 Iterator 객체를 사용하므로 위 코드는 다음과 같이 해석된다.
Iterator 객체를 통해 소스를 질의하고, Collection 객체 자체에 remove()를 호출해 요소를 삭제하고있다. 따라서 반복자의 상태와 컬렉션의 상태가 동기화되지 않는다.
Iterator 객체를 명시적으로 사용하고 그 객체의 remove() 메서드를 호출해줘야 정상적으로 동작한다.
위 코드 패턴은 자바8의 removeIf 메서드로 바꿀 수 있다. removeIf는 삭제할 요소를 가리키는 프레디케이트를 인수로 받는다.
8.2.2 replaceAll 메서드
리스트의 각 요소를 새로운 요소로 바꾸고자할때 스트림 API를 사용하면 다음과 같다.
하지만 이 코드는 새 문자열 컬렉션을 만든다. 기존 컬렉션을 바꾸려면 ListIterator 객체를 이용해야한다.
코드가 복잡해졌다. 그리고 이처럼 컬렉션 객체와 Iterator 객체를 혼용하면 반복과 변경이 동시에 이루어져 쉽게 문제를 일으킨다.
자바 8의 replaceAll 메서드를 사용하면 간단하게 구현할 수 있다.
8.3 맵 처리
8.3.1 forEach 메서드
맵에서 키와 값을 반복하면서 확인하는 작업은 번거롭다.
자바8의 Map 인터페이스는 BiConsumer(키와 값을 인수로 받음)를 인수로 받는 forEach 메서드를 지원한다.
8.3.2 정렬 메서드
새로 추가된 유틸리티를 이용해 맵의 항목을 값 또는 키를 기준으로 정렬할 수 있다.
Entry.comparingByKey
Entry.comparingByValue
8.3.3 getOutDefault 메서드
이 메서드는 첫 번째 인수로 키를, 두 번째 인수로 기본값을 받으며 맵에 키가 존재하지 않으면 두 번째 인수로 받은 기본값을 반환한다. 요청결과를 확인하지 않아도 NullPointerException 문제를 해결할 수 있다.
8.3.4 계산 패턴
맵에 키가 존재하는지 여부에 따라 동작을 수행하고자 할 때에 사용할 수 있는 연산이 있다.
computeIfAbsent : 제공된 키에 해당하는 값이 없으면, 키를 이용해 새 값을 계산하고 맵에 추가한다.
computeIfPresent : 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다.
comput : 제공된 키로 새 값을 계산하고 맵에 저장한다.
8.3.5 삭제 패턴
키가 특정한 값일 경우 항목을 제거하도록 remove 메서드를 사용할 수 있다.
8.3.6 교체 패턴
맵의 항목을 바꾸는데 사용할 수 있는 두 개의 메서드가 추가되었다.
replaceAll : BiFunction을 적용한 결과로 각 항목의 값을 교체한다.
Replce : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전도 있다.
8.3.7 합침
맵을 합칠때는 putAll을 사용할 수 있다.
중복된 키가 있다면 merge 메서드를 이용할 수 있다. 이 메서드는 중복된 키를 어떻게 합칠지 결정하는 BiFunction을 인수로 받는다.
8.4 개선된 ConcurrentHashMap
ConcurrentHashMap는 병렬 친화적인 HashMap이다. 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용한다.
8.4.1 리듀스와 검색
ConcurrentHashMap은 스트림과 비슷한 세 가지 새로운 연산을 지원한다.
forEach : 각 (키,값) 쌍에 주어진 액션을 실행
reduce : 모든 (키,값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
search : 널이 아닌 값을 반환할 때까지 각 (키,값) 쌍에 함수를 적용
다음처럼 키, 값, Map.entry 인수를 이용해 네 가지 연산 형태를 지원한다.
키, 값으로 연산 (forEach, reduce, search)
키로 연산 (forEachKey, reduceKeys, searchKeys)
값으로 연산 (forEachValue, reduceValues, searchValues)
Map.Entry 객체로 연산(forEachEntry, reduceEntries, searchEntries)
이들 연산은 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행하므로, 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다.
또한 병렬성 기준값(threshhold)를 지정해야 하는데, 맵의 크기가 주어진 기준값보다 작으면 순차적으로 연산을 실행한다. 기준값이 1일때는 공통 스레드 풀을 이용해 병렬성을 극대화하며, 기준값이 Long.MAX_VALUE일때는 한 개의 스레드로 연산을 실행한다.
이 예제에서는 reduceValues 메서드를 이용해 맵의 최댓값을 찾는다.
8.4.2 계수
ConcurrentHashMap 클래스는 맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다. size 대신 mappingCount 를 사용해야 매핑의 개수가 int의 범위를 넘어서는 이후의 상황을 대처할 수 있다.
8.4.3 집합뷰
ConcurrentHashMap을 집합 뷰로 반환하는 keySet 메서드를 제공한다. 맵을 바꾸면 집합도 바뀌고 집합을 바꾸면 맵도 영향을 받는다.
newkeySet 메서드를 이용하면 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다.
Last updated