자바 8, 9 - 컬렉션 API 개선
1. 컬렉션 팩토리
자바 9에서 추가된 팩토리 메서드들은 사용 시 주의할 점은 다음 세 가지이며 자세한 내용은 1-4. 주의사항을 참고하시길 바랍니다.
- 파라미터로 전달한 요소(Element)들은 null이면 안된다.
- 생성된 Collection은 추가, 삭제, 수정이 불가하다.
- Collection에서 지원하는 메서드들의 파라미터 또한 null이면 안된다.
1-1. List.of(E e, ..)
@DisplayName("List 팩토리 사용")
@Test
void list_factory_test() {
List<String> coffeeList = List.of("메가커피", "스타벅스", "빽다방");
System.out.println("coffeeList = " + coffeeList);
}
// 결과
coffeeList = [메가커피, 스타벅스, 빽다방]
Arrays.asList
와List.of
의 차이점이 존재하며 이후에 포스팅할 예정입니다.
1-2. Set.of(E e, ..)
@DisplayName("Set 팩토리 사용1")
@Test
void set_factory_test1() {
Set<String> coffeeSet = Set.of("메가커피", "스타벅스", "빽다방");
System.out.println("coffeeSet = " + coffeeSet);
}
// 결과
coffeeSet = [메가커피, 빽다방, 스타벅스]
@DisplayName("Set 팩토리 사용2")
@Test
void set_factory_test2() {
// 💡 java.lang.IllegalArgumentException: duplicate element: 메가커피 예외 발생
Set<String> coffeeSet = Set.of("메가커피", "스타벅스", "빽다방", "메가커피");
System.out.println("coffeeSet = " + coffeeSet);
}
1-3.
Map.of(key, value, ..)
Map.ofEntries(Entry<? extends K, ? extends V>... entries)
Map.of
는 최대 10개의 key와 value를 가진 작은 크기의 불변 맵을 생성할 수 있으며 Map.ofEntries
는 원하는 개수의 Entry만큼 불변 맵을 생성할 수 있습니다. 따라서 Map의 크기가 10개 이하인 경우에는 Map.of
를 사용하고 그 이상일 경우 Map.Entry<K, V> 객체를 가변 인수로 받는 Map.ofEntries
를 사용하는 것이 좋습니다.
@DisplayName("Map 팩토리 사용")
@Test
void map_factory_test() {
Map<String, Integer> coffeeOfMap = Map.of("메가커리", 1500, "스타벅스", 3500, "백다방", 2000);
Map<String, Integer> coffeOfEntiriesMap = Map.ofEntries(
Map.entry("메가커리", 1500),
Map.entry("스타벅스", 3500),
Map.entry("백다방", 2000)
);
System.out.println("coffeeOfMap = " + coffeeOfMap);
System.out.println("coffeOfEntiriesMap = " + coffeOfEntiriesMap);
}
// 결과
coffeeOfMap = {스타벅스=3500, 백다방=2000, 메가커리=1500}
coffeOfEntiriesMap = {스타벅스=3500, 백다방=2000, 메가커리=1500}
1-4. 주의사항
앞서 언급한 것처럼 자바 9 에서 추가된 팩토리 메서드들은 사용하는데 주의사항이 있습니다. 첫 번째로 컬렉션 팩토리로 만들어진 컬렉션은 추가, 삭제, 변경을 할 경우 UnsupportedOperationException
예외가 발생합니다.
@DisplayName("팩토리로 만들어진 컬렉션에 추가 시 예외")
@Test
void list_factory_ex_test1() {
List<String> coffeeList = List.of("메가커피", "스타벅스", "빽다방");
assertThrows(UnsupportedOperationException.class, () -> coffeeList.add("이디야"));
}
@DisplayName("팩토리로 만들어진 컬렉션에 요소 삭제 시 예외")
@Test
void list_factory_ex_test2() {
List<String> coffeeList = List.of("메가커피", "스타벅스", "빽다방");
assertThrows(UnsupportedOperationException.class, () -> coffeeList.remove("스타벅스"));
}
@DisplayName("팩토리로 만들어진 컬렉션에 요소 수정 시 예외")
@Test
void list_factory_ex_test3() {
List<String> coffeeList = List.of("메가커피", "스타벅스", "빽다방");
assertThrows(UnsupportedOperationException.class, () -> coffeeList.set(1, "이디야"));
}
두 번째로 컬렉션 팩토리 파라미터로 전달된 요소들이 null일 경우 NullPointerException
예외가 발생합니다.
@DisplayName("팩토리의 파라미터로 null을 전달 시 예외")
@Test
void list_factory_ex_test4() {
assertThrows(NullPointerException.class, () -> List.of("메가커피", "스타벅스", "빽다방", null));
}
세 번째로 컬렉션에서 제공하는 메서들의 파라미터 값이 null 일 경우 NullPointerException
예외가 발생합니다.
@DisplayName("컬렉션에서 제공하는 메서드의 파라미터로 null을 전달 시 예외")
@Test
void list_factory_ex_test5() {
List<String> coffeeList = List.of("메가커피", "스타벅스", "빽다방");
assertThrows(NullPointerException.class, () -> coffeeList.contains(null));
}
따라서 컬렉션 팩토리를 사용하게 되면 위 세 가지 주의사항을 지켜줘야 합니다.
2. List와 Set에 추가된 메서드
2-1. removeIf(Predicate<? super E> filter)
List나 Set을 상속받은 모든 클래스에서 사용할 수 있으며 Predicate에 만족하는 요소들을 제거해 줍니다. 'remoceIf' 메서드가 추가되어 코드의 가독성이 향상되었다는 장점이 있습니다. 아래 예시 코드는 자바 9 이전에 사용했던 코드와 자바 9 버전에서 사용되는 'removeIf'를 이용한 예제 코드입니다.
@DisplayName("리스트 컬렉션의 removeIf")
@Test
void list_removeIf() {
// ** 자바 9 이전 코드 **
// List의 특정 요소 제거
List<String> coffeeList1 = new ArrayList<>();
coffeeList1.add("메거커피");
coffeeList1.add("스타벅스");
coffeeList1.add("빽다방");
for (String s : coffeeList1) {
if (s.equals("스타벅스")) {
coffeeList1.remove("스타벅스");
}
}
assertThat(coffeeList1.size()).isEqualTo(2);
assertThat(coffeeList1).isNotEqualTo("스타벅스");
// ** 자바 9 이후 코드 **
// List의 특정 요소 제거
List<String> coffeeList2 = new ArrayList<>();
coffeeList2.add("메거커피");
coffeeList2.add("스타벅스");
coffeeList2.add("빽다방");
// ✨ 코드의 가독성 향상
coffeeList2.removeIf(name -> name.equals("스타벅스"));
assertThat(coffeeList2.size()).isEqualTo(2);
assertThat(coffeeList2).isNotEqualTo("스타벅스");
}
현재 구글링 하다 보면 List for-each를 이용하여 삭제하고자 할 때 ConcurrentModificationException
이 발생한다는 게시글들이 많습니다. Iterator 객체가 for-each 내부에서 사용되기 때문에 List 요소를 삭제하고자 하면 ConcurrentModificationException
예외가 발생하였지만 현재는 코드가 개선되어 for-each에서 요소를 삭제해도 해당 예외가 발생하지 않습니다.
2-2. replaceAll(UnaryOperator<E> operator)
List에서 추가된 기능(Set에는 해당 메서드가 없다)으로 UnaryOperator
함수를 이용해 요소를 변경해 줄 수 있습니다.
@Test
void list_replaceAll() {
List<String> coffeeList = new ArrayList<>();
coffeeList.add("메가커피");
coffeeList.add("스타벅스");
coffeeList.add("빽다방");
coffeeList.replaceAll(coffee -> coffee + " 가맹점");
System.out.println("coffeeList = " + coffeeList);
}
// 결과
coffeeList = [메가커피 가맹점, 스타벅스 가맹점, 빽다방 가맹점]
Stream을 이용해도 각 요소를 새로운 요소로 변경할 수 있지만 Stream은 처리 결과로 새로운 컬렉션을 만들기 때문에 불필요한 자원 낭비의 원인이 됩니다. 따라서 Stream을 사용해야 할 이유가 없다면 List에서 제공하는 'replaceAll'을 사용하는 것을 추천합니다.
3. Map에 추가된 메서드
3-1. forEach(BiConcumer<? super K, ? super V> action)
Map.forEach
는 자바 8에서 추가된 메서드로 코드의 가독성을 높일 수 있습니다. 아래는 Map.forEach
를 사용하기 전 코드와 사용한 예제 코드입니다.
@DisplayName("Map forEach 메서드 사용하기 이전 코드")
@Test
void map_for_each() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2000);
// 💡 자바 8 이전에 사용했던 코드
for (Map.Entry<String, Integer> entry : coffeeMap.entrySet()) {
String coffee = entry.getKey();
Integer price = entry.getValue();
System.out.println("coffeeMap = " + coffee + ":" + price);
}
}
@DisplayName("Map forEach 메서드를 사용한 코드")
@Test
void map_forEach() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2000);
// ✨ Map 컬렉션에 추가된 forEach 사용
coffeeMap.forEach((coffee, price) -> System.out.println("coffeeMap = " + coffee + ":" + price));
}
// 결과
coffeeMap = 빽다방:2000
coffeeMap = 메가커피:2000
coffeeMap = 스타벅스:3500
3-2. 정렬 메서드
key 기준 정렬 - Entry.comparingByKey
value 기준 정렬 - Entry.comparingByValue
Map 컬렉션은 두 개의 새로운 유틸리티를 사용하여 key 또는 value를 기준으로 정렬할 수 있습니다.
Entry.comparingByKey
@DisplayName("Map key 기준 정렬")
@Test
void map_comparing_by_key() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2100);
// 기존 정렬 순서
System.out.println("coffeeMap = " + coffeeMap);
// ✨ key 기준으로 정렬
coffeeMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(System.out::println);
}
// 결과
coffeeMap = {빽다방=2100, 메가커피=2000, 스타벅스=3500}
메가커피=2000
빽다방=2100
스타벅스=3500
Entry.comparingByValue
@DisplayName("Map key, value 중심으로 정렬")
@Test
void map_comparing_by_key() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2100);
// 기존 정렬 순서
System.out.println("coffeeMap = " + coffeeMap);
// ✨ value 기준으로 정렬
coffeeMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.forEachOrdered(System.out::println);
}
// 결과
coffeeMap = {빽다방=2100, 메가커피=2000, 스타벅스=3500}
메가커피=2000
빽다방=2100
스타벅스=3500
3-3. getOrDefault(Object key, V defaultValue)
자바 8 이전에는 해당 Key가 Map에 존재하지 않으면 null이 반환되므로 NullPointerException
을 방지하기 위한 코드가 추가되어야 했지만 자바 8에서 추가된 'getOrDefault' 메서드를 사용하면 해당 Key가 Map에 존재하지 않을 경우 Default 값을 반환합니다. 따라서 Key 존재 유무를 확인과 없을 경우 기본 값에 대한 로직을 추가하지 않아도 되는 장점이 있습니다.
@DisplayName("Map - getOrDefault 메서드")
@Test
void map_getOrDefault_test() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2100);
// ✨ Map key에 메가커피가 있으므로 2000이 반환된다.
Integer megaCoffePrice = coffeeMap.getOrDefault("메가커피", 1500);
System.out.println("megaCoffePrice = " + megaCoffePrice);
// ✨ Map key에 이디야가 없으므로 두 번째 인수로 받은 값인 3200이 반환된다.
Integer ediyaPrice = coffeeMap.getOrDefault("이디야", 3200);
System.out.println("ediyaPrice = " + ediyaPrice);
// 💡 getOrDefault 를 사용하지 않은 코드는 다음과 같다.
Integer ediyaPriceByGet = coffeeMap.get("이디야");
if (ObjectUtils.isEmpty(ediyaPriceByGet)) {
ediyaPriceByGet = 3200;
}
System.out.println("ediyaPriceByGet = " + ediyaPriceByGet);
}
// 결과
megaCoffePrice = 2000
ediyaPrice = 3200
ediyaPriceByGet = 3200
3-4. 계산 관련 메서드
computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
Key가 Map에 존재하는지에 따라 특정 동작을 실행하고 결과를 저장해야 하는 상황이 필요한 경우가 많습니다. 이를 지원하기 위해 자바 8에 추가된 세 가지 메서드가 존재합니다.
computeIfAbsent(K key, Function<? super K, ?extends V> mappingFunction)
key에 해당되는 값이 없으면 Map에 추가해 주는 메서드입니다. 첫 번째 파라미터로 Key를 받고, 두 번째 파라미터로는 Key에 해당되는 값이 Map에 없을 경우 실행되는 람다식을 받습니다.
@DisplayName("Map - 계산 관련 메서드")
@Test
void map_computeIfAbsent() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2100);
// ✨ key(이디야)가 Map에 없으므로 Map에 추가해준다.
// coffeeMap.put("이디야", 3200) 실행이 됨
coffeeMap.computeIfAbsent("이디야", coffee -> 3200);
System.out.println("coffeeMap = " + coffeeMap);
}
// 결과
coffeeMap = {빽다방=2100, 메가커피=2000, 스타벅스=3500, 이디야=3200}
@DisplayName("Map - 계산 관련 메서드2")
@Test
void map_computeIfAbsent2() {
Map<String, List<Integer>> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", new ArrayList<>());
coffeeMap.put("스타벅스", new ArrayList<>());
coffeeMap.put("빽다방", new ArrayList<>());
coffeeMap.get("메가커피").add(2000);
// ✨ key(이디야)가 Map에 없으므로 Map에 추가해준다.
// coffeeMap.put("이디야", new ArrayList<>())
// coffeMap.get("이디야").add(3200) 실행한 것과 같다.
coffeeMap.computeIfAbsent("이디야", coffee -> new ArrayList<>())
.add(3200);
System.out.println("coffeeMap = " + coffeeMap);
}
// 결과
coffeeMap = {빽다방=[], 메가커피=[2000], 스타벅스=[], 이디야=[3200]}
computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
Key에 해당되는 값이 Map에 존재하며 Value 값이 null이 아닌 경우에만 새로운 value 값으로 변경하는 메서드입니다.
@DisplayName("Map - 계산 관련 메서드3")
@Test
void map_computeIfPresent1() {
Map<String, List<Integer>> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", List.of(1000));
coffeeMap.put("스타벅스", null);
coffeeMap.put("빽다방", new ArrayList<>());
List<Integer> addList = new ArrayList<>();
addList.add(2000);
addList.add(3000);
// ✨ 메가커피의 value가 null이 아니므로 value 값은 addList로 변경된다.
coffeeMap.computeIfPresent("메가커피", (coffee, price) -> addList);
// ❗ 스타벅스의 value가 null이므로 해당 메서드는 실행되지 않는다.
coffeeMap.computeIfPresent("스타벅스", (coffee, price) -> addList);
System.out.println("coffeeMap = " + coffeeMap);
}
// 결과
coffeeMap = {빽다방=[], 메가커피=[2000, 3000], 스타벅스=null}
compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
'computeIfPresent'와는 달리 Key에 해당되는 값이 Map에 존재할 경우 value의 값이 null이어도 실행됩니다. 즉, value값이 null이든 아니든 상관없이 실행되는 메서드입니다.
@DisplayName("Map - 계산 관련 메서드4")
@Test
void map_compute1() {
Map<String, List<Integer>> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", new ArrayList<>());
coffeeMap.put("스타벅스", null);
coffeeMap.put("빽다방", new ArrayList<>());
List<Integer> addList = new ArrayList<>();
addList.add(2000);
addList.add(3000);
coffeeMap.compute("메가커피", (coffee, price) -> addList);
// ✨ value가 null이어도 실행 된다.
coffeeMap.compute("스타벅스", (coffee, price) -> addList);
System.out.println("coffeeMap = " + coffeeMap);
}
// 결과
coffeeMap = {빽다방=[], 메가커피=[2000, 3000], 스타벅스=[2000, 3000]}
3-5. 제거 메서드
remove(key)
remove(key, value)
Map 역시 remove 메서드를 통해 해당되는 값을 제거할 수 있습니다. 'remove(key)' 메서드는 Key 값이 동일할 경우 제거하는 메서드이고, 'remove(key, value)' 메서드는 Key와 Value 값이 모두 일치하는 경우 제거하는 메서드입니다.
@DisplayName("Map - remove")
@Test
void map_remove() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2100);
// ✨ 메가커피 key가 존재하므로 제거
coffeeMap.remove("메가커피");
// ✨ 스타벅스 key가 존재하고, value 값이 동일하므로 제거
coffeeMap.remove("스타벅스", 3500);
// ❗ 빽다방 key가 존재하지만 value 값이 동일하지 않으므로 제거되지 않는다.
coffeeMap.remove("빽다방", 2200);
System.out.println("coffeeMap = " + coffeeMap);
}
// 결과
coffeeMap = {빽다방=2100}
3-6. replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
List.replcateAll과 비슷한 메서드로 Map의 모든 Entry에 대해 주어진 람다식을 실행하고, 각 Entry의 값을 새로운 값으로 대체합니다.
@DisplayName("Map - replaceAll")
@Test
void map_replaceAll() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2100);
coffeeMap.replaceAll((coffee, price) -> price + 500);
System.out.println("coffeeMap = " + coffeeMap);
}
// 결과
coffeeMap = {빽다방=2600, 메가커피=2500, 스타벅스=4000}
3-7.
replace(K key, V value)
replace(K key, V oldValue, V newValue)
Key에 해당되는 값이 Map에 존재할 경우 Value로 변경합니다. 'replace(K key, V oldValue, V newValue)'의 경우 Key와 oldValue 값이 모두 일치할 경우 newValue 값으로 변경합니다.
@DisplayName("Map - replace")
@Test
void map_replace() {
Map<String, Integer> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", 2000);
coffeeMap.put("스타벅스", 3500);
coffeeMap.put("빽다방", 2100);
// ✨ 메가커피인 key를 찾아 value를 2500으로 변경
coffeeMap.replace("메가커피", 2500);
System.out.println("coffeeMap = " + coffeeMap);
// ❗ 스타벅스인 key를 찾아 value가 3000인 경우 4000으로 변경
// oldValue가 3000이기 때문에 실행되지 않는다.
coffeeMap.replace("스타벅스", 3000, 4000);
System.out.println("coffeeMap = " + coffeeMap);
// ✨ 스타벅스인 key를 찾아 value가 3000인 경우 4000으로 변경
// value가 3500이기 때문에 실행된다.
coffeeMap.replace("스타벅스", 3500, 4000);
System.out.println("coffeeMap = " + coffeeMap);
}
// 결과
coffeeMap = {빽다방=2100, 메가커피=2500, 스타벅스=3500}
coffeeMap = {빽다방=2100, 메가커피=2500, 스타벅스=3500}
coffeeMap = {빽다방=2100, 메가커피=2500, 스타벅스=4000}
3-8. merge(K key, V value, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
'merge' 메서드는 2개의 Map을 합칠 때, 중복된 Key가 있을 경우 처리하는 메서드입니다.
@DisplayName("Map - merge")
@Test
void map_merge() {
Map<String, List<Integer>> coffeeMap = new HashMap<>();
coffeeMap.put("메가커피", new ArrayList<>());
coffeeMap.get("메가커피").add(2000);
coffeeMap.put("스타벅스", new ArrayList<>());
coffeeMap.get("스타벅스").add(3500);
coffeeMap.put("빽다방", new ArrayList<>());
coffeeMap.get("빽다방").add(2100);
Map<String, List<Integer>> coffeeMap2 = new HashMap<>();
coffeeMap2.put("메가커피", new ArrayList<>());
coffeeMap2.get("메가커피").add(3000);
coffeeMap2.put("스타벅스", new ArrayList<>());
coffeeMap2.get("스타벅스").add(4500);
coffeeMap.forEach((k, v) -> {
// ✨ 중복된 Key가 있을경우 처리
coffeeMap2.merge(k, v, (price1, price2) -> {
System.out.println("price1 = " + price1);
System.out.println("price2 = " + price2);
price1.addAll(price2);
return price1;
});
});
System.out.println("coffeeMap2 = " + coffeeMap2);
}
// 결과
price1 = [3000]
price2 = [2000]
price1 = [4500]
price2 = [3500]
coffeeMap2 = {빽다방=[2100], 메가커피=[3000, 2000], 스타벅스=[4500, 3500]}
'Backend > Java' 카테고리의 다른 글
[Java] 자바 8 - 올바른 Optional 사용법 (0) | 2023.08.13 |
---|---|
[Java] 자바 8 - Optional 사용법 (1) | 2023.07.31 |
[Java] 자바 8 - (3) Stream : Collectors (0) | 2023.07.24 |
[Java] 자바 8 - (2) Stream : 데이터 처리 연산 (0) | 2023.07.23 |
[Java] 자바 8 - (1) Stream : 람다 표현식의 등장 배경 (0) | 2023.07.23 |