공공부하자개발 · 영어 학습 노트
자바
명령어자주 쓰는 API 참조0/8 완료
  • 01String · StringBuilder · 정규식
  • 02Collections · Arrays · Map
  • 03Stream · Collectors · 함수형 인터페이스
  • 04Files · Path · I/O
  • 05java.time · BigDecimal · 기타 유틸
  • 06Oracle SQL
  • 07예외 · 디버깅
  • 08Spring Boot 설정 키
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 명령어 › 03 / 8

Stream · Collectors · 함수형 인터페이스

섹션 11진행 0 / 8

Stream · Collectors · 함수형 인터페이스 명령어

스트림 생성 → 중간 연산 → 최종 연산 → Collectors의 전체 표, 기본형 스트림, Optional 전 메서드, java.util.function 인터페이스 표를 한 페이지에 모았다. 원리(지연 평가, 파이프라인)는 고급 04 레슨에서, 여기서는 시그니처와 결과만 확인한다. Ctrl+F로 메서드명(예: groupingBy, teeing, mapMulti, orElseGet)을 검색하면 된다. 모든 결과는 JDK 21 실행값이다.

이 페이지의 예제 데이터:

java
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));
01한눈에 보기
메서드 설명 예제 결과
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"
02스트림 생성
메서드/시그니처 설명 한 줄 예제 결과
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
  • 스트림은 1회용. 최종 연산 후 다시 쓰면 IllegalStateException: stream has already been operated upon or closed.
03중간 연산 (lazy)
메서드/시그니처 설명 한 줄 예제 결과
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로 방출합니다.

java
Stream.of(1, 2, 3).<Integer>mapMulti((x, sink) -> {
    if (x != 2) {
        sink.accept(x);
        sink.accept(x * 10);
    }
});
  • 중간 연산은 최종 연산이 호출될 때 한 번에 실행된다. peek만 있는 파이프라인은 아무 일도 하지 않는다.
  • sorted(), distinct()는 전체 원소를 봐야 하므로 무한 스트림에서는 limit을 먼저 걸어야 한다.
04최종 연산
메서드/시그니처 설명 한 줄 예제 결과
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이 실행되지 않을 수 있다.
05Collectors 전체
메서드/시그니처 설명 한 줄 예제 결과
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

위 표에서 "아래 코드"로 표시한 예제는 다음과 같습니다.

java
// 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나 수동 루프를 쓴다.
06기본형 스트림 (IntStream / LongStream / DoubleStream)
메서드/시그니처 설명 한 줄 예제 결과
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)으로 기본값.
07Optional 전 메서드
메서드/시그니처 설명 한 줄 예제 (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() 예제는 다음과 같습니다.

java
Stream.of(op, em, Optional.of("w")).flatMap(Optional::stream).toList();
  • Optional은 반환 타입으로만 쓴다. 필드, 파라미터, 컬렉션 원소로 쓰지 않는다.
  • isPresent() + get() 조합은 null 체크와 다를 게 없다. map/orElse/ifPresent 체인으로 쓴다.
08함수형 인터페이스 (java.util.function)
인터페이스 메서드 설명 한 줄 예제 결과
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

위 표에서 "아래 코드"로 표시한 예제는 다음과 같습니다.

java
// 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

생성자 참조 예제는 다음과 같습니다.

java
StringBuilder::new;
((IntFunction<int[]>) int[]::new).apply(3).length; // 3
  • 람다에서 캡처하는 지역 변수는 effectively final이어야 한다. 카운터가 필요하면 AtomicInteger나 배열, 또는 IntStream.range로 인덱스를 돈다.
09실무 스니펫

스니펫 1: 상위 N개 (전체 / 그룹별)

정렬 후 limit. 그룹별 상위 N은 groupingBy + collectingAndThen으로 각 그룹 리스트를 다시 정렬·자른다. 데이터가 크면 그룹별 PriorityQueue로 힙을 유지하는 편이 낫다.

java
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]}

스니펫 2: 중첩 그룹핑 (부서 → 연령대 → 이름)

groupingBy 안에 groupingBy. 각 단계에 TreeMap::new를 주면 출력 순서가 안정된다. 3단 이상은 record 키(record Key(String dept, String band))로 한 단계로 펴는 편이 읽기 쉽다.

java
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명}

스니펫 3: Map을 값 기준 정렬해 LinkedHashMap으로

Map.Entry.comparingByValue()로 정렬하고, 순서를 보존하는 LinkedHashMap::new를 toMap의 4번째 인자로 넘긴다. 같은 값은 키로 2차 정렬한다. toMap 기본(HashMap)으로 모으면 정렬이 사라진다.

java
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

스니펫 4: 인덱스 포함 순회 (번호 매기기, zip)

스트림에는 인덱스가 없다. IntStream.range(0, size)로 인덱스를 돌며 list.get(i)를 쓰거나, AtomicInteger로 카운터를 캡처한다. 두 리스트를 짝지을 때(zip)도 같은 방법이다.

java
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]

스니펫 5: 조건별 분리 (2분할 partitioningBy, N분할 groupingBy)

참/거짓 두 그룹은 partitioningBy(빈 그룹도 키가 존재). 셋 이상은 분류 함수를 만들어 groupingBy.

java
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]}

스니펫 6: 스트림 디버깅 peek과 지연 평가 확인

peek을 단계 사이에 끼우면 각 원소가 어느 단계까지 갔는지 보인다. 출력 순서를 보면 원소 하나가 파이프라인 전체를 통과한 뒤 다음 원소가 시작된다(수직 처리). findFirst 같은 short-circuit은 필요한 만큼만 처리한다.

java
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는 처리되지 않음

스니펫 7: 키 기준 중복 제거와 BigDecimal 합계

distinct()는 equals 기준이라 "부서가 같은 첫 사람만" 같은 조건에는 못 쓴다. Set.add의 반환값을 필터로 쓰거나, toMap의 병합 함수로 첫 값을 유지한다. 금액 합계는 reduce(BigDecimal.ZERO, BigDecimal::add).

java
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

스니펫 8: 슬라이딩 윈도우와 빈도순 정렬

연속 구간은 IntStream.range + subList. 빈도순은 groupingBy(counting) 후 값으로 정렬.

java
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]
10자주 하는 실수

❌ 실수 1: 스트림 재사용

java
Stream<Integer> s = Stream.of(1);
s.count();
s.count();   // IllegalStateException: stream has already been operated upon or closed
// 필요할 때마다 list.stream()으로 새로 만든다.

❌ 실수 2: 최종 연산 없이 끝냄 (아무 일도 안 일어남)

java
list.stream().map(x -> { save(x); return x; });   // save 호출 안 됨 (lazy)
list.stream().peek(this::save);                    // 역시 실행 안 됨
list.forEach(this::save);                          // 부수효과는 forEach로

❌ 실수 3: 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())) 중복을 리스트로

❌ 실수 4: Stream.toList()를 수정

java
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);         // OK

❌ 실수 5: orElse에 비싼 호출

java
cache.get(key).orElse(loadFromDb(key));         // 값이 있어도 loadFromDb 실행됨
cache.get(key).orElseGet(() -> loadFromDb(key)); // 없을 때만 실행

❌ 실수 6: Optional.get() / isPresent()+get() 남용

java
if (opt.isPresent()) return opt.get().name(); else return "?";   // null 체크와 같음
return opt.map(Emp::name).orElse("?");                             // 관용구

❌ 실수 7: 람다에서 지역 변수 변경

java
int count = 0;
list.forEach(x -> count++);   // 컴파일 오류: effectively final 아님
AtomicInteger count = new AtomicInteger();
list.forEach(x -> count.incrementAndGet());   // OK (단, 그냥 count()나 for문이 더 낫다)

❌ 실수 8: 병렬 스트림에서 순서·공유 상태

java
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(...)          // 출력 순서 보장 없음 → forEachOrdered

❌ 실수 9: sorted().findFirst()로 최소값

java
list.stream().sorted().findFirst();          // 전체 정렬 O(n log n)
list.stream().min(Comparator.naturalOrder()); // O(n)

❌ 실수 10: 기본형 스트림과 객체 스트림 혼동

java
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]
11관련 레슨
  • 고급 04 스트림: 파이프라인 원리, 지연 평가, 병렬 스트림, Collectors 설계
  • 고급 03 람다: 함수형 인터페이스 정의, 메서드 참조, 캡처 규칙, 커스텀 함수형 인터페이스
  • 고급 05 Optional: Optional 설계 의도, 안티패턴
  • 고급 01 제네릭과 와일드카드: Collector<T, A, R>, Function<? super T, ? extends R> 시그니처 읽기
  • 중급 05 컬렉션 프레임워크: 스트림 결과를 담는 컬렉션 선택
  • 명령어 02 Collections: groupingBy 결과 Map 조작, Comparator 체인
목차
  • 한눈에 보기
  • 스트림 생성
  • 중간 연산 (lazy)
  • 최종 연산
  • Collectors 전체
  • 기본형 스트림 (IntStream / LongStream / DoubleStream)
  • Optional 전 메서드
  • 함수형 인터페이스 (java.util.function)
  • 실무 스니펫
  • 자주 하는 실수
  • 관련 레슨
참고 영상 · 인터넷 연결 시 유튜브 검색이 열립니다자바 Stream · Collectors · 함수형 인터페이스명령어 Stream Collectors groupingBy
이전02 Collections · Arrays · Map다음04 Files · Path · I/O