Object.requireNonNull(), Map - getOrDefault (), compute(), computeIfAbsent()

3 분 소요

// 개선 전
// friend는 [["json", "john"], ["john", "mike"]...] 이런 데이터가 담긴 List이다.
private static Map<String, List<String>> makeFriendRelation(List<List<String>> friends) {
    HashMap<String, List<String>>friendsRelation = new HashMap<>();
    for (List<String> friendPair:friends) {
        Objects.requireNonNull(friendsRelation.put(friendPair.get(FRIEND_A),new ArrayList<>()))
        .add(friendPair.get(FRIEND_B));
        Objects.requireNonNull(friendsRelation.put(friendPair.get(FRIEND_B),new ArrayList<>()))
        .add(friendPair.get(FRIEND_A));
    }
    return friendsRelation;
}

makeFriendRelation() 메서드는 friends 리스트에 담긴 친구관계를 Map<String, List<String>> 타입으로 담아서 반환하려는 목적으로 만든 메서드이다. friends의 요소 friendPair에서 두 친구를 꺼내어 Map에 양방향으로 저장하는 로직을 생각하고 Objects.requireNonNull()를 사용했는데 내가 생각한 동작과 전혀 다른 메서드였다.


Objects.requireNonNull()

Java의 Objects.requireNonNull 메서드는 객체 참조가 null인지 확인하는데 사용된다. 이 메서드는 주어진 객체가 null이 아닌 경우 그대로 반환하며, 만약 null인 경우에는 NullPointerException을 발생시킨다.

  • Objects.requireNonNull(T obj): 이 형태의 메서드는 인자로 전달된 객체가 null인 경우 NullPointerException을 발생시킨다.
  • Objects.requireNonNull(T obj, String message): 이 형태의 메서드는 인자로 전달된 객체가 null인 경우 지정된 에러 메시지와 함께 NullPointerException을 발생시킨다.

Objects.requireNonNull은 일반적으로 입력 인수를 검증하는데 사용되며, 이를 통해 개발자들은 런타임에 적절한 예외 처리를 할 수 있다.

public void setName(String name) {
    this.name=Objects.requireNonNull(name,"Name cannot be null");
}

위 코드에서 setName 함수는 name 매개변수가 null일 때 “Name cannot be null”라는 메세지와 함께 NullPointerException을 발생시킨다.

다시 본론으로 돌아와서 내가 원한 동작은 Map인터페이스에서 제공하는 getOrDefault()를 사용하여 간단하게 구현할 수 있었다.


getOrDefault()

friendsRelation에서 getOrDefault(Object key, V defaultValue)를 사용하면 key로 해당 Map을 검색하여 key가 존재하지 않을 경우 defaultValue를 반환한다. 만약 해당 key가 존재한다면 keyvalue를 반환한다. 따라서 getOrDefault()를 사용하면 키가 없을 때 null 대신 원하는 기본값을 받을 수 있어서 NullPointerException을 예방할 수 있다.

private static Map<String, List<String>> makeFriendRelation(List<List<String>> friends) {
    HashMap<String, List<String>> friendsRelation = new HashMap<>();
    for (List<String> friendPair : friends) {
        String friendA=friendPair.get(FRIEND_A);
        String friendB=friendPair.get(FRIEND_B);
        // friendsRelation에 friendA가 없다면 new ArrayList<>()를 반환.
        // friendA가 있다면 해당 value를 반환.
        List<String> listA=friendsRelation.getOrDefault(friendA,new ArrayList<>());
        List<String> listB=friendsRelation.getOrDefault(friendB,new ArrayList<>());
        listA.add(friendB);
        listB.add(friendA);
        friendsRelation.put(friendA,listA);
        friendsRelation.put(friendB,listB);
    }
    return friendsRelation;
}

주의할 점은 getOrDefault() 메서드의 key가 존재하지 않을 경우 해당 키에 defaultValue를 저장하는 건 아니라는 점이다.

getOrDefault()는 단순히 주어진 키에 대응하는 값을 찾거나 없다면 기본값을 반환할 뿐, 키가 없을 때 해당 키에 기본값을 저장할 수는 없었다. 하지만 Map 인터페이스에 compute(), computeIfAbsent()Map 내의 특정 키에 대한 값을 계산하고 저장할 수 있다.


compute()

Map<String, Integer> map = new HashMap<>();
map.put("Apple", 10);
map.compute("Apple", (k, v) -> v == null?42:v+5);
// "Apple" 키의 현재 값은 10이므로 새로운 값은 15가 된다.
map.compute("Banana", (k, v) -> v == null?42:v+5);
// "Banana" 키는 현재 map에 없으므로 새로운 "Banana" 키에 42가 map에 저장된다.
map.compute("Apple", (k, v) -> v < 10 ? v : null);
// "Apple" 키의 현재 값은 15이므로 15 < 10 == false가 되고 
// 3항 연산자 결과가 null이 되어 "Apple" 키와 그 value인 15 가 map에서 삭제된다.

compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)

이 메서드는 주어진 키와 현재 매핑된 값을 사용해 새로운 값을 계산하고 저장한다. 만약 remappingFunctionnull을 반환하면 해당 키-값 쌍은 Map에서 제거된다. 이 메서드 역시 최종적으로 계산된 값을 반환한다.

private static void computeScore(Map<String, Integer> recommendFriends, String target, int x) {
    // compute()를 사용하여 recommendFriends 맵에서 target을 key로 찾는다.
    // remappingFunction을 (key, value) -> value == null ? x : value + x 로 하여
    // key target의 value를 얻고 그 value가 null이라면 x를 저장,
    // null이 아니라면 저장되어있던 value에 x를 더하여 저장한다.
    recommendFriends.compute(target, (key, value) -> value == null ? x : value + x);
}

compute()를 사용하여 Map에 키가 존재할 때 연산과, 존재하지 않을 때 기본값을 바로 저장할 수 있었다.


computeIfAbsent()

Map<String, Integer> map = new HashMap<>();
Integer value = map.computeIfAbsent("Apple", k -> 10);
// "Apple" 키가 없으므로 10을 계산하여 저장하고 반환합니다.
map.computeIfAbsent("Apple", k -> 20);
// "Apple" 키가 10으로 저장되어있어서 null이 아니기 때문에 computeIfAbsent()가 수행되지 않는다.
// 결과적으로 "Apple"의 키로 10이 저장되어있다.
map.put("Banana", null); // "Banana" 키로 null값이 저장되었다.
map.computeIfAbsent("Banana", k -> 30);
// "Banana" 키는 존재하지만, 값이 null인 상태이므로 computeIfAbsent() 메서드가 수행된다.
// 결과적으로 "Banana" 키에 값이 30으로 저장된다.

computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)

이 메서드는 주어진 키가 Map에 존재하지 않거나 그 값이 null인 경우에만 mappingFunction을 사용해 값을 계산하고 저장한다. 이미 값이 있는 경우에는 기존의 값을 그대로 반환한다. 이 메서드는 계산된 값(또는 기존의 값)을 반환한다.

이 메서드를 사용하여 위의 getOrDefault()를 사용하여 구현한 코드를 최적화했다.

private static Map<String, List<String>> makeFriendRelation(List<List<String>> friends) {
    HashMap<String, List<String>> friendsRelation = new HashMap<>();
    for (List<String> friendPair : friends) {
        String friendA = friendPair.get(FRIEND_A);
        String friendB = friendPair.get(FRIEND_B);
        
        // friendsRelation에 friendA 키가 없으면 new ArrayList<>()를 반환하고
        // add(friendB); 하여 빈 리스트에 friendB를 넣게된다.
        // friendsRelation에 friendA가 있다면 computeIfAbsent()는 수행되지 않고,
        // 기존 friendA 키에 들어있는 List에 .add(friendB);가 수행된다.
        friendsRelation.computeIfAbsent(friendA, key -> new ArrayList<>()).add(friendB);
        friendsRelation.computeIfAbsent(friendB, key -> new ArrayList<>()).add(friendA);
    
        // getOrDefault() 사용하여 구현했던 코드
        /**
         List<String> listA = friendsRelation.getOrDefault(friendA, new ArrayList<>());
         List<String> listB = friendsRelation.getOrDefault(friendB, new ArrayList<>());
         listA.add(friendB);
         listB.add(friendA);
         friendsRelation.put(friendA, listA);
         friendsRelation.put(friendB, listB);
         */
    }
    return friendsRelation;
}

getOrDefault()를 사용하여 friendA의 값이 null이면 빈 리스트를 받아서 friendB를 저장하고, 그 리스트를 firendsRelationput하는 과정을 computeIfAbsent()한 줄로 줄일 수 있었다.

댓글남기기