Java 8 이전(즉, Java 7까지)
Java 7까지는 Stream API가 존재하지 않았기 때문에
, 메서드에서 여러 원소(시퀀스)를 반환해야 할 때 사용할 수 있는 타입은 다음과 같았다.
Collection
(인터페이스): 여러 값을 담는 일반적인 컬렉션 타입. 예:ArrayList
,HashSet
등Set
,List
:Collection
의 하위 타입Iterable
:for-each
문에서 순회할 수 있는 최소 요건을 가진 타입- 배열:
String[]
,int[]
같은 고전적인 방식
특별한 이유가 없다면, 보통은 Collection
이나 그 하위 타입 (List
, Set
)을 반환 타입으로 선택했다.
Collection이 다양한 유용한 메서드를 제공하기 때문이다.
예: size(), contains(), isEmpty() 등
Iterable은 for-each 문에서 사용할 수 있도록 최소한의 기능만을 제공하는 인터페이스이다.
메서드에서 반환하는 원소들이 단순히 한 번만 순회하면 되는 경우나,
size()나 contains(Object)처럼 추가적인 컬렉션 연산이 필요하지 않은 경우에는 Collection 대신 Iterable을 반환하는 것이 더 적절하다.
Java 8 이후, Stream 등장
자바 7까지는 반복 가능한 데이터를 다루기 위해 Iterable이나 Collection을 사용했고,
그 이후 자바 8에서는 스트림(Stream)이라는 새로운 접근 방식이 등장했다.
그런데, 스트림은 기존의 for-each 문과는 다르게 동작한다.
스트림은 반복(iteration)을 지원하지 않는다.
왜 스트림은 for-each 문에서 사용할 수 없을까?
스트림(Stream)은 데이터를 반복해서 처리할 수 있지만, for-each 문에서는 직접 사용할 수 없다.
그 이유는 스트림이 Iterable 인터페이스를 상속하지 않았기 때문이다.
for-each 문은 내부적으로 Iterable 타입만 사용할 수 있게 되어 있다.
스트림도 iterator() 메서드를 가지고 있지만, 자바 컴파일러는 이게 Iterable이 아니면 for-each 문에서 쓸 수 없다고 판단한다.
그래서 다음과 같은 코드는 컴파일 오류가 난다.
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator)
를 컴파일하려고 하면,
Method reference expression is not expected here
라는 오류가 발생할 수 있다.
이 오류는 "여기에서는 메서드 참조 문법(::)을 쓸 수 없다"는 의미이다.
Iterator로 적절히 형변환을 해준다. 하지만 코드 유지보수 측면에서 좋지 않다.
- 가독성이 떨어진다.
- 직관적이지 않다.
- 메서드 참조를 직접 캐스팅하는 방식이라 난해하게 느껴진다.
타입을 중개해주는 어댑터 메소드
자바에서 타입 간 중개를 할 때 이렇게 어댑터(adapter) 클래스를 만들어서 Stream
과 Iterable
간 변환을 명확하게 처리하는 게 좋은 방법이다.
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class Adapters {
// Adapter from Stream<E> to Iterable<E> (
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
// Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
}
iterableOf(Stream<E> stream)
:스트림
을Iterable
로 감싸서for-each
등에 쓸 수 있게 한다.streamOf(Iterable<E> iterable)
: 기존Iterable
을 스트림으로 바꿔서 스트림 API 활용을 가능하게 한다.
Collection이 반복과 스트림을 모두 지원하는 이유
Collection
인터페이스는 Iterable
의 하위 타입이기 때문에, for-each
문을 사용한 반복(iteration)도 지원하고,
stream()
메서드를 통해 스트림(stream) 처리도 지원한다.
즉, Collection은 "반복 + 스트림 처리"를 모두 지원하는 가장 범용적인 반환 타입
입니다.
따라서 어떤 메서드가 여러 개의 원소(시퀀스)를 반환해야 한다면, 특별한 이유가 없다면
그 반환 타입을 Collection 또는 List, Set 등 그 하위 타입으로 선언하는 것이 일반적으로 가장 좋다.
또한 배열도 마찬가지로, Arrays.asList()나 Stream.of() 같은 도구를 통해 쉽게 반복과 스트림 처리 모두에 사용할 수 있다.
그러나, 단순히 Collection을 반환하는 것이 항상 좋은 것은 아니다.
단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.
반환 대상이 매우 크거나, 메모리에 한꺼번에 올리기 어려운 시퀀스라면,
굳이 모든 원소를 메모리에 로딩하여 Collection 형태로 반환하는 것은 비효율적이다.
이럴 땐 오히려 Stream이나 Iterable을 반환하는 것이 낫다.
- Stream: 지연 처리(lazy evaluation)로 필요할 때마다 데이터를 흘려보낸다.
- Iterable: 메모리를 덜 쓰면서도 for-each 문에서 사용할 수 있다.
반환하는 컬렉션이 너무 크다면 전용 컬렉션도 고려하라.
반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는 방안을 검토해보자.
예제 : Set의 멱집합(power set)을 구하는 전용 컬렉션
package effectivejava.chapter7.item47;
import java.util.*;
public class PowerSet {
// Returns the power set of an input set as custom collection (Page 218)
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
if (src.size() > 30)
throw new IllegalArgumentException("Set too big " + s);
return new AbstractList<Set<E>>() {
@Override public int size() {
return 1 << src.size(); // 2 to the power srcSize
}
@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;
}
};
}
public static void main(String[] args) {
Set s = new HashSet(Arrays.asList(args));
System.out.println(PowerSet.of(s));
}
}
멱집합
: 한 집합의 모든 부분집합을 원소로 하는 집합.
{a,b,c}의 멱집합은 {{}, {a}, {b}, {c}, {a,b}, {a,c}, {b,c}, {a,b,c}}다.
원소 개수가 n개면 멱집합 원소 개수는 2ⁿ개가 된다. (생각보다 덩치가 커진다.)
get(int index)를 통해 해당 인덱스에 해당하는 부분집합을 반환한다.
하지만 전용 컬렉션을 활용하면, 모든 부분집합을 한 번에 생성하지 않고,
요청이 들어올 때마다 get(int index)를 통해 해당 인덱스에 해당하는 부분집합을 지연 계산(lazy evaluation)하여 반환한다.
AbstractionCollection을 활용해서 Collection 구현체를 작성할 때 Iterable용 메서드 외에 contains, size를 더 구현하면 된다.
또한 이 전용 컬렉션은 AbstractList
를 상속받아 필요한 최소한의 메서드만 구현했고, size()
나 contains()
같은 메서드도 지원한다.
입력 집합의 원소 수가 30을 넘으면 PowerSet.of가 예외를 던진다.
Collection의 size() 메서드가 int 값을 반환하므로 PowerSet.of가 반환되는 시퀀스의 최대 길이는 Integer.MAX_VALUE 혹은 2³¹-1로 제환된다.
입력 집합의 원소 수가 31 이상이면 이 한계를 넘어서게 된다.
따라서 안전하게 30개까지만 허용하고, 그 초과 시 예외를 던지는 방식입니다.
Integer.MAX_VALUE = 2,147,483,647
2³¹ = 2,147,483,648
예를 들어 {a,b,c}의 멱집합은 원소가 8이므로 유효한 인덱스는 0~7이며, 이진수로는 000 ~ 111이다.
인덱스를 이진수로 나타내면, 각 n번째 자리의 값이 각각 원소 a,b,c를 포함하는지 여부를 알려준다.
즉, 000번째 원소는 {}, 001번째는 {a}, ..111번째 원소는 {a, b, c}가 된다.
Stream이 Collection보다 나을 때도 있다.
결과 시퀀스가 매우 크고, 그 중 일부만 처리하거나 순차적으로 처리하고자 할 때는 Stream이 유리하다.
예제 : Stream을 사용하여 필요한 순간에만 부분 리스트를 생성한다.
예제에서 등장한 주요 Stream API 메서드들을 살펴보자.
1. suffixes(List list)
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
- 리스트의 모든 sublist 리스트를 생성한다.
IntStream.range(0, list.size())
: 0부터 size - 1까지 반복한다.- 각 start 인덱스에서 시작해 끝까지 잘라낸
list.subList(start, list.size())
를 반환한다.
예: 입력 리스트가 [a, b, c]이면
start = 0 → [a, b, c]
start = 1 → [b, c]
start = 2 → [c]
결과: [a, b, c], [b, c], [c]
2. flatMap
prefixes(list).flatMap(SubLists::suffixes)
- flatMap()은 스트림의 형태가 배열과 같을 때, 모든 원소를 단일 원소 스트림으로 반환할 수 있다.
prefixes(list).flatMap(SubLists::suffixes)
은 리스트에서 만들 수 있는 모든 "연속된 부분리스트(sublist)"를 생성하는 과정이다.
# 가정:
입력 리스트: [a, b, c]이 주어졌을 때
prefixes:
[a]
[a, b]
[a, b, c]
각각의 prefix에 대해 suffixes 실행:
[a] → [a]
[a, b] → [a, b], [b]
[a, b, c] → [a, b, c], [b, c], [c]
최종 결과 (flatten):
[a], [a, b], [b], [a, b, c], [b, c], [c]
3. Stream.concat(Stream a, Stream b)
return Stream.concat(Stream.of(Collections.emptyList()),
prefixes(list).flatMap(SubLists::suffixes));
두 개의 스트림을 앞뒤로 이어 붙인다.
여기서는 가장 앞에 빈 리스트 []를 붙이고, 뒤에 모든 sublist들을 이어붙인다.
Stream.of([]) + Stream.of([a], [a,b], ..., [c])
4. IntStream.range(int startInclusive, int endExclusive)
IntStream.range(0, list.size())
startInclusive부터 endExclusive - 1까지의 반복한다.
suffixes()에서 사용됨 → 인덱스 0부터 list.size() - 1까지 순회
결론
공개 API에서 원소 시퀀스를 반환할 때는 특별한 이유가 없는 한 Collection 또는 그 하위 타입(List, Set 등)을 사용하는 것이 최선이다.
하지만 데이터가 너무 크거나 지연 계산이 필요한 경우, Stream이나 전용 컬렉션 구현체가 더 나은 선택일 수 있다.
'Dev Books > Effective Java' 카테고리의 다른 글
[item 11] equals를 재정의하려거든 hashCode도 재정의하라 (2) | 2025.07.17 |
---|---|
[item 48] 스트림 병렬화는 주의해서 적용하라. (0) | 2025.07.16 |
[item 46] 스트림에서는 부작용 없는 함수를 사용하라 (2) | 2025.07.11 |
[item 45] 스트림은 주의해서 사용하라 (3) | 2025.07.09 |
[item 44] 표준 함수형 인터페이스를 사용하라 (0) | 2025.07.01 |
댓글