18. 람다와 함수형
람다(lambda) 는 짧게 표현하는 **익명 함수** 입니다. JDK 8 이후 컬렉션·Stream·Spring 등 거의 모든 곳에서 만나는 핵심 문법입니다. 메서드 참조와 표준 함수형 인터페이스(`Function`/`Predicate`/`Consumer`/`Supplier`) 도 함께 익혀 둡니다.
이 강의에서 배우는 것
- 1lambda 식의 형태를 안다 (`(a, b) -> a + b`)
- 2메서드 참조(`Foo::bar`) 4가지를 안다
- 3`java.util.function` 의 핵심 4 인터페이스를 사용한다
- 4람다가 캡처할 수 있는 변수의 제약(effective final) 을 안다
소개
람다(lambda) 는 짧게 표현하는 **익명 함수** 입니다. JDK 8 이후 컬렉션·Stream·Spring 등 거의 모든 곳에서 만나는 핵심 문법입니다. 메서드 참조와 표준 함수형 인터페이스(`Function`/`Predicate`/`Consumer`/`Supplier`) 도 함께 익혀 둡니다.
핵심 개념
1) lambda 표현식
Runnable r = () -> System.out.println("hi");
Comparator<String> byLen = (a, b) -> a.length() - b.length();타입은 대상 함수형 인터페이스로부터 추론됩니다.
2) 메서드 참조 4가지
| 종류 | 예 |
|---|---|
| 정적 메서드 | `Integer::parseInt` |
| 인스턴스 메서드 (특정 객체) | `System.out::println` |
| 인스턴스 메서드 (클래스 명) | `String::toUpperCase` |
| 생성자 | `ArrayList::new` |
3) 표준 함수형 인터페이스
| 인터페이스 | 추상 메서드 | 의미 |
|---|---|---|
| `Function<T, R>` | `R apply(T)` | T → R 변환 |
| `Predicate<T>` | `boolean test(T)` | T 조건 검사 |
| `Consumer<T>` | `void accept(T)` | T 소비 (출력 등) |
| `Supplier<T>` | `T get()` | 매개변수 없이 T 공급 |
| `BiFunction<T,U,R>` | `R apply(T, U)` | 두 입력 → 한 출력 |
4) effective final
int x = 10;
Runnable r = () -> System.out.println(x);
// x = 20; // 컴파일 에러: 캡처된 변수는 사실상 final 이어야 함람다 내부에서 캡처하는 지역 변수는 (final 키워드 없이도) **한 번 정해진 뒤 안 바뀌어야** 합니다.
핵심 예제
예제 1 — `LambdaBasics.java` : 다양한 형태
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class LambdaBasics {
public static void main(String[] args) {
Runnable r = () -> System.out.println("hi");
r.run();
Comparator<String> byLen = (a, b) -> a.length() - b.length();
List<String> words = Arrays.asList("ccc", "a", "bb");
words.sort(byLen);
System.out.println(words);
}
}**실행 결과**
hi
[a, bb, ccc]**메모:** 람다 본문이 한 줄이면 `return` 과 중괄호를 생략할 수 있습니다.
예제 2 — `MethodReference.java` : 메서드 참조
import java.util.List;
import java.util.stream.Collectors;
public class MethodReference {
public static void main(String[] args) {
List<String> langs = List.of("java", "kotlin", "scala");
// (s) -> s.toUpperCase()
List<String> upper = langs.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper);
// (s) -> System.out.println(s)
langs.forEach(System.out::println);
}
}**실행 결과**
[JAVA, KOTLIN, SCALA]
java
kotlin
scala**메모:** 람다가 단순히 "어떤 메서드 호출만" 한다면 메서드 참조가 더 짧고 명확합니다.
예제 3 — `FunctionInterfaces.java` : 표준 4종
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class FunctionInterfaces {
public static void main(String[] args) {
Function<Integer, Integer> square = n -> n * n;
Predicate<Integer> isEven = n -> n % 2 == 0;
Consumer<String> printUp = s -> System.out.println(s.toUpperCase());
Supplier<String> hello = () -> "Hello";
System.out.println(square.apply(5));
System.out.println(isEven.test(10));
printUp.accept("java");
System.out.println(hello.get());
}
}**실행 결과**
25
true
JAVA
Hello**메모:** 람다는 **무엇을 받아 무엇을 반환하느냐** 만 보면 자연스럽게 인터페이스가 결정됩니다.
예제 4 — `Capture.java` : 변수 캡처
public class Capture {
public static void main(String[] args) {
int x = 10;
Runnable r = () -> System.out.println("x=" + x);
r.run();
// x = 20; // 주석 풀면 컴파일 에러
for (int i = 0; i < 3; i++) {
final int idx = i;
Runnable rr = () -> System.out.println("idx=" + idx);
rr.run();
}
}
}**실행 결과**
x=10
idx=0
idx=1
idx=2**메모:** `for` 의 `i` 자체는 매 회 새로 만들어지지만, 람다 안에서 캡처하려면 그 회차의 final 사본을 만들어야 합니다.
전체 예제 코드 (src/)
src/Capture.java
public class Capture {
public static void main(String[] args) {
int x = 10;
Runnable r = () -> System.out.println("x=" + x);
r.run();
for (int i = 0; i < 3; i++) {
final int idx = i;
Runnable rr = () -> System.out.println("idx=" + idx);
rr.run();
}
}
}
src/FunctionInterfaces.java
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class FunctionInterfaces {
public static void main(String[] args) {
Function<Integer, Integer> square = n -> n * n;
Predicate<Integer> isEven = n -> n % 2 == 0;
Consumer<String> printUp = s -> System.out.println(s.toUpperCase());
Supplier<String> hello = () -> "Hello";
System.out.println(square.apply(5));
System.out.println(isEven.test(10));
printUp.accept("java");
System.out.println(hello.get());
}
}
src/LambdaBasics.java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class LambdaBasics {
public static void main(String[] args) {
Runnable r = () -> System.out.println("hi");
r.run();
Comparator<String> byLen = (a, b) -> a.length() - b.length();
List<String> words = Arrays.asList("ccc", "a", "bb");
words.sort(byLen);
System.out.println(words);
}
}
src/MethodReference.java
import java.util.List;
import java.util.stream.Collectors;
public class MethodReference {
public static void main(String[] args) {
List<String> langs = List.of("java", "kotlin", "scala");
List<String> upper = langs.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper);
langs.forEach(System.out::println);
}
}
자주 하는 실수
- 한 줄 람다에 `return` 을 강제로 적기 (없어도 됨)
- 캡처 변수에 재할당 → 컴파일 에러
- 람다 안의 예외 처리를 잊고 `Function` 시그니처와 충돌 (checked 예외)
- `String::toUpperCase` 가 인스턴스 메서드인지 정적 메서드인지 헷갈림
- 메서드 참조 사용 가능한데도 길게 람다 사용
정리
- 람다는 함수형 인터페이스에 대한 짧은 구현
- 메서드 참조로 더욱 간결하게
- 표준 4 함수형 인터페이스만 익혀도 80% 활용 가능
- 캡처 변수는 effective final 이어야 함
과제
# 과제 - 18. 람다와 함수형
## 문제 1 — `Predicate` 로 필터링
- 파일명: `Homework01.java`
- 핵심 개념: `Predicate`, `Stream.filter`
요구사항
- `List<Integer> nums = List.of(3, 7, 1, 9, 4, 6, 8)` 에서
- 짝수만
- 5 이상
- 두 조건 모두(짝수 AND 5 이상)
- 각각 필터한 결과를 출력.
예상 출력
짝수: [4, 6, 8]
5 이상: [7, 9, 6, 8]
짝수 AND 5 이상: [6, 8]## 문제 2 — 메서드 참조로 정렬
- 파일명: `Homework02.java`
- 핵심 개념: `Comparator`, 메서드 참조
요구사항
- `record Book(String title, int pages)` 리스트를
- `pages` 오름차순
- `title` 알파벳순
- 두 번 정렬해 출력.
예상 출력
페이지 오름차순: [Book[title=A, pages=100], Book[title=C, pages=200], Book[title=B, pages=300]]
제목 알파벳순: [Book[title=A, pages=100], Book[title=B, pages=300], Book[title=C, pages=200]]## 정답 확인 직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.
정답 코드 (homework/answer/)
answer/Homework01.java
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/** Predicate 로 필터링. */
public class Homework01 {
public static void main(String[] args) {
List<Integer> nums = List.of(3, 7, 1, 9, 4, 6, 8);
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> geFive = n -> n >= 5;
System.out.println("짝수: " + filter(nums, even));
System.out.println("5 이상: " + filter(nums, geFive));
System.out.println("짝수 AND 5 이상: " + filter(nums, even.and(geFive)));
}
static List<Integer> filter(List<Integer> xs, Predicate<Integer> p) {
return xs.stream().filter(p).collect(Collectors.toList());
}
}
answer/Homework02.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/** 메서드 참조로 정렬. */
public class Homework02 {
record Book(String title, int pages) {}
public static void main(String[] args) {
List<Book> books = new ArrayList<>(List.of(
new Book("B", 300),
new Book("A", 100),
new Book("C", 200)
));
books.sort(Comparator.comparingInt(Book::pages));
System.out.println("페이지 오름차순: " + books);
books.sort(Comparator.comparing(Book::title));
System.out.println("제목 알파벳순: " + books);
}
}
직접 해 보기
cd 05_모던_자바/18_람다와_함수형/src
javac LambdaBasics.java
java LambdaBasics다음 단원
[19_Optional](../19_Optional/) — null 회피 패턴과 `Optional` 사용법을 배웁니다.