CS/Java

[Java]Lambda 와 Stream

munsik22 2026. 7. 31. 22:55

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