반환 타입으로는 스트림보다 컬렉션이 낫다😵💫 스트림은 반복을 지원하지 않는다.재미난 사실? 🥸 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하자👀 Stream이 더 나을 때도 있다.🧑🏻⚖️ 결론
반환 타입으로는 스트림보다 컬렉션이 낫다
- 자바 7까지는 일련의 원소를 반환하는 메서드 반환 타입으로 Collection, Set, List 등의 컬렉션 인터페이스, Iterable, 배열을 사용해 왔다.
- 선택하는 우선순위
- 컬렉션 인터페이스
- for-each 문에서만 쓰이거나 반환된 원소 시퀀스가 일부 Collection 메서드를 구현할 수 없을 때는 Iterable
- 반환 원소들의 기본 타입이거나 성능에 민감한 상황이라면 배열
- 자바8 스트림이 등장하면서 이런 선택이(시퀀스의 반환타입을 정하는 것) 복잡해졌다.
😵💫 스트림은 반복을 지원하지 않는다.
- 아이템45에서 이야기했듯이 스트림은 반복을 지원하지 않는다. 따라서 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다.
- API를 스트림만 반환하도록 짜놓으면 반환된 스트림을 for-each로 반복하길 원하는 사용자는 당연히 불만을 토로할 것이다.
재미난 사실?
- 사실 Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, Iterable 인터페이스가 정의한 방식대로 동작한다.
- 그럼에도 for-each로 스트림을 반복할 수 없는 이유는 Stream이 Iterable을 확장하지 않기 때문이다.
// 자바 타입 추론의 한계로 컴파일 되지 않는다. Stream<String> names = Stream.of("김병연","김수미","김형욱","이연우","이용훈"); for (String name : names::iterator) { } // 이 오류를 잡으려면 메서드 참조를 매개변수화 된 Iterable로 적절히 형변환을 해야한다. for (String name : (Iterable<String>)names::iterator) { }


- 위의 수정된 코드는 동작은 하지만 실전에 쓰기에는 너무 난잡하고 직관성이 떨어진다.
- 다행히 어댑터 메서드를 사용하면 상황이 나아진다!
- 자바는 어댑터라는 메서드를 제공하지 않지만 쉽게 만들어낼 수 있다. 어댑터 메서드를 이용하면 타입 추론이 문맥을 잘 파악하여 어댑터 메서드 안에서 따로 형변환하지 않아도 된다.
- adapter pattern
- 한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.
- 어댑터를 사용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.
- 스트림 파이프 라인에서만 쓰인다면 > Stream 반환
- 반복문에서만 쓰인다면 > Iterable 반환
- 여러 상황을 고려하여 두가지 방법 모두 구현해야 한다.
- 사용자 대부분이 한 방식만 사용할 거라는 근거가 없기 때문이다.
public static<E> Iterable<E> iterableOf(Stream<E> stream) { return stream::iterator; } // 어댑터를 사용하면 어떤 스트림도 foreach 문으로 반복할 수 있다. for (String name : Adapters.iterableOf(names)) { ... } // Stream을 Iterable로 바꿔준 것처럼 반대도 구현해야한다. public static <E> Steram<E> streamOf(Iterable<E> iterable) { return StreamSupport.stream(iterable.spliterator(), flase); }
- 원소 시퀀스를 반환하는 공개 API 반환 타입에는 Collection이나 그 하위 타입을 쓰는게 일반적으로 최선이다.
- Arrays → Arrays.asList(Iterable), Stream.of(Stream)
🥸 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하자
- 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안된다.
- 반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는 방안을 검토해야 한다.
- 예를 들어 주어진 집합의 멱집합(한 집합의 모든 부분집합을 원소로 하는 집합)을 반환하는 상황에서 원소의 개수가 n개면 멱집합의 원소 개수는 2^n-1 개가 된다.
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 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; } @Override public int size() { return 1 << src.size(); } }; } }
- 집합 원소의 수가 30을 넘으면 예외를 던지도록 했는데, 이는 Stream 이나 Iterable 처럼 size 고려가 필요없는 것은 아니기 때문에 Collection을 쓸 때의 단점이다.
👀 Stream이 더 나을 때도 있다.
- AbstractCollection을 활용해서 Collection 구현체를 작성할 때는 contains와 size만 더 구현하면 된다.
- contains와 size를 구현하는게 불가능일 경우는 Stream이나 Iterable을 반환하는 편이 더 낫다.
// IntStream : int를 스트림으로 다룰 수 있도록 해준다 // rangeClosed(1,5) : 1,2,3,4,5에 대한 int 스트림 생성 // range(1,5) : 1,2,3,4에 대한 int 스트림 생성 // flatMat : 스트림의 형태가 배열과 같을 때 모든 원소를 단일 원소 스트림으로 반환해준다. // mapToOjb : 객체 스트림으로 변환 // subList : List<E> subList(int fromIndex, int toIndex) public class SubLists { // (a, b, c)의 prefixes : (a), (a,b), (a,b,c) private static <E> Stream<List<E>> prefixes(List<E> list) { return IntStream.rangeClosed(1, list.size()) .mapToObj(end -> list.subList(0, end)); } // a의 suffixes : (a) // a,b의 suffixes : (a,b), (a) // a,b,c의 suffixes : (a,b,c), (b,c), (a) private static <E> Stream<List<E>> suffixes(List<E> list) { return IntStream.range(0, list.size()) .mapToObj(start -> list.subList(start, list.size())); } // 입력 리스트의 모든 부분 리스트를 스트림으로 반환한다. // 3줄이면 충분하지만 입력 리스트 크기의 거듭제곱만큼 메모리를 차지하며 좋은 방법은 아니다. public static <E> Stream<List<E>> of(List<E> list) { return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list).flatMap(SubLists::suffixes)); } public static void main(String[] args) { List<String> list = Arrays.asList(args); SubLists.of(list).forEach(System.out::println); } }
- 빈 리스트는 concat을 사용하다가 rangeClosed 호출을 1 → Math.signum(start)로 고치면 된다.
- for 반복문보다 간결하지만 읽기에는 오히려 더 좋지않다.
🧑🏻⚖️ 결론
- Stream이나 Iterable을 리턴하는 API 에서는 stream → Iterable, Iterable → stream 으로 변환해주는 어댑터를 만들어서 사용하자.
- 하지만 어댑터는 클라이언트 코드를 어수선하게 만들며 약 2,3배 느리다는 것을 명심하자 !
- 원소 시퀀스를 반환하는 메서드를 작성할 때에는 Stream, Iterable 모두 지원할 수 있게 둘다 작성해주자
- 컬렉션을 반환할 수 있다면 컬렉션으로 반환하자.
- 원소의 갯수가 많다면 전용 컬렉션을 리턴하는 방법을 고려하자