컬렉션 팩토리
List<String> friends = new ArrayList<>();
friends.add(“Raphael”);
friends.add(“Olivia”);
friends.add(“Thibaut”);
Arrays.asList() 팩토리 메서드를 이용하면 코드를 간단하게 줄일 수 있다.
List<String friends = new Arrays.asList(“Raphael”, “Olivia”, “Thibaut”);
고정 크기의 리스트를 만들었으므로 요소를 갱신할 순 잇지만 요소를 추가하거나 삭제할 순 없다.
요소를 추가하려 하면 Unsupported OperationException이 발생한다.
UnsupportedOperationException 예외 발생
내부적으로 고정된 크기의 변환할 수 있는 배열로 구현되었기 때문에 이와 같은 일이 일어난다.
집합은 Arrays.asSet()이라는 팩토리 메서드가 없으므로 리스트를 인수로 받는 HashSet 생성자를 사용할 수 있다.
Set<String> friends = new HashSet<>(Arrays.asList(“Raphael”, “Olivia”, “Thibaut”)):
다음 처럼 스트림 API를 사용할 수 있다.
Set<String> friends = Stream.of(“Raphael”, “Olivia”, “Thibaut”).collect(Collectors.toSet());
En 방법 모두 매끄럽지 못하며 내부적으로 불필요한 객체 할당을 필요로 한다. 그리고 집합이라는 사실도 주목하자.
리스트 팩토리
List.of 팩토리 메소드를 이용해서 간단하게 리스트를 만들 수 있다.
List<String> friends = List.of(“Raphael”, “Olivia”, “Thibaut”);
System.out.println(friends); // [Raphael, Olivia, Thilbaut]
List<String> friends = List.of(“Raphael”, Olivia”, “Thibaut”);
friends.add(“Chih-Chun”);
위 코드를 실행하면 java.lang.UnsupportedOperationException이 발생한다. 사실 변경할 수 없는 리스트가 만들어졌기 때문이다. set() 메서드로 아이템을 바꾸려해도 비슷한 예외가 발생한다. 따라서 set 메서드로도 리스트를 바꿀 수 없다.
집합 팩토리
List.of와 비슷한 방법으로 바꿀 수 없는 집합을 만들 수 있다.
Set<String> friends = Set.of(“Raphael”, “Olivia”, “Thibaut”); System.out.println(friends); // [Raphael, Olivia, Thibaut]
중복된 요소를 제공해 집합을 만드려고 하면 Olivia라는 요소가 중복되어 있다는 설명과 함께 IllegalArgumentException이 발생한다.
맵 팩토리
Map.of 팩토리 메서드에 키와 값을 번갈아 제공하는 방법으로 맵을 만들 수 있다.
Map<String, Integer> ageOfFriends =
Map.of(“Raphael”, 30, “Olivia”, 25, “Thibaut”, 26);
System.out.println(ageOfFriends); // {Olivia=25, Raphael=30, Thibaut=26}
열 개 이하의 키와 값 싸을 가진 작은 맵을 만들 때는 이 메소드가 유용하다. 그 이상의 맵에서는 Map.Entry<K, V> 객체를 인수로 받으며 가변 인수로 구현된 Map.ofEntries 팩토리 메서드를 이용하는 것이 좋다.
import static java.util.Map.entry; Map<String, Integer> ageOfFriends =
Map.ofEntries(entry(“Raphael”, 30),
entry(“Olivia”, 25),
entry(“Thibaut”, 25);
System.out.println(ageOfFriends); // {Olivia=25, Raphael=30, Thibaut=26}
Map.entry는 Map.Entry 객체를 만드는 새로운 팩토리 메서드다.
[퀴즈 8-1]
다음 코드를 실행한 결과는?
List<String> actors = List.of(“Keanu”, “Jessica”)
Actors.set(0, “Brad”);
System.out.println(actors);
정답
UnsupportedOperationException이 발생한다. List.of로 만든 컬렉션은 바꿀 수 없기 때문이다.
리스트와 집합 처리
removeIf : 프레디케이트를 만족하는 요소를 제거한다. List나 Set을 구현하거나 그 구현을 상속받은 모든 클래스에서 이용할 수 있다.
replaceAll : 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꾼다.
sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.
이들 메서드는 호출한 컬렉션 자체를 바꾼다. 컬렉션을 바꾸는 동작은 에러를 유발하며 복잡합을 더한다.
removeIf 메서드
for (Transaction 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 객체, next(), hasNext()를 이용해서 소스를 질의한다.
Collection 객체 자체, remove()를 호출해 요소를 삭제한다.
Iterator 객체를 명시적으로 사용하고 그 객체의 remove() 메서드를 호출함으로 이 문제를 해결할 수 있다.
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을 제공한다.
replaceAll 메서드
List 인터페이스의 replaceAll 메서드를 이용해 리스트의 각 요소를 새로운 요소로 바꿀 수 있다.
스트림 API를 사용하면 다음처럼 문제를 해결할 수 있었다.
referenceCodes.stream() // [a12, c14, b13]
.map(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1)
.collect(Collectors.toList())
.forEach(System.out::println); outputs A12, C14, B13
위 코드는 새 문자열 컬렉션을 만든다. 기존 컬렉션을 변경해보자. 다음처럼 ListIterator 객체(요소를 바꾸는 set() 메서드를 지원)를 이용할 수 있다.
for (ListIterator<String> iterator = referenceCodes.listIterator(); iterator.hasNext(); ) {
String code = iterator.next();
Iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1));
}
자바 8기능을 이용하여 간단하게 구현 가능하다.
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0) + code.substring(1));
맵 처리
forEach 메서드
for(Map.Entry<String, Integer> entry: ageOfFriends.entrySet()) {
String friend = entry.getKey();
Integer age = entry.getValue();
System.out.println(friend + “ is “ + age + “ years old”);
}
위 코드를 아래와 같이 변경 가능하다.
ageOfFriends.forEach((friend, age) -> System.out.println(friend + “ IS “ + age + “ years old”)
정렬 메서드
Entry.comparingByValue, Entry.comparingByKey
Map<String, String> favouriteMovies =
Map.ofEntries(entry(“Raphael”, “Star Wars”),
entry(“Cristina”, “Matrix”),
entry(“Olivia”, “James Bond”));
favouriteMovies
.entrySet()
.stream()
.sorted(Entry.comparingByKey())
.forEachOrdered(System.out::println); // 사람의 이름을 알파벳 순으로 스트림 요소를 처리한다.
결과
Cristina=Matrix, Olivia=James Bond, Raphael=Star Wars
getOrDefault 메서드
찾으려는 키가 존재하지 않으면 널이 반환되므로 NullPointerException을 방지하려면 요청 결과가 널인지 확인해야 한다. 기본값을 반환하는 방식으로 이 문제를 해결할 수 있다.
첫 번째 인수로 키를, 두 번째 인수로 기본값을 받으며 맵에 키가 존재하지 않으면 두 번째 인수로 받은 기본값을 반환한다.
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 출력
계산 패턴
computeIfAbsent : 제공된 키에 해당하는 값이 없으면(값이 없거나 널), 키를 이용해 새 값을 계산하고 맵에 추가한다.
computeIfPresent: 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다.
Compute : 제공된 키로 새 값을 계산하고 맵에 저장한다.
정보를 캐시할 때 computeIfAbsent를 활용할 수 있다.
Map<String, byte[]> dataToHash = new HashMap<>();
MessageDigest messageDigest = MessageDigest.getInstance(“SHA-256”);
lines.forEach(line -> dataToHash.computeIfAbsent(line, // line은 맵에서 찾을 키
this::calculateDigest)); // 키가 존재하지 않으면 동작을 실행
private byte[] calculateDigest(String key) { // 헬퍼가 제공된 키의 해시를 계산할 것이다.
return messageDigest.digest(key.getBytes(StandardCharsets.UTF_8));
}
Map<K, List<V>>에 요소를 추가하려면 항목이 초기화되어 있는지 확인해야 한다.
String friend = “Raphael”;
List<String> movies = friendsToMovies.get(friend);
if(movies == null) //리스트가 초기화되었는지 확인
movies = new ArrayList<>();
friendsToMovies.put(friend, movies);
}
movies.add(“Star Wars”); // 영화를 추가
System.out.println(friendsToMovies); // {Raphael:[Star Wars]]
computeIfAbsent는 키가 존재하지 않으면 값을 계산해 맵에 추가하고 키가 존재하면 기존 값을 반환한다.
friendsToMovies.computeIfAbsent(“Raphael”, name -> new ArrayList<>())
.add(“Star Wars”); // {Raphael[Star War]}
computeIfPresent 메서드는 현재 키와 관련된 값이 맵에 존재하면 널이 아닐 때만 새 값을 계산한다. 하지만 매핑을 제거할 때는 remove 메서드를 오버라이드하는 것이 더 적합하다.
삭제 패턴
String key = “Raphael”;
String value = “Jack Reacher 2”;
if (favouriteMovies.containsKey(key) && Objects.equals(favouriteMovies.get(key), value)) {
favouriteMovies.remove(key);
return true;
} else {
return false;
}
이와 같이 간결하게 구현 가능하다.
favouriteMovies.remove(key, value);
교체 패턴
replaceAll : BiFunction을 적용한 결과로 각 항목의 값을 교체한다. 이 메서드는 이전에 살펴본 List의 replaceAll과 비슷한 동작을 수행한다.
Replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전도 있다.
Map<String, String> favouriteMovies = new HashMap<>(); // replaceAll을 적용할 것이므로 바꿀 수 있는 맵을 사용해야 한다.
favouriteMovies.put(“Raphael”, “Star Wars”);
favouriteMovies.put(“Olivia”, “james bond”);
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
System.out.println(favouriteMovies); // {Olivia=JAMES BOND, Raphael=STAR WARS}
합침
두 그룹의 연락처를 포함하는 두 개의 맵을 합친다고 가정하면 putAll을 사용할 수 있다.
Map<String, String> family = Map.ofEntries(
entry(“Teo”, “Star Wars”), entry(“Cristina”, “James Bond”));
Map<String, String> friends = Map.ofEntries(
entry(“Raphael”, “Star Wars”));
Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends); // friends의 모든 항목을 everyone으로 복사
System.out.println(everyone); // {Cristina=James Bond, Raphael=Star Wars, Teo=Star Wars}
중복 키가 없다면 위 코드는 잘 동작한다.
Map<String, String> family = Map.ofEntries(
entry(“Teo”, “Star Wars”), entry(“Cristina”, “James Bond”));
Map<String, String> friends = Map.ofEntries(
entry(“Raphael”, “Star Wars”), entry(“Cristina”, “Matrix”));
Chistinal가 중복일 경우 forEach와 merge 메서드를 통해 출동을 해결할 수 있다.
Map<String, String> everyone = new Hash<>(family);
friends.forEach((k, v) ->
everyone.merge(k, v, (movie1, movie2) -> movie1 + “ & “ + movie2)); // 중복된 키가 있으면 두 값을 연결
System.out.println(everyone); // Outputs {Raphael=Star Wars, Cristina=James Bond & Matrix, Teo=Star Wars}
Merge 메서드는 널값과 관련된 복잡한 상황도 처리한다.
“지정된 키와 연관된 값이 없거나 값이 널이면 [merge]는 키를 널이 아닌 값과 연결한다. 아니면 [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, (key, count) -> count + 1L);
merge의 두 번째 인수는 1L이다. 자바독에 따르면 이 인수는 “키와 연관된 기존 값에 합쳐질 널이 아닌 값 또는 값이 없거나 키에 널 값이 연관되어 있다면 이 값을 키와 연결”하는데 사용된다.
[퀴즈 8-2]
다음 코드가 어떤 작업을 수행하는지 파악한 다음 코드를 단순화할 수 잇는 방법을 설명하시오.
Map<String, Integer> movies = new HashMap<>();
movies.put(“JamesBond”, 20);
movies.put(“Matrix, 15);
movies.put(“Harry Potter”, 5);
Iterator<Map.Entry<String, Integer>> iterator = movies.entrySet().iterator();
While(iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
if(entry.getValue() < 10) {
Iterator.remove();
}
}
System.out.println(movies); // {Matrix=15, JamesBond=20}
정답
맵의 항목 집합에 프레디케이트를 인수로 받아 항목을 삭제하는 removeIf 메서드를 사용할 수 있다.
movies.entrySet().removeIf(entry -> entry.getValue() < 10);
개선된 ConcurrentHashMap
동시성 친화적이며 최신 기술을 반영한 HashMap 버전이다. 동기화된 Hashtable 버전에 비해 읽기 쓰기 연산 성능이 월등하다.(참고로, 표준 HashMap은 비동기로 동작함).
리듀스와 검색
forEach : 각 (키, 값) 쌍에 주어진 액션을 실행
reduce : 모든 (키, 값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
search : 널이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용
다음처럼 키에 함수 받기, 값, Map.Entry, (키, 값) 인수를 이용한 네가지 연산 형태를 지원한다.
키, 값으로 연산(forEach, reduce, search)
키로 연산(forEachKey, reduceKeys, searchKeys)
값으로 연산(forEachValue, reduceValues, searchValues)
Map.Entry 객체로 연산(forEachEntry, reduceEntries, searchEntries)
ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행한다. 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다.
또한 이들 연산에 병렬성 기준값(threshold)을 지정해야 한다. 맵의 크기가 주어진 기준값보다 작으면 순차적으로 연산을 실행한다. 기준값을 1로 지정하면 공통 스레드 풀을 이용해 병렬성을 극대화한다. Long.Max_VALUE를 기준값으로 설정하면 한 개의 스레드로 연산을 실행한다.
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>(); // 여러키와 값을 포함되도록 갱신될 ConcurrentHashMap
long parallelismThreshold = 1;
Optional<Integer> maxValue =
Optional.ofNullable(map.reduceValues(parallelismThreshold, Long::max));
Int, long double 등의 기본값에는 전용 each reduce 연산이 제공되므로 reduceValuesToInt, reduceKeysToLong 등을 이용하면 박싱 작업을 할 필요가 없고 효율적으로 작업을 처리할 수 있다.
개수
ConcurrentHashMap 클래스는 맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다. 기존의 size 메서드 대신 새 코드에서는 int를 반환하는 mappingCount 메서드를 사용하는 것이 좋다.
집합뷰
ConcurrentHashMap 클래스는 ConcurrentHashMap을 집합 뷰로 반환하는 keySet이라는 새 메서드를 제공한다. 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다. newKeySet이라는 새 메서드를 이용해 ConcurrentHashMap으로 유지되는 집합을 생성할 수 있다.
'Programming > Java' 카테고리의 다른 글
| EPISODE 10. 모던 자바 인 액션 (CHAPTER 10. 람다를 이용한 도메인 전용 언어) (0) | 2023.07.18 |
|---|---|
| EPISODE 9. 모던 자바 인 액션(CHAPTER 9 리팩터링, 테스팅, 디버킹) (0) | 2023.07.17 |
| EPISODE 7. 모던 자바 인 액션(CHAPTER 7 벙렬 데이터 처리와 성능) (0) | 2023.07.13 |
| EPISODE 6. 모던 자바 인 액션(CAHPTER 6 스트림으로 데이터 수집) (0) | 2023.07.11 |
| EPISODE 5. 모던 자바 인 액션(CHAPTER 5 스트림 활용) (1) | 2023.07.10 |