목차
->)@FunctionalInterface 어노테이션Runnable, Comparator, Predicate, Function, Consumer, Supplier)forEach (각 요소 순회)map (요소 변환)filter (요소 필터링)reduce (요소 집계)sort (정렬)람다식 (Lambda Expression) 은 "익명 함수 (Anonymous Function)" 를 간결하게 표현하는 방법이다. 익명 함수란 이름 없이 정의되는 함수를 의미하며, 람다식을 사용하면 클래스 선언 없이 함수 자체를 값처럼 사용할 수 있다.
핵심 개념:
기존 방식 (클래스와 메소드):
// 1. 요리책 (클래스) 만들기
class 요리책 {
// 2. 레시피 (메소드) 만들기
public void 라면레시피() {
// ... 복잡한 라면 레시피 ...
}
}
// 3. 요리책 보고 요리하기
요리책 myCookBook = new 요리책();
myCookBook.라면레시피();
람다식 (익명 함수):
// 1. 레시피 (람다식) 만들기 (요리책 없이 레시피만!)
() -> { // 람다식 시작
// ... 간단한 라면 레시피 ...
} // 람다식 끝
// 2. 레시피 (람다식) 사용하기 (레시피 자체를 값처럼!)
요리사.요리하기( () -> { /* 간단한 라면 레시피 */ } ); // 요리사에게 람다식 레시피 전달
기존 방식은 요리책(클래스)을 먼저 만들고, 그 안에 레시피(메소드)를 넣어야 했다. 하지만 람다식은 요리책 없이 레시피 자체만을 간단하게 만들어서 요리사에게 바로 전달할 수 있는 것처럼, 함수 자체를 간결하게 표현하고 값처럼 사용할 수 있게 해준다.
람다식을 사용하는 주된 이유는 다음과 같다.
람다식의 핵심 가치: "코드를 더 짧고, 더 읽기 쉽고, 더 유연하게 만들어준다!"
람다식은 다음과 같은 기본 문법 구조를 가진다.
(파라미터) -> { 람다 몸체 }
람다식은 크게 세 부분으로 구성된다.
->): 람다 파라미터와 람다 몸체를 구분하는 화살표 기호.각 구성 요소를 좀 더 자세히 알아보자
람다 파라미터는 람다식으로 전달될 입력 값을 정의한다. 메소드의 매개변수 선언과 유사하지만, 람다식에서는 몇 가지 문법적 특징이 있다.
() 로 묶어서 선언: 파라미터 목록은 항상 소괄호 () 로 묶어서 선언한다.() 를 생략할 수 있다. 하지만 파라미터가 없거나, 여러 개인 경우에는 소괄호 () 를 반드시 사용해야 한다.final 키워드 자동 적용 (effectively final): 람다 파라미터는 암묵적으로 final 로 선언된 것처럼 동작한다. 람다식 내부에서 파라미터 값을 변경하려고 하면 컴파일 에러가 발생한다. (람다 캡처링과 관련)람다 파라미터 선언 예시:
| 형태 | 설명 | 예시 |
|---|---|---|
() | 파라미터 없음 | () -> { ... } |
(parameter) | 파라미터 1개, 타입 생략 가능 | (name) -> { ... }, name -> { ... } |
(type parameter) | 파라미터 1개, 타입 명시 | (String name) -> { ... } |
(param1, param2, ...) | 파라미터 여러 개, 타입 생략 가능, 소괄호 필수 | (name, age) -> { ... } |
(type param1, ...) | 파라미터 여러 개, 타입 명시, 소괄호 필수 | (String name, int age) -> { ... } |
->)화살표 토큰 -> 은 람다 파라미터와 람다 몸체를 구분하는 핵심적인 기호이다. 람다식에서 "~을 받아서 (파라미터) ~을 실행한다 (람다 몸체)" 라는 의미를 명확하게 나타낸다. 화살표 토큰은 람다식 문법에서 반드시 사용해야 한다.
람다 몸체는 람다식의 핵심 로직이 구현되는 부분이다. 람다 파라미터를 이용하여 특정 연산을 수행하고 결과를 반환하거나, 특정 동작을 실행한다. 람다 몸체는 크게 표현식 (Expression Body) 과 블록 (Block Body) 두 가지 형태를 가질 수 있다.
표현식 람다 몸체는 단일한 실행문으로 구성된 경우에 사용된다. 실행문이 return 문인 경우, return 키워드와 {} 중괄호를 생략하고 표현식만 작성할 수 있다. 표현식 람다 몸체는 코드를 더욱 간결하게 만들어준다.
표현식 람다 몸체 문법:
(파라미터) -> 표현식 // return 키워드, {} 중괄호 생략
표현식 람다 몸체 예시:
// 1. 덧셈 람다식 (표현식 몸체)
(a, b) -> a + b // return 키워드, {} 중괄호 생략
// 2. 문자열 길이 반환 람다식 (표현식 몸체)
name -> name.length() // return 키워드, {} 중괄호 생략
// 3. 숫자 짝수 판별 람다식 (표현식 몸체)
num -> num % 2 == 0 // return 키워드, {} 중괄호 생략
블록 람다 몸체는 여러 개의 실행문 또는 복잡한 로직을 포함하는 경우에 사용된다. 실행문들을 {} 중괄호로 묶어서 블록 형태로 작성하며, return 문을 사용하여 명시적으로 반환값을 지정해야 한다. 블록 람다 몸체는 좀 더 복잡한 함수 로직을 표현할 수 있도록 유연성을 제공한다.
블록 람다 몸체 문법:
(파라미터) -> { // {} 중괄호 블록 시작
// 실행문 1;
// 실행문 2;
// ...
return 반환값; // return 키워드 명시
} // {} 중괄호 블록 끝
블록 람다 몸체 예시:
// 1. 덧셈 후 로그 출력 람다식 (블록 몸체)
(a, b) -> {
System.out.println("덧셈 연산 시작");
int sum = a + b;
System.out.println("덧셈 결과: " + sum);
return sum; // return 키워드 명시
}
// 2. 문자열 검증 및 길이 반환 람다식 (블록 몸체)
name -> {
if (name == null || name.isEmpty()) {
System.out.println("이름이 유효하지 않습니다.");
return 0; // return 키워드 명시
}
System.out.println("이름 길이 계산");
return name.length(); // return 키워드 명시
}
// 3. 숫자 짝수 판별 및 메시지 출력 람다식 (블록 몸체)
num -> {
boolean isEven = num % 2 == 0;
if (isEven) {
System.out.println(num + "은 짝수입니다.");
} else {
System.out.println(num + "은 홀수입니다.");
}
return isEven; // return 키워드 명시
}
람다 몸체 선택 가이드:
람다식은 함수형 인터페이스 (Functional Interface) 라는 특별한 인터페이스와 함께 사용될 때 진정한 힘을 발휘한다. 함수형 인터페이스는 람다식을 "담는 그릇" 역할을 하며, 람다식이 어떤 형태로 사용될지를 정의힌다.
함수형 인터페이스 (Functional Interface) 는 "추상 메소드 (Abstract Method) 를 딱 하나만 가지고 있는 인터페이스" 를 의미한다. 인터페이스는 원래 여러 개의 추상 메소드를 가질 수 있지만, 함수형 인터페이스는 오직 하나의 추상 메소드만 허용된다.
함수형 인터페이스 조건:
@interface 로 선언되는 타입함수형 인터페이스 예시:
// 1. Runnable 인터페이스 (Java 기본 제공, 함수형 인터페이스)
@FunctionalInterface // 함수형 인터페이스임을 명시하는 어노테이션 (선택 사항)
public interface Runnable {
void run(); // 추상 메소드 1개 (매개변수, 반환값 없음)
}
// 2. Comparator 인터페이스 (Java 기본 제공, 함수형 인터페이스)
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); // 추상 메소드 1개 (매개변수 2개, int 반환값)
// ... default 메소드, static 메소드 ...
}
// 3. MyFunctionalInterface (사용자 정의 함수형 인터페이스)
@FunctionalInterface
interface MyFunctionalInterface {
int calculate(int a, int b); // 추상 메소드 1개 (매개변수 2개, int 반환값)
}
@FunctionalInterface 어노테이션@FunctionalInterface 어노테이션은 인터페이스가 함수형 인터페이스임을 명시적으로 선언하는 데 사용된다. @FunctionalInterface 어노테이션은 선택 사항이지만, 함수형 인터페이스를 만들 때는 붙여주는 것을 권장한다.
@FunctionalInterface 어노테이션의 장점:
@FunctionalInterface 어노테이션이 붙은 인터페이스가 함수형 인터페이스 조건을 만족하지 못하면 (추상 메소드가 2개 이상인 경우), 컴파일 에러를 발생시켜 개발자가 실수하는 것을 방지해 준다.Java 8 에서는 람다식과 함께 다양한 용도로 활용할 수 있는 java.util.function 패키지를 통해 여러 종류의 함수형 인터페이스를 기본적으로 제공한다. 주요 함수형 인터페이스 몇 가지를 살펴보자.
| 함수형 인터페이스 | 추상 메소드 | 설명 | 람다식 형태 예시 |
|---|---|---|---|
Runnable | void run() | 매개변수 없고, 반환값 없는 작업 (쓰레드 실행, 이벤트 처리 등) | () -> { ... } |
Callable<V> | V call() throws Exception | 매개변수 없고, 반환값 있는 작업 (비동기 작업 결과, Future와 함께 사용) | () -> 값 |
Comparator<T> | int compare(T o1, T o2) | 객체 비교 (정렬, 검색 등) | (o1, o2) -> 비교 결과 (int) |
Predicate<T> | boolean test(T t) | 조건 검사 (필터링, 유효성 검증 등) | (t) -> 조건 (boolean) |
Function<T, R> | R apply(T t) | 입력값 T를 받아서 값 R로 변환 (데이터 변환, 매핑 등) | (t) -> 변환 결과 (R) |
Consumer<T> | void accept(T t) | 입력값 T를 받아서 소비 (출력, 로깅, 특정 동작 실행 등), 반환값 없음 | (t) -> { ... } |
Supplier<T> | T get() | 값을 제공 (생성, 획득 등), 매개변수 없이 값 T 반환 | () -> 값 (T) |
함수형 인터페이스 활용:
함수형 인터페이스는 람다식을 효과적으로 사용하기 위한 핵심적인 타입 시스템을 제공한다. 람다식을 통해 간결하게 표현된 익명 함수를 함수형 인터페이스 타입으로 다루면서, Java는 함수형 프로그래밍 패러다임을 더욱 강력하게 지원할 수 있게 되었다.
람다식이 실제로 어떻게 활용되는지 다양한 예제 코드를 통해 자세히 살펴보자.
람다식은 익명 내부 클래스를 대체하여 코드를 더욱 간결하게 만들 수 있다. 특히 익명 내부 클래스가 함수형 인터페이스를 구현하는 경우, 람다식으로 훨씬 짧고 명료하게 코드를 작성할 수 있다.
쓰레드 생성 시 Runnable 함수형 인터페이스를 익명 내부 클래스로 구현하던 코드를 람다식으로 간결하게 바꿀 수 있다.
기존 코드 (익명 내부 클래스):
// 쓰레드 생성 (익명 내부 클래스)
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("쓰레드 실행 (익명 내부 클래스)");
}
});
thread.start();
람다식 코드:
// 쓰레드 생성 (람다식)
Thread thread = new Thread(() -> { // Runnable 함수형 인터페이스를 람다식으로 구현
System.out.println("쓰레드 실행 (람다식)");
});
thread.start();
람다식으로 코드가 훨씬 간결해진 것을 확인할 수 있다. 익명 내부 클래스 선언, @Override 어노테이션, 메소드 이름(run) 등 불필요한 코드를 줄이고, 핵심 로직 (System.out.println()) 에 집중할 수 있도록 코드를 개선했다.
GUI 프로그래밍 (Swing, JavaFX) 또는 웹 프로그래밍 (JavaScript) 에서 이벤트 핸들러를 구현할 때 익명 내부 클래스를 많이 사용하는데, 이 또한 람다식으로 대체하여 코드를 간결하게 만들 수 있다. (JavaFX 예시)
기존 코드 (익명 내부 클래스):
Button button = new Button("클릭!");
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("버튼 클릭 이벤트 발생 (익명 내부 클래스)");
}
});
람다식 코드:
Button button = new Button("클릭!");
button.setOnAction(event -> { // EventHandler 함수형 인터페이스를 람다식으로 구현
System.out.println("버튼 클릭 이벤트 발생 (람다식)");
});
람다식을 사용하면 이벤트 핸들러 코드를 훨씬 짧고 읽기 쉽게 만들 수 있다. 이벤트 처리 로직 (System.out.println()) 에 더욱 집중할 수 있도록 코드를 개선하고, 불필요한 코드 작성을 줄여 개발 생산성을 향상시켰다.
람다식은 Java 8 에서 함께 도입된 Stream API 와 함께 사용할 때 컬렉션 데이터 처리를 더욱 강력하고 효율적으로 만들어준다. Stream API 는 컬렉션 데이터를 선언형 (declarative) 방식으로 처리할 수 있도록 지원하며, 람다식은 Stream API 에서 데이터 처리 로직을 간결하게 표현하는 데 핵심적인 역할을 한다.
forEach (각 요소 순회)컬렉션의 각 요소에 대해 특정 동작을 수행할 때 forEach 메소드와 람다식을 함께 사용하면 코드를 간결하게 만들 수 있습니다.
기존 코드 (for-each 루프):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// for-each 루프를 사용한 요소 순회
for (String name : names) {
System.out.println("이름: " + name);
}
람다식 코드 (forEach 메소드):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// forEach 메소드와 람다식을 사용한 요소 순회
names.forEach(name -> { // Consumer 함수형 인터페이스를 람다식으로 구현
System.out.println("이름 (forEach + 람다식): " + name);
});
람다식을 사용하면 컬렉션 요소 순회 코드를 한 줄로 간결하게 표현할 수 있다.
for-each 루프에 비해 코드 라인 수를 줄이고, 코드 가독성을 높여준다.
map (요소 변환)컬렉션의 각 요소를 특정 기준으로 변환하여 새로운 컬렉션을 만들 때 map 메소드와 람다식을 함께 사용한다.
기존 코드 (for-each 루프 + 새로운 리스트 생성):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> nameLengths = new ArrayList<>();
// for-each 루프를 사용한 요소 변환
for (String name : names) {
nameLengths.add(name.length()); // 이름 길이를 새로운 리스트에 추가
}
System.out.println("이름 길이 리스트: " + nameLengths);
람다식 코드 (map 메소드):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// map 메소드와 람다식을 사용한 요소 변환
List<Integer> nameLengths = names.stream() // Stream 생성
.map(name -> name.length()) // Function 함수형 인터페이스를 람다식으로 구현 (이름 -> 이름 길이)
.collect(Collectors.toList()); // List로 수집
System.out.println("이름 길이 리스트 (map + 람다식): " + nameLengths);
람다식을 사용하면 컬렉션 요소 변환 코드를 더욱 간결하고 선언적으로 표현할 수 있다. Stream API 의 map 메소드는 각 요소에 람다식을 적용하여 변환된 요소들을 새로운 Stream 으로 반환하고, collect(Collectors.toList()) 메소드는 Stream 을 다시 List 로 변환한다. 복잡한 데이터 변환 로직을 짧은 코드로 구현할 수 있다.
filter (요소 필터링)컬렉션에서 특정 조건을 만족하는 요소만 골라 새로운 컬렉션을 만들 때 filter 메소드와 람다식을 함께 사용한다.
기존 코드 (for-each 루프 + 조건 검사 + 새로운 리스트 생성):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> longNames = new ArrayList<>();
// for-each 루프와 조건 검사를 사용한 요소 필터링
for (String name : names) {
if (name.length() > 5) { // 이름 길이가 5보다 큰 경우
longNames.add(name); // 새로운 리스트에 추가
}
}
System.out.println("긴 이름 리스트: " + longNames);
람다식 코드 (filter 메소드):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// filter 메소드와 람다식을 사용한 요소 필터링
List<String> longNames = names.stream() // Stream 생성
.filter(name -> name.length() > 5) // Predicate 함수형 인터페이스를 람다식으로 구현 (이름 길이 > 5)
.collect(Collectors.toList()); // List로 수집
System.out.println("긴 이름 리스트 (filter + 람다식): " + longNames);
람다식을 사용하면 컬렉션 요소 필터링 코드를 매우 간결하고 직관적으로 표현할 수 있다. Stream API 의 filter 메소드는 각 요소에 람다식을 적용하여 조건이 참인 요소만 Stream 으로 반환하고, collect(Collectors.toList()) 메소드는 Stream 을 다시 List 로 변환합니다. 복잡한 조건 필터링 로직을 짧은 코드로 구현할 수 있다.
reduce (요소 집계)컬렉션의 요소들을 특정 연산을 통해 하나의 값으로 집계할 때 reduce 메소드와 람다식을 함께 사용한다.
기존 코드 (for-each 루프 + 누적 변수):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
// for-each 루프를 사용한 요소 합계 계산
for (int number : numbers) {
sum += number; // 누적 변수에 합산
}
System.out.println("숫자 합계: " + sum);
람다식 코드 (reduce 메소드):
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// reduce 메소드와 람다식을 사용한 요소 합계 계산
int sum = numbers.stream() // Stream 생성
.reduce(0, (a, b) -> a + b); // BinaryOperator 함수형 인터페이스를 람다식으로 구현 (누적 연산)
System.out.println("숫자 합계 (reduce + 람다식): " + sum);
람다식을 사용하면 컬렉션 요소 집계 코드를 매우 간결하고 유연하게 표현할 수 있다. Stream API 의 reduce 메소드는 초기값 (0) 과 람다식을 인자로 받아서, 각 요소에 대해 람다식을 누적 적용하여 최종 결과값을 반환한다. 합계, 평균, 최대/최소값, 사용자 정의 집계 등 다양한 연산을 간결하게 구현할 수 있다.
sort (정렬)컬렉션을 특정 기준으로 정렬할 때 sort 메소드와 람다식 (Comparator 함수형 인터페이스 구현) 을 함께 사용합니다.
기존 코드 (익명 내부 클래스 Comparator 구현):
List<String> names = Arrays.asList("Charlie", "Alice", "Bob", "David");
// 익명 내부 클래스를 사용한 Comparator 구현 (이름 길이 기준 오름차순 정렬)
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length()); // 이름 길이 비교
}
});
System.out.println("정렬된 이름 리스트 (익명 내부 클래스): " + names);
람다식 코드 (sort 메소드 + 람다식 Comparator 구현):
List<String> names = Arrays.asList("Charlie", "Alice", "Bob", "David");
// sort 메소드와 람다식을 사용한 Comparator 구현 (이름 길이 기준 오름차순 정렬)
Collections.sort(names, (s1, s2) -> Integer.compare(s1.length(), s2.length())); // Comparator 함수형 인터페이스를 람다식으로 구현
System.out.println("정렬된 이름 리스트 (람다식): " + names);
// 더 간결하게 메소드 참조 사용 가능
names.sort(Comparator.comparingInt(String::length)); // Comparator.comparingInt 메소드 + 메소드 참조
System.out.println("정렬된 이름 리스트 (메소드 참조): " + names);
람다식을 사용하면 컬렉션 정렬 기준을 훨씬 간결하고 직관적으로 표현할 수 있다. Collections.sort 메소드 또는 List 자체의 sort 메소드에 람다식으로 구현된 Comparator 를 전달하여 정렬 기준을 유연하게 정의할 수 있다. 메소드 참조 (String::length) 를 함께 사용하면 코드를 더욱 간결하게 만들 수 있다.
Stream API 와 람다식을 함께 사용하면 컬렉션 데이터 처리 코드를 극적으로 간결하고 효율적으로 만들 수 있다. 데이터 필터링, 변환, 집계, 정렬 등 복잡한 데이터 처리 로직을 짧고 가독성 높은 코드로 구현하여 개발 생산성을 크게 향상시킬 수 있다.
람다식은 Java 코드 작성 방식을 혁신적으로 변화시키고, 다양한 장점을 제공한다.
코드 간결성 및 가독성 향상:
함수형 프로그래밍 패러다임 지원:
병렬 처리 효율 증대 (Stream API와 함께):
parallelStream()) 을 사용하여 멀티 코어 CPU 환경에서 데이터 처리 성능 극대화개발 생산성 향상:
람다식은 강력한 기능이지만, 몇 가지 주의사항과 한계점도 존재한다.
this 키워드 동작 방식 차이:this 는 익명 내부 클래스 객체 자신을 가리키지만, 람다식의 this 는 람다식을 감싸는 외부 클래스 객체를 가리킨다. 람다식 내부에서 this 를 사용하는 경우, 익명 내부 클래스와 다른 동작 방식에 주의해야 한다.람다식은 익명 내부 클래스를 간결하게 대체하는 기능이지만, 몇 가지 중요한 차이점이 존재한다.
| 구분 | 람다식 (Lambda Expression) | 익명 내부 클래스 (Anonymous Inner Class) |
|---|---|---|
| 목적 | 함수형 인터페이스의 단일 추상 메소드 구현 (익명 함수) | 클래스 또는 인터페이스 상속/구현 (익명 객체 생성) |
| 코드 간결성 | 매우 간결 (익명 내부 클래스 대비 코드 라인 수 현저히 감소) | 상대적으로 복잡 (클래스 선언, 메소드 구현, @Override 등 필요) |
this 키워드 | 외부 클래스 객체 참조 | 익명 내부 클래스 객체 자신 참조 |
| 변수 캡처링 (Variable Capturing) | effectively final 변수만 캡처 가능 (값 변경 불가) | final 또는 effectively final 변수 캡처 가능 (Java 8 이전에는 final 만 가능) |
| 생성자 | 람다식 자체는 생성자 없음 (객체 생성 메커니즘 다름) | 익명 내부 클래스는 생성자 가질 수 있음 |
| 성능 | 일반적으로 익명 내부 클래스보다 약간 더 성능 우수 (람다식 컴파일 최적화) | 람다식에 비해 성능 면에서 약간 불리할 수 있음 |
| 주요 사용처 | 함수형 인터페이스 구현, Stream API, 콜백 함수, 이벤트 핸들러 (간단한 로직) | 함수형 인터페이스 구현, 복잡한 로직, 상태를 가지는 객체, 다중 메소드 인터페이스 구현, 익명 객체 생성 필요한 모든 경우 |
람다식과 익명 내부 클래스 선택 가이드:
람다식을 무분별하게 사용하는 것은 오히려 코드 가독성을 해치고 유지보수를 어렵게 만들 수 있다. 여기서는 람다식을 효과적으로 사용하기 위한 가이드라인을 제시한다.
this 키워드를 사용하는 경우: 람다식의 this 와 익명 내부 클래스의 this 동작 방식이 다르므로, this 사용에 주의해야 한다.람다식 활용 핵심: "코드 간결성, 가독성, 유지보수성 향상" 에 긍정적인 효과를 줄 수 있는 경우에 람다식을 적극적으로 활용하고, 그렇지 않은 경우에는 기존 방식 (익명 내부 클래스, 일반 클래스) 과 람다식을 적절히 혼용하여 사용하는 것이 좋다.
Java 람다식에 대한 모든 것을 자세하게 알아보았다. 람다식은 Java 개발 방식을 혁신적으로 변화시키고, 코드를 더욱 간결하고 유연하며 강력하게 만들어주는 핵심 기능이다.
핵심 정리:
(파라미터) -> { 람다 몸체 } (표현식 몸체, 블록 몸체)this 동작 방식, 디버깅, 과도한 사용, 함수형 인터페이스 타입 제한this, 변수 캡처링, 성능 등 차이점 존재