-
Notifications
You must be signed in to change notification settings - Fork 1
item 47 JihoonKim
일련의 원소(원소 시퀀스)를 리턴할 때의 타입
- Collection, Set, List 같은 컬렉션 인터페이스(기본)
- Iterable
- 배열(성능에 민감한 상황)
- 스트림
스트림이 도입되면서 리턴 타입의 선택이 복잡해졌다.
스트림은 Iterable을 확장(extends)하지 않아 반복(iteration)을 지원하지 않는다. 따라서, 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다.
// Stream의 iterator 메서드에 메서드 참조를 건네 스트림의 반복을 지원 - Compile 에러(타입 추론 한계)
for (ProcessHandle p : ProcessHandle.allProcesses()::iterator) {
// process 처리
}
// Stream의 iterator 메서드에 메서드 참조를 건네어 스트림의 반복을 지원 - 스트림 반복을 위한 '끔찍한' 우회 방법
for (ProcessHandle p : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator) {
// process 처리
}
어댑터 메서드 작성을 통해 개선 가능하다.
// Stream<E>를 Iterable<E>로 중개해주는 어댑터
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
// process 처리
}
// Iterable<E>를 Stream<E>로 중개해주는 어댑터
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false); // 2nd param은 parallel 여부
}
공개 API를 작성한다면 스트림 파이프라인 사용자와 반복문 사용자 모두를 배려해야 한다.
Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다.
따라서, 원소 시퀀스를 리턴하는 공개 API의 리턴 타입에는 일반적으로 Collection 또는 그 하위 타입을 쓰는 게 최선이다.
하지만, 단지 컬렉션을 리턴한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.
리턴할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 Collection을 구현하는 방안도 생각해보자.
주어진 집합의 멱집합(한 집합의 모든 부분집합을 원소로 하는 집합)을 리턴하는 상황
원소 개수가 n개 이면 멱집합의 원소 개수는 2^n개(지수배로 증가)
참고)
{a, b, c}의 멱집합은 { {}, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c} }
AbstractList를 이용하여 컬렉션 구현
- 각 원소의 인덱스를 비트벡터로 사용(n번째 비트 값은 해당 원소가 n번째 원소 포함 여부)
- 0부터 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) { // Collection을 리턴 타입으로 쓸 때의 단점
throw new IllegalArgumentException("집합에 원소가 너무 많습니다(최대 30개).: " + s);
}
return new AbstractList<Set<E>>() {
@Override
public int size() {
return 1 << src.size(); // 2^n
}
@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용 메서드 외의 contains와 size만 더 구현하면 된다.
contains와 size를 구현하는 게 불가능하다면 컬렉션보다는 스트림이나 Iterable을 리턴하는 편이 낫다(별도의 메서드를 부어 두 방식 모두 제공해도 된다).
부분 리스트를 구하는 방법
- (a, b, c)의 prefixes - (a), (a, b), (a, b, c)
- (a, b, c)의 suffixes - (c), (b, c), (a, b, c)
- 빈 리스트(empty list)
public class SubList {
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(Stream.of(Collections.emptyList()), // 빈 리스트
prefixes(list).flatMap(SubList::suffixes));
}
public static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size())
.mapToObj(end -> list.subList(0, end));
}
public static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.rangeClosed(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
}
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);
}
스트림 리턴만 제공되니 반복문 사용이 필요할 때에는 Iterable로 변환해주는 어댑터를 이용해야 한다. (코드가 어수선해지고, 작가 컴퓨터로 2.3배 더 느리다.) (책에는 안 나온) 직접 구현한 Collection이 스트림보다 1.4배(작가 컴퓨터) 빨랐다(코드는 더 지저분해졌다).
- 원소 시퀀스를 리턴하는 메서드를 작성할 때 스트림 사용자와 반복문 사용자가 모두 있을 수 있기에 양쪽을 만족시키려고 노력하자
- 컬렉션을 리턴할 수 있다면 컬렉션을 이용하자
- 원소 개수가 적거나 이미 컬렉션에 담아 관리하고 있으면 표준 컬렉션(E.g ArrayList)을 이용하자
- 그 외에는 전용 컬렉션(E.g 멱집합)을 구현하는 것을 고려하자
- 컬렉션을 리턴할 수 없다면 스트림이나 Iterable 중 더 나은 것을 이용하자
- 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 수정된다면, 스트림 처리와 반복에 모두 사용 가능하니 안심하고 스트림을 리턴하면 될 것이다.