TIL #24 람다식과 스트림

HYEON JIN CHOI·2024년 5월 26일

Java 8 은 언어의 현대화와 프로그래밍 생산성을 향상시키기 위해 람다식스트림을 도입했다. 람다식Java 에 함수형 프로그래밍의 개념을 도입하여 코드를 간결하게 만들고, 스트림은 대량의 데이터를 효율적으로 처리할 수 있는 강력한 도구를 제공한다. 이번 글에서는 람다식과 스트림의 개념과 필요성, 기본 사용법, 그리고 이 둘의 결합 활용 방법에 대해 자세히 알아보려고 한다.

1. 람다식의 기본 개념

1.1. 람다식의 개념

람다식익명 함수(Anonymous Function)라고도 불리며, 코드 내에서 이름 없는 함수를 정의하고 사용할 수 있는 표현식이다. Java 에서는 주로 간결하고 직관적인 방식으로 코드를 작성할 수 있도록 하기 위해 람다식을 도입했다.

1.2. 람다식의 필요성

람다식이 도입되기 전에는 Java 에서 함수형 프로그래밍을 구현하는 것이 복잡하고 번거로웠다. 특히, 익명 내부 클래스를 사용할 때 많은 반복되는 코드가 필요했다. 람다식을 사용하면 이러한 복잡성을 줄이고, 코드를 더욱 간결하고 가독성 있게 작성할 수 있다. 람다식의 주요 필요성은 다음과 같다.

  • 코드 간결화 : 람다식을 사용하면 코드를 간결하게 작성할 수 있다.
  • 가독성 향상 : 코드의 가독성이 향상되어 유지보수가 용이해진다.
  • 함수형 프로그래밍 도입 : 자바에 함수형 프로그래밍의 개념을 도입하여 더 직관적이고 효율적인 코딩을 가능하게 한다.

2. 람다식의 기본 사용법

2.1. 람다식 의 문법

람다식의 문법은 간단하고 직관적이다. 다음은 람다식의 기본 문법이다.

(parameters) -> expression

또는 블록을 사용하는 경우

(parameters) -> { statements; }

예를 들어, 두 수를 더하는 람다식은 다음과 같다.

(int x, int y) -> x + y

2.2. 함수형 인터페이스

람다식을 사용하기 위해서는 함수형 인터페이스(Functional Interface)가 필요하다. 함수형 인터페이스는 단 하나의 추상 메서드만을 가지는 인터페이스를 의미한다. Java 8 에서는 @FunctionalInterface 어노테이션을 제공하여 함수형 인터페이스임을 명시할 수 있다. 다음은 함수형 인터페이스의 예이다.

@FunctionalInterface
public interface Calculator {
    int calculate(int x, int y);
}

람다식을 사용하여 이 인터페이스를 구현할 수 있다.

Calculator adder = (int x, int y) -> x + y;
System.out.println(adder.calculate(10, 20)); // 출력: 30

2.3. 자주 사용되는 함수형 인터페이스

Java 8 은 몇 가지 자주 사용되는 함수형 인터페이스를 제공한다. 대표적인 예는 다음과 같다.

  • Predicate<T> : 입력을 받아 boolean 값을 반환한다.
  • Function<T, R> : 입력을 받아 변환된 값을 반환한다.
  • Supplier<T> : 입력 없이 값을 반환한다.
  • Consumer<T> : 입력을 받아 소비하고 반환값이 없다.
  • BiFunction<T, U, R> : 두 입력을 받아 변환된 값을 반환한다.

다음은 이러한 인터페이스를 사용하는 예이다.

Predicate<String> isEmpty = s -> s.isEmpty();
Function<String, Integer> length = s -> s.length();
Supplier<String> supplier = () -> "Hello, World!";
Consumer<String> printer = s -> System.out.println(s);

System.out.println(isEmpty.test("")); // 출력: true
System.out.println(length.apply("Hello")); // 출력: 5
System.out.println(supplier.get()); // 출력: Hello, World!
printer.accept("Hello, Lambda!"); // 출력: Hello, Lambda!

3. 스트림의 기본 개념

3.1. 스트림의 개념

스트림(Stream)은 컬렉션, 배열 등의 데이터 소스를 처리하기 위한 추상화된 도구이다. 스트림을 사용하면 대량의 데이터를 효율적으로 처리할 수 있으며, 함수형 프로그래밍의 개념을 도입하여 복잡한 데이터 처리 작업을 간단하게 표현할 수 있다. 스트림의 주요 특징은 다음과 같다.

  • 선언적 프로그래밍 : 스트림을 사용하면 선언적 방식으로 데이터를 처리할 수 있다.
  • 병렬 처리 : 스트림은 병렬 처리를 쉽게 구현할 수 있는 기능을 제공한다.
  • 중간 연산과 최종 연산 : 스트림은 중간 연산과 최종 연산으로 구성된다. 중간 연산은 스트림을 변환하고, 최종 연산은 결과를 생성한다.

3.2. 스트림의 필요성

스트림이 도입되기 전에는 Java 에서 대량의 데이터를 처리하는 것이 복잡하고 번거로웠다. 스트림을 사용하면 이러한 복잡성을 줄이고, 데이터를 효율적으로 처리할 수 있다. 스트림의 주요 필요성은 다음과 같다.

  • 코드 간결화 : 스트림을 사용하면 데이터를 처리하는 코드를 간결하게 작성할 수 있다.
  • 가독성 향상 : 코드의 가독성이 향상되어 유지보수가 용이해진다.
  • 병렬 처리 지원 : 스트림을 사용하면 병렬 처리를 쉽게 구현할 수 있어 성능이 향상된다.

4. 스트림의 기본 사용법

4.1. 스트림 생성

스트림은 다양한 방법으로 생성할 수 있다. 컬렉션, 배열, 파일, 특정 값 등을 소스로 스트림을 생성할 수 있다. 다음은 스트림을 생성하는 예이다.

// 컬렉션에서 스트림 생성
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

// 배열에서 스트림 생성
String[] array = {"a", "b", "c"};
Stream<String> arrayStream = Arrays.stream(array);

// 특정 값에서 스트림 생성
Stream<String> valueStream = Stream.of("a", "b", "c");

// 빈 스트림 생성
Stream<String> emptyStream = Stream.empty();

4.2. 스트림 연산

스트림은 중간 연산과 최종 연산으로 구성된다. 중간 연산은 스트림을 변환하고, 최종 연산은 결과를 생성한다. 다음은 중간 연산과 최종 연산의 예이다.

List<String> list = Arrays.asList("a", "b", "c", "d", "e");

// 중간 연산: 필터링, 매핑
Stream<String> filteredStream = list.stream().filter(s -> s.compareTo("c") > 0);
Stream<String> mappedStream = filteredStream.map(String::toUpperCase);

// 최종 연산: forEach, collect
mappedStream.forEach(System.out::println);

List<String> collectedList = list.stream()
    .filter(s -> s.compareTo("c") > 0)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

System.out.println(collectedList); // 출력: [D, E]

스트림의 중간 연산

스트림의 중간 연산은 스트림을 변환하는 연산으로, 여러 번 적용할 수 있다. 대표적인 중간 연산은 다음과 같다.

  • filter(Predicate<T>) : 조건을 만족하는 요소만 남긴다.
  • map(Function<T, R>) : 요소를 다른 형태로 변환한다.
  • flatMap(Function<T, Stream<R>>) : 요소를 스트림으로 변환하고 평탄화한다.
  • sorted() : 요소를 정렬한다.
  • distinct(): 중복 요소를 제거한다.
  • limit(long n) : 처음 n개의 요소를 반환한다.
  • skip(long n) : 처음 n개의 요소를 건너뛴다.

스트림의 최종 연산

스트림의 최종 연산은 스트림을 종료하고 결과를 생성하는 연산이다. 대표적인 최종 연산은 다음과 같다.

  • forEach(Consumer<T>) : 각 요소에 대해 동작을 수행한다.
  • collect(Collector<T, A, R>) : 스트림의 요소를 컬렉션으로 변환한다.
  • reduce(BinaryOperator<T>) : 스트림의 요소를 하나로 합친다.
  • toArray() : 스트림의 요소를 배열로 변환한다.
  • count() : 스트림의 요소 수를 반환한다.
  • anyMatch(Predicate<T>) : 스트림의 요소 중 하나라도 조건을 만족하면 true 를 반환한다.
  • allMatch(Predicate<T>) : 스트림의 모든 요소가 조건을 만족하면 true 를 반환한다.
  • noneMatch(Predicate<T>) : 스트림의 모든 요소가 조건을 만족하지 않으면 true 를 반환한다.
  • findFirst() : 스트림의 첫 번째 요소를 반환한다.
  • findAny() : 스트림의 임의의 요소를 반환한다.

5. 람다식과 스트림의 결합 활용

람다식스트림을 결합하면 더욱 강력하고 유연한 데이터 처리를 구현할 수 있다. 람다식스트림의 각 요소에 대한 동작을 간결하게 표현할 수 있으며, 스트림은 대량의 데이터를 효율적으로 처리할 수 있다.

예제 1 : 숫자 리스트 처리

다음은 숫자 리스트에서 짝수만 필터링하고 제곱하여 출력하는 예제이다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * n)
    .forEach(System.out::println);

예제 2 : 문자열 리스트 처리

다음은 문자열 리스트에서 길이가 3보다 큰 문자열만 필터링하고 대문자로 변환하여 리스트로 수집하는 예제이다.

List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "fig", "grape");

List<String> result = words.stream()
    .filter(w -> w.length() > 3)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

System.out.println(result); // 출력: [APPLE, BANANA, CHERRY, GRAPE]

예제 3 : 객체 리스트 처리

다음은 객체 리스트에서 특정 조건을 만족하는 객체만 필터링하고, 그 객체의 속성을 추출하여 리스트로 수집하는 예제이다.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

List<Person> people = Arrays.asList(
    new Person("Alice", 30),
    new Person("Bob", 20),
    new Person("Charlie", 40)
);

List<String> names = people.stream()
    .filter(p -> p.getAge() > 25)
    .map(Person::getName)
    .collect(Collectors.toList());

System.out.println(names); // 출력: [Alice, Charlie]

6. 회고

람다식과 스트림은 Java 8에서 도입된 중요한 기능으로, Java 프로그래밍의 생산성과 효율성을 크게 향상시킨다. 람다식을 사용하면 코드를 간결하게 작성할 수 있으며 스트림을 사용하면 대량의 데이터를 효율적으로 처리할 수 있기 때문에, 적극적으로 활용해야겠다는 생각이 들었다.

0개의 댓글