스트림 생성 → 중간 연산 → 최종 연산 →
Collectors의 전체 표, 기본형 스트림,Optional전 메서드,java.util.function인터페이스 표를 한 페이지에 모았다. 원리(지연 평가, 파이프라인)는 고급 04 레슨에서, 여기서는 시그니처와 결과만 확인한다.Ctrl+F로 메서드명(예:groupingBy,teeing,mapMulti,orElseGet)을 검색하면 된다. 모든 결과는 JDK 21 실행값이다.
이 페이지의 예제 데이터:
record Emp(String name, String dept, int salary, int age) {}
List<Emp> emps = List.of(
new Emp("Kim", "Dev", 5000, 30), new Emp("Lee", "Dev", 7000, 40),
new Emp("Park", "Ops", 4000, 25), new Emp("Choi", "Ops", 6000, 35),
new Emp("Jung", "HR", 4500, 28));| 메서드 | 설명 | 예제 | 결과 |
|---|---|---|---|
list.stream() |
컬렉션 → 스트림 | List.of("a").stream().count() |
1 |
filter(pred) |
조건 통과만 | IntStream.rangeClosed(1,6).filter(x->x%2==0).boxed().toList() |
[2, 4, 6] |
map(fn) |
변환 | Stream.of("a", "bb").map(String::length).toList() |
[1, 2] |
flatMap(fn) |
중첩 펼치기 | Stream.of(List.of(1,2), List.of(3)).flatMap(List::stream).toList() |
[1, 2, 3] |
sorted(cmp) |
정렬 | Stream.of(3, 1, 2).sorted().toList() |
[1, 2, 3] |
distinct() |
중복 제거 | Stream.of(1, 1, 2, 3, 3).distinct().toList() |
[1, 2, 3] |
limit(n) / skip(n) |
앞 n개 / n개 건너뛰기 | IntStream.rangeClosed(1, 5).skip(2).boxed().toList() |
[3, 4, 5] |
toList() |
불변 리스트 (JDK 16+) | Stream.of(1).toList() |
[1] |
collect(Collectors.toList()) |
가변 리스트 | Stream.of(1).collect(toList()) |
[1] (ArrayList) |
forEach(consumer) |
순회 | list.forEach(System.out::println) |
출력 |
count() |
개수 | Stream.of(1, 2).count() |
2 |
anyMatch(pred) |
하나라도 | Stream.of(1, 2).anyMatch(x -> x > 1) |
true |
findFirst() |
첫 원소 Optional | Stream.of(3, 1, 2).filter(x -> x < 3).findFirst() |
Optional[1] |
reduce(id, op) |
누적 | Stream.of(1, 2, 3).reduce(0, Integer::sum) |
6 |
mapToInt(fn).sum() |
합계 | Stream.of("a", "bb").mapToInt(String::length).sum() |
3 |
max(cmp) |
최대 Optional | emps.stream().max(comparingInt(Emp::salary)).map(Emp::name) |
Optional[Lee] |
Collectors.joining(sep) |
문자열 결합 | Stream.of("a", "b").collect(joining(", ")) |
"a, b" |
Collectors.groupingBy(fn) |
그룹핑 | emps.stream().collect(groupingBy(Emp::dept, counting())) |
{Dev=2, HR=1, Ops=2} |
Collectors.toMap(k, v) |
맵 생성 | Stream.of("a", "bb").collect(toMap(identity(), String::length)) |
{bb=2, a=1} |
Optional.orElse(x) |
기본값 | Optional.empty().orElse("d") |
"d" |
| 메서드/시그니처 | 설명 | 한 줄 예제 | 결과 |
|---|---|---|---|
Collection.stream() |
컬렉션에서 | List.of("a").stream().count() |
1 |
Stream.of(T...) |
값 나열 | Stream.of(1, 2, 3).toList() |
[1, 2, 3] |
Arrays.stream(arr) |
배열 (int[]는 IntStream) | Arrays.stream(new int[]{1, 2}).sum() |
3 |
Stream.iterate(seed, fn) |
무한 (limit 필수) | Stream.iterate(1, x -> x * 2).limit(5).toList() |
[1, 2, 4, 8, 16] |
Stream.iterate(seed, hasNext, fn) |
유한 (JDK 9+) | Stream.iterate(1, x -> x < 20, x -> x * 3).toList() |
[1, 3, 9] |
Stream.generate(supplier) |
무한 생성 | Stream.generate(() -> "x").limit(3).toList() |
[x, x, x] |
IntStream.range(a, b) |
[a, b) | IntStream.range(0, 5).boxed().toList() |
[0, 1, 2, 3, 4] |
IntStream.rangeClosed(a, b) |
[a, b] | IntStream.rangeClosed(1, 5).sum() |
15 |
Stream.empty() |
빈 스트림 | Stream.empty().count() |
0 |
Stream.ofNullable(x) |
null이면 빈 스트림 (JDK 9+) | Stream.ofNullable(null).count()+"/"+Stream.ofNullable("a").count() |
0/1 |
Stream.concat(a, b) |
두 스트림 연결 | Stream.concat(Stream.of(1), Stream.of(2)).toList() |
[1, 2] |
Stream.builder() |
빌더 | Stream.<String>builder().add("a").add("b").build().toList() |
[a, b] |
"str".chars() |
문자 IntStream | "abc".chars().mapToObj(c -> (char) c).toList() |
[a, b, c] |
"str".lines() |
줄 스트림 | "a\nb".lines().toList() |
[a, b] |
new Random().ints(n, a, b) |
난수 스트림 | new Random(1).ints(3, 0, 10).boxed().toList() |
[5, 8, 7] |
Pattern.splitAsStream(s) |
정규식 분할 | Pattern.compile(",").splitAsStream("a,b").toList() |
[a, b] |
map.entrySet().stream() |
Map 순회 | new TreeMap<>(Map.of("a", 1)).entrySet().stream().toList() |
[a=1] |
Files.lines(path) |
파일 줄 스트림 (close 필수) | try (var s = Files.lines(p)) { s.count(); } |
줄 수 |
stream.parallel() |
병렬 전환 | IntStream.rangeClosed(1, 100).parallel().sum() |
5050 |
IllegalStateException: stream has already been operated upon or closed.| 메서드/시그니처 | 설명 | 한 줄 예제 | 결과 |
|---|---|---|---|
filter(Predicate) |
조건 필터 | IntStream.rangeClosed(1, 6).filter(x -> x % 2 == 0) |
[2, 4, 6] |
map(Function) |
1:1 변환 | Stream.of("a", "bb").map(String::length) |
[1, 2] |
mapToInt/Long/Double(fn) |
기본형 스트림으로 | Stream.of("a", "bb").mapToInt(String::length).sum() |
3 |
mapToObj(fn) |
기본형 → 객체 | IntStream.of(65, 66).mapToObj(c -> (char) c).toList() |
[A, B] |
flatMap(fn → Stream) |
1:N 펼치기 | Stream.of("a b", "c").flatMap(s -> Arrays.stream(s.split(" "))) |
[a, b, c] |
mapMulti((x, sink) -> ...) |
조건부 0..N 방출 (JDK 16+) | 아래 코드 | [1, 10, 3, 30] |
distinct() |
중복 제거 (equals) | Stream.of(1, 1, 2, 3, 3).distinct() |
[1, 2, 3] |
sorted() |
자연순 | Stream.of(3, 1, 2).sorted() |
[1, 2, 3] |
sorted(Comparator) |
비교자 정렬 | Stream.of("bb","a","ccc").sorted((x,y)->y.length()-x.length()) |
[ccc, bb, a] |
limit(n) |
앞 n개 (short-circuit) | Stream.iterate(1, x -> x + 1).limit(3) |
[1, 2, 3] |
skip(n) |
n개 건너뜀 | IntStream.rangeClosed(1, 5).skip(2) |
[3, 4, 5] |
takeWhile(pred) |
조건 깨질 때까지 (JDK 9+) | Stream.of(1, 2, 5, 1, 2).takeWhile(x -> x < 5) |
[1, 2] |
dropWhile(pred) |
조건 깨진 뒤부터 (JDK 9+) | Stream.of(1, 2, 5, 1, 2).dropWhile(x -> x < 5) |
[5, 1, 2] |
peek(Consumer) |
디버깅용 들여다보기 | Stream.of(1,2).peek(x->log.add("saw "+x)).map(x->x*2).toList() |
[2, 4], log [saw 1, saw 2] |
peek 최종 연산 없음 |
실행 안 됨 (lazy) | Stream.of(1).peek(x -> l.add("x")) |
l = [] |
boxed() |
기본형 → 래퍼 | IntStream.of(1, 2).boxed().toList() |
[1, 2] |
asDoubleStream() / asLongStream() |
기본형 간 변환 | IntStream.of(1, 2).asDoubleStream().boxed().toList() |
[1.0, 2.0] |
parallel() / sequential() |
병렬/순차 전환 | stream.parallel() |
병렬 스트림 |
unordered() |
순서 무시 힌트 (병렬 distinct 등 최적화) | stream.unordered().distinct() |
순서 보장 없음 |
onClose(Runnable) |
close 시 실행 | Files.lines(p).onClose(() -> log("closed")) |
close 시 호출 |
mapMulti는 원소 하나당 0개 이상을 sink로 방출합니다.
Stream.of(1, 2, 3).<Integer>mapMulti((x, sink) -> {
if (x != 2) {
sink.accept(x);
sink.accept(x * 10);
}
});peek만 있는 파이프라인은 아무 일도 하지 않는다.sorted(), distinct()는 전체 원소를 봐야 하므로 무한 스트림에서는 limit을 먼저 걸어야 한다.| 메서드/시그니처 | 설명 | 한 줄 예제 | 결과 |
|---|---|---|---|
forEach(Consumer) |
순회 (병렬 시 순서 보장 없음) | Stream.of(1, 2).forEach(System.out::println) |
1 2 |
forEachOrdered(Consumer) |
순서 보장 순회 | stream.parallel().forEachOrdered(...) |
원래 순서 |
toList() |
불변 리스트 (JDK 16+) | Stream.of(1).toList().getClass().getSimpleName() |
ListN |
collect(Collector) |
수집 | Stream.of(1).collect(Collectors.toList()) |
[1] (ArrayList) |
toArray() / toArray(T[]::new) |
배열 | Stream.of(1, 2).toArray(Integer[]::new) |
Integer[]{1, 2} |
reduce(identity, op) |
누적 (초기값) | Stream.of(1, 2, 3).reduce(0, Integer::sum) |
6 |
reduce(op) |
누적 (Optional) | Stream.of(1, 2, 3).reduce(Integer::max) |
Optional[3] |
reduce(op) 빈 스트림 |
비어 있음 | Stream.<Integer>empty().reduce(Integer::max) |
Optional.empty |
reduce(id, acc, combiner) |
타입 변환 누적 (병렬용) | Stream.of("a","bb").reduce(0,(a,s)->a+s.length(),Integer::sum) |
3 |
count() |
개수 | Stream.of(1, 2).count() |
2 |
min(cmp) / max(cmp) |
최소/최대 Optional | Stream.of(3, 1, 2).min(naturalOrder()) |
Optional[1] |
findFirst() |
첫 원소 | Stream.of(3, 1, 2).filter(x -> x < 3).findFirst() |
Optional[1] |
findAny() |
아무거나 (병렬에서 빠름) | Stream.of(3, 1, 2).findAny().isPresent() |
true |
anyMatch(pred) |
하나라도 | Stream.of(1, 2).anyMatch(x -> x > 1) |
true |
allMatch(pred) |
모두 | Stream.of(1, 2).allMatch(x -> x > 1) |
false |
noneMatch(pred) |
하나도 없음 | Stream.of(1, 2).noneMatch(x -> x > 5) |
true |
allMatch 빈 스트림 |
공허한 참 | Stream.empty().allMatch(x -> false) |
true |
iterator() |
반복자 | Stream.of(1).iterator().next() |
1 |
sum() (기본형) |
합 | IntStream.of(1, 2, 3).sum() |
6 |
average() (기본형) |
평균 OptionalDouble | IntStream.of(1, 2).average() |
OptionalDouble[1.5] |
average() 빈 스트림 |
비어 있음 | IntStream.empty().average() |
OptionalDouble.empty |
max() / min() (기본형) |
OptionalInt | IntStream.of(1, 5, 2).max().getAsInt() |
5 |
summaryStatistics() |
통계 한번에 | IntStream.of(1, 5, 2).summaryStatistics() |
count=3, sum=8, min=1, average=2.666667, max=5 |
getMin/getMax/getAverage/getSum/getCount |
통계 필드 | s.getAverage() |
2.6666666666666665 |
anyMatch/findFirst/limit은 short-circuit: 조건을 만족하면 나머지 원소는 처리하지 않는다.count()는 JDK 9+에서 소스 크기를 알면 파이프라인을 건너뛸 수 있어 peek이 실행되지 않을 수 있다.| 메서드/시그니처 | 설명 | 한 줄 예제 | 결과 |
|---|---|---|---|
toList() |
ArrayList (가변) | emps.stream().map(Emp::name).collect(toList()) |
[Kim, Lee, Park, Choi, Jung] |
toSet() |
HashSet | Stream.of(1, 1, 2).collect(toSet()) |
[1, 2] |
toUnmodifiableList/Set() |
불변 | Stream.of(1).collect(toUnmodifiableList()) |
List12 (불변) |
toCollection(Supplier) |
컬렉션 지정 | Stream.of(3, 1).collect(toCollection(TreeSet::new)) |
[1, 3] |
toMap(kFn, vFn) |
HashMap (키 중복 시 예외) | Stream.of("a", "bb").collect(toMap(identity(), String::length)) |
{bb=2, a=1} |
toMap 중복 키 |
예외 | emps.stream().collect(toMap(Emp::dept, Emp::name)) |
IllegalStateException: Duplicate key Dev |
toMap(k, v, merge) |
중복 병합 | toMap(Emp::dept, Emp::name, (a, b) -> a + "+" + b, TreeMap::new) |
{Dev=Kim+Lee, HR=Jung, Ops=Park+Choi} |
toMap(k, v, merge, mapSupplier) |
Map 구현 지정 | toMap(Emp::name, Emp::salary, (a, b) -> a, TreeMap::new) |
{Choi=6000, Jung=4500, Kim=5000, Lee=7000, Park=4000} |
toMap null 값 |
예외 | Stream.of("a").collect(toMap(s -> s, s -> null)) |
NullPointerException |
toUnmodifiableMap(k, v) |
불변 Map | Stream.of("a").collect(toUnmodifiableMap(s -> s, String::length)) |
{a=1} |
toConcurrentMap(k, v) |
ConcurrentHashMap | .collect(toConcurrentMap(s -> s, String::length)) |
ConcurrentHashMap |
groupingBy(fn) |
Map<K, List<T>> (HashMap) |
emps.stream().collect(groupingBy(Emp::dept)) |
HashMap of lists |
groupingBy(fn, downstream) |
그룹별 수집 | groupingBy(Emp::dept, counting()) |
{Dev=2, HR=1, Ops=2} |
groupingBy(fn, mapFactory, downstream) |
Map 구현 지정 | groupingBy(Emp::dept, TreeMap::new, mapping(Emp::name, toList())) |
{Dev=[Kim, Lee], HR=[Jung], Ops=[Park, Choi]} |
groupingBy + summingInt |
그룹 합 | groupingBy(Emp::dept, TreeMap::new, summingInt(Emp::salary)) |
{Dev=12000, HR=4500, Ops=10000} |
groupingBy + averagingInt |
그룹 평균 | groupingBy(Emp::dept, TreeMap::new, averagingInt(Emp::salary)) |
{Dev=6000.0, HR=4500.0, Ops=5000.0} |
groupingBy + maxBy |
그룹 최대 (Optional) | groupingBy(Emp::dept,maxBy(comparingInt(Emp::salary))) |
{Dev=Optional[Lee], ...} |
groupingBy + collectingAndThen(maxBy, unwrap) |
Optional 벗기기 | 아래 코드 | {Dev=Lee, HR=Jung, Ops=Choi} |
groupingBy + mapping(fn, joining) |
그룹 문자열 | groupingBy(Emp::dept,TreeMap::new,mapping(Emp::name,joining("/"))) |
{Dev=Kim/Lee, HR=Jung, Ops=Park/Choi} |
groupingBy + filtering |
빈 그룹 유지 | 아래 코드 | {Dev=[Kim, Lee], HR=[], Ops=[Choi]} |
filter 후 groupingBy |
빈 그룹 사라짐 | filter(e->e.salary()>=5000).collect(groupingBy(Emp::dept,...)) |
{Dev=[Kim, Lee], Ops=[Choi]} |
groupingBy 중첩 |
2단 그룹 | 아래 코드 | {Dev={30+=[Kim,Lee]},HR={20s=[Jung]},Ops={20s=[Park],30+=[Choi]}} |
groupingByConcurrent(fn) |
병렬용 | .collect(groupingByConcurrent(s -> s)) |
ConcurrentHashMap |
partitioningBy(pred) |
Map<Boolean, List<T>> (항상 두 키) |
partitioningBy(e->e.salary()>=5000,mapping(Emp::name,toList())) |
{false=[Park, Jung], true=[Kim, Lee, Choi]} |
partitioningBy 빈 스트림 |
두 키 모두 존재 | Stream.<Integer>empty().collect(partitioningBy(x -> x > 0)) |
{false=[], true=[]} |
joining() |
결합 | Stream.of("a", "b").collect(joining()) |
"ab" |
joining(sep) |
구분자 | Stream.of("a", "b").collect(joining(", ")) |
"a, b" |
joining(sep, prefix, suffix) |
접두/접미 | Stream.of("a", "b").collect(joining(", ", "[", "]")) |
"[a, b]" |
counting() |
개수 (Long) | Stream.of(1, 2).collect(counting()) |
2 |
summingInt/Long/Double(fn) |
합 | emps.stream().collect(summingInt(Emp::salary)) |
26500 |
averagingInt/Long/Double(fn) |
평균 (Double) | emps.stream().collect(averagingInt(Emp::salary)) |
5300.0 |
summarizingInt/Long/Double(fn) |
통계 객체 | emps.stream().collect(summarizingInt(Emp::salary)) |
count=5, sum=26500, min=4000, average=5300.000000, max=7000 |
minBy(cmp) / maxBy(cmp) |
최소/최대 Optional | 아래 코드 | Optional[Park] |
mapping(fn, downstream) |
변환 후 수집 | emps.stream().collect(mapping(Emp::name, joining(","))) |
"Kim,Lee,Park,Choi,Jung" |
filtering(pred, downstream) |
필터 후 수집 (JDK 9+) | Stream.of(1, 2, 3).collect(filtering(x -> x > 1, toList())) |
[2, 3] |
flatMapping(fn, downstream) |
펼친 후 수집 (JDK 9+) | 아래 코드 | [1, 2, 3] |
reducing(identity, op) |
리듀스 | Stream.of(1, 2, 3).collect(reducing(0, Integer::sum)) |
6 |
reducing(identity, mapper, op) |
변환 + 리듀스 | 아래 코드 | 3 |
reducing(op) |
Optional 리듀스 | Stream.of(1, 2, 3).collect(reducing(Integer::max)) |
Optional[3] |
teeing(c1, c2, merger) |
두 수집 결과 합치기 (JDK 12+) | 아래 코드 | "10/4" |
teeing 최소·최대 |
한 번에 | 아래 코드 | "1..9" |
collectingAndThen(c, finisher) |
수집 후 변환 | 아래 코드 | UnmodifiableRandomAccessList |
collectingAndThen(toList(), List::size) |
후처리 예 | Stream.of(3, 1).collect(collectingAndThen(toList(), List::size)) |
2 |
위 표에서 "아래 코드"로 표시한 예제는 다음과 같습니다.
// groupingBy + collectingAndThen(maxBy, unwrap)
groupingBy(Emp::dept, TreeMap::new,
collectingAndThen(maxBy(comparingInt(Emp::salary)), o -> o.get().name()));
// groupingBy + filtering (빈 그룹 유지)
groupingBy(Emp::dept, TreeMap::new,
filtering(e -> e.salary() >= 5000, mapping(Emp::name, toList())));
// groupingBy 중첩 (2단 그룹)
groupingBy(Emp::dept, TreeMap::new,
groupingBy(e -> e.age() >= 30 ? "30+" : "20s", TreeMap::new,
mapping(Emp::name, toList())));
// minBy(cmp) / maxBy(cmp)
emps.stream().collect(minBy(comparingInt(Emp::age))).map(Emp::name);
// flatMapping(fn, downstream)
Stream.of(List.of(1, 2), List.of(2, 3))
.collect(flatMapping(List::stream, toSet()));
// reducing(identity, mapper, op)
Stream.of("a", "bb").collect(reducing(0, String::length, Integer::sum));
// teeing(c1, c2, merger)
Stream.of(1, 2, 3, 4).collect(
teeing(counting(), summingInt(x -> x), (cnt, sum) -> sum + "/" + cnt));
// teeing 최소·최대
teeing(minBy(naturalOrder()), maxBy(naturalOrder()),
(mn, mx) -> mn.get() + ".." + mx.get());
// collectingAndThen(c, finisher)
Stream.of(3, 1).collect(
collectingAndThen(toList(), Collections::unmodifiableList));groupingBy의 기본 Map은 HashMap(순서 없음). 키 정렬은 TreeMap::new, 삽입 순서는 LinkedHashMap::new.toMap은 키 중복·null 값 모두 예외다. 중복 가능하면 병합 함수를, null 가능하면 groupingBy나 수동 루프를 쓴다.| 메서드/시그니처 | 설명 | 한 줄 예제 | 결과 |
|---|---|---|---|
IntStream.range(a, b) |
[a, b) | IntStream.range(0, 5).boxed().toList() |
[0, 1, 2, 3, 4] |
IntStream.rangeClosed(a, b) |
[a, b] | IntStream.rangeClosed(1, 5).sum() |
15 |
IntStream.of(...) |
값 나열 | IntStream.of(1, 2, 3).sum() |
6 |
IntStream.iterate(seed, fn) |
무한 | IntStream.iterate(0, i -> i + 2).limit(4).boxed().toList() |
[0, 2, 4, 6] |
sum() |
합 (int) | IntStream.of(1, 2, 3).sum() |
6 |
average() |
OptionalDouble | IntStream.of(1, 2).average() |
OptionalDouble[1.5] |
max() / min() |
OptionalInt | IntStream.of(1, 5, 2).min() |
OptionalInt[1] |
count() |
long | IntStream.range(0, 3).count() |
3 |
summaryStatistics() |
IntSummaryStatistics | IntStream.of(1, 5, 2).summaryStatistics() |
count=3, sum=8, min=1, average=2.666667, max=5 |
boxed() |
Stream<Integer> |
IntStream.of(1, 2).boxed().toList() |
[1, 2] |
mapToObj(fn) |
객체 스트림으로 | IntStream.of(1, 2).mapToObj(String::valueOf).collect(joining(",")) |
"1,2" |
asLongStream() / asDoubleStream() |
넓히기 | IntStream.of(1).asLongStream().sum() |
1 |
toArray() |
int[] |
IntStream.of(1, 2).toArray() |
[1, 2] |
LongStream.rangeClosed(1, 20).reduce(1, (a, b) -> a * b) |
20! | 왼쪽 | 2432902008176640000 |
DoubleStream.of(1.5, 2.5).sum() |
실수 합 | 왼쪽 | 4.0 |
Stream.mapToInt/Long/Double |
객체 → 기본형 | Stream.of("a").mapToLong(String::length).sum() |
1 |
Arrays.stream(int[]) |
배열 → IntStream | Arrays.stream(new int[]{1, 2}).sum() |
3 |
list.stream().mapToInt(Integer::intValue).toArray() |
List<Integer> → int[] |
왼쪽 | int[] |
mapToInt 등으로 바꾸거나 Collectors.summingInt를 쓴다.IntStream.average()는 빈 스트림에서 OptionalDouble.empty. .orElse(0)으로 기본값.| 메서드/시그니처 | 설명 | 한 줄 예제 (op = Optional.of("v"), em = empty) | 결과 |
|---|---|---|---|
Optional.of(x) |
값 (null이면 NPE) | Optional.of(null) |
NullPointerException |
Optional.ofNullable(x) |
null 허용 | Optional.ofNullable(null) |
Optional.empty |
Optional.empty() |
빈 값 | Optional.empty() |
Optional.empty |
isPresent() / isEmpty() |
존재 여부 (isEmpty는 JDK 11+) | op.isPresent() + "/" + em.isPresent() |
true/false |
get() |
값 (비면 예외) — 지양 | em.get() |
NoSuchElementException: No value present |
orElse(x) |
기본값 (항상 평가됨) | em.orElse("d") |
"d" |
orElseGet(supplier) |
기본값 지연 평가 | em.orElseGet(() -> "g") |
"g" |
orElseThrow() |
비면 NoSuchElement (JDK 10+) | op.orElseThrow() |
"v" |
orElseThrow(supplier) |
커스텀 예외 | em.orElseThrow(() -> new IllegalArgumentException("없음")) |
IllegalArgumentException: 없음 |
map(fn) |
변환 (fn이 null 반환 시 empty) | op.map(String::toUpperCase) / op.map(s -> null) |
Optional[V] / Optional.empty |
flatMap(fn → Optional) |
중첩 방지 | op.flatMap(s -> Optional.of(s + "!")) |
Optional[v!] |
filter(pred) |
조건 불일치 시 empty | op.filter(s -> s.length() > 3) |
Optional.empty |
ifPresent(consumer) |
있으면 실행 | op.ifPresent(System.out::println) |
v 출력 |
ifPresentOrElse(c, r) |
있으면/없으면 (JDK 9+) | em.ifPresentOrElse(sb::append, () -> sb.append("none")) |
"none" |
or(supplier → Optional) |
대체 Optional (JDK 9+) | em.or(() -> Optional.of("alt")) |
Optional[alt] |
stream() |
0~1개 스트림 (JDK 9+) | 아래 코드 | [v, w] |
equals / toString |
값 기반 | Optional.of("a").equals(Optional.of("a")) |
true |
OptionalInt.of(3).getAsInt() |
기본형 Optional | OptionalInt.empty().orElse(-1) |
-1 |
OptionalDouble |
OptionalDouble.of(1.5) |
OptionalDouble[1.5] |
|
orElse vs orElseGet 평가 |
orElse 인자는 항상 실행 | op.orElse(expensive()) 실행 / op.orElseGet(()->expensive()) 생략 |
[called] (orElse만) |
stream() 예제는 다음과 같습니다.
Stream.of(op, em, Optional.of("w")).flatMap(Optional::stream).toList();Optional은 반환 타입으로만 쓴다. 필드, 파라미터, 컬렉션 원소로 쓰지 않는다.isPresent() + get() 조합은 null 체크와 다를 게 없다. map/orElse/ifPresent 체인으로 쓴다.| 인터페이스 | 메서드 | 설명 | 한 줄 예제 | 결과 |
|---|---|---|---|---|
Function<T,R> |
R apply(T) |
변환 | Function<Integer,Integer> f = x -> x + 1; f.apply(1) |
2 |
Function.andThen(g) |
f 후 g | 합성 | f.andThen(x -> x * 2).apply(1) |
4 |
Function.compose(g) |
g 후 f | 합성 (역순) | f.compose(x -> x * 2).apply(1) |
3 |
Function.identity() |
항등 | toMap(identity(), ...) |
Function.<String>identity().apply("a") |
"a" |
BiFunction<T,U,R> |
R apply(T, U) |
2인자 변환 | 아래 코드 | 30 |
UnaryOperator<T> |
T apply(T) |
같은 타입 변환 | ((UnaryOperator<Integer>) x -> -x).apply(3) |
-3 |
BinaryOperator<T> |
T apply(T, T) |
같은 타입 2인자 | BinaryOperator.minBy(naturalOrder()).apply(3, 1) |
1 |
Predicate<T> |
boolean test(T) |
조건 | Predicate<String> ne = s -> !s.isEmpty(); ne.test("") |
false |
Predicate.and/or/negate |
조합 | ne.and(s -> s.length() < 3).test("ab") |
true |
|
Predicate.not(p) |
부정 (JDK 11+) | 메서드 참조 부정 | Stream.of("", "a").filter(Predicate.not(String::isEmpty)).toList() |
[a] |
Predicate.isEqual(x) |
동등 조건 | Predicate.isEqual("a").test("a") |
true |
|
BiPredicate<T,U> |
boolean test(T, U) |
2인자 조건 | ((BiPredicate<String,Integer>)(s,n)->s.length()==n).test("ab",2) |
true |
Supplier<T> |
T get() |
생성/지연 | ((Supplier<List<String>>) ArrayList::new).get() |
[] |
Consumer<T> |
void accept(T) |
소비 | 아래 코드 | "hi!" |
BiConsumer<T,U> |
void accept(T, U) |
2인자 소비 | BiConsumer<String,Integer> bc = map::put; bc.accept("a", 1) |
{a=1} |
Runnable |
void run() |
인자·반환 없음 | new Thread(() -> work()).start() |
실행 |
Callable<V> |
V call() throws Exception |
반환 + 예외 | executor.submit(() -> 42).get() |
42 |
Comparator<T> |
int compare(T, T) |
비교 | 아래 코드 | -1 |
위 표에서 "아래 코드"로 표시한 예제는 다음과 같습니다.
// BiFunction<T,U,R>
((BiFunction<Integer, Integer, Integer>) Integer::sum)
.andThen(x -> x * 10).apply(1, 2);
// Consumer<T>
Consumer<String> c = sb::append;
c.andThen(s -> sb.append("!")).accept("hi");
// Comparator<T>
((Comparator<String>) (a, b) -> a.length() - b.length())
.compare("a", "bb");기본형 특화 (박싱 회피):
| 인터페이스 | 메서드 | 한 줄 예제 | 결과 |
|---|---|---|---|
IntFunction<R> |
R apply(int) |
((IntFunction<String>) Integer::toString).apply(5) |
"5" |
ToIntFunction<T> |
int applyAsInt(T) |
((ToIntFunction<String>) String::length).applyAsInt("abc") |
3 |
ToIntBiFunction<T,U> |
int applyAsInt(T, U) |
(a, b) -> a.length() + b.length() → ("a", "bb") |
3 |
IntToDoubleFunction |
double applyAsDouble(int) |
i -> i / 2.0 → 3 |
1.5 |
IntPredicate |
boolean test(int) |
IntStream.range(0, 5).filter(((IntPredicate) i -> i > 2).negate()) |
[0, 1, 2] |
IntUnaryOperator |
int applyAsInt(int) |
i -> i * i → 4 |
16 |
IntBinaryOperator |
int applyAsInt(int, int) |
((IntBinaryOperator) Math::max).applyAsInt(2, 9) |
9 |
IntSupplier |
int getAsInt() |
() -> 7 |
7 |
IntConsumer |
void accept(int) |
IntStream.range(0, 3).forEach(i -> ...) |
실행 |
ObjIntConsumer<T> |
void accept(T, int) |
(s, i) -> ... |
실행 |
DoubleUnaryOperator |
double applyAsDouble(double) |
((DoubleUnaryOperator) Math::sqrt).applyAsDouble(16) |
4.0 |
LongSupplier |
long getAsLong() |
((LongSupplier) System::nanoTime).getAsLong() > 0 |
true |
BooleanSupplier |
boolean getAsBoolean() |
() -> true |
true |
Long*, Double* 변형 |
Int와 동일 패턴 | LongFunction, ToLongFunction, DoubleBinaryOperator … |
메서드 참조 4종:
| 종류 | 형태 | 예제 | 동등 람다 |
|---|---|---|---|
| 정적 메서드 | Class::staticMethod |
Integer::parseInt |
s -> Integer.parseInt(s) |
| 특정 객체의 인스턴스 메서드 | obj::method |
sb::append |
s -> sb.append(s) |
| 임의 객체의 인스턴스 메서드 | Class::method |
String::toUpperCase |
s -> s.toUpperCase() |
| 생성자 | Class::new / int[]::new |
아래 코드 | s -> new StringBuilder(s) / 3 |
생성자 참조 예제는 다음과 같습니다.
StringBuilder::new;
((IntFunction<int[]>) int[]::new).apply(3).length; // 3AtomicInteger나 배열, 또는 IntStream.range로 인덱스를 돈다.정렬 후 limit. 그룹별 상위 N은 groupingBy + collectingAndThen으로 각 그룹 리스트를 다시 정렬·자른다. 데이터가 크면 그룹별 PriorityQueue로 힙을 유지하는 편이 낫다.
emps.stream().sorted(Comparator.comparingInt(Emp::salary).reversed()).limit(3).map(Emp::name).toList();
// 출력: [Lee, Choi, Kim]
emps.stream().collect(Collectors.groupingBy(Emp::dept, TreeMap::new,
Collectors.collectingAndThen(Collectors.toList(),
l -> l.stream().sorted(Comparator.comparingInt(Emp::salary).reversed()).limit(1).map(Emp::name).toList())));
// 출력: {Dev=[Lee], HR=[Jung], Ops=[Choi]}groupingBy 안에 groupingBy. 각 단계에 TreeMap::new를 주면 출력 순서가 안정된다. 3단 이상은 record 키(record Key(String dept, String band))로 한 단계로 펴는 편이 읽기 쉽다.
emps.stream().collect(Collectors.groupingBy(Emp::dept, TreeMap::new,
Collectors.groupingBy(e -> e.age() >= 30 ? "30+" : "20s", TreeMap::new,
Collectors.mapping(Emp::name, Collectors.toList()))));
// 출력: {Dev={30+=[Kim, Lee]}, HR={20s=[Jung]}, Ops={20s=[Park], 30+=[Choi]}}
// 부서별 급여 합계와 인원을 한 번에 (teeing)
emps.stream().collect(Collectors.groupingBy(Emp::dept, TreeMap::new,
Collectors.teeing(Collectors.summingInt(Emp::salary), Collectors.counting(),
(sum, cnt) -> sum + "/" + cnt + "명")));
// 출력: {Dev=12000/2명, HR=4500/1명, Ops=10000/2명}Map.Entry.comparingByValue()로 정렬하고, 순서를 보존하는 LinkedHashMap::new를 toMap의 4번째 인자로 넘긴다. 같은 값은 키로 2차 정렬한다. toMap 기본(HashMap)으로 모으면 정렬이 사라진다.
Map<String, Integer> wordCount = new HashMap<>(Map.of("java", 5, "kotlin", 2, "go", 8, "rust", 5));
wordCount.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed().thenComparing(Map.Entry.comparingByKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
// 출력: {go=8, java=5, rust=5, kotlin=2}
// 최대값 엔트리 하나만
wordCount.entrySet().stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse("");
// 출력: go스트림에는 인덱스가 없다. IntStream.range(0, size)로 인덱스를 돌며 list.get(i)를 쓰거나, AtomicInteger로 카운터를 캡처한다. 두 리스트를 짝지을 때(zip)도 같은 방법이다.
List<String> names = List.of("Kim", "Lee", "Park");
IntStream.range(0, names.size()).mapToObj(i -> (i + 1) + "." + names.get(i)).collect(Collectors.joining(" "));
// 출력: 1.Kim 2.Lee 3.Park
var idx = new AtomicInteger();
names.stream().map(n -> idx.getAndIncrement() + ":" + n).toList();
// 출력: [0:Kim, 1:Lee, 2:Park]
List<Integer> scores = List.of(10, 20);
IntStream.range(0, Math.min(names.size(), scores.size())).mapToObj(i -> names.get(i) + "=" + scores.get(i)).toList();
// 출력: [Kim=10, Lee=20]참/거짓 두 그룹은 partitioningBy(빈 그룹도 키가 존재). 셋 이상은 분류 함수를 만들어 groupingBy.
emps.stream().collect(Collectors.partitioningBy(e -> e.age() >= 30, Collectors.mapping(Emp::name, Collectors.toList())));
// 출력: {false=[Park, Jung], true=[Kim, Lee, Choi]}
emps.stream().collect(Collectors.groupingBy(
e -> e.salary() >= 6000 ? "HIGH" : e.salary() >= 4500 ? "MID" : "LOW",
TreeMap::new, Collectors.mapping(Emp::name, Collectors.toList())));
// 출력: {HIGH=[Lee, Choi], LOW=[Park], MID=[Kim, Jung]}peek을 단계 사이에 끼우면 각 원소가 어느 단계까지 갔는지 보인다. 출력 순서를 보면 원소 하나가 파이프라인 전체를 통과한 뒤 다음 원소가 시작된다(수직 처리). findFirst 같은 short-circuit은 필요한 만큼만 처리한다.
Stream.of("a", "bb", "ccc")
.filter(s -> s.length() > 1)
.peek(s -> System.out.println(" filtered: " + s))
.map(String::toUpperCase)
.peek(s -> System.out.println(" mapped: " + s))
.toList();
// 출력:
// filtered: bb
// mapped: BB
// filtered: ccc
// mapped: CCC
// 결과: [BB, CCC]
List<String> seen = new ArrayList<>();
Stream.of(1, 2, 3, 4).peek(x -> seen.add("p" + x)).filter(x -> x > 1).findFirst();
// 출력: seen = [p1, p2] — 3, 4는 처리되지 않음distinct()는 equals 기준이라 "부서가 같은 첫 사람만" 같은 조건에는 못 쓴다. Set.add의 반환값을 필터로 쓰거나, toMap의 병합 함수로 첫 값을 유지한다. 금액 합계는 reduce(BigDecimal.ZERO, BigDecimal::add).
Set<String> seenDept = new HashSet<>();
emps.stream().filter(e -> seenDept.add(e.dept())).map(Emp::name).toList();
// 출력: [Kim, Park, Jung]
new ArrayList<>(emps.stream()
.collect(Collectors.toMap(Emp::dept, e -> e, (a, b) -> a, LinkedHashMap::new)).values())
.stream().map(Emp::name).toList();
// 출력: [Kim, Park, Jung]
Stream.of("1.10", "2.20").map(BigDecimal::new).reduce(BigDecimal.ZERO, BigDecimal::add);
// 출력: 3.30
emps.stream().filter(e -> e.dept().equals("None")).mapToInt(Emp::salary).average().orElse(0);
// 출력: 0.0연속 구간은 IntStream.range + subList. 빈도순은 groupingBy(counting) 후 값으로 정렬.
List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntStream.range(0, nums.size() - 1).mapToObj(i -> nums.subList(i, i + 2)).toList();
// 출력: [[1, 2], [2, 3], [3, 4], [4, 5]]
Stream.of("b", "a", "b", "c", "b", "a")
.collect(Collectors.groupingBy(s -> s, Collectors.counting()))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.map(e -> e.getKey() + ":" + e.getValue()).toList();
// 출력: [b:3, a:2, c:1]Stream<Integer> s = Stream.of(1);
s.count();
s.count(); // IllegalStateException: stream has already been operated upon or closed
// 필요할 때마다 list.stream()으로 새로 만든다.list.stream().map(x -> { save(x); return x; }); // save 호출 안 됨 (lazy)
list.stream().peek(this::save); // 역시 실행 안 됨
list.forEach(this::save); // 부수효과는 forEach로toMap 키 중복 / null 값| 코드 | 결과 |
|---|---|
emps.stream().collect(toMap(Emp::dept, Emp::name)) |
IllegalStateException: Duplicate key Dev |
toMap(Emp::dept, Emp::name, (a, b) -> a) |
첫 값 유지 |
toMap(s -> s, s -> null) |
NullPointerException |
groupingBy(Emp::dept, mapping(Emp::name, toList())) |
중복을 리스트로 |
Stream.toList()를 수정Stream.of(1).toList().add(2); // UnsupportedOperationException
Stream.of(1).collect(Collectors.toList()).add(2); // OK (ArrayList)
new ArrayList<>(Stream.of(1).toList()).add(2); // OKorElse에 비싼 호출cache.get(key).orElse(loadFromDb(key)); // 값이 있어도 loadFromDb 실행됨
cache.get(key).orElseGet(() -> loadFromDb(key)); // 없을 때만 실행Optional.get() / isPresent()+get() 남용if (opt.isPresent()) return opt.get().name(); else return "?"; // null 체크와 같음
return opt.map(Emp::name).orElse("?"); // 관용구int count = 0;
list.forEach(x -> count++); // 컴파일 오류: effectively final 아님
AtomicInteger count = new AtomicInteger();
list.forEach(x -> count.incrementAndGet()); // OK (단, 그냥 count()나 for문이 더 낫다)List<Integer> out = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(out::add); // 경쟁 조건, 크기가 1000이 아닐 수 있음
IntStream.range(0, 1000).parallel().boxed().toList(); // 수집기를 쓰면 안전
IntStream.range(0, 1000).parallel().forEach(...) // 출력 순서 보장 없음 → forEachOrderedsorted().findFirst()로 최소값list.stream().sorted().findFirst(); // 전체 정렬 O(n log n)
list.stream().min(Comparator.naturalOrder()); // O(n)Stream.of(1, 2, 3).sum(); // 컴파일 오류: Stream<Integer>에 sum 없음
Stream.of(1, 2, 3).mapToInt(Integer::intValue).sum(); // 6
IntStream.of(1, 2).collect(Collectors.toList()); // 컴파일 오류: IntStream.collect는 3인자
IntStream.of(1, 2).boxed().toList(); // [1, 2]Collectors 설계Optional 설계 의도, 안티패턴Collector<T, A, R>, Function<? super T, ? extends R> 시그니처 읽기groupingBy 결과 Map 조작, Comparator 체인