8.1 컬렉션 팩토리
자바 9에서는 작은 컬렉션 객체를 쉽게 만들 수 있는 몇 가지 방법을 제공한다.
왜 이와 같은 기능이 필요한 지 살펴보고 새 팩토리 메서드를 사용하는 방법을 알아본다.
자바에서 적은 요소의 리스트를 만드는 일반적인 방법
List<String> friends = new ArrayList<>();
friends.add("Raphael");
friends.add("Olivia");
friends.add("Thibaut");
Arrays.asList() 팩토리 메서드 이용
List<String> friends = Arrays.asList("Raphael", "Olivia", "Thibaut");
고정 크기의 리스트를 만들었으므로 요소를 갱신할 수는 있지만 새 요소를 추가하거나 요소를 삭제할 순 없다.
요소를 추가하거나 삭제하려 하면 UnsupportedOperationException이 발생
List<String> friends = Arrays.asList("Raphael", "Olivia");
friends.set(0, "Richard"); // OK
friends.add("Thibaut"); // UnsupportedOperationException이
UnsupportedOperationException 예외 발생
내부적으로 고정된 크기를 가지면서 요소를 변환할 수 있는 배열로 구현되었기 때문에 리스트 요소 갱신은 가능하고 요소 추가나 삭제는 UnsupportedOperationException이 발생하는 것이다.
집합의 경우에는 Arrays.asSet()이라는 팩토리 메서드는 없으므로 다른 방법이 필요하다.
Set<String> friends
= new HashSet<>(Arrays.asList("Raphael", "Olivia", "Thibaut"));
Set<String> friends
= Stream.of("Raphael", "Olivia", "Thibaut")
.collect(Collectors.toSet())
두 방법 모두 매끄럽지 못하며 내부적으로 불필요한 객체 할당 필요
맵의 경우에는 작은 맵을 만들 수 있는 방법이 따로 없다.
자바 9에서 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 팩토리 메서드를 제공하게 되었다.
8.1.1 리스트 팩토리
List.of 팩토리 메서드를 이용해서 간단하게 리스트를 만들 수 있다.
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
List.of 팩토리 메서드로 만든 리스트는 add 메서드와 set 메서드 모두 UnsupportedOperationException을 발생시킨다.
List.of 팩토리 메서드로 만든 리스트는 변경할 수 없는 리스트이기 때문이다.
이런 제약이 나쁜 것만은 아니다. 컬렉션이 의도치 않게 변하는 것을 막을 수 있다.
리스트를 바꿔야 하는 상황이라면 직접 리스트를 만들면 된다.
새로운 컬렉션 팩토리 메서드 대신 스트림 API를 사용해 리스트를 만들어야 하는 경우는 데이터 처리 형식을 따로 설정하거나 데이터를 변환할 필요가 있는 상황이다.
그런 경우가 아니면 간편한 팩토리 메서드를 이용할 것을 권장한다.
오버로딩 vs 가변인수
List 인터페이스 List.of의 다양한 오버로드 버전이 있다.
...
static <E> List<E> of(E e1, E e2, E e3, E e4);
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5);
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6);
...
static <E> List<E> of(E... elements)와 같은 가변 인수 버전은 없다.
내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싼다. => 가비지 컬렉션 비용 지불 필요
고정된 숫자의 요소(최대 10개까지)를 API로 정의하므로 이런 비용을 제거 가능
List.of로 열 개 이상의 요소를 가진 리스트를 만들 수도 있지만 이때는 가변 인수를 이용하는 메서드가 사용
Set.of와 Map.of에서도 이와 같은 패턴이 등장
8.1.2 집합 팩토리
List.of와 비슷한 방법으로 바꿀 수 없는 집합을 만들 수 있다.
Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
중복된 요소를 제공해 집합을 만들려고 하면 IllegalArgumentException 발생
Set<String> friends = Set.of("Raphael", "Olivia", "Olivia");
8.1.3 맵 팩토리
Map<String, Integer> ageOfFriends
= Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
열 개가 넘는 키와 값 쌍을 갖는 맵을 만들 때는 Map.Entry<K,V> 객체를 인수로 받으며 가변 인수로 구현된 Map.ofEtries 팩토리 메서드를 이용하는 것이 좋다
import static java.util.Map.entry;
Map<String, Integer> ageOfFriends
= Map.ofEntries(entry("Raphael", 30),
entry("Olivia", 25),
entry("Thibaut", 26));
Map.entry는 Map.Entry 객체를 만드는 새로운 팩토리 메서드
8.2 리스트와 집합 처리
자바 8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.
removeIf: 프레디케이트를 만족하는 요소를 제거, List나 Set을 구현하거나 그 구현을 상속받은 모든 클래스에서 이용 가능
replaceAll: 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소 변경
sort: List 인터페이스에서 제공하는 기능으로 리스트를 정렬
위 메서드들은 호출한 컬렉션 자체를 바꾼다.
새로운 결과를 만들어내는 스트림 동작과 달리 기존 컬레션 자체를 바꾸는 것이다.
컬렉션 자체를 바꾸는 동작은 에러를 유발하며 복잡함을 더하는데 이 때문에 자바 8에서 removeIf와 replaceAll 메서드를 추가한 것이다.
8.2.1 removeIf 메서드
for(Transactin transaction: transactions){
if(Character.isDigit(transaction.getReferenceCode().charAt(0))){
transactions.remove(transaction);
}
}
위 코드는 ConcurrentModificationException을 발생시킨다.
내부적으로 for-each 루프는 Iterator 객체를 사용한다.
for(Iterator<Transaction> iterator = transactions.iterator();
iterator.hasNext();){
Transaction transaction = iterator.next();
if(Character.isDigit(transaction.getReferenceCode().charAt(0))){
transactions.remove(transaction);
}
}
두 개의 개별 객체(Iterator 객체와 Collectiono 객체 자체)가 컬렉션을 관리
결과적으로 반복자의 상태가 컬렉션의 상태와 동기화되지 않는다.
for-each 루프를 사용하지 않고 Iterator 객체를 명시적으로 사용, 그 객체의 remove 메서드를 호출하는 방식으로 해결 가능
for(Iterator<Transaction> iterator = transactions.iterator();
iterator.hasNext();){
Transaction transaction = iterator.next();
if(Character.isDigit(transaction.getReferenceCode().charAt(0))){
iterator.remove();
}
}
이 코드 패턴을 자바 8에서는 removeIf 메서드로 바꿀 수 있다.
transactions.removeIf(transaction ->
Character.isDigit(transaction.getReferenceCode().charAt(0));
8.2.2 replaceAll 메서드
요소를 바꾸는 set 메서드를 지원하는 ListIterator 객체를 이용하여 기존 컬렉션을 변경할 수 있다.
for(ListIterator<String> iterator = referenceCodes.listIterator();
iterator.hasNext();){
String code = iterator.next();
iterator.set(Character.toUpper(code.charAt(0)) + code.substring(1));
}
자바 8의 replaceAll 메서드를 이용해서 간단화할 수 있다.
referenceCodes.replaceAll(code ->
Character.toUpper(code.charAt(0)) + code.substring(1));
8.3 맵 처리
8.3.1 forEach 메서드
Map<String, Integer> ageOfFriends
= Map.ofEntries(entry("Raphael", 30),
entry("Olivia", 25),
entry("Thibaut", 26));
ageOfFriends.forEach((friend, age) ->
System.out.println(friend + " is " + age
" years old"+ ));
8.3.2 정렬 메서드
Map 인터페이스의 내부 인터페이스 Entry의 디폴트 메서드 comparingByKey()와 comparingByValue()를 이용해서 맵의 항목을 키 또는 값을 기준으로 정렬 가능
Map<String, Integer> favoriteMovies
= Map.ofEntries(entry("Raphael", "Star Wars"),
entry("Cristina", "Matrix"),
entry("Olivia", "James Bond"));
favoriteMovies.entrySet()
.stream()
.sorted(Entry.comparingByKey())
.forEachOrdered(System.out::println);
Cristina=Matrix
Olivia=James Bond
Raphael=Star Wars
8.3.3 getOrDefault 메서드
기존에는 찾으려는 키가 존재하지 않으면 널이 반환되므로 NullPointerException을 방지하려면 요청 결과가 널인지 확인이 필요
getOrDefault 메서드를 이용하면 기본값을 반환하여 문제 해결 가능
getOrDefault 메서드는 첫 번째 인수로 키를, 두 번째 인수로 기본값을 받으며 맵에 키가 존재하지 않으면 두 번째 인수로 받은 기본값을 반환
Map<String, Integer> favoriteMovies
= Map.ofEntries(entry("Raphael", "Star Wars"),
entry("Cristina", "Matrix"),
entry("Olivia", "James Bond"));
favoriteMovies.getOrDefault("Olivia", "Matrix"); // James Bond
favoriteMovies.getOrDefault("Thibaut", "Matrix");// Matrix
8.3.4 계산 패턴
맵에 키가 존재하는지, 존재하지 않는지 여부에 따라 어떤 동작을 실행하고 결과를 저장해야 하는 상황이 필요할 때가 있다.
computeIfAbsent: 제공된 키에 해당하는 값이 없으면(값이 없거나 널), 키를 이용해 새 값을 계산하고 맵에 키, 값을 추가 혹은 수정한 다음 계산된 값을 반환
computeIfPresent: 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한 다음, 계산될 결과 반환
compute: 제공된 키로 새 값을 계산하고 맵에 저장한 다음 계산된 값을 반환
Map<String, List<String>> friendToMovies = Map.of();
friendToMovies.computeIfAbsent("Raphael",
name -> new ArrayList<>()) // ArrayList 반환
.add("Star Wars"); // 반환된 ArrayList에 값 추가
// {Raphel:[Star Wars]}
8.3.5 삭제 패턴
제공된 키에 해당하는 맵 항목을 제거하는 remove 메서드는 이미 알고 있다.
String key = "Raphael";
String value = "Jack Reacher 2";
if (favoriteMovies.containsKey(key) &&
Objects.equals(favoriteMovies.get(key), value)){
favoriteMovies.remove(key);
return true;
}else {
return false;
}
자바 8에서는 키가 특정한 값과 연관되었을 때만 항목을 제거하는 오버로드 버전 메서드를 제공
favoriteMovies.remove(key, value);
8.3.6 교체 패턴
맵의 항목을 바꾸는 데 사용할 수 있는 두 개의 메서드가 맵에 추가되었다.
replaceAll: BiFunction을 적용한 결과로 맵 각 항목의 값을 교체한다.
replace: 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매피되었을 때만 값을 교체하는 오버로드 버전도 있다.
Map<String, String> favoriteMovies = new HashMap<>();
favoriteMovies.put("Raphael", "Star Wars");
favoriteMovies.put("Olivia", "james bond");
favoriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
8.3.7 합침
merge는 첫 번째 인수로 주어지는 key와 연결된 값(oldValue)이 널이면 두 번째 인수로 주어지는 value를 키와 연결되는 새로운 값(newValue)으로 put하고 키와 연결된 새로운 값(newValue) 반환(단, 두 번째 인수로 주어지는 value가 널이면 첫 번째 인수 key에 해당하는 항목 remove )
만약 첫 번째 인수로 주어지는 key와 연결된 값(oldValue)이 널이 아니면 세 번째 인수로 주어지는 람다의 실행 결과값을 키와 연결되는 새로운 값(newValue)으로 put하고 키와 연결된 새로운 값(newValue) 반환(단, 두 번째 인수로 주어지는 value가 널이면 첫 번째 인수 key에 해당하는 항목 remove )
세 번째 인수로 주어지는 람다에 주어지는 파라미터는 merge 메서드의 첫 번째 인수로 주어지는 key와 연결된 값(oldValue)과 merge 메서드의 두 번째 인수로 주어지는 value이고 람다의 바디에서는 이 두 값을 병합(merge, 합침)하는 연산을 해줘야 한다.
Map<String, Long> moviesToCount = new HashMap()();
String movieName = "JamesBond";
long count = moviesToCount.get(movieName);
if(count == null) {
moviesToCount.put(movieName, 1);
}else {
moviesToCount.put(movieName, count + 1);
}
moviesToCount.merge(movieName, 1L, (count, increment) -> count + increment);
8.4 개선된 ConcurrentHashMap
ConcurrentHashMap 클래스는 동시성 친화적이며 최신 기술을 반영한 HashMap 버전이다.
ConcurrentHashMap은 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용
모든 연산 작업이 동기화되는 Hashtable 버전에 비해 읽기 쓰기 연산 성능이 월등하다.
참고로 표준 HashMap은 비동기로 동작한다.
8.4.1 리듀스와 검색
ConcurrentHashMap은 스트림과 비슷한 종류의 세 가지 새로운 연산을 지원한다.
forEach: 각 (키, 값) 쌍에 주어진 액션을 수행
forEach(키, 값으로 연산)
forEachKey(키로 연산)
forEachValue(값으로 연산)
forEachEntry(Map.Entry 객체로 연산)
reduce
reduce(키, 값으로 연산)
reduceKeys(키로 연산)
reduceValues(값으로 연산)
reduceEnties(Map.Entry 객체로 연산)
search
search(키, 값으로 연산)
searchKeys(키로 연산)
searchValues(값으로 연산)
searchEnties(Map.Entry 객체로 연산)
이들 연산은 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행하기 때문에 연산에 제공하는 함수(람다)는 계산이 진행되는 동안 상태가 변경될 수 있는 객체, 값, 순서 등에 의존해선 안 된다.
또한 이들 연산에는 병렬성 기준값을 지정해야 하는데 맵의 크기가 주어진 병렬성 기준값보다 작으면 병렬 연산이 아닌 순차 연산이 실행된다.
병렬성 기준값을 1로 하면 공통 스레드 풀을 이용해 병렬성을 극대화할 수 있다.
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
long parallelismThreshold = 1;
Optional<Integer> maxValue =
Optional.ofNullable(map.reduceValues(parallelismThreshold, Long::max));
int, long, double 등의 기본값에는 전용 each reduce 연산이 제공되므로 reduceValuesToInt, reduceKeysToLong 등을 이용하면 박싱 작업을 할 필요가 없고 효율적으로 작업을 처리할 수 있다.
8.4.2 계수
ConcurrentHashMap 클래스는 맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공
기존의 size 메서드 대신 새 코드에서는 long을 반환하는 mappingCount 메서드를 사용하는 것이 좋다.
매핑 개수가 int를 넘어서는 이후의 상황을 대처할 수 있도록 한다.
8.4.3 집합뷰
ConcurrentHashMap 클래스는 ConcurrentHashMap을 집합 뷰로 반환하는 keySet 메서드를 제공한다.
맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다.
newKeySet이라는 새 메서드를 이용해 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다.