728x90

 

큰틀에서 챕터 8은 아래와 같은 내용을 공유한다.

  • 컬렉션 팩토리 사용하기
  • 리스트 및 집합과 사용할 새로운 관용 패턴 배우기
  • 맵과 사용할 새로운 관용 패턴 배우기

앞서, Modern Java in Action 에서는 챕터 8~10 까지는 Part 3으로 ‘스트림과 람다를 이용한 효과적 프로그래밍’ 으로 분류를 합니다.

책의 저자는 ‘컬랙션 API 가 없었다면 자바 개발자의 삶은 많이 외로웠을 것’이라고 표현합니다.

책에서 표현 하듯이, 거의 모든 자바 애플리케이션에서 컬렉션을 사용하고, 지금까지 컬렉션과 스트림 API를 이용해 데이터 처리 쿼리를 어떻게 효율적으로 처리할 수 있을지 살펴보려고 합니다.

컬렉션 API 에는 성가시고, 에러를 유발하는 여러 단점이 존재합니다.

챕터 8에서는 자바 8,9 에서 추가되어 우리의 삶을 편리하게 만들어 줄 새로운 컬렉션 API 의 기능을 배우게됩니다.

목차

  • 작은 리스트, 집합, 맵을 쉽게 만들수 있는 컬렉션 팩토리
  • 자바 9에서 새로 추가된 컬렉션 팩토리
  • 자바 8의 개선 사항으로 리스트와 집합에서 요소를 삭제 또는 바꾸는 관용 패턴 적용에 대해서 배운다.
  • 맵 작업과 관련해 추가된 새로운 편리 기능을 살펴본다.

컬렉션 팩토리

  • 왜? 그리고 새 팩토리 메서드의 사용 방법

자바에서는 적은 요소를 포함하는 리스트를 어떻게 만들까?

여러명이 함께하는 그룹을 만드려고 할때….

List<String> friends = new ArrayList<>();
friends.add("Raphael");
friends.add("Oliva");
friends.add("Thibaut");

위 처럼 세 문자열을 저장하는데도 많은 코드가 필요하다. 다음처럼 Arrays.asList()팩토리 메서드를 이용하면 코드를 간단하게 줄일 수 있다.

List<String>friends
		= Arrays.asList("Raphael", "Olibia", "Thibuat");
		

단점 : 고정 크기의 리스트를 만들었으므로 요소를 갱신할 순 있지만, 새 요소를 추가하거나 요소를 삭제할 순 없다.

예시) 요소를 갱신하는 작업은 괜찮지만 요소를 추가하려 하면 Unsupported OperationException 이 발생한다.

List<String> friends = Arrays.asList("Raphael","Olivia");
friends.set(0, "Richard");
friends.add("Thibaut");

Unsupported OperationException 예외 발생

내부적으로 고정된 크기의 변환할 수 있는 배열로 구현되었기 때문에 이와 같은 일이 일어난다.

그렇다면… 집합에선? 안타깝게도 Arrays.asSet() 이라는 팩토리 메서드는 없으므로 다른 방법이 필요하다.

리스트를 인수로 받는 HashSet 생성자를 사용할 수 있다.

Set<String> friends

= new HashSet<>(Arrays.asList(”Raphael”, “Olivia”, “Thibaut”);

또는 다음 처럼 스트림 API 를 사용할 수 있다.

Set<String> friends

= Stream.of(”Raphael”, “Olivia”, “Thibaut”)

          .collection(Collectors.toSet());

위의 두 방법 모두 매끄럽지 못하며 내부적으로 불필요한 객체 할당을 필요로 한다.

그리고 결과는 변환할 수 있는 집합이라는 사실도 주목하자.

그렇다면 맵은 어떨까?

작은 맵을 만들 수 있는 좋은 방법은 따로 없지만 걱정할 필요가 없다. 자바 9에서 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 팩토리 메서드를 제공하기 때문이다.

우선 리스트의 새로운 기능 부터 시작해보자.

  • 컬렉션 리터럴
  • 파이썬, 그루비 등을 포함한 일부 언어는 컬렉션 리터럴 즉 [42,1,5] 같은 특별한 문법을 이용해 컬렉션을 만들 수 있는 기능을 지원한다. 자바에서는 너무 큰 언어 변화와 관련된 비용이 든다는 이유로 이와 같은 기능을 지원하지 못했다. 자바 9에서는 대신 컬렉션 API를 개선했다.

리스트 팩토리

List.of 팩토리 메소드를 이용해서 간단하게 리스트를 만들 수 있다.

ex1)
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
System.out.println(friends); <-- [Raphael, Olivia, Thibaut]

만약 friends 리스트에 요소를 추가해보자.

ex2)
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
friends.add("Chih-Chun");

위 ex2 코드를 실행하면 java.lang.UnsupportedOperationException이 발생한다.

변경할 수 없는 리스트가 만들어졌기 때문에 set() 메서드로 아이템을 바꾸려해도 비슷한 예외가 발생한다.

따라서 set 메서드로도 리스트를 바꿀 수 없다. 하지만 이런 제약이 꼭 나쁜것은 아니다.

그 이유는, 컬렉션이 의도치 않게 변하는 것을 막을 수 있기 때문이다.

다만, 요소 자체가 변하는 것을 막을 수는 없다. 혹여 리스트를 바꿔야 하는 상황이라면 직접 리스트를 만들면 된다.

마지막으로 null 요소는 금지하므로 의도치 않은 버그를 방지하고 조금 더 간결한 내부 구현을 달성했다.

  • 다중 요소를 받지 않은 자바 API내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싼다. 따라서 배열을 할당하고 초기화하며 나중에 가비지 컬렉션을 하는 비용을 지불해야 한다. 고정된 숫자의 요소(최대 열개까지)를 API로 정의하므로 이런 비용을 제거할 수 있다. List.of로 열 개 이상의 요소를 가진 리스트를 만들 수 있지만 이 때는 가변 인수를 이용하는 메소드가 사용된다. Set.of와 Map.of에서도 이와 같은 패턴이 등장함을 확인할 수 있다.
  • static <E> List<E> of(E…. elements)

Collectors.toList() 컬렉터로 스트림을 리스트로 변환할 수 있다. 데이터 처리 형식을 설정하거나 데이터를 변환할 필요가 없다면 사용하기 간편한 팩토리 메서드를 이용할 것을 권장한다.

팩토리 메서드 구현이 더 단순하고 목적을 달성하는데 충분하기 때문이다.

집합 팩토리

  • List.of 와 비슷한 방법으로 바꿀 수 없는 집합을 만들 수 있다.
Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut"); 
System.out.println(friends); <-- [Raphael, Olivia, Thibaut]

중복된 요소를 제공해 집합을 만들려고 하면 Olivia 라는 요소가 중복되어 있다는 설명과 함께 IllegalArgumentException이 발생한다. 집합은 오직 고유의 요소만 포함할 수 있다는 원칙을 상기시킨다.

Set<String> friends = Set.of("Raphael", "Olivia", "Olivia");

맵 팩토리

맵을 만드는 것은 리스트나 집합을 만드는 것에 비해 조금 복잡한데 맵을 만들려면 키와 같이 있어야 하기 때문이다. 자바 9에서는 두 가지 방법으로 바꿀 수 없는 맵을 초기화 할 수 있다.

  1. Map.of 팩토리 메서드에 키와 값을 번갈아 제공하는 방법으로 맵을 생성
  2. Map<String, Integer> ageOfFriends = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26); System.out.println(ageOfFriends); <-- {Olivia=25, Raphael=30, Thibaue=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", 26);
System.out.println(ageOfFriends); <-- {Olivia=25, Raphael=30, Thibaut=26}

Map.entry 는 Map.Entry 객체를 만드는 새로운 팩토리 메서드이다.

리스트와 집합 처리

자바 8에서는 List, Set 인터페이스에 다음과 같은 메서드가 추가됨.

  • removeIf : 프레디케이트를 만족하는 요소를 제거한다. List 나 Set 을 구현하거나 그 구현을 상속 받은 모든 클래스에서 이용할 수 있다.
  • replaceAll : 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꾼다.
  • sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.

위와 같은 메서드는 호출한 컬렉션 자체를 바꾼다.

새로운 결과를 만드는 스트림 동작과 다르게 위 메서드는 기존 컬렉션을 바꾼다.

추가된 이유 : 컬렉션을 바꾸는 동작은 에러를 유발하며 복잡함을 더한다. 그러한 이유로 removeIf 와 replaceAll를 추가한 것이다.

removeIf 메서드란?

장점 : 명시적인 반복문을 사용하지 않고도 간단하게 요소를 제거할 수 있다는 점입니다.

또한 함수형 프로그래밍 스타일에 잘 맞는 기능이기도 합니다.

예시 코드)

import java.util.ArrayList;
import java.util.List;

public class RemoveIfExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(10);
        numbers.add(7);
        numbers.add(3);
        numbers.add(12);

        // 조건: 짝수인 경우 제거
        numbers.removeIf(n -> n % 2 == 0);

        System.out.println("조건을 만족하는 요소 제거 후 리스트: " + numbers);
    }
}

/*
위 예제에서 removeIf 메서드는 리스트 numbers에서 짝수인 요소들을 제거합니다. 
removeIf 메서드는 매개변수로 Predicate를 받습니다. 
여기서 n -> n % 2 == 0는 짝수를 나타내는 조건입니다. 
이 조건을 만족하는 요소들이 리스트에서 제거됩니다.
*/

removeIf 메서드는 특정 조건을 만족하는 요소들을 컬렉션에서 효율적으로 제거할 때 유용하게 사용될 수 있습니다.

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        // 리스트를 순회하면서 짝수를 제거하려는 코드
        for (Integer number : numbers) {
            if (number % 2 == 0) {
                numbers.remove(number); // ConcurrentModificationException 발생!
            }
        }

        System.out.println("제거 후 리스트: " + numbers);
    }
}

/***
위 코드는 리스트를 순회하면서 짝수를 제거하려고 시도하는 예제입니다.
하지만 이 코드는 ConcurrentModificationException을 발생시킵니다. 
이는 리스트를 순회하면서 동시에 리스트의 구조가 변경되기 때문에 발생합니다.
***/

해결 방법 :

1. Iterator를 사용한 방법

Iterator를 사용하면 컬렉션을 순회하면서 안전하게 요소를 제거할 수 있습니다. Iterator는 내부적으로 컬렉션의 상태를 체크하여 수정 중에 예외를 방지합니다.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ConcurrentModificationSolution {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        // Iterator를 사용하여 짝수를 제거하는 코드
        Iterator<Integer> iterator = numbers.iterator();
        while (iterator.hasNext()) {
            Integer number = iterator.next();
            if (number % 2 == 0) {
                iterator.remove(); // 안전하게 요소 제거
            }
        }

        System.out.println("제거 후 리스트: " + numbers);
    }
}

2. 새로운 리스트를 생성하여 요소를 복사하는 방법

원래의 리스트를 수정하지 않고 새로운 리스트를 생성하여 원하는 요소만 복사하는 방법입니다.

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationSolution {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        // 새로운 리스트를 생성하여 짝수를 제거하는 코드
        List<Integer> newList = new ArrayList<>();
        for (Integer number : numbers) {
            if (number % 2 != 0) {
                newList.add(number);
            }
        }

        System.out.println("제거 후 리스트: " + newList);
    }
}

다시 한번 요약 하자면….

ConcurrentModificationException은 Java에서 동시에 수정을 시도할 때 발생할 수 있는 예외입니다. 이를 해결하기 위해 Iterator를 사용하거나, 새로운 컬렉션을 생성하여 수정하는 방법을 사용할 수 있습니다. Iterator를 사용하는 방법은 수정 중에 안전하게 요소를 제거할 수 있는 장점이 있으며, 새로운 리스트를 생성하는 방법은 원래 리스트를 보존하면서 원하는 조건의 요소를 처리할 수 있습니다.

replaceAll 메서드란?

이 메서드는 리스트나 맵 등의 컬렉션 내의 모든 요소에 대해 주어진 함수(연산)를 적용하여 요소들을 변환하는 역할을 합니다.

replaceAll 메서드는 주어진 함수에 따라 모든 요소를 변환합니다. 주로 리스트에서 사용되며, 리스트 내의 모든 요소에 대해 특정 연산을 수행한 결과로 요소를 교체합니다.

예제 코드)

import java.util.ArrayList;
import java.util.List;

public class ReplaceAllExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        // 모든 요소를 제곱하여 교체
        numbers.replaceAll(n -> n * n);

        System.out.println("제곱 연산 후 리스트: " + numbers);
    }
}

추가적인 소스 설명 :

위 예제에서 replaceAll 메서드는 리스트 numbers의 모든 요소에 대해 n -> n * n 연산을 적용하여 제곱한 값을 각 요소로 대체합니다. 따라서 출력은 [1, 4, 9, 16, 25]가 됩니다.

주의사항

  • replaceAll 메서드는 함수형 인터페이스 UnaryOperator를 매개변수로 받습니다. 이 함수형 인터페이스는 하나의 입력을 받고 하나의 결과를 반환하는 함수입니다.
  • replaceAll 메서드는 기존 컬렉션의 구조를 변경하지 않고 각 요소를 변환하여 새로운 요소로 교체합니다.
  • 리스트나 맵과 같은 수정 가능한 컬렉션에서만 사용할 수 있습니다.

예외 사례

replaceAll 메서드는 컬렉션을 수정하는 방법으로 사용될 수 있으며, 이 과정에서 ConcurrentModificationException과 같은 문제가 발생할 수 있습니다. 이는 동시에 다른 스레드에서 컬렉션을 수정하려고 할 때 발생할 수 있는 예외입니다. 따라서 멀티 스레드 환경에서는 동기화에 주의해야 합니다.

맵 처리

자바 8에서는 Map 인터페이스에 몇 가지 디폴트 메서드를 추가 했다.

자주 사용되는 패턴을 개발자가 직접 구현할 필요가 없도록 이들 메서드를 추가한 것이다. forEach 를 시작으로 다양한 메서드를 살펴보자.

forEach 메서드

forEach 메서드는 주어진 함수(Consumer)를 각 요소에 적용하여 컬렉션을 순회하고 작업을 수행합니다.

주로 리스트나 맵과 같은 컬렉션에서 사용된다. 또한 맵에서 키 와 값을 반복하면서 확인하는 작업은 잘 알려진 귀찮은 작업 중 하나이고 실제로는 Map.Entry<K,V>의 반복자를 이용해 맵의 항목 집합을 반복할 수 있다.

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

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

ageOfFriends.forEach((friend,age) 
-> System.out.println(friend + " is " + age + "years old"));

정렬은 반복과 관련한 오래된 고민거리다. 자바 8에서는 맵의 항목을 쉽게 비교할 수 있는 몇가지 방법을 제공한다.

주의사항

  • forEach 메서드는 함수형 인터페이스 Consumer를 매개변수로 받습니다. Consumer는 void 반환형을 가지며 입력을 받아 작업을 수행하는 함수형 인터페이스입니다.
  • forEach 메서드는 순서가 있는 컬렉션(리스트 등)에서 주로 사용됩니다. 맵의 경우에는 forEach 메서드를 통해 키-값 쌍을 처리할 수 있습니다.
  • forEach 메서드를 사용하면 명시적인 반복문을 사용하지 않고도 간결하게 컬렉션을 순회하고 각 요소에 작업을 적용할 수 있습니다.
  • forEach 메서드는 컬렉션의 각 요소에 대해 작업을 수행하므로, 멀티 스레드 환경에서는 동기화에 주의해야 합니다.

예외 사례

forEach 메서드는 컬렉션을 순회하면서 요소를 변경하거나 추가하는 작업을 수행할 수 없습니다. 이런 수정 작업을 하려고 할 때 ConcurrentModificationException과 같은 문제가 발생할 수 있습니다. 따라서 forEach 메서드는 주로 요소를 읽고 처리하는 용도로 사용하는 것이 좋습니다.

정렬 메서드

Entry.comparingByValue()와 Entry.comparingByKey()는 각각 맵의 값과 키를 기준으로 Comparator를 생성하는 정적 메서드입니다. 그리고 이들 Comparator를 사용하면 맵의 엔트리를 값이나 키를 기준으로 정렬하거나 비교할 수 있습니다. 정렬된 결과는 보통 리스트에 저장하여 사용하며, 출력이나 추가적인 연산에 활용될 수 있습니다.

Entry.comparingByValue

Entry.comparingByValue 메서드는 맵의 값(value)을 기준으로 Map.Entry를 비교하는 Comparator를 생성합니다. 이 Comparator는 값에 따라 엔트리를 비교하고 정렬할 수 있습니다.

import java.util.*;
import java.util.Map.Entry;

public class EntryComparingByValueExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("Alice", 25);
        map.put("Bob", 30);
        map.put("Charlie", 20);

        // Map.Entry를 값(value) 기준으로 정렬하는 Comparator 생성
        Comparator<Entry<String, Integer>> byValue = Entry.comparingByValue();

        // 엔트리를 값(value) 기준으로 정렬하여 리스트에 저장
        List<Entry<String, Integer>> sortedEntries = new ArrayList<>(map.entrySet());
        sortedEntries.sort(byValue);

        // 정렬된 결과 출력
        System.out.println("값(value) 기준 정렬:");
        for (Entry<String, Integer> entry : sortedEntries) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

Entry.comparingByValue()를 사용하여 맵의 값으로 정렬하는 Comparator를 생성하고, 이를 이용하여 엔트리를 값 기준으로 정렬합니다.

Entry.comparingByKey

Entry.comparingByKey 메서드는 맵의 키(key)를 기준으로 Map.Entry를 비교하는 Comparator를 생성합니다. 이 Comparator는 키에 따라 엔트리를 비교하고 정렬할 수 있습니다.

import java.util.*;
import java.util.Map.Entry;

public class EntryComparingByKeyExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("Alice", 25);
        map.put("Bob", 30);
        map.put("Charlie", 20);

        // Map.Entry를 키(key) 기준으로 정렬하는 Comparator 생성
        Comparator<Entry<String, Integer>> byKey = Entry.comparingByKey();

        // 엔트리를 키(key) 기준으로 정렬하여 리스트에 저장
        List<Entry<String, Integer>> sortedEntries = new ArrayList<>(map.entrySet());
        sortedEntries.sort(byKey);

        // 정렬된 결과 출력
        System.out.println("키(key) 기준 정렬:");
        for (Entry<String, Integer> entry : sortedEntries) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

Entry.comparingByKey()를 사용하여 맵의 키로 정렬하는 Comparator를 생성하고, 이를 이용하여 엔트리를 키 기준으로 정렬합니다.

getOrDefault 메서드

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

getOrDefault 메서드를 이용하면 쉽게 이 문제를 해결할 수 있다. 이 메서드는 첫 번째 인수로 키를, 두번째 인수로 기본값을 받으며 맵에 키가 존재하지 않으면 두 번째 인수로 받은 기본값을 반환한다.

형식 : 
V getOrDefault(Object key, V defaultValue)
예제 코드 : 
import java.util.HashMap;
import java.util.Map;

public class GetOrDefaultExample {
    public static void main(String[] args) {
        Map<String, Integer> ageMap = new HashMap<>();

        // 맵에 몇 가지 값 추가
        ageMap.put("Alice", 25);
        ageMap.put("Bob", 30);

        // getOrDefault 메서드를 사용하여 값을 가져오기
        int aliceAge = ageMap.getOrDefault("Alice", 0);
        int charlieAge = ageMap.getOrDefault("Charlie", 0);

        System.out.println("Alice's age: " + aliceAge);   // 출력: Alice's age: 25
        System.out.println("Charlie's age: " + charlieAge); // 출력: Charlie's age: 0 (기본 값)
    }
}
****

설명

  • 위 예제에서 ageMap은 이름을 키로 하고 나이를 값으로 하는 맵입니다.
  • ageMap.getOrDefault("Alice", 0)을 호출하면 맵에서 "Alice"라는 키에 해당하는 값을 반환합니다. "Alice" 키가 맵에 있으므로 값 25를 반환합니다.
  • ageMap.getOrDefault("Charlie", 0)을 호출하면 맵에서 "Charlie"라는 키에 해당하는 값이 없으므로 기본 값으로 지정한 0을 반환합니다.

주의사항

  • getOrDefault 메서드는 맵에서 특정 키의 값이 없을 때만 기본 값을 반환합니다. 따라서 기본 값이 반환되는 경우에 대비하여 적절한 기본 값(defaultValue)을 설정하는 것이 중요합니다.
  • 기본 값으로 반환되는 값은 맵에 저장된 값과 동일한 타입이어야 합니다. 만약 맵의 값 타입이 Integer인 경우, defaultValue도 Integer 타입이어야 합니다.

요약

getOrDefault 메서드는 맵에서 지정된 키의 값을 가져오거나, 해당 키가 없을 경우 지정된 기본 값을 반환하는 유용한 메서드입니다. 이를 사용하여 특정 키의 존재 여부를 확인하고 기본 값으로 처리할 수 있습니다.

계산 패턴 -맵에 키 존재 여부에 따라 동작을 실행하고 결과를 저장해야 하는 상황이 필요할때가 있다.

예시) 키를 이용해 값 비싼 동작을 실행해서 얻은 결과를 캐시하려 한다.

키가 존재하면 결과를 다시 계산할 필요가 없다. 다음의 세 가지 연산이 어런 상황에서 도움을 준다.

  • computeIfAbsent : 제공된 키에 해당하는 값이 없으면(값이 없거나 널), 키를 이용해 새 값을 계산하고 맵에 추가한다.
  • computeIfPresent : 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다.
  • compute : 제공된 키로 새 값을 계산하고 맵에 저장한다.

기존에 이미 데이터를 처리했다면 이 값을 다시 계산할 필요가 없다.

V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
//key: 맵에서 계산하려는 키입니다.
//mappingFunction: 주어진 키로 값을 계산하는 함수입니다.
//예제 소스 
import java.util.HashMap;
import java.util.Map;

public class ComputeIfAbsentExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();

        // computeIfAbsent를 사용하여 값 추가
        map.computeIfAbsent("Alice", key -> 25);
        map.computeIfAbsent("Bob", key -> 30);

        System.out.println("맵의 값: " + map); // 출력: 맵의 값: {Alice=25, Bob=30}

        // 이미 존재하는 키에 대해 computeIfAbsent를 사용하면 아무런 변화가 없음
        map.computeIfAbsent("Alice", key -> 35);

        System.out.println("맵의 값: " + map); // 출력: 맵의 값: {Alice=25, Bob=30}
    }
}
//computeIfPresent
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
//key: 맵에서 계산하려는 키입니다.
//remappingFunction: 주어진 키와 현재 값으로 새로운 값을 계산하는 함수입니다.
//예제 소스 )
import java.util.HashMap;
import java.util.Map;

public class ComputeIfPresentExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("Alice", 25);
        map.put("Bob", 30);

        // computeIfPresent를 사용하여 값 수정
        map.computeIfPresent("Alice", (key, value) -> value + 1);

        System.out.println("맵의 값: " + map); // 출력: 맵의 값: {Alice=26, Bob=30}

        // 존재하지 않는 키에 대해서는 아무런 변화가 없음
        map.computeIfPresent("Charlie", (key, value) -> value + 1);

        System.out.println("맵의 값: " + map); // 출력: 맵의 값: {Alice=26, Bob=30}
    }
}

//compute
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
// key: 맵에서 계산하려는 키입니다.
// remappingFunction: 주어진 키와 현재 값(있을 경우)으로 새로운 값을 계산하는 함수입니다.

//예제 소스
import java.util.HashMap;
import java.util.Map;

public class ComputeExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("Alice", 25);
        map.put("Bob", 30);

        // compute를 사용하여 값 추가 또는 수정
        map.compute("Alice", (key, value) -> value + 1); // 기존 값에 1을 더함

        System.out.println("맵의 값: " + map); // 출력: 맵의 값: {Alice=26, Bob=30}

        // 존재하지 않는 키에 대해서는 새로운 값을 추가
        map.compute("Charlie", (key, value) -> value == null ? 1 : value + 1);

        System.out.println("맵의 값: " + map); // 출력: 맵의 값: {Alice=26, Bob=30, Charlie=1}
    }
}

추가된 패턴들

삭제 패턴 - 키가 특정한 값과 연관되었을 때만 항목을 제거하는 오버로드 버전 메서드를 제공한다.

교체 패턴 - 맵의 항목을 바꾸는 데 사용할 수 있는 두 개의 메서드가 맵에 추가됨.

  • replaceAll : BiFunction 을 적용한 결과로 각 항목의 값을 교체한다. 이 메서드는 이전에 살펴본 List의 replaceAll 과 비슷한 동작을 수행한다.
  • Replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전이 있다.
Map<String, String> favouritemovies = new HashMap<>();
favouritemovies.put("Raphael", "Start Wars");
favouritemovies.put("Olivia", "james bond");
favouritemovies.replaceAll((friend, movie) -> movie.toUpperCase());
System.out.println(favouriteMovies); <-- {Olivia=JAMES BOND, Raphael=STAR WARS}

합침 - 두 그룹의 연락처를 포함하는 두 개의 맵을 합친다면 putAll을 사용할 수 있다.

중복된 키가 없다면 코드는 잘 동작한다. 또한 값을 좀 더 유연하게 합쳐야 한다면 새로운 merge메서드를 이용할 수 있다. 이 메서드는 중복된 키를 어떻게 합칠지 결정하는 BiFunction을 인수로 받는다. family 와 friend 두 맵 모두에 “” 가 다른 영화 값으로 존재한다고 가정해보자.

forEach 와 merge 메서드를 이용해 충돌을 해결할 수 있다.

import java.util.HashMap;
import java.util.Map;

public class MergeExample {
    public static void main(String[] args) {
        // 첫 번째 맵
        Map<String, Integer> map1 = new HashMap<>();
        map1.put("Alice", 25);
        map1.put("Bob", 30);

        // 두 번째 맵
        Map<String, Integer> map2 = new HashMap<>();
        map2.put("Alice", 26); // 충돌 발생 예정
        map2.put("Charlie", 28);

        // map2의 각 항목을 map1에 병합하며 충돌 해결
        map2.forEach((key, value) -> map1.merge(key, value, (v1, v2) -> v1 + v2));

        // 결과 출력
        System.out.println("병합 후 맵의 값: " + map1);
    }
}

자바독에서 설명한 것처럼 merge 메서드는 널값과 관련된 복잡한 상황도 처리한다.

“지정된 키와 연관된 값이 없거나 값이 널이면[merge]는 키를 널이 아닌 값과 연결한다. 아니면 [merge]는 연결된 값을 주어진 매핑 함수의 [결과]값으로 대치하거나 결과가 널이면[항목]을 제거한다.

merge를 이용해 초기화 검사를 구현할 수 있다. 영화를 몇 회 시청했는지 기록하는 맵이 있다고 가정하자.

해당 값을 증가시키기 전에 관련 영화가 이미 맵에 존재하는지 확인해야된다.

Map<String, Long> movieToCount = 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+1L);

//코드 설명
//moviesToCount: 맵(Map) 객체입니다. 이 맵은 특정 영화 제목(movieName)을 키로 하고, 
//해당 영화가 등장한 횟수를 값으로 저장합니다.

//movieName: 맵에 추가하거나 업데이트할 영화의 이름입니다.

//1L: 기본값(default value)입니다. 이 값은 맵에서 주어진 키(movieName)가 존재하지 않을 경우 사용됩니다. 
//즉, 만약 movieName이라는 키가 맵에 없으면 이 값(1L)을 해당 키의 값으로 설정합니다.

//(count, increment) -> count + 1L: 충돌 시 수행할 병합 함수입니다.

//count: 현재 맵에 저장된 특정 키(movieName)의 값입니다.
//increment: 기본값(default value)으로 설정된 값입니다.
//병합 함수는 현재 값(count)과 기본값(increment)을 사용하여 새로운 값을 계산합니다. 
//여기서는 count + 1L을 통해 기존 값에 1을 더한 값을 반환합니다.

동작 설명

  • merge 메서드는 다음과 같은 동작을 수행합니다:
    • movieName이라는 키가 이미 moviesToCount 맵에 존재할 경우, 기존 값(count)과 기본값(increment)을 사용하여 병합 함수를 호출합니다.
    • 병합 함수는 기존 값(count)에 1을 더한 값을 반환하여 해당 키(movieName)의 값을 업데이트합니다.
    • movieName이라는 키가 moviesToCount 맵에 없을 경우, 기본값(1L)을 사용하여 새로운 항목을 맵에 추가합니다.
//예제 소스
import java.util.HashMap;
import java.util.Map;

public class MergeExample {
    public static void main(String[] args) {
        Map<String, Long> moviesToCount = new HashMap<>();

        // "Interstellar"이라는 영화가 맵에 없으면 추가하고 값은 1로 설정
        String movieName = "Interstellar";
        moviesToCount.merge(movieName, 1L, (count, increment) -> count + 1L);

        // 맵의 내용 출력
        System.out.println("맵의 값: " + moviesToCount); // 출력: 맵의 값: {Interstellar=1}

        // "Interstellar"이라는 영화가 이미 맵에 있으면 값을 1 증가시킴
        moviesToCount.merge(movieName, 1L, (count, increment) -> count + 1L);

        // 맵의 내용 출력
        System.out.println("맵의 값: " + moviesToCount); // 출력: 맵의 값: {Interstellar=2}
    }
}

개선된 ConcurrentHashMap

ConcurrentHashMap은 Java에서 제공하는 동시성(Concurrency)을 지원하는 해시 맵(HashMap) 구현체입니다. 이는 여러 스레드에서 동시에 맵을 안전하게 사용할 수 있도록 설계되었습니다. 기존의 Hashtable이나 동기화된 HashMap보다 훨씬 더 효율적이며, 동시성을 고려한 다양한 기능을 제공합니다.

  • 주요 특징
    • 동시성 지원: ConcurrentHashMap은 내부적으로 세분화된(segmented) 잠금(locking)을 사용하여 맵의 부분적인 영역만 잠그고, 이를 통해 여러 스레드가 동시에 맵을 안전하게 수정할 수 있습니다. 이로 인해 전체 맵을 잠그는 것보다 효율적인 동시성을 달성할 수 있습니다.
    • 확장 가능한 구조: ConcurrentHashMap은 내부적으로 세그먼트(segment)라는 작은 부분 맵들로 나뉘어져 있습니다. 이는 맵에 동시 접근할 때 세그먼트 단위로 잠금을 걸어서 동시성을 확보하며, 성능을 최적화하는 데 도움을 줍니다.
    • 비동기적 연산: ConcurrentHashMap은 일부 연산에서는 동기화된 블록을 사용하지 않고 비동기적으로 동작할 수 있습니다. 예를 들어 putIfAbsent, remove, replace 등의 연산은 잠금을 걸지 않고 동작할 수 있습니다.
    • Fail-safe iterator: ConcurrentHashMap의 반복자(iterator)는 맵이 수정되어도 예외를 던지지 않고, 반복 도중 수정 사항을 반영합니다. 이는 ConcurrentModificationException을 방지하기 위한 기능입니다.

리듀스(Reduce)

ConcurrentHashMap에서 리듀스 연산은 맵의 모든 요소를 조합하여 단일 결과를 생성하는 과정을 말합니다. 이는 여러 스레드에서 병렬로 수행될 수 있으며, 다양한 방법으로 사용될 수 있습니다. 대표적인 예시로는 reduce 메서드를 사용하여 맵의 값을 합산하거나 최댓값, 최솟값 등을 계산하는 작업이 있습니다.

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 맵에 여러 값 추가...

int sum = map.reduceValues(10, (value1, value2) -> value1 + value2); // 모든 값의 합을 계산
System.out.println("전체 합계: " + sum);
//위 코드에서 reduceValues 메서드는 모든 값을 합산하는 리듀스 연산을 수행합니다. 
//여기서 10은 병렬 실행 시 초기값으로 사용됩니다.

검색(Search)

ConcurrentHashMap에서의 검색은 특정 조건을 만족하는 요소를 찾는 작업을 말합니다. 이는 search, searchKeys, searchValues 메서드를 통해 수행될 수 있습니다. 이들 메서드는 각각 키, 값, 혹은 키-값 쌍에서 검색 작업을 수행합니다.

예를 들어, 특정 값이 존재하는지 확인하는 예제는 다음과 같습니다:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 맵에 여러 값 추가...

boolean exists = map.searchValues(10, value -> value > 50); // 값이 50보다 큰지 확인
if (exists) {
    System.out.println("값 50 초과의 요소가 존재합니다.");
} else {
    System.out.println("값 50 초과의 요소가 존재하지 않습니다.");
}

따라서..

ConcurrentHashMap은 여러 스레드에서 안전하게 사용할 수 있는 효율적인 해시 맵 구현체입니다. 동시성을 고려한 설계로 인해 멀티스레드 환경에서도 성능을 최적화할 수 있으며, 리듀스와 검색 등의 다양한 연산을 지원하여 유연하고 효율적인 데이터 처리를 가능하게 합니다.

728x90
복사했습니다!