Notice
Recent Posts
Recent Comments
Link
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags more
Archives
Today
Total
관리 메뉴

프로그래밍 공부

[자바] #17. 람다(Lambda)와 함수형 인터페이스 본문

자바

[자바] #17. 람다(Lambda)와 함수형 인터페이스

하 냥 2025. 2. 19. 21:36

🔍 람다 표현식이란?

람다 표현식(Lambda Expression)은 익명 함수를 간결하게 표현하는 방법이다.

메서드를 간단한 식으로 작성할 수 있어 코드의 가독성유지보수성을 높인다.

✅ 람다 표현식의 핵심 목적

 

  1. 간결한 코드 작성 (익명 클래스 대체)
  2. 함수형 프로그래밍 지원
  3. 컬렉션 처리 및 멀티스레딩에서 코드 단순화
  4. 함수형 인터페이스와 함께 사용

✍ 람다 표현식 기본 문법

(매개변수) -> { 실행문 }

 

요소 설명
(매개변수) 메서드의 매개변수 (없을 수도 있음)
-> 람다 식별자 ("goes to" 의미)
{ 실행문 } 메서드의 본문 (하나의 문장이면 중괄호 생략 가능)

 

🎯 예제: 다양한 람다 표현식

// 매개변수와 반환값이 없는 경우
() -> System.out.println("Hello, Lambda!");

// 매개변수가 하나인 경우 (괄호 생략 가능)
name -> System.out.println("Hello, " + name);

// 매개변수가 여러 개인 경우
(a, b) -> a + b;

// 매개변수 타입 명시
(int a, int b) -> a * b;

// 여러 문장을 포함하는 경우 (return 필요)
(a, b) -> {
    int sum = a + b;
    return sum;
};
interface Calculate {
	int cal(int a, int b);
}

class Lambda {
	public static void main(String[] args) {
    	Calculate c;
        c = (a, b) -> { return a + b; };
        System.out.println(c.cal(4, 3));
    }
}

위와 같이 메서드 몸체에 해당하는 내용이 return문이면,

그 문장이 하나이더라도 중괄호의 생략이 불가능하다.

그러나 위의 람다식은 다음과 같이 대체할 수 있다.

c = (a, b) -> a + b

return 문이 메서드 몸체를 이루는 유일한 문장이면 이렇게 작성할 수 있다. 이것이 더 보편적인 방식!


💡 함수형 인터페이스 (Functional Interface)와 람다

람다 표현식은 반드시 단 하나만의 추상 메서드를 가진 인터페이스(=함수형 인터페이스)와 함께 사용해야 한다.

 

함수형 인터페이스는 @FunctionalInterface 어노테이션으로 표시할 수 있다.

@FunctionalInterface
interface Greeting {
    void sayHello(String name);
}

public class LambdaExample {
    public static void main(String[] args) {
        Greeting greet = name -> System.out.println("안녕하세요, " + name + "님!");
        greet.sayHello("홍길동");
    }
}

 

안녕하세요, 홍길동님!

 

@FunctionalInterface 는 함수형 인터페이스에 부합하는지를 확인하기 위한 어노테이션이다.

인터페이스에 두 개 이상의 추상 메서도가 존재하면, 이는 함수형 인터페이스가 아니어서 컴파일 오류가 발생한다.

그러나 static 이나 default 선언이 붙은 메서드의 정의는 함수형 인터페이스 정의에 영향을 미치지 않는다.


🎨 자바 내장 함수형 인터페이스 (java.util.function 패키지)

함수형 인터페이스 추상 메서드 시그니처 설명
Predicate<T> boolean test(T t) 입력값을 평가하여 true/false 반환 (조건 검사)
Function<T,R> R apply(T t) 입력값을 출력값으로 변환 (함수형 변환)
Consumer<T> void accept(T t) 입력값을 소비(=사용) (반환 없음)
Supplier<T> T get() 입력값 없이 결과 반환 (값 생성)
UnaryOperator<T> T apply(T t) 같은 타입 입력과 출력 (단항 연산)
BinaryOperator<T> T apply(T t1, T t2) 같은 타입 입력과 출력 (이항 연산)
import java.util.function.*;

public class BuiltInFunctionExample {
    public static void main(String[] args) {
        // 1. Predicate: 조건 검사
        Predicate<Integer> isEven = n -> n % 2 == 0;
        System.out.println(isEven.test(4));  // true 출력

        // 2. Function: 변환
        Function<String, Integer> strLength = s -> s.length();
        System.out.println(strLength.apply("Lambda"));  // 6 출력

        // 3. Consumer: 출력
        Consumer<String> print = s -> System.out.println("Hello, " + s);
        print.accept("Lambda"); // Hello, Lambda 출력

        // 4. Supplier: 값 생성
        Supplier<Double> randomValue = () -> Math.random();
        System.out.println(randomValue.get()); // (랜덤 값 출력)

        // 5. BinaryOperator: 두 값의 합
        BinaryOperator<Integer> add = (a, b) -> a + b;
        System.out.println(add.apply(10, 20));  // 30 출력
    }
}

 


Predicate<T>

: 조건 검사를 위한 함수형 인터페이스, 입력값을 받아 true 또는 false를 반환

boolean test(T t);

Supplier<T>

: 값을 생성(supply) 하고 반환하는 함수형 인터페이스, 입력 없이 값을 반환

T get();

Consumer<T>

: 입력값을 받아 소비(consume) 하지만 반환값이 없는 함수형 인터페이스,

  출력, 로깅, 파일 쓰기, DB 저장 등 부수 효과(side effect)를 발생시키는 작업에 사용

void accept(T t);

Function<T>

: 입력값을 받아 출력값으로 변환(transform) 하는 함수형 인터페이스, 데이터를 가공하거나 변환하는 데 사용

R apply(T t);

⚡ 실전 종합 예제: 학생 정보 처리

import java.util.*;
import java.util.function.*;

class Student {
    String name;
    int score;

    Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return name + " (" + score + "점)";
    }
}

public class FunctionalInterfaceExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("홍길동", 85),
            new Student("김철수", 92),
            new Student("이영희", 78)
        );

        // Predicate: 90점 이상 학생 필터링
        Predicate<Student> isTopStudent = s -> s.score >= 90;

        // Function: 이름 대문자로 변환
        Function<Student, String> nameToUpper = s -> s.name.toUpperCase();

        // Consumer: 학생 정보 출력
        Consumer<Student> printStudent = s -> System.out.println(s);

        // Supplier: 새로운 학생 생성
        Supplier<Student> newStudent = () -> new Student("박민수", 88);

        // 실행
        students.stream()
                .filter(isTopStudent)       // Predicate 적용
                .map(nameToUpper)           // Function 적용
                .forEach(System.out::println);  // Consumer 적용

        System.out.println("새로운 학생: " + newStudent.get());
    }
}
김철수
새로운 학생: 박민수 (88점)

📌 분석

스트림 처리 중:

  • filter(isTopStudent)로 90점 이상 학생(김철수) 필터링
  • map(nameToUpper)로 이름 대문자 변환 (KIM철수)
  • forEach(System.out::println)로 최종 출력 (Consumer 적용)
  • Supplier<Student>인 newStudent가 호출(get() 메서드)되어 새로운 Student 객체(박민수 (88점)) 생성 후 출력

❓ forEach 부분에서 Consumer에 대한 정보가 보이지 않는데 어떻게 Consumer가 적용되는 것인가 ..?

 

💡 핵심 개념 정리

forEach(System.out::println)의 의미

  • forEach() 메서드는 Consumer<T>를 인자로 받는다.
  • System.out::println은 Consumer<T>의 메서드 참조 형태이다.
    • 실제로는 (s) -> System.out.println(s)와 동일하다.
  • 즉, 리스트의 각 요소를 System.out.println() 메서드에 전달하여 출력하는 역할을 한다.

Collection<E> 인터페이스는 Iterable<T>를 상속한다.

따라서 컬렉션 클러스들은 Iterable<T>를 대부분 상속하는데, 이 인터페이스에는 다음 디폴트 메소드가 정의되어 있다.

default void forEach(Consumer<? super T> action) {
    for (T t : this) // this는 이 메서드가 속한 컬렉션 인스턴스를 의미
    	action.accept(t); // 이때 t는 저장되어 있는 인스턴스 각각을 의미
}

우리는 forEach 메서드 호출을 위해서 Consumer<T> 인터페이스에 대한 람다식 또는 메서드 참조를 전달해야 한다.

그런데 Consumer<T>의 추상 메서드는 다음과 같다: void accept(T t).

반환하지 않고, 전달된 인자를 대상으로 어떠한 결과를 보이는 것이다.

그리고 이에 딱 맞는 메서드가 System.out.println이다.

이는 아래에서 보이듯이 accpet와 반환형 및 매개변수 선언이 동일하다. (물론 accept의 T가 String일 경우에)

public void println(String x)

그리고 System.out이 PrintStream 인스턴스를 참조하는 참조변수이므로,

forEach(System.out::println) 또는 forEach(s-> System.out.println(s)) 와 같이 적을 수 있다.