woowacourse-study / 2022-modern-java-in-action

우아한테크코스 4기 모던 자바 인 액션 스터디
10 stars 4 forks source link

내장 Collector의 Characteristics 파악하기 #50

Open bcc0830 opened 2 years ago

bcc0830 commented 2 years ago

문제

실질적으로 미션에 필요한 toList 같은 Collector는 어떤 Characteristic의 가지는가?

선정 배경

단순히 Enum 형태의 Characteristic가 있다고만 언급하고 비중이 크지 않았기 때문에 기본 내장 Collect의 Characteristics는 어떻게 되어 있는지 궁금해졌기 때문

관련 챕터

bcc0830 commented 2 years ago

concurrent와 unordered의 차이

그렇다면 둘의 관계는..?

public final class Collectors {
    static final Set<Collector.Characteristics> CH_CONCURRENT_ID
                = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
                                                         Collector.Characteristics.UNORDERED,
                                                         Collector.Characteristics.IDENTITY_FINISH));

    static final Set<Collector.Characteristics> CH_CONCURRENT_NOID
                = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
                                                         Collector.Characteristics.UNORDERED));

    static final Set<Collector.Characteristics> CH_ID
                = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));

    static final Set<Collector.Characteristics> CH_UNORDERED_ID
                = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED,
                                                         Collector.Characteristics.IDENTITY_FINISH));

    static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();

    static final Set<Collector.Characteristics> CH_UNORDERED_NOID
                = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED));

....
}

대표적인 Collector의 characteristics는?

public static <T, C extends Collection<T>>
    Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
        return new CollectorImpl<>(collectionFactory, Collection<T>::add,
                                   (r1, r2) -> { r1.addAll(r2); return r1; },
                                   CH_ID);
    }
  1. Collection 타입으로 collect 해주는 Collector이다. 이 때, 단순히 자기자신으로만 매핑하는 Characteristic을 가지는데, 왜 이런 특성을 가지는지 생각해보면, List 계열의 경우는 절대로 UNORDERED나 CONCURRENT를 가질 수 없기 때문이다. 또한 순서가 큰 의미가 없는 Map이나 Set도 비효율적이긴 하지만 순서대로 보장해도 손해가 없기 때문에 큰 문제가 되지 않는다.
public static <T>
    Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }
  1. List 타입으로 collect 해주는 Collector이다. 앞서 본 바와 같이 자기자신으로만 매핑하는 특성만 있다.
public static <T>
    Collector<T, ?, List<T>> toUnmodifiableList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   list -> (List<T>)List.of(list.toArray()),
                                   CH_NOID);
    }
  1. List 타입으로 collect 해주는 Collector이다. toList와의 차이는 NOID라는 점, 즉 자기자신으로 매핑하지 않는다는 것이다. 이러한 차이는 finisher의 유무로 나뉘는 데, toList의 경우는 Supplier에서 생성한 리스트를 바로 반환하여 별도의 finisher가 필요없지만, toUnmodifiableList의 경우는 마지막에 새로운 리스트로 리턴해주기 때문에(방어적 복사 용도) finisher가 존재한다. 따라서 원래 만들어진 Supplier의 리스트와 최종적으로 다른 리스트이기 때문에 자기 자신으로의 매핑이 아니게 된다.
public static Collector<CharSequence, ?, String> joining() {
        return new CollectorImpl<CharSequence, StringBuilder, String>(
                StringBuilder::new, StringBuilder::append,
                (r1, r2) -> { r1.append(r2); return r1; },
                StringBuilder::toString, CH_NOID);
    }
  1. CharSequence 타입으로 collect 해주는 Collector이다. 앞서 본 toUnmodifiableList와 같이 NOID인데, 당연히 Delimiter를 붙여주기 때문에 이전의 문자가 아니게 됨은 자명하기 때문에 NOID를 가질 수 밖에 없다.

public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
        return new CollectorImpl<>(HashMap::new,
                                   uniqKeysMapAccumulator(keyMapper, valueMapper),
                                   uniqKeysMapMerger(),
                                   CH_ID);
    }
  1. Map 타입으로 collect 해주는 Collector이다. 개인적으로 위에서 설명한 toCollection에서의 List,Map,Set 중 어떤게 될지 모르기에 REMOVE나 CONCURRENT를 하지 않았다고 생각했다. 따라서, toMap을 단독으로 쓰는 상황, 더구나 TreeMap도 아닌 HashMap으로 Supply를 하는데 왜 REMOVE가 아닌지 의아했다
public static <T, K, U>
    Collector<T, ?, ConcurrentMap<K,U>> toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                                                        Function<? super T, ? extends U> valueMapper) {
        return new CollectorImpl<>(ConcurrentHashMap::new,
                                   uniqKeysMapAccumulator(keyMapper, valueMapper),
                                   uniqKeysMapMerger(),
                                   CH_CONCURRENT_ID);
    }
  1. Map 타입으로 collect 해주는 Collector이다. 5번과의 차이는 동시성을 지원하지 않느냐, 지원하냐의 차이만 존재한다.
public static <T>
    Collector<T, ?, Set<T>> toSet() {
        return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
                                   (left, right) -> {
                                       if (left.size() < right.size()) {
                                           right.addAll(left); return right;
                                       } else {
                                           left.addAll(right); return left;
                                       }
                                   },
                                   CH_UNORDERED_ID);
    }
  1. Set 타입으로 collect 해주는 Collector이다. 정확히 5번 (toMap)에서의 의아했던 부분의 예시이다. 하나 더 의아했던 부분은 왜 ConcurrentMap이 있는데 ConcurrentSet이 없는가였다. 그 부분은 여기 에서 해소가 되었는데, 결론은 Set의 경우는 Map을 통해 유도가 가능하기 때문이다. 간단히, 모든 value들을 True로만 하면 되기 때문이다. 그런 논리라면 ConcurrentList까지도 확장이 된다고 생각한다. 왜냐면 단순히 value가 자기자신이면 간단히 유도될 수 있기 때문이다. 실제로 그래서 ConcurrentList가 없는지는 의문이다

결론

어쨌든 책의 한페이지 남짓한 부분을 파고 들어보았는데, 예상처럼 맞는 부분도 있었지만 직관스럽지 않은 부분도 존재했다. 그러나 스트림 자체가 원본 컬렉션의 순서를 유지하는 것이 디폴트 이기 때문에 순서를 유지하지 않아도 의미적으로도 문제가 없으며 실제로 성능도 나아지는 부분에서는 유연성을 발휘하기 위해 이와 같은 특성을 추가한 것으로 보인다.