[교재 EffectiveJava] 아이템 47. 반환 타입으로는 스트림보다 컬렉션이 낫다

반응형
728x90
반응형

스트림 반환타입

스트림은 for-each 반복을 제공하지 않는다. API를 스트림만 반환하도록 짜놓으면 반환된 스트림을 for-each로 반복하길 원하는 사용자는 불편하다. Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함하고, Iterable 인터페이스가 정의한 방식대로 동작한다. 그럼에도 for-each로 스트림을 반복할 수 없는 까닭은 Stream이 Iterable을 확장하지 않아서다. 

 

Iterator
List<Integer> list = List.of(1,2,3);

Iterator<Integer> iterator = list.iterator();

while(iterator.hasNext()) {
	system.out.println(iterator.next());
}

스트림의 경우에는 내부에 Iterator 을 response 하는 메서드를 가지고있다. 

 

컴파일 오류 발생 코드
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {

}

 

스트림 반복을 위한 끔찍한 우회 방법
for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator) {

}

 

좀더 나은 방법인 어댑터 메서드 사용
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
    return stream::iterator;
}

어댑터 메서드 사용하여 반복처리

for (ProcessHandle ph : iterfableOf(ProcessHandle.allProcesses())) {

}

 

 

이전 예제 코드로 알아보기

반복버전 - 스캐너 사용
 File dictionary = new File(args[0]);
 
 try (Scanner s = new Scanner(dictionary)) {
     ....
 }

 

스트림 - Files.lines 메서드 사용
 File dictionary = new File(args[0]);
 
 try (Stream<String> words = Files.lines(dictionary)) {
     ....
 }

파일을 읽는동안 발생하는 모든 예외를 알아서 처리해준다는 점에서 Files.lines 쪽이 더 우수하다. 이는 스트림만 반환하는 API가 반환한 값을 for-each로 반복하길 원하는 프로그래머가 감수해야할 부분이다.

 

 

스트림 어댑터 메서드

API가 Iterable만 반환하면 이를 스트림 파이프라인에서 처리하려는 프로그래머가 불만을 가질 수 있다. 이를 어댑터로 구현해보자.

 

Iterable 은 스트림 변환 메서드를 제공하지 않기 때문에 StreamSupport.stream 메서드를 사용하여 구현해야한다.

public static <E> Stream<E> streamOf(Iterable<E> iterable) {
    return StreamSupport.stream(iterable.spliterator(), false);
}

 

StreamSupport.stream 메서드
public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
    Objects.requireNonNull(spliterator);
    return new ReferencePipeline.Head<>(spliterator,
                                        StreamOpFlag.fromCharacteristics(spliterator),
                                        parallel);
}

객체 시퀀스를 반환하는 메서드를 작성하는데, 이 메서드가 오직 스트림 파이프라인에서만 쓰일 걸 안다면 마음 놓고 스트림을 반환하게 해주자. 반대로 반환된 객체들이 반복문에서만 쓰일걸 안다면 Iterable을 반환하다.

 

하지만 공개 API를 작성할때는 스트림 파이프라인을 사용하는 사람과 반복문에서 쓰려는 사람 모두를 배려해야한다. Collection 인터페이스는 Iterable 의 하위타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 제공한다.

따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection 이나 그 하위 타입으로 쓰는게 최선이다.

Arrays 역시 Arrays.asList, Stream.of 메서드로 손쉽게 반복과 스트림을 지원할 수 있다. 반환하는 시퀀스의 크기가 메모리에 올려도 안전할만큼 작다면 ArrayList, HashSet과 같은 표준 컬렉션 구현체를 반환하는게 최선일 수 있다. 하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안된다.

 

반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는 방안을 검토해보자. 

 

 

전용 컬렉션 구현

멱집합을 구성하는 각 원소의 인덱스를 비트 벡터로 사용하고, 인덱스의 n 번째 비트 값은 멱집합의 해당 원소가 원래 집합의 n번째 원소를 포함하는지 여부를 알려주어 훌륭한 전용 컬렉션을 구현해보자.

 

{a, b, c}의 멱집합 : {{a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}}
package com.java.effective.item47;

import java.util.*;

public class PowerSet {
    public static final <E> Collection<Set<E>> of(Set<E> s) {
        List<E> src = new ArrayList<>(s);
        if (src.size() > 30)
            throw new IllegalArgumentException(
                    "집합에 원소가 너무 많습니다(최대 30개).: " + s);
        return new AbstractList<Set<E>>() {
            @Override
            public int size() {
                // 멱집합의 크기는 2를 원래 집합의 원소 수만큼 거듭제곱 것과 같다.
                return 1 << src.size();
            }

            @Override
            public boolean contains(Object o) {
                return o instanceof Set && src.containsAll((Set) o);
            }

            @Override
            public Set<E> get(int index) {
                Set<E> result = new HashSet<>();
                for (int i = 0; index != 0; i++, index >>= 1)
                    if ((index & 1) == 1)
                        result.add(src.get(i));
                return result;
            }
        };
    }

}

AbstractCollection을 활용해서 Collection 구현체를 작성할때는 Iterable용 메서드 외에 2개만 더 구현하면 된다.

1) contains
2) size

만약 여기서 반복이 시작되기 전에는 시퀀스의 내용을 확정할 수 없는 등의 사유로 contains, size를 구현하는게 불가능할때는 컬렉션보다는 스트림이나 Iterable 을 반환하는 편이 낫다. 

 

 

리스트의 부분 리스트를 반환하는 메서드 구현

1) 첫번째 원소를 포함하는 부분리스트 : 그 리스트의 prefix

(a, b, c)의 프리픽스 : (a), (a,b) (a,b,c)

 

2) 마지막 원소를 포함하는 부분 리스트 : 그 리스트의 suffix

(a, b, c)의 서픽스 : (a,b,c) (b,c) (c) 

 

package com.java.effective.item47;

import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class SubLists {
    public static <E> Stream<List<E>> of(List<E> list) {
        // Stream.concat : 반환되는 스트림에 빈 리스트를 추가
        return Stream.concat(Stream.of(Collections.emptyList()),
                // flatMap : 모든 프리픽스의 모든 서픽스로 구성된 하나의 스트림을 만든다.
                prefixes(list).flatMap(SubLists::suffixes));
    }

    /**
     for (int start = 0; start < src.size(); start++) {
        for (int end = start + 1; end <= src.size(); end++ {
            System.out.println(src.subList(start, end));
        }
     }

     * @param list
     * @param <E>
     * @return
     */
    private static <E> Stream<List<E>> prefixes(List<E> list) {
        // rangeClosed 가 반환하는 연속된 정수값들을 매핑
        return IntStream.rangeClosed(1, list.size())
                .mapToObj(end -> list.subList(0, end));
    }

    private static <E> Stream<List<E>> suffixes(List<E> list) {
        // range 가 반환하는 연속된 정수값들을 매핑
        return IntStream.range(0, list.size())
                .mapToObj(start -> list.subList(start, list.size()));
    }
}

prefixes, suffixes 메서드는 아래 for 반복문을 중첩해 만든 것과 취지가 비슷하다.

for (int start = 0; start < src.size(); start++) {
    for (int end = start + 1; end <= src.size(); end++ {
        System.out.println(src.subList(start, end));
    }
 }

이를 스트림으로 변환해보자.

public static<E> Stream<List<E>> of(List<E> list) {
    return IntStream.range(0, list.size())
            .mapToObj(start -> IntStream.rangeClosed(start + 1, list.size())
                                    .mapToObj(end -> list.subList(start, end))).
            flatMap(x -> x);
}

 

 

정리

원소 시퀀스를 반환하는 메서드를 반환할때는, 이를 스트림으로 처리하기를 원하는 사용자와 반복을 처리하길 원하는 사용자 모두를 배려해야한다. 컬렉션을 반환할 수 있다면, 컬렉션을 반환하자. 반환 전부터 이미 원소들을 컬렉션에 담아 관리하고있거나, 컬렉션을 하나 더 만들어도 될 정도로 원소 개수가 적다면 ArrayList 와 같은 표준 컬렉션에 담아 반환하자. 그렇지 않으면 위 '전용 컬렉션 구현' 단원의 멱집합 예제코드처럼 전용 컬렉션을 구현할지 고민하자. 컬렉션을 반환하는게 불가능하다면 스트림과 Iterable 중 더 자연스러운 것을 반환하자. 추후 Stream 인터페이스가 Iterable 을 지원하도록 자바가 업데이트된다면 그때 안심하고 스트림을 반환하자.

 

 

 

반응형

Designed by JB FACTORY