💥 개요
원소 시퀀스, 즉 일련의 원소를 반환하는 메소드는 많다. 자바 7까지는 이런 메서드의 반환 타입으로 Collection, List, Set 같은 컬렉션 인터페이스나 Iterable이나 배열을 썼다. 그런데 자바 8이 스트림이라는 개념을 들고 오면서 문제가 발생했는데 스트림은 반복(iteration)을 지원하지 않는다. API를 스트림을 반환하도록 작성하면 for-each로 반복하길 원하는 사용자는 불만을 토론할 것이다.
사실 Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, Iterable 인터페이스가 정의한 방식대로 동작한다. 그럼에도 for-each로 스트림을 반복할 수 없는 이유는 extend하지 않아서다.
🔎 해결방법
이 문제를 해결해줄 멋진 우회로는 없다. 물론 어댑터를 구현하여 사용할 수 있겠지만 최선은 아니다.
// 스트림 <-> 반복자 어댑터 (285-286쪽)
public class Adapters {
// 코드 47-3 Stream<E>를 Iterable<E>로 중개해주는 어댑터 (285쪽)
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
// 코드 47-4 Iterable<E>를 Stream<E>로 중개해주는 어댑터 (286쪽)
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
}
결국 해결 방법은 Collection 인터페이스를 반환하는 것인데, 그 이유는 Collection 인터페이스가 Iterable의 하위 타입이고 stream 메소드도 제공하니 반복과 스트림을 동시에 지원한다. 따라서 원소 시퀀스를 반환하는 공개 API의 반환타입에는 Collection이나 그 하위 타입을 쓰는게 일반적으로 최선이다.
반환하는 시퀀스의 크기가 메모리에 올려도 안전할 만큼 작다면 ArrayList나 HashSet 같은 표준 컬렉션 구현체를 반환하는 게 최선일 수도 있다. 하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.
🎁 반환할 시퀀스가 큰 경우
반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션의 구현을 고려해보도록 한다. 이때 AbstractList를 이용하면 전용 컬렉션을 손쉽게 구현할 수 있다.
AbstractCollection을 활용해서 Collection 구현체를 작성할 때는 Iterable용 메서드 외에 contains와 size만 더 구현하면 된다. 만약 반복이 시작되기 전에 시퀀스의 내용을 확정할 수 없는 등의 이유로 contains와 size를 구현하는 게 불가능할 때는 컬렉션보다는 스트림이나 Iterable을 반환하는 편이 낫다.
물론 별도의 메서드를 두어 두 방식을 모두 제공할 수도 있다.
❓ Stream을 반환하는 방법 2가지
첫 번째 방법은 입력 리스트의 모든 부분리스트를 스트림을 반환하는 방법이다.
아래 코드는 정수 인덱스를 사용한 표준 for 반복문의 스트림 버전이라 할 수 있다.
public class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list).flatMap(SubLists::suffixes));
}
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size())
.mapToObj(end -> list.subList(0, end));
}
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
}
아래의 중첩 for 반복문과 비슷한 개념이다.
import java.util.ArrayList;
public class Temp {
public static void main(String[] args) {
List<String> src = new ArrayList<>(); // 편의상 String 타입으로 작성하였음.
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 class SubLists {
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을 지원하도록 자바가 수정된다면, 그 때는 안심하고 스트림을 반환하면 될것이다.(스트림 처리와 반복 모두에 사용할 수 있으니)
'Study > 이펙티브 자바' 카테고리의 다른 글
[Effective Java] 예외는 진짜 예외 상황에만 사용하라 (0) | 2024.05.06 |
---|---|
[Effective Java] 객체는 인터페이스를 사용해 참조하라 (0) | 2024.04.22 |
[Effective Java] 정확한 답이 필요하다면 float와 double은 피하라 (0) | 2024.04.03 |
[Effective Java] 공개된 API 요소에는 항상 문서화 주석을 작성하라 (0) | 2024.03.18 |