1. 소개
솔직히 말해서: 한두 줄짜리 코드를 위해 익명 클래스를 쓰는 것은 빵집에서 가게까지 빵 하나 옮기려고 거대한 트럭을 부르는 것과 같습니다.
예를 들어 문자열 목록을 길이로 정렬하고 싶다면, Java 8 이전에는 이렇게 작성해야 했습니다:
List<String> list = Arrays.asList("사과", "바나나", "키위");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
문제는 단순한데 코드가 화면의 반을 차지합니다. 특히 이런 작업이 많아지면 코드가 “시끄러워져”서 핵심이 흐려집니다. 개발자들은 울고불고하다가 람다식을 만들었습니다. 우리는 이미 조금 배웠으니, 이제 복습하고 더 깊이 들어가 보겠습니다.
람다식이란 무엇인가
람다식은 기능적 인터페이스(추상 메서드가 하나뿐인 인터페이스, 예: Comparator, Runnable, Consumer 등)의 구현을 간결하게 표현하는 방법입니다.
쉽게 말해, 람다식은 필요한 자리에서 곧바로 ‘함수’를 작성할 수 있게 해 주며, 별도의 클래스나 메서드를 선언할 필요가 없습니다.
일반 문법:
(parametry) -> { telo }
예시:
- (a, b) -> a + b — 두 수를 더하는 함수
- x -> x * x — 제곱 함수
- () -> System.out.println("Hello!") — 매개변수가 없는 함수
기능적 인터페이스와의 관계:
람다식은 항상 기능적 인터페이스 타입의 변수에 대입할 수 있고, 그러한 인터페이스를 인자로 받는 메서드에 전달할 수도 있습니다.
2. 람다식 문법
매개변수 없음
Runnable r = () -> System.out.println("안녕, 세상!");
r.run(); // 출력: 안녕, 세상!
매개변수 하나
매개변수가 하나면 괄호를 생략할 수 있습니다:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java는 멋지다!");
여러 매개변수
괄호는 필수입니다:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
본문이 한 개의 표현식일 때
본문이 하나의 표현식이면 중괄호와 return이 필요 없습니다:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
본문이 블록일 때
여러 문장이 필요하면 중괄호와 return(반환값이 있는 경우)을 사용합니다:
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
매개변수 타입
대부분의 경우 매개변수 타입은 생략해도 컨텍스트로부터 컴파일러가 추론합니다. 원한다면 명시할 수도 있습니다:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
반환값이 없는 람다
인터페이스가 void를 반환하면, 명령만 작성하면 됩니다:
list.forEach(s -> System.out.println("요소: " + s));
표: 람다식 문법 변형
| 무엇을 하는가 | 예 | 설명 |
|---|---|---|
| 매개변수 없음 | |
예: Runnable에 사용 |
| 매개변수 하나 | |
괄호 생략 가능 |
| 여러 매개변수 | |
괄호 필수 |
| 단일 표현식 | |
return과 중괄호 없음 |
| 코드 블록 | |
결과가 있으면 return 필요 |
3. 활용: 람다식을 어디서 어떻게 사용할까
람다식은 보통 ‘동작’—즉 함수—을(를) 인자로 전달해야 할 때 가장 자주 사용됩니다. 컬렉션, 스트림(Stream API), 이벤트 등에서 혁신을 가져왔습니다.
리스트 정렬
Java 8 이전:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
람다식으로:
list.sort((a, b) -> a.length() - b.length());
스레드(Thread) 생성
Thread t = new Thread(() -> System.out.println("스레드가 시작되었습니다!"));
t.start();
컬렉션 처리
list.forEach(s -> System.out.println(s.toUpperCase()));
리스트 필터링
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
예제: 학습용 애플리케이션 확장
사용자 목록이 있다고 가정해 봅시다:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
이름이 4자보다 긴 모든 사용자를 출력:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("사용자: " + name));
4. 람다식에서의 변수 스코프
람다식은 외부 메서드의 변수를 사용할 수 있지만, 몇 가지 주의점이 있습니다!
변수와 람다
Java의 람다는 초기화된 뒤 더 이상 변경되지 않는 변수만 ‘캡처’할 수 있습니다. 변수에 final이 붙어 있으면 당연히 가능하고, final이 없어도 컴파일러가 값이 바뀌는지 검사합니다. 바뀌지 않으면 ‘사실상 final(effectively final)’로 간주되어 람다에서 사용할 수 있습니다.
예:
int minLength = 4; // 값이 어디에서도 변경되지 않음
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
여기서는 문제가 없습니다. minLength가 그대로이기 때문입니다.
하지만 람다에서 사용한 뒤 minLength에 다시 값을 대입하려 하면 컴파일 오류가 발생합니다:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // 오류! 람다가 이미 값을 '고정'했습니다
핵심 규칙은 아주 간단합니다: 람다에 포획된 변수는 불변이어야 합니다.
익명 클래스와의 차이
익명 클래스와 람다식에서 외부 메서드의 변수 사용 규칙은 동일합니다: final/사실상 final만 허용됩니다.
하지만!
람다식에서의 this는 외부 객체(현재 클래스의 인스턴스)를 가리키지만, 익명 클래스에서는 익명 클래스 자신의 인스턴스를 가리킵니다. 람다 내부에서 현재 클래스의 필드나 메서드에 접근할 때 중요한 차이입니다.
예:
public class Example {
String name = "외부 클래스";
void demo() {
Runnable r1 = new Runnable() {
String name = "익명 클래스";
@Override
public void run() {
System.out.println(this.name); // "익명 클래스"
}
};
Runnable r2 = () -> System.out.println(this.name); // "외부 클래스"
r1.run();
r2.run();
}
}
5. 람다식 사용 시 흔한 실수
오류 №1: final/사실상 final이 아닌 변수를 사용. 람다에서 사용한 뒤 변수를 바꾸려 하면 컴파일러가 즉시 오류를 보고합니다. 이는 안전을 위한 것으로, 그렇지 않으면 어떤 값이 사용되어야 하는지 애매해집니다.
오류 №2: this와의 혼동. 람다식에서 this는 외부 클래스이고, 익명 클래스에서는 익명 클래스 자신입니다. 람다에서 외부 클래스의 메서드를 호출하는 것은 동작하지만, 익명 클래스에서 외부 클래스 컨텍스트를 기대하면 그렇지 않을 수 있습니다.
오류 №3: 컨텍스트가 없는 람다. 람다식은 그 자체로 사용할 수 없습니다. 반드시 기능적 인터페이스 타입의 변수에 대입하거나, 그 인터페이스를 기대하는 곳에 전달해야 합니다. 아무 컨텍스트 없이 x -> x + 1을 적어 넣으면 오류가 발생합니다.
오류 №4: 지나치게 복잡한 람다. 람다식이 3–5줄을 넘어가면 읽기 어렵습니다. 이런 경우에는 로직을 별도의 메서드로 분리하는 것이 좋습니다.
GO TO FULL VERSION