Java의 Lambda와 Stream은 Java 8에서 도입된 기능으로, 컬렉션 데이터를 더 간결하고 선언적으로 처리하도록 도와준다.
🧩 Lambda
Lambda는 익명 함수를 간단히 표현하는 문법이다.
/* 기존 익명 클래스 방식 */
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("실행");
}
};
/* Lambda 방식 */
Runnable runnable = () -> System.out.println("실행");
람다식에서 ()는 매개변수, -> 뒤는 실행할 코드이다.
Lambda 기본 문법
(매개변수) -> { 실행문 }
// 매개변수 없음
() -> System.out.println("Hello");
// 매개변수 1개: 괄호 생략 가능
x -> x * 2;
// 매개변수 2개
(a, b) -> a + b;
// 여러 줄 코드
(a, b) -> {
int sum = a + b;
return sum;
};
함수형 인터페이스
Lambda는 아무 인터페이스에나 사용할 수 있는 것이 아니라 추상 메서드가 1개만 있는 인터페이스에만 사용할 수 있는데, 이를 Functional Interface라고 한다.
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
Calculator add = (a, b) -> a + b;
System.out.println(add.calculate(3, 5)); // 8
| 인터페이스 | 역할 | 예시 |
Predicate<T> |
조건 검사, boolean 반환 |
x -> x > 10 |
Function<T> |
값 변환 | s -> s.length() |
Consumer<T> |
값 소비 (반환 없음) | x -> System.out.println(x) |
Supplier<T> |
값 생성 (입력 없음) | () -> new Random().nextInt() |
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
메서드 참조
Lambda를 더 짧게 표현할 수 있는 문법이다.
// 기본 람다 표현식
names.forEach(name -> System.out.println(name));
// 메서드 참조 방식
names.forEach(System.out::println);
생성자 참조도 가능하다.
Supplier<List<String>> supplier = ArrayList::new;
🧩 Stream
Stream은 컬렉션, 배열 등의 데이터를 반복 처리하고 가공하기 위한 API다.
중요한 점은 Stream은 데이터를 저장하는 자료구조가 아니라 데이터를 처리하는 흐름(파이프라인)이라는 것이다.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.forEach(System.out::println);
// 4
// 16
Stream 처리 구조
Stream은 보통 다음 세 단계로 구성된다.
데이터 소스 → 중간 연산 → 최종 연산
List<String> names = List.of("Kim", "Lee", "Park", "Choi");
List<String> result = names.stream() // 데이터 소스
.filter(name -> name.length() >= 4) // 중간 연산
.map(String::toUpperCase) // 중간 연산
.toList(); // 최종 연산
// 결과: [ PARK, CHOI ]
자주 사용되는 Stream 연산
- filter(): 조건에 맞는 데이터 선택
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.toList();
// 결과: [2, 4]
map(): 데이터를 다른 형태로 변환
List<String> names = List.of("kim", "lee", "park");
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.toList();
// 결과: [KIM, LEE, PARK]
sorted(): 정렬
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 오름차순 정렬
List<Integer> sorted = numbers.stream()
.sorted()
.toList();
// 결과: [1, 2, 3, 4, 5]
// 내림차순 정렬
List<Integer> reversed = numbers.stream()
.sorted(Comparator.reverseOrder())
.toList();
// 결과: [5, 4, 3, 2, 1]
distinct(): 중복 제거
List<Integer> values = List.of(1, 2, 2, 3, 3, 3);
List<Integer> unique = values.stream()
.distinct()
.toList();
// 결과: [1, 2, 3]
limit()/skip(): 일부 데이터 처리
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.stream().skip(2).forEach(System.out::println);
// 3
// 4
// 5
numbers.stream().limit(2).forEach(System.out::println);
// 1
// 2
numbers.stream().skip(2).limit(2).forEach(System.out::println);
// 3
// 4
count(),findFirst(),anyMatch()
long count = numbers.stream()
.filter(n -> n > 2)
.count();
Optional<Integer> first = numbers.stream() // 결과가 없을 수 있으므로 Optional 반환
.filter(n -> n % 2 == 0)
.findFirst();
boolean exists = numbers.stream()
.anyMatch(n -> n > 4);
System.out.println(count); // 3
first.ifPresent(System.out::println); // 2
System.out.println(exists); // true
collect()/toList(): 결과를 컬렉션으로 만들기
List<String> names = List.of("kim", "lee", "park");
// Java 16+
List<String> result1 = names.stream()
.map(String::toUpperCase)
.toList();
// Java 8
List<String> result2 = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(result1); // [KIM, LEE, PARK]
System.out.println(result2); // [KIM, LEE, PARK]
map()과 flatMap()의 차이
map(): 각 요소를 하나의 값으로 변환
List<String> names = List.of("kim", "lee");
List<Integer> lengths = names.stream()
.map(String::length)
.toList();
// 결과: [3, 3]
flatMap(): 중첩된 구조를 평평하게 펼칠 때 사용
List<List<Integer>> data = List.of(
List.of(1, 2),
List.of(3, 4)
);
List<Integer> result = data.stream()
.flatMap(List::stream)
.toList();
// 결과: [1, 2, 3, 4]
Stream 사용 시 주의점
- 원본 컬렉션을 직접 변경하지 않음
List<Integer> original = List.of(1, 2, 3, 4);
List<Integer> result = original.stream()
.filter(n -> n % 2 == 0)
.toList();
System.out.println(original); // [1, 2, 3, 4]
System.out.println(result); // [2, 4]
- 한 번만 사용 가능: 최종 연산이 실행된 Stream은 다시 사용할 수 없다.
Stream<Integer> stream = numbers.stream();
stream.forEach(System.out::println);
// 오류 발생: 이미 사용된 stream
// stream.count();
- 지연 연산
filter, map과 같은 중간 연산은 바로 실행되지 않고 toList, forEach, count와 같은 최종 연산이 호출될 때 실제로 처리가 시작된다.
Stream<Integer> stream = numbers.stream()
.filter(n -> {
System.out.println("검사: " + n);
return n % 2 == 0;
});
// 아직 실행되지 않음
long count = stream.count(); // 여기서 실제 실행
- 부작용 있는 코드는 피하는 것이 좋음: Stream 내부에서 외부 변수를 변경하면 코드가 복잡해지고, 특히 병렬 처리 시 문제가 생길 수 있다.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 권장되지 않는 방식
// List<Integer> result = new ArrayList<>();
// numbers.stream()
// .filter(n -> n % 2 == 0)
// .forEach(result::add);
// 권장 방식
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.toList();
System.out.println(result); // [2, 4]
병렬 Stream
여러 CPU 코어를 사용하고 싶다면 parallelStream()을 사용할 수도 있다.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
long count = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.count();
System.out.println(count); // 2
하지만 항상 더 빠른 것은 아니다.
- 데이터가 적으면 오히려 더 느릴 수 있음
- 순서 보장이 필요한 작업은 비용이 더 커질 수 있음
- 공유 변수 변경 등 부작용이 있으면 위험함
- I/O 작업보다는 CPU 연산이 많은 작업에 적합함
Lambda와 Stream의 관계
- Lambda: 동작(함수)을 값처럼 전달하는 문법
- Stream: 데이터를 처리하는 파이프라인 API
Stream의 filter, map, sorted 등에 Lambda를 전달해 원하는 처리 규칙을 작성할 수 있다.
List<String> result = List.of("apple", "banana", "kiwi", "avocado")
.stream()
.filter(word -> word.startsWith("a"))
.map(String::toUpperCase)
.sorted()
.toList();
System.out.println(result); // [APPLE, AVOCADO]'CS > Java' 카테고리의 다른 글
| [Java]Comparable과 Comparator (0) | 2026.08.01 |
|---|---|
| [Java]Generics (0) | 2026.07.26 |
| Java로 멀티 쓰레드 찍먹하기 (0) | 2026.02.21 |