람다 표현식이란
람다 표현식(Lambda Expression)은 Java 8부터 도입된 함수형 프로그래밍 기능 중 하나로 이름이 없는(익명) 함수를 간결하게 표현하는 구문이다.
Java 8 이전에는 단일 메소드를 구현하려 해도 반드시 해당 메소드를 포함하는 클래스를 정의해야 했지만, 람다 표현식을 사용하면 불필요한 boilerplate 코드가 크게 줄어들고 함수 자체를 값처럼 전달하거나 변수에 바로 할당할 수 있게 되었다.
// 입력 받은 두 정수의 합산 결과를 리턴하는 메소드
int add(int x, int y) {
return x + y;
}
// 람다 표현식을 이용해 아래와 같이 단축 가능 (메소드 반환 타입, 메소드 이름 생략 가능)
(int x, int y) -> {
return x + y;
};
// 매개변수 타입도 생략이 가능
(x, y) -> {
return x + y;
};
// 함수에 리턴문 한줄만 있을 경우 더 단축시킬 수 있다. (중괄호, return 생략)
(x, y) -> x + y;
* 타입을 생략해도 컴파일러가 에러를 띄우지 않는 이유는 타입 추론 기능이 작동하기 때문이다.
람다 표현식과 함수형 인터페이스
람다식의 형태를 보면 마치 자바의 메소드를 “값” 처럼 다루는 듯 보이지만, 자바 문법상 메소드는 독립적으로 선언할 수 없다. 타입에 대한 제약 사항이 강한 자바에서 이와 같은 방식이 가능한 이유는 함수형 인터페이스 덕분이다.
함수형 인터페이스란 java 8 부터 등장한 기술로 단 하나의 추상 메소드만을 갖는 인터페이스를 말하며 @FunctionalInterface 어노테이션을 붙여 컴파일 시점에 이를 보장할 수 있다.
이러한 함수형 인터페이스는 함수를 객체처럼 취급하는 함수형 프로그래밍을 지원하기 위해 등장했다.
함수형 프로그래밍과 함수형 인터페이스에 대한 보다 자세한 설명은 아래 링크를 참고하면 된다.
https://jaystorage.tistory.com/55
함수형 프로그래밍
프로그래밍 패러다임(Programming Paradigm) 프로그래밍 패러다임이란 코드를 어떻게 구조화 할지 정의하는 방식을 의미한다. 이러한 프로그래밍 패러다임은 그 종류가 매우 다양하나 큰 틀로 나눴을
jaystorage.tistory.com
https://jaystorage.tistory.com/56
함수형 인터페이스
함수형 인터페이스란 자바에서 함수형 인터페이스란 추상 메소드가 1개만 정의된 인터페이스를 의미한다. 이러한 함수형 인터페이스는 java 8 부터 등장했는데 람다 표현식을 이용해 함수형 프
jaystorage.tistory.com
@FunctionalInterface
public interface Converter<F, T> {
T convert(F from);
}
// 람다 표현식으로 구현
Converter<String, Integer> converter = s -> Integer.valueOf(s);
람다식은 이처럼 인터페이스의 단일 추상 메소드를 구현하는 익명 객체를 생성하는 간단한 방법을 제공한다.
* 자바에서 람다식은 함수형 인터페이스에만 바인딩 될 수 있도록 설계되어 있다.
람다 표현식의 타입 추론
앞서 람다식은 함수형 인터페이스의 추상 메소드를 구현한다고 설명했다. 그런데 리턴 타입도 파라미터도 없는 람다식을 컴파일러가 대체 어떤 타입의 함수인 줄 알고 문법을 허용하는 것일까?
그 비밀은 바로 제네릭 타입 변수 추론에 있다.
- 제네릭 메소드 호출
- filter 선언부에 <T>가 있으므로 호출부에서 넘긴 List<String>을 보고 타입 변수 T를 String 으로 추론한다. - 타겟 타입(Target Type) 결정
- filter의 두 번째 파라미터 타입은 Predicate<? super T>
- 여기서 T가 String으로 결정되었으므로 Predicate<? super String> 이 되고, 결과적으로 Predicate<String> 처럼 사용된다. - 람다식 바인딩
- 이제 컴파일러는 람다식 (name -> name.length() > 3) 을 boolean test(String name) 를 구현하는것으로 간주하여 name 파라미터를 String으로, 반환값을 boolean으로 설정한다.
이처럼 람다식은 “메소드 호출 문맥”에서 제네릭 메소드의 타입 변수 T를 먼저 추론하고, 그 T로 함수형 인터페이스(Predicate<? super T> 등)의 실제 타입을 결정한다. 이후 그 인터페이스의 단일 추상 메소드 시그니처를 보고 파라미터, 반환 타입을 채워 넣어 컴파일한다.
람다 표현식 사용법
기본 문법
람다 표현식은 크게 파라미터, 화살표, 본문으로 구성된다.
1. 파라미터
- 0개 : 빈 괄호 사용
() -> System.out.println("No args");
- 1개 : 괄호 생략 가능
x -> x * x
- 2개 이상 : 반드시 괄호로 묶기
(a, b) -> a + b
- 타입 명시(선택)
(int x, int y) -> x + y
- var 키워드(Java 11 이상)
// Java 10까지: 타입 생략 시 컴파일러가 문맥(타깃 타입)으로 추론
(x, y) -> x + y
// Java 11+: 매개변수에 var 사용 가능
// x와 y를 컴파일러가 추론한 타입으로 잡되, 문법적으로는 var 키워드를 쓴다는 선언
(var x, var y) -> x + y
// 매개변수에 @NonNull 같은 어노테이션을 붙이려면 타입을 명시해야 하는데 이전 방식에서는 불가능 함
(@NonNull var x, var y) -> x + y
2. 화살표
- 파라미터와 본문을 구분하는 기호로 반드시 -> 형태여야 함
3. 본문
1) 단일 표현식(Expression)
- 중괄호, return 생략
- 표현식 결과가 자동으로 반환
(a, b) -> a + b
2) 블록(Block)
- 여러 문장 작성 가능
- return 문 필요
(a, b) -> {
System.out.println("Adding numbers");
return a + b;
}
메소드 참조
람다 본문이 단순히 기존 메소드를 호출하는 한 줄 뿐이라면 ClassOrInstance::methodName 형태로 더 축약할 수 있다.
1. 정적 메소드 참조
Function<String,Integer> f = Integer::parseInt;
2. 특정 인스턴스의 인스턴스 메소드 참조
PrintStream out = System.out;
Consumer<String> c = out::println;
3. 임의 객체의 인스턴스 메소드 참조 (unbound instance method)
BiPredicate<String,String> p = String::equalsIgnoreCase;
// (a,b) -> a.equalsIgnoreCase(b) 와 동일
4. 생성자 참조
Supplier<List<String>> s = ArrayList::new;
// () -> new ArrayList<>()
5. 배열 생성자 참조
Function<Integer,String[]> arr = String[]::new;
// length -> new String[length]
예외 처리
람다식에서 Checked Exception을 던지려면 함수형 인터페이스에 throws 선언이 필요하다.
@FunctionalInterface
interface IOConsumer<T> {
void accept(T t) throws IOException;
}
IOConsumer<Path> reader = p -> Files.readString(p);
변수 캡쳐 규칙
람다식은 사실상 final(effectively final) 인 로컬 변수만 참조할 수 있다. (여기에서 말하는 사실상 final이란 변수를 final로 선언하지 않아도 한 번만 값이 정해지고 그 이후로 재할당되지 않는 경우를 의미함)
int base = 5; // 한 번만 값이 할당되고 변경되지 않음 → effectively final
Function<Integer,Integer> f = x -> x + base;
// 람다 안에서 base를 읽기만 함
아래처럼 변수에 재할당이 일어나면 컴파일 오류가 발생한다.
int count = 0;
Function<Integer,Integer> f2 = x -> x + count; // 여기까지는 OK
count = 1; // 오류: “local variables referenced from a lambda expression must be final or effectively final”
단 람다 안에서 list.add(...) 같은 객체 내부 상태 변경은 가능하다.
List<String> list = new ArrayList<>();
// 람다 안에서 list.add(...)로 리스트 내부 상태를 변경 가능
Consumer<String> addToList = s -> list.add(s);
addToList.accept("Hello");
addToList.accept("World");
System.out.println(list); // 출력: [Hello, World]
클래스의 멤버 변수(인스턴스 필드와 static 필드)는 final 여부에 상관없이 람다식 안에서 자유롭게 읽고 변경할 수 있다.
public class FieldCaptureDemo {
private int instanceCounter = 0; // 인스턴스 필드
private static int staticCounter = 0;// 정적(static) 필드
public static void main(String[] args) {
FieldCaptureDemo demo = new FieldCaptureDemo();
// 인스턴스 필드 변경
Runnable incInstance = () -> demo.instanceCounter++;
// static 필드 변경
Runnable incStatic = () -> staticCounter++;
incInstance.run();
incStatic.run();
incInstance.run();
incStatic.run();
System.out.println("instanceCounter = " + demo.instanceCounter); // 출력: 2
System.out.println("staticCounter = " + staticCounter); // 출력: 2
}
}
람다 표현식의 한계
람다식은 boilerplate 코드를 줄이고 가독성을 높이는데 크게 일조하지만 모든 상황에 적합한 것은 아니다.
- 문서화 불가
- 람다 자체가 이름이 없는 함수이기 때문에 메소드와 클래스와 다르게 문서화가 불가능 - 디버깅이 어려움
- 람다 내부는 익명 함수로 처리되어 스택 트레이스에 명확한 클래스명, 메서드명이 나오지 않음 - Checked Exception 처리 불편
- 표준 함수형 인터페이스(Function, Consumer…)의 apply나 accept 시그니처에 throws가 없어 람다 본문에서 Checked Exception을 직접 던질 수 없음 - stream api 에서 람다를 사용할 시 for문 보다 성능이 떨어짐
- 과도한 중첩 시 가독성 저하
- 복잡한 로직을 람다 안에 모두 넣을 경우 가독성이 크게 저하됨
출처 : https://www.youtube.com/watch?v=4ZtKiSvZNu4&t=152s https://www.youtube.com/watch?v=nsK4TP_uvaY&t=326s https://inpa.tistory.com/entry/%E2%98%95-Lambda-Expression |
'Java' 카테고리의 다른 글
함수형 인터페이스 (3) | 2025.06.08 |
---|---|
함수형 프로그래밍 (2) | 2025.06.06 |
Java Collection Framework (0) | 2025.05.25 |
Enum (0) | 2025.05.11 |
Singleton 패턴 (0) | 2024.07.16 |