본문 바로가기

Java

Map 인터페이스의 유용한 메서드들

Map에 대해 나름 잘 알고있다고 생각했었다.
우테코에서 이런저런 미션을 해나가면서, Map에 대해 잘 모르고있었다는 걸 깨달았다.

Map을 잘 알고, 잘 사용할 수 있도록, 몰랐던 메소드들과 그 내용에 대해 정리하고 학습하고자 한다.

put

put은 Map에 삽입해주는 메소드이다.
Map에서는 key값의 중복이 허용되지 않는다.
중복된 key 값을 put할 경우 값이 대체된다.

public void example1() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 1);
    map.put("포도", 1);
    map.put("사과", 2);

    System.out.println(map);
}

{포도=1, 사과=2}


putIfAbsent, computeIfAbsent

Absent는 부재를 뜻하는 단어이다.
putIfAbsent와 computeIfAbsent 두 메소드는 Map에 해당하는 Key값이 발견되지 않을 때, Value를 넣어주는 메소드이다.

putIfAbsent

첫 번째 인자에는 key를
두 번째 인자에는 value를 넣어주면
Map에 key가 존재하지 않을때만 value값을 넣어준다.

public void example2() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 1);
    map.put("포도", 1);

    map.putIfAbsent("딸기", 1);
    map.putIfAbsent("포도", 3);

    System.out.println(map);
}

{포도=1, 사과=1, 딸기=1}

그냥 put 메소드였다면 대체 되었어야할 포도가
putIfAbsent 메소드는 이미 포도가 존재하기 때문에 대체되지 않았다.

다음과 같이 직접적인 value가 아닌 메소드를 통한 리턴값으로도 가능하다.

public void example2() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 1);
    map.put("포도", 1);

    map.putIfAbsent("딸기", one());
    map.putIfAbsent("포도", 3);

    System.out.println(map);
}

public int one() {
    return 1;
}

{포도=1, 사과=1, 딸기=1}

computeIfAbsent

첫 번째 파라미터에는 key를
두 번째 파라미터에는 함수형 인터페이스 Function을 넣어주면
Map에 key가 존재하지 않을때만 Fucntion을 연산한 결과를 넣어준다.

Function<T, R>은 T형의 하나의 인자를 받아서 R형으로 리턴해주는 함수형 인터페이스이다.

key 값이 과일 이름, value 값이 과일 이름의 길이 일때 다음과 활용할 수 있다.

public void example3() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 2);

    map.computeIfAbsent("파인애플", (key) -> key.length());
    map.computeIfAbsent("딸기", (key) -> key.length());

    System.out.println(map);
}

{사과=2, 파인애플=4, 딸기=2}

다음과 같이도 가능하다.

public void example3() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 2);

    Function<String, Integer> function = (key) -> key.length(); 
    map.computeIfAbsent("파인애플", function);
    map.computeIfAbsent("딸기", function);

    System.out.println(map);
}

{사과=2, 파인애플=4, 딸기=2}

공통점

  • putIfAbsent와 computeIfAbsent는 둘 다 Map에 key가 없을 때만 적용되는 메소드이다.

차이점

  • putIfAbsent는 value를 바로 가지지만 computeIfAbsent는 Function을 통한 계산된 value를 가진다.
  • putIfAbsent의 value에 메소드호출이 온다면 Map에 key값이 중복됐을 때, 값은 저장되지 않아도 메소드는 호출된다. -> 메소드 호출 비용이 비싸다면 성능이 저하된다.
  • computeIfAbsent의 Function은 key값이 중복됐을 때 실행되지 않고, key 값이 발견되지 않았을 때만 실행된다.

compute, computeIfPresent

computeIfAbsent가 key가 존재하지 않는 경우에 사용되는 메소드라면
computeIfPresent는 key가 존재하는 경우에 사용되는 메소드이다.

인자로 key와 BiFunction을 받는다.
BiFunction은 인자가 하나인 Function이 인자가 두 개인 버전이다.

public void example4() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 1);

    map.computeIfPresent("사과", (key, value) -> value + 1);
    map.computeIfPresent("딸기", (key, value) -> value + 1);

    System.out.println(map);
}

{사과=2}

BiFunction의 반환값을 value에 remapping한다.

사과라는 키가 존재하기 때문에 value에 1이 더해져있고
딸기라는 키는 존재하지 않으므로 결과에 아무런 변화가 없다.

computeIfAbsent에서 처럼 key가 존재하지 않는다면 BiFunction은 실행되지 않는다.

compute

compute 메소드는 computeIfPresent나 computeIfAbsent와 다르게
어떤 경우를 따지지 않고 무조건 실행된다.

computeIfPresent랑 똑같이 key와 BiFunction을 인자로 가진다.
BiFunction의 반환값을 value에 remapping된다.

public void example5() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 1);

    map.compute("사과", (key, value) -> value + 1);
    map.compute("딸기", (key, value) -> value + 1);

    System.out.println(map);
}

사과는 value가 2로 remapping 됐었겠지만,
위의 코드는 NPE(NullPointerException)가 발생한다.

딸기가 key로 존재하지 않아서 value에 null이 들어왔는데, + 1을 하려고 접근했기 때문이다.

무조건 실행되어서 value를 remapping하는 compute 메소드를 어디에 활용하는지는 잘 모르겠다.


merge

merge는 개인적으로 map에서 제일 유용한 메소드인 것 같다.

여태까지 5 ~ 6 라인으로 구현하던 것을 한 줄에 구현할 수 있게 해준다.

인자가 세 개 존재하는데,

  • 첫 번째 인자는 key를 받는다.
  • 두 번째 인자는 key가 존재하지 않을 경우 삽입될 value를 받는다.
  • 세 번째 인자는 key가 존재할 경우 remapping할 BiFunction을 받는다.

putIfAbsent와 computeIfPresent의 기능을 동시에 가진 메소드 같다.

즉,
첫 번째 인자인 key가 맵에서 존재하지 않으면 두 번째 인자와 함께 map에 삽입되고
key가 맵에서 존재하면 세 번째 인자인 BiFunction의 연산 결과를 value에 remapping한다.

Key가 맵에서 존재할 때 BiFunction의 연산 결과가 null이면 map에서 삭제된다.

public static void example7() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 1);

    map.merge("사과", 1, (value, putValue) -> value + 1);
    map.merge("딸기", 1, (value, putValue) -> value + 1);
    map.merge("포도", 1, (value, putValue) -> value + 1);
    map.merge("포도", 1, (value, putValue) -> map.get("수박"));

    System.out.println(map);
}

{사과=2, 딸기=1}

포도는 value 1로 삽입 되었다가 map.get("수박")이 null을 리턴하기 때문에 map에서 삭제됐다.

주의해야할 점은
computeIfPresent 나 compute는 BiFunction에서 파라미터로 key와 value값이 들어왔다면
merge는 BiFunction에서 파라미터로 key가 들어오지 않는다.
첫 번째 파라미터로 value값, 두 번째 파라미터로 merge의 두 번째 인자로 받은 key가 없을 때의 삽일할 value값이 들어온다.

등장 횟수를 Map을 통해 저장하는 로직을 작성할 때 매우 유용하다.
파라미터로 단어들을 받고, 단어의 등장횟수를 Map에 저장한다고 할 때 다음과 같이 간단하게 작성할 수 있다.

public static void example6(String[] words) {
    Map<String, Integer> map = new HashMap<>();
    for (String word : words) {
        map.merge(word, 1, (value, putValue) -> value + 1);
    }
}

getOrDefault

Map의 get메소드는 존재하지 않는 key값이면 null을 반환한다.
getOrDefault메소드는 key가 존재하면 value를 반환하고, key가 존재하지 않으면 지정한 Default값을 반환한다.

public static void example6() {
    Map<String, Integer> map = new HashMap<>();
    map.put("사과", 1);
    map.put("포도", 1);

    int count = map.getOrDefault("수박", 0);
    System.out.println(count);
}

출력 : 0

Map에 사과와 포도만 put했기 때문에
그냥 get메소드를 사용했다면 null이 반환됐을 것이다.

그리고 int count에 null을 넣는 과정에서 NPE가 발생했을 것이다.

하지만 getOrDefault를 사용하면 Map에서 null을 반환할 일 없이,
key가 존재하지 않으면 두 번째 인자로 지정해준 값을 반환한다.

null이 반환되면 NPE의 위험이 있기 때문에, 항상 get보다는 getOfDefault를 사용해서
null-safety하게 작성하는게 좋은 방법 같다.


Map에 이렇게 유용한 메소드들이 있는 줄 몰랐다.

알았더라면 내가 여태 작성한 Map관련 코드 라인 수의 반은 줄였을 텐데, 아쉽다.
앞으로는 자신감있게 Map을 사용할 수 있을 것 같다.

'Java' 카테고리의 다른 글

정규식 간단 정리  (0) 2020.05.11
하드코딩 대신 상수를 활용하자.  (0) 2020.05.06
null 반환보다는 Optional을 활용하자.  (5) 2020.04.10
Command Query Separation 원칙  (2) 2020.03.13
Exception 정리  (0) 2020.03.04