프로그래밍 공부
[자바] #17. 람다(Lambda)와 함수형 인터페이스 본문
🔍 람다 표현식이란?
람다 표현식(Lambda Expression)은 익명 함수를 간결하게 표현하는 방법이다.
메서드를 간단한 식으로 작성할 수 있어 코드의 가독성과 유지보수성을 높인다.
✅ 람다 표현식의 핵심 목적
- 간결한 코드 작성 (익명 클래스 대체)
- 함수형 프로그래밍 지원
- 컬렉션 처리 및 멀티스레딩에서 코드 단순화
- 함수형 인터페이스와 함께 사용
✍ 람다 표현식 기본 문법
(매개변수) -> { 실행문 }
| 요소 | 설명 |
| (매개변수) | 메서드의 매개변수 (없을 수도 있음) |
| -> | 람다 식별자 ("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)) 와 같이 적을 수 있다.
'자바' 카테고리의 다른 글
| [자바] #19. Optional 총정리 (0) | 2025.02.20 |
|---|---|
| [자바] #18. 메서드 참조(+ 람다식에서 접근 가능한 참조변수 제한) (0) | 2025.02.20 |
| [자바] #16. 네스티드(Nested) 클래스, 이너(Inner) 클래스 (0) | 2025.02.19 |
| [자바] #15. 열거형, 가변 인자, 어노테이션 (0) | 2025.02.19 |
| [자바] #14. Collections 클래스 안의 여러 알고리즘 구현 메서드들 (0) | 2025.02.19 |