TinyStream과 내부 반복
필터·변환·최종 연산 파이프라인을 만들고 즉시·지연 실행과 외부·내부 반복의 차이를 비교합니다.
Stream API를 사용하기만 하면 메서드 체이닝이 편리하다는 인상에 머물 수 있습니다.
작은 스트림을 직접 구현하면 각 연산이 무엇을 저장하고 언제 실행되는지, 타입이 어떻게 바뀌는지, 반복을 누가 소유하는지 분명해집니다.
이 장의 첫 정상 구현은 목록 구조를 고정한 즉시 파이프라인이며, 연습 문제에서 Iterator 기반 지연 시퀀스로 확장합니다.
of(source)는 팩터리, filter와 map은 중간 연산, toList와 count는 최종 연산 역할을 합니다.
실제 JDK Stream은 한 번 최종 연산을 실행하면 재사용할 수 없고 지연 평가를 사용하지만, 학습 구현은 스냅샷 값으로 새 객체를 반환해 재사용 가능합니다.
이름이 같아도 수명 계약이 다름을 명시합니다.
가변 체이닝 오염
다음 구현은 filter()가 내부 목록에 removeIf()를 적용하고 this를 반환합니다. 목록 참조를 교체하는 것이 아니라 같은 목록의 원소를 제거합니다.
기반에서 두 파이프라인을 분기했다고 생각해도 첫 필터가 기반 상태를 바꿔 두 번째 분기가 축소된 원본을 봅니다.
map으로 원소 타입이 바뀌는 연산은 같은 제네릭 인스턴스를 안전하게 반환하기도 어렵습니다.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public final class MutableSelfReturningStream<T> {
private List<T> values;
MutableSelfReturningStream(List<T> source) {
values = new ArrayList<>(source);
}
MutableSelfReturningStream<T> filter(Predicate<? super T> predicate) {
values.removeIf(predicate.negate());
return this;
}
List<T> toList() {
return values;
}
public static void main(String[] args) {
MutableSelfReturningStream<Integer> base = new MutableSelfReturningStream<>(List.of(1, 2, 3, 4));
List<Integer> even = base.filter(value -> value % 2 == 0).toList();
List<Integer> large = base.filter(value -> value >= 3).toList();
System.out.println(even);
System.out.println(large);
}
}두 결과 변수가 같은 목록을 가리킨다
base.filter가 같은 가변 목록을 수정하여 even과 large도 마지막 [4]를 함께 가리킵니다. [1,2,3,4]→[2,4]→[4] 전환과 alias를 고정해 공유 변경을 설명할 수 있습니다.
공유 목록의 참조
두 결과 변수가 같은 목록을 가리킨다
두 번째 filter 뒤에도 base.values·even·large는 같은 목록을 가리킵니다. 그 목록에는 4 하나만 남습니다.
두 결과 변수가 같은 목록을 가리킨다
두 번째 filter가 끝난 시점입니다. 화살표는 같은 목록의 참조를 뜻하며, even과 large를 출력하면 모두 [4]입니다. 원문 코드의 참조 관계를 그린 것이며 실행 캡처가 아닙니다.
이 예의 조건
- 원 API는 동일한 가변 목록을 반환합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원 두 번째 필터가 끝난 뒤 base.values·even·large는 원소 4만 있는 같은 목록을 참조한다. 화살표는 객체 참조이며 호출 순서가 아니다. 원 wide의 4개 노드와 3개 관계를 같은 의미로 모바일 240에 재배치한다. |
| 구분 | 내용 |
|---|---|
| 설명 | 원 두 번째 필터가 끝난 뒤 base.values·even·large는 원소 4만 있는 같은 목록을 참조한다. 화살표는 객체 참조이며 호출 순서가 아니다. 원 wide의 4개 노드와 3개 관계를 같은 의미로 모바일 240에 재배치한다. |
| 전체 읽기 정보 | 두 결과 변수가 같은 목록을 가리킨다 두 결과 변수가 같은 목록을 가리킨다 공유 목록의 참조 두 결과 변수가 같은 목록을 가리킨다 두 번째 filter 뒤에도 base.values·even·large는 같은 목록을 가리킵니다. 그 목록에는 4 하나만 남습니다. 두 결과 변수가 같은 목록을 가리킨다 두 번째 filter가 끝난 시점입니다. 화살표는 같은 목록의 참조를 뜻하며, even과 large를 출력하면 모두 [4]입니다. 원문 코드의 참조 관계를 그린 것이며 실행 캡처가 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 기반 목록 base.values가 1,2,3,4를 보관합니다. |
|
|
| 첫 반환 참조 even이 [2,4]인 같은 목록을 받습니다. |
|
|
| 두 번째 필터 3 이상인 값만 남기면서 2를 제거하고, large도 같은 참조를 받습니다. |
|
|
두 결과 변수가 같은 목록을 가리킨다
base.filter가 같은 가변 목록을 수정하여 even과 large도 마지막 [4]를 함께 가리킵니다. [1,2,3,4]→[2,4]→[4] 전환과 alias를 고정해 공유 변경을 설명할 수 있습니다.
공유 목록의 참조
두 결과 변수가 같은 목록을 가리킨다
두 번째 filter 뒤에도 base.values·even·large는 같은 목록을 가리킵니다. 그 목록에는 4 하나만 남습니다.
두 결과 변수가 같은 목록을 가리킨다
두 번째 filter가 끝난 시점입니다. 화살표는 같은 목록의 참조를 뜻하며, even과 large를 출력하면 모두 [4]입니다. 원문 코드의 참조 관계를 그린 것이며 실행 캡처가 아닙니다.
이 예의 조건
- 원 API는 동일한 가변 목록을 반환합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원 두 번째 필터가 끝난 뒤 base.values·even·large는 원소 4만 있는 같은 목록을 참조한다. 화살표는 객체 참조이며 호출 순서가 아니다. 원 wide의 4개 노드와 3개 관계를 같은 의미로 모바일 240에 재배치한다. |
| 구분 | 내용 |
|---|---|
| 설명 | 원 두 번째 필터가 끝난 뒤 base.values·even·large는 원소 4만 있는 같은 목록을 참조한다. 화살표는 객체 참조이며 호출 순서가 아니다. 원 wide의 4개 노드와 3개 관계를 같은 의미로 모바일 240에 재배치한다. |
| 전체 읽기 정보 | 두 결과 변수가 같은 목록을 가리킨다 두 결과 변수가 같은 목록을 가리킨다 공유 목록의 참조 두 결과 변수가 같은 목록을 가리킨다 두 번째 filter 뒤에도 base.values·even·large는 같은 목록을 가리킵니다. 그 목록에는 4 하나만 남습니다. 두 결과 변수가 같은 목록을 가리킨다 두 번째 filter가 끝난 시점입니다. 화살표는 같은 목록의 참조를 뜻하며, even과 large를 출력하면 모두 [4]입니다. 원문 코드의 참조 관계를 그린 것이며 실행 캡처가 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 기반 목록 base.values가 1,2,3,4를 보관합니다. |
|
|
| 첫 반환 참조 even이 [2,4]인 같은 목록을 받습니다. |
|
|
| 두 번째 필터 3 이상인 값만 남기면서 2를 제거하고, large도 같은 참조를 받습니다. |
|
|
플루언트 API가 반드시 가변해야 하는 것은 아닙니다.
연산마다 새 파이프라인 값을 반환하면 기반을 안전하게 분기할 수 있습니다.
목록 구성을 스냅샷으로 고정하면 원본 목록의 추가·삭제는 반영되지 않습니다. 다만 원소 객체는 얕게 공유하므로 가변 원소의 상태 변화까지 막지는 않습니다.
실제 Stream이 단일 사용인 이유에는 자원 관리와 지연 순회 상태가 포함됩니다.
사용자 정의 API가 재사용 가능하다고 해서 JDK Stream도 같은 방식으로 보관하면 안 됩니다.
가변 예제의 첫 필터 직후 목록은 [2, 4]입니다. even은 그 목록의 복사본이 아니라 같은 참조입니다. 다음 필터가 2를 제거하면 base.values·even·large 모두 [4]를 읽으므로 두 출력 모두 [4]입니다. 각 호출 때의 반환값만 보지 않고, 저장해 둔 참조가 이후 어떤 객체를 읽는지 확인해야 합니다.
독립 확인: 가변 예제의 두 번째 조건만 value>=5로 바꾸면 even과 large 출력은 무엇인가요? 불변 예제 구현의 base.filter(짝수)와 base.filter(value>=3)는 왜 서로 다른 원소 목록을 유지하나요?
풀이 확인
가변 예제는 두 번째 필터가 같은 목록의 2·4를 모두 제거하므로 even·large가 모두 []입니다. 불변 구현은 새 목록을 가진 새 TinyStream을 반환하므로 base의 [1,2,3,4]를 바꾸지 않습니다. 짝수 결과 [2,4]와 큰 값 결과 [3,4]의 목록 구조는 별개이며 base.count()는 4입니다. 원소 객체 자체는 얕게 공유할 수 있다는 제한도 남습니다.
Supplier<Stream<T>>를 보관해 필요할 때 새 스트림을 만드는 패턴이 더 맞습니다.
불변 중간 연산
아래 TinyStream은 생성자를 비공개로 두고 of() 팩터리로 원본 스냅샷을 만듭니다.
필터와 map은 즉시 반복문을 실행한 뒤 새 TinyStream을 반환합니다.
map의 결과 타입 R이 새로운 제네릭 인스턴스에 반영됩니다.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public final class ImmutableEagerTinyStream<T> {
private final List<T> values;
private ImmutableEagerTinyStream(List<? extends T> values) {
this.values = List.copyOf(values);
}
static <T> ImmutableEagerTinyStream<T> of(List<? extends T> source) {
return new ImmutableEagerTinyStream<>(source);
}
ImmutableEagerTinyStream<T> filter(Predicate<? super T> predicate) {
List<T> selected = new ArrayList<>();
for (T value : values) {
if (predicate.test(value)) {
selected.add(value);
}
}
return new ImmutableEagerTinyStream<>(selected);
}
<R> ImmutableEagerTinyStream<R> map(Function<? super T, ? extends R> mapper) {
List<R> mapped = new ArrayList<>(values.size());
for (T value : values) {
mapped.add(mapper.apply(value));
}
return new ImmutableEagerTinyStream<>(mapped);
}
void forEach(Consumer<? super T> consumer) {
for (T value : values) {
consumer.accept(value);
}
}
long count() {
return values.size();
}
List<T> toList() {
return values;
}
public static void main(String[] args) {
ImmutableEagerTinyStream<Integer> base = of(List.of(1, 2, 3, 4));
List<String> even = base
.filter(value -> value % 2 == 0)
.map(value -> "N" + value)
.toList();
List<Integer> large = base.filter(value -> value >= 3).toList();
System.out.println(even);
System.out.println(large);
System.out.println("baseCount=" + base.count());
}
}메서드 체이닝은 각 반환값을 다음 수신 객체로 쓰는 중첩 호출입니다.
줄바꿈은 실행 경계가 아니며, 이 즉시 구현에서는 필터 호출이 끝난 뒤 map이 시작됩니다.
지연 스트림에서는 중간 호출이 연산 메타데이터만 쌓고 최종 연산에서 순회합니다.
toList()는 수정 불가 목록을 반환하므로 호출자가 목록의 추가·삭제·치환으로 내부 구조를 바꿀 수 없습니다. 원소 객체의 불변성과는 별개입니다.
가변 ArrayList를 보관한다면 방어적 복사가 필요합니다.
불변 파이프라인은 별도 잠금 없이 여러 판독기가 사용할 수 있지만 전달된 함수가 가변 캡처를 가진 경우 실행 전체가 스레드 안전하다는 뜻은 아닙니다.
외부 반복과 내부 반복
for 반복문과 Iterator는 호출자가 hasNext, next, break 시점을 제어하는 외부 반복입니다.
forEach, 필터, map 파이프라인은 컬렉션 라이브러리가 순회를 소유하고 호출자는 원소 동작만 제공합니다.
단락 평가가 필요하면 boolean을 반환하는 방문자나 findFirst 같은 전용 최종 연산이 있어야 합니다.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
public final class InternalExternalIteration {
@FunctionalInterface
interface VisitStep<T> {
boolean visit(T value);
}
static <T> List<T> externalUntil(List<T> source, Predicate<? super T> stop) {
List<T> visited = new ArrayList<>();
Iterator<T> iterator = source.iterator();
while (iterator.hasNext()) {
T value = iterator.next();
if (stop.test(value)) {
break;
}
visited.add(value);
}
return List.copyOf(visited);
}
static <T> void forEachUntil(Iterable<T> source, VisitStep<? super T> step) {
for (T value : source) {
if (!step.visit(value)) {
return;
}
}
}
static <T> List<T> internalUntil(Iterable<T> source, Predicate<? super T> stop) {
List<T> visited = new ArrayList<>();
forEachUntil(source, value -> {
if (stop.test(value)) {
return false;
}
visited.add(value);
return true;
});
return List.copyOf(visited);
}
static <T> T findFirst(List<T> source, Predicate<? super T> predicate) {
for (T value : source) {
if (predicate.test(value)) {
return value;
}
}
return null;
}
public static void main(String[] args) {
List<Integer> source = List.of(1, 2, 3, 4);
System.out.println(externalUntil(source, value -> value == 3));
System.out.println(internalUntil(source, value -> value == 3));
System.out.println(findFirst(source, value -> value % 2 == 0));
}
}예제의 forEachUntil()은 라이브러리가 순회를 소유하고 콜백의 boolean 결과를 계속·중단 신호로 해석하는 전용 내부 반복 연산입니다.
표준 Iterable.forEach()가 받는 Consumer에는 중단 신호가 없으므로, 중간 종료가 필요하면 이처럼 계약을 확장하거나 findFirst() 같은 전용 최종 연산을 제공해야 합니다.
내부 반복은 라이브러리가 순차, 병렬, 지연 실행 전략을 선택할 여지를 줍니다.
대신 콜백이 호출 횟수와 스레드, 순서에 의존하지 않아야 합니다.
외부 반복문은 복잡한 break와 continue가 필요할 때 더 직접적일 수 있습니다.
지연 파이프라인 상태
지연 시퀀스는 원본 Iterator를 필터 반복자와 map 반복자로 감쌉니다.
이 풀이의 of는 원본 목록 스냅샷을 즉시 확보합니다. filter와 map을 연결할 때는 함수를 실행하지 않으며, 최종 연산의 hasNext()나 next() 요청에 따라 필터 준비와 매핑이 일어납니다.
findFirst나 take와 결합하면 전체 입력을 보지 않을 수 있습니다.
Iterator는 가변 순회 커서이므로 같은 반복자를 여러 최종 연산이 공유하면 재사용 문제가 생깁니다.
시퀀스가 새 반복자를 만드는 Supplier<Iterator<T>>를 보관하면 최종 연산마다 독립 커서를 만들 수 있습니다.
원본 자체가 일회성 네트워크 스트림이면 공급자로 재생할 수 없으므로 단일 사용 계약을 택해야 합니다.
연습 문제
Sequence.of(List)가 새 반복자 팩터리를 보관하고, 필터와 map이 래퍼 반복자 팩터리를 반환하게 합니다.
take(n) 최종 연산은 최대 n개의 결과만 모아야 합니다. 필터가 원소를 제외하면 결과 n개를 얻기 위해 더 많은 원본 원소를 읽을 수 있습니다.
매퍼 호출 횟수를 세어 원본 전체가 평가되지 않았음을 확인합니다.
해답 보기
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public final class LazySequenceSolution<T> {
private final Supplier<Iterator<T>> iteratorFactory;
private LazySequenceSolution(Supplier<Iterator<T>> iteratorFactory) {
this.iteratorFactory = iteratorFactory;
}
static <T> LazySequenceSolution<T> of(List<? extends T> source) {
List<T> snapshot = List.copyOf(source);
return new LazySequenceSolution<>(snapshot::iterator);
}
LazySequenceSolution<T> filter(Predicate<? super T> predicate) {
return new LazySequenceSolution<>(() -> new Iterator<>() {
private final Iterator<T> source = iteratorFactory.get();
private T next;
private boolean ready;
private void prepare() {
while (!ready && source.hasNext()) {
T candidate = source.next();
if (predicate.test(candidate)) {
next = candidate;
ready = true;
}
}
}
@Override
public boolean hasNext() {
prepare();
return ready;
}
@Override
public T next() {
prepare();
if (!ready) {
throw new NoSuchElementException();
}
T value = next;
ready = false;
next = null;
return value;
}
});
}
<R> LazySequenceSolution<R> map(Function<? super T, ? extends R> mapper) {
return new LazySequenceSolution<>(() -> new Iterator<>() {
private final Iterator<T> source = iteratorFactory.get();
@Override
public boolean hasNext() {
return source.hasNext();
}
@Override
public R next() {
return mapper.apply(source.next());
}
});
}
List<T> take(int limit) {
if (limit < 0) {
throw new IllegalArgumentException("negative limit");
}
List<T> result = new ArrayList<>(limit);
Iterator<T> iterator = iteratorFactory.get();
while (result.size() < limit && iterator.hasNext()) {
result.add(iterator.next());
}
return List.copyOf(result);
}
public static void main(String[] args) {
AtomicInteger mapped = new AtomicInteger();
LazySequenceSolution<String> sequence = LazySequenceSolution
.of(List.of(1, 2, 3, 4, 5, 6))
.filter(value -> value % 2 == 0)
.map(value -> {
mapped.incrementAndGet();
return "N" + value;
});
System.out.println(sequence.take(2));
System.out.println("mapped=" + mapped.get());
System.out.println(sequence.take(1));
}
}최종 호출마다 새 커서로 평가하는 Sequence
같은 지연 정의의 take(2)는 1..4를 검사하고 take(1)은 새 커서로 1..2를 다시 검사합니다. 매퍼 호출 횟수와 커서 수명 변화가 캐시와 지연을 구분합니다.
최종 호출마다 새 커서로 평가하는 Sequence
최종 호출마다 새 커서
최종 호출마다 새 커서로 평가하는 Sequence
구성 단계에서는 조건식과 매퍼를 실행하지 않습니다. take마다 새 반복자를 만들어 처음부터 필요한 결과만 구합니다.
of 뒤 filter·map 구성
- 검사한 원본 값·반환값
- 스냅샷만 확보; 조건식·매퍼 미실행
- 매퍼 호출 수
- 0
첫 take(2)
- 검사한 원본 값·반환값
- 1·2·3·4 검사 → [N2, N4]
- 매퍼 호출 수
- 이번 2회; 누적 2
다음 take(1)
- 검사한 원본 값·반환값
- 새 커서로 1·2 검사 → [N2]
- 매퍼 호출 수
- 이번 1회; 누적 3
직접 작성한 반복자와 조건식을 따라간 결과입니다. 원문의 mapped=2 출력은 첫 take 뒤이며 마지막 누적 3은 추가 출력 없이 코드에서 도출한 상태입니다.
지연 평가와 결과 캐시는 별개입니다. 다음 take(1)은 저장된 N2를 꺼내는 대신 새 커서로 1·2를 다시 검사합니다.
이 예의 조건
- 지연 시퀀스 정의는 메모이제이션이 아니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 두 take는 같은 스냅샷·함수 정의를 쓰지만 iteratorFactory.get으로 커서를 새로 만든다. 첫 take(2)는 1..4를 검사해 매퍼 2회이고 다음 take(1)은 1·2부터 다시 검사해 매퍼 1회다. 원 mapped=2 출력은 첫 take 직후이고 뒤 누적 3은 코드에서 구한 상태다. 화살표는 별도 최종 호출의 커서·평가·반환 관계이며 결과 캐시를 뜻하지 않는다. 모바일도 같은 7개 노드와 6개 관계이다. |
| 구분 | 내용 |
|---|---|
| 설명 | 두 take는 같은 스냅샷·함수 정의를 쓰지만 iteratorFactory.get으로 커서를 새로 만든다. 첫 take(2)는 1..4를 검사해 매퍼 2회이고 다음 take(1)은 1·2부터 다시 검사해 매퍼 1회다. 원 mapped=2 출력은 첫 take 직후이고 뒤 누적 3은 코드에서 구한 상태다. 화살표는 별도 최종 호출의 커서·평가·반환 관계이며 결과 캐시를 뜻하지 않는다. 모바일도 같은 7개 노드와 6개 관계이다. |
| 전체 읽기 정보 | 최종 호출마다 새 커서로 평가하는 Sequence 최종 호출마다 새 커서로 평가하는 Sequence 최종 호출마다 새 커서로 평가하는 Sequence 최종 호출마다 새 커서 최종 호출마다 새 커서로 평가하는 Sequence 구성 단계에서는 조건식과 매퍼를 실행하지 않습니다. take마다 새 반복자를 만들어 처음부터 필요한 결과만 구합니다. of 뒤 filter·map 구성 검사한 원본 값·반환값 스냅샷만 확보; 조건식·매퍼 미실행 매퍼 호출 수 0 첫 take(2) 검사한 원본 값·반환값 1·2·3·4 검사 → [N2, N4] 매퍼 호출 수 이번 2회; 누적 2 다음 take(1) 검사한 원본 값·반환값 새 커서로 1·2 검사 → [N2] 매퍼 호출 수 이번 1회; 누적 3 직접 작성한 반복자와 조건식을 따라간 결과입니다. 원문의 mapped=2 출력은 첫 take 뒤이며 마지막 누적 3은 추가 출력 없이 코드에서 도출한 상태입니다. 지연 평가와 결과 캐시는 별개입니다. 다음 take(1)은 저장된 N2를 꺼내는 대신 새 커서로 1·2를 다시 검사합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 정의만 보관 snapshot과 filter/map 함수를 가진 시퀀스를 만듭니다. |
|
|
| 첫 take(2) 새 cursor 1이 1·2·3·4를 검사합니다. |
|
|
| 다음 take(1) 시작 새 cursor 2가 1부터 다시 검사합니다. |
|
|
| 다음 결과 이번 순회에서 N2 하나를 얻습니다. |
|
|
최종 호출마다 새 커서로 평가하는 Sequence
같은 지연 정의의 take(2)는 1..4를 검사하고 take(1)은 새 커서로 1..2를 다시 검사합니다. 매퍼 호출 횟수와 커서 수명 변화가 캐시와 지연을 구분합니다.
최종 호출마다 새 커서로 평가하는 Sequence
최종 호출마다 새 커서
최종 호출마다 새 커서로 평가하는 Sequence
구성 단계에서는 조건식과 매퍼를 실행하지 않습니다. take마다 새 반복자를 만들어 처음부터 필요한 결과만 구합니다.
of 뒤 filter·map 구성
- 검사한 원본 값·반환값
- 스냅샷만 확보; 조건식·매퍼 미실행
- 매퍼 호출 수
- 0
첫 take(2)
- 검사한 원본 값·반환값
- 1·2·3·4 검사 → [N2, N4]
- 매퍼 호출 수
- 이번 2회; 누적 2
다음 take(1)
- 검사한 원본 값·반환값
- 새 커서로 1·2 검사 → [N2]
- 매퍼 호출 수
- 이번 1회; 누적 3
직접 작성한 반복자와 조건식을 따라간 결과입니다. 원문의 mapped=2 출력은 첫 take 뒤이며 마지막 누적 3은 추가 출력 없이 코드에서 도출한 상태입니다.
지연 평가와 결과 캐시는 별개입니다. 다음 take(1)은 저장된 N2를 꺼내는 대신 새 커서로 1·2를 다시 검사합니다.
이 예의 조건
- 지연 시퀀스 정의는 메모이제이션이 아니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 두 take는 같은 스냅샷·함수 정의를 쓰지만 iteratorFactory.get으로 커서를 새로 만든다. 첫 take(2)는 1..4를 검사해 매퍼 2회이고 다음 take(1)은 1·2부터 다시 검사해 매퍼 1회다. 원 mapped=2 출력은 첫 take 직후이고 뒤 누적 3은 코드에서 구한 상태다. 화살표는 별도 최종 호출의 커서·평가·반환 관계이며 결과 캐시를 뜻하지 않는다. 모바일도 같은 7개 노드와 6개 관계이다. |
| 구분 | 내용 |
|---|---|
| 설명 | 두 take는 같은 스냅샷·함수 정의를 쓰지만 iteratorFactory.get으로 커서를 새로 만든다. 첫 take(2)는 1..4를 검사해 매퍼 2회이고 다음 take(1)은 1·2부터 다시 검사해 매퍼 1회다. 원 mapped=2 출력은 첫 take 직후이고 뒤 누적 3은 코드에서 구한 상태다. 화살표는 별도 최종 호출의 커서·평가·반환 관계이며 결과 캐시를 뜻하지 않는다. 모바일도 같은 7개 노드와 6개 관계이다. |
| 전체 읽기 정보 | 최종 호출마다 새 커서로 평가하는 Sequence 최종 호출마다 새 커서로 평가하는 Sequence 최종 호출마다 새 커서로 평가하는 Sequence 최종 호출마다 새 커서 최종 호출마다 새 커서로 평가하는 Sequence 구성 단계에서는 조건식과 매퍼를 실행하지 않습니다. take마다 새 반복자를 만들어 처음부터 필요한 결과만 구합니다. of 뒤 filter·map 구성 검사한 원본 값·반환값 스냅샷만 확보; 조건식·매퍼 미실행 매퍼 호출 수 0 첫 take(2) 검사한 원본 값·반환값 1·2·3·4 검사 → [N2, N4] 매퍼 호출 수 이번 2회; 누적 2 다음 take(1) 검사한 원본 값·반환값 새 커서로 1·2 검사 → [N2] 매퍼 호출 수 이번 1회; 누적 3 직접 작성한 반복자와 조건식을 따라간 결과입니다. 원문의 mapped=2 출력은 첫 take 뒤이며 마지막 누적 3은 추가 출력 없이 코드에서 도출한 상태입니다. 지연 평가와 결과 캐시는 별개입니다. 다음 take(1)은 저장된 N2를 꺼내는 대신 새 커서로 1·2를 다시 검사합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 정의만 보관 snapshot과 filter/map 함수를 가진 시퀀스를 만듭니다. |
|
|
| 첫 take(2) 새 cursor 1이 1·2·3·4를 검사합니다. |
|
|
| 다음 take(1) 시작 새 cursor 2가 1부터 다시 검사합니다. |
|
|
| 다음 결과 이번 순회에서 N2 하나를 얻습니다. |
|
|
캐시가 필요하면 지연과 메모이제이션을 별도로 설계합니다.
독립 확인: 예제의 지연 sequence를 만든 직후 take(0), take(3), take(1)을 차례로 호출합니다. 각 최종 호출에서 검사한 원본값, 매퍼 호출 수, 누적 매퍼 수를 쓰세요. take(-1)은 원소 검사에 도달하나요?
풀이 확인
take(0)은 반복 조건이 처음부터 false라 조건식·매퍼를 호출하지 않고 []를 반환합니다. take(3)은 원본 1~6을 검사해 2·4·6만 매핑하므로 [N2,N4,N6], 매퍼 호출은 3회입니다. 다음 take(1)은 새 커서에서 1·2를 다시 검사해 [N2], 매퍼 호출은 1회이며 누적 호출은 4회입니다. 앞서 얻은 N2를 캐시에서 꺼내지 않습니다. 음수는 limit 검사에서 IllegalArgumentException이 나며 커서 생성과 원소 검사 전에 중단합니다. 이 재사용은 기존 목록 스냅샷과 새 커서 팩터리가 있는 학습 구현의 계약이고 JDK Stream의 재사용 보장은 아닙니다.