Java 8에서의 람다, 메서드 참조, 함수형 인터페이스 및 스트림 연산 간의 상호작용

목차

  • 1. 람다 표현식과 인터페이스의 관계
  • 2. 람다, 익명 내부 클래스와 this의 차이
  • 3. 표준 함수형 인터페이스와 메서드 참조
    • 3.1 함수형 인터페이스 정의 및 특성
    • 3.2 메서드 참조의 사용 방식
  • 4. 람다, 메서드 참조, 함수형 인터페이스와 스트림 연산의 통합 활용
    • 4.1 filter, map 메서드와 Predicate, Function 인터페이스
    • 4.2 Optionalmap 메서드와 Function
    • 4.3 mapToIntToIntFunction
    • 4.4 reduceBinaryOperator
    • 4.5 Supplier를 이용한 스트림 생성
    • 4.6 flatMap을 통한 다차원 데이터 평탄화
  • 결론

1. 람다 표현식과 인터페이스의 호환성

람다 표현식은 해당 인터페이스의 추상 메서드 시그니처와 일치할 경우, 직접 대체 가능하다. 예를 들어, Runnable 인터페이스는 매개변수 없고 반환값 없는 run() 메서드를 정의하고 있다.

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("안녕하세요");
    }
});

Thread t2 = new Thread(() -> System.out.println("안녕하세요"));
t1.start();
t2.start();

두 코드는 동일한 결과를 출력한다. 이는 람다 () -> System.out.println("안녕하세요")Runnable의 요구사항과 완전히 일치하기 때문이다.

또 다른 예로, Comparator<String>compare 메서드는 두 개의 String 인자를 받고 int 값을 반환한다.

String[] data = {"abc", "ab", "abcd"};
Arrays.sort(data, (a, b) -> a.length() - b.length());

이 람다 표현식도 compare(String, String)와 형식이 일치하므로 사용 가능하다.

2. 람다 표현식과 this의 의미

람다는 익명 내부 클래스처럼 보이지만, 실제로는 다르다. 특히 람다 내부에서 this는 람다가 포함된 외부 객체를 가리킨다.

class Example {
    Runnable r1 = () -> System.out.println(this);
    Runnable r2 = () -> System.out.println(this);

    void execute() {
        r1.run();
        r2.run();
    }
}

Example ex = new Example();
System.out.println(ex); // 출력: Example@...
ex.execute(); // 같은 객체 출력

이러한 동작은 람다가 자신이 속한 외부 클래스의 인스턴스를 참조하기 때문이며, 익명 내부 클래스와 동일한 클로저 기능을 갖는다.

3. 표준 함수형 인터페이스와 메서드 참조

3.1 함수형 인터페이스의 개념

함수형 인터페이스는 단 하나의 추상 메서드만을 가지며, @FunctionalInterface 어노테이션으로 명시된다.

@FunctionalInterface
interface Operation {
    int apply(int a, int b);
}

위 인터페이스는 람다로 구현할 수 있다.

Operation add = (a, b) -> a + b;
System.out.println(add.apply(3, 4)); // 출력: 7

자주 사용되는 표준 인터페이스들:

  • Consumer<T>: void accept(T t) – 입력을 소비
  • Function<T, R>: R apply(T t) – T를 R로 변환
  • Predicate<T>: boolean test(T t) – 조건 검사
  • Supplier<T>: T get() – 값 제공

3.2 메서드 참조의 활용

메서드 참조는 기존 메서드를 직접 참조하여 람다보다 더 간결하게 표현할 수 있다.

List<String> list = Arrays.asList("A", "B", "C");
list.forEach(System.out::println); // 메서드 참조 사용

이것은 e -> System.out.println(e)와 동일한 동작을 한다.

사용자 정의 클래스에서도 가능하다.

class Processor {
    static <T> void print(T item) {
        System.out.println("처리: " + item);
    }

    <T> void process(T item) {
        System.out.println("프로세스: " + item);
    }
}

List<String> items = Arrays.asList("X", "Y");
items.forEach(Processor::print); // 정적 메서드 참조
items.forEach(new Processor()::process); // 인스턴스 메서드 참조

4. 람다, 메서드 참조, 함수형 인터페이스와 스트림 연산의 통합

4.1 filter, mapPredicate, Function

Stream.filter()Predicate<? super T>를 받으며, 조건에 따라 요소를 걸러낸다.

List<String> strings = Arrays.asList("1", "", "3", null);
Stream<Integer> result = strings.stream()
    .filter(s -> s != null && !s.isEmpty())
    .map(Integer::parseInt)
    .map(n -> n * 2);

result.forEach(System.out::println); // 출력: 2, 6

mapFunction<T, R>를 받아 각 요소를 변환한다. 여기서 Integer::parseIntStringint로 변환하는 함수이다.

4.2 Optional.mapFunction

Null 안전 처리를 위해 Optional을 사용하면, map 메서드를 통해 체인 방식으로 데이터 접근이 가능하다.

public class Student {
    private String name;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

public class Course {
    private List<Student> students = new ArrayList<>();

    public Optional<Student> getStudent(int index) {
        if (index < 0 || index >= students.size()) return Optional.empty();
        return Optional.ofNullable(students.get(index));
    }

    public List<Student> getStudents() { return students; }
}

// 사용 예:
Optional<String> name = Optional.ofNullable(course)
    .flatMap(c -> c.getStudent(0))
    .map(Student::getName)
    .orElse("없음");

System.out.println(name); // 출력: 이름 또는 "없음"

4.3 mapToIntToIntFunction

mapToIntIntStream를 반환하며, ToIntFunction 인터페이스를 사용한다.

String[] words = {"hi", "hello", "world"};
IntStream lengths = Arrays.stream(words)
    .filter(w -> w != null)
    .mapToInt(String::length);

lengths.forEach(System.out::println); // 출력: 2, 5, 5

4.4 reduceBinaryOperator

reduce는 스트림의 모든 요소를 결합하는 데 사용되며, BinaryOperator<T>를 인자로 받는다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
System.out.println(sum.orElse(0)); // 출력: 15

// 초기값 포함 버전
Integer total = numbers.stream().reduce(10, (a, b) -> a + b);
System.out.println(total); // 출력: 25

4.5 Supplier로 스트림 재생성

스트림은 한 번만 사용 가능하므로, 반복 작업을 위해 Supplier를 사용해 새로운 스트림을 생성할 수 있다.

Supplier<Stream<Integer>> streamFactory = () -> Stream.of(1, 2, 3);
Stream<Integer> s1 = streamFactory.get();
Stream<Integer> s2 = streamFactory.get();

System.out.println(s1 == s2); // false (다른 객체)

4.6 flatMap을 통한 2차원 배열 평탄화

다차원 배열을 단일 스트림으로 변환하려면 flatMap을 사용한다.

Integer[][] matrix = {{1, 2}, {2, 3}};
Stream<Integer> flat = Arrays.stream(matrix)
    .flatMap(Arrays::stream)
    .distinct(); // 중복 제거

flat.forEach(System.out::println); // 출력: 1, 2, 3

기본형 배열(int[][])에는 flatMap이 적용되지 않으므로, Integer 참조형 배열을 사용해야 한다.

결론

Java 8의 람다, 메서드 참조, 함수형 인터페이스는 스트림 파이프라인과 밀접하게 연결되어 있으며, 각각의 역할과 유연성이 통합적으로 작동한다. 이를 이해함으로써 코드의 가독성과 안정성을 동시에 높일 수 있다.

태그: Java 8 lambda Method Reference Functional Interface Stream API

6월 12일 18:14에 게시됨