람다
#Java/adv3
람다 정의
- 자바 8부터 도입된 람다는 자바에서 함수형 프로그래밍을 지원하기 위한 핵심 기능이다.
- 람다는 익명 함수이다. 따라서 이름 없이 함수를 표현한다.
(매개변수) -> {본문}
- 이름, 반환타입을 적지 않는다.
- 익명 클래스처럼
new
, 클래스명, 메서드 명, 반환 타입 등을 나열할 필요가 없다.
- 자바는 독립적인 함수를 지원하지 않으며, 메서드는 반드시 클래스나 인터페이스에 속한다
용어 - 람다 vs 람다식(Lambda Expression)
- 람다: 익명 함수를 지칭하는 일반적인 용어다. 쉽게 이야기해서 개념이다.
- 람다식:
(매개변수) -> {본문}
형태로 람다를 구현하는 구체적인 문법 표현을 지칭한다.쉽게 이야기해서 람다는 개념을, 람다식은 자바에서 그 개념을 구현하는 구체적인 문법을 의미한다.
람다는 변수처럼 다룰 수 있다
Procedure procedure = () -> { // 람다를 변수에 담음
System.out.println("hello! lambda");
};
procedure.run(); // 변수를 통해 람다를 실행
- 람다를
procedure
라는 변수에 담았고, 이 변수를 통해 람다를 실행할 수 있다.
람다도 클래스가 만들어지고, 인스턴스가 생성된다
- 람다도 익명 클래스처럼 클래스가 만들어지고, 인스턴스가 생성된다.
- 익명 클래스:
$
+ 숫자class.class = class lambda.lambda1.InstanceMain$1
- 람다의 경우
$$
로 구분하고 뒤에 복잡한 문자가 붙는다.lambda.class = class lambda.lambda1.InstanceMain$$Lambda/0x00000008000c2618
lambda.instance = lambda.lambda1.InstanceMain$$Lambda/0x00000008000c2618@3796751b
- 익명 클래스:
정리
- 람다를 사용하면 익명 클래스 사용의 보일러플레이트 코드를 크게 줄이고, 간결한 코드로 생산성과 가독성을 높일 수 있다.
- 대부분의 익명 클래스는 람다로 대체할 수 있다.
- 참고로 람다가 익명 클래스를 완전히 대체할 수 있는 것은 아니다. 람다와 익명 클래스의 차이는 뒤에서 따로 정리하겠다.
- 람다를 사용할 때
new
키워드를 사용하지 않지만, 람다도 익명 클래스처럼 인스턴스가 생성된다. - 지금은 람다를 익명 클래스의 구현을 간단히 표현할 수 있는 문법 설탕(Syntactic sugar, 코드를 간결하게 만드는 문법적 편의) 역할 정도로 생각하자. 람다와 익명 클래스의 차이는 뒤에서 설명한다.
함수형 인터페이스
- 함수형 인터페이스는 정확히 하나의 추상 메서드를 가지는 인터페이스를 말한다. (인터페이스의 메서드는 모두 추상 메서드!)
- 람다는 추상 메서드가 하나인 함수형 인터페이스에만 할당할 수 있다.
- 단일 추상 메서드를 줄여서 SAM(Single Abstract Method)이라 한다.
- 참고로 람다는 클래스, 추상 클래스에는 할당할 수 없다. 오직 단일 추상 메서드를 가지는 인터페이스에만 할당할 수 있다
SAM이 아닌 경우: 람다 할당 불가
public interface NotSamInterface {
void run();
void go();
}
// 컴파일 오류
/*
NotSamInterface notSamInterface = () -> {
System.out.println("not sam");
}; // 어디로 들어가야 할지 모르기때문에 오류
notSamInterface.run(); //?
notSamInterface.go(); //?
- 람다는 하나의 함수이다. 따라서 람다를 인터페이스에 담으려면 하나의 메서드(함수) 선언만 존재해야 한다.
- 추상 메서드가 여러 개면, 어디에 담아야 할 지 결정할 수 없다.
- 자바는 단 하나의 추상 메서드 (SAM: Single Abstract Method)만을 포함하는 함수형 인터페이스에만 람다를 할당할 수 있도록 제한했다.
SAM: 람다 할당 가능
public interface SamInterface {
void run();
}
- /@FunctionalInterface
@Override
를 통해 개발자의 실수를 컴파일 시점에 막을 수 있던 것처럼, 람다에서 또한 단 하나의 추상 메서드를 포함한다는 것을 보장하기 위해@FunctionalInterface
애노테이션을 붙여준다.- 이 애노테이션이 있으면 단 하나의 추상 메서드가 아니면 컴파일 단계에서 오류가 발생한다. 따라서 함수형 인터페이스임을 보장할 수 있다.
- 람다를 사용할 함수형 인터페이스에는
@FunctionalInterface
를 필수로 붙이는 걸 권장한다.(@Override 처럼)
@FunctionalInterface // 애노테이션 추가
public interface SamInterface {
void run();
}
람다와 시그니처
- 람다는 결국 함수형 인터페이스의 추상 메서드에 할당하는 거다 메서드의 형태를 정의하는 요소인 메서드 시그니처가 일치해야 한다.
- 메서드의 시그니처 구성요소
- 메서드 이름
- 매개변수의 수와 타입(순서 포함)
- 반환 타입
- 메서드의 시그니처 구성요소
- 람다는 익명 함수이므로 시그니처에서 이름은 제외하고, 매개변수, 반환 타입이 함수형 인터페이스에 선언한 메서드와 맞아야 한다.
람다와 생략
- 표현식: 하나의 값으로 평가되는 코드 조각. ex) 산술 논리 표현식, 메서드 호출, 객체 생성
- /단일 표현식1
- 단일 표현식인 경우 중괄호와 리턴 생략 가능
MyFunction function2 = (int a, int b) -> a + b;
- 단일 표현식이 아닐 경우 중괄호와 리턴 모두 필수
- 단일 표현식인 경우 중괄호와 리턴 생략 가능
MyFunction function3 = (int a, int b) -> {
System.out.println("람다 실행");
return a + b;
};
- /단일 표현식2:
- 단일 표현식이고, 매개변수와 반환 값이 없는 경우도 마찬가지로 중괄호 생략 가능하다. (원래 리턴은 생략 가능)
Procedure procedure2 = () -> System.out.println("hello! lambda");
- 타입 추론: 자바 컴파일러는 람다가 사용되는 함수형 인터페이스 메서드 타입을 기반으로 매개변수와 반환값의 타입 추론
MyFunction function1 = (int a, int b) -> a + b;
- 함수형 인터페이스인
MyFunction
의apply()
메서드를 보면 이미int a
,int b
로 매개변수의 타입이 정의되어 있다. - 따라서 이 정보를 사용하면 람다의
(int a, int b)
에서 타입 정보를 생략할 수 있다. - 타입 정보 생략:
MyFunction function2 = (a, b) -> a + b;
- 함수형 인터페이스인
- 매개변수 타입: 생략 가능하지만 필요하다면 명시적으로 작성할 수 있다.
- 반환 타입: 문법적으로 명시할 수 없고, 식의 결과를 보고 컴파일러가 항상 추론한다.
- /매개변수의 괄호 생략
- 매개변수가 정확히 하나이면서, 타입을 생략하고, 이름만 있는 경우 소괄호
()
를 생략할 수 있다.MyCall call3 = value -> value * 2;
- 매개변수가 없는 경우에는
()
가 필수이다. - 매개변수가 둘 이상이면
()
가 필수이다.
- 매개변수가 정확히 하나이면서, 타입을 생략하고, 이름만 있는 경우 소괄호
- 단일 표현식이고, 매개변수와 반환 값이 없는 경우도 마찬가지로 중괄호 생략 가능하다. (원래 리턴은 생략 가능)
람다의 전달
람다는 함수형 인터페이스를 통해 변수에 대입하거나, 메서드에 전달하거나 반환할 수 있다.
- /람다를 변수에 대입하기
MyFunction add = (a, b) -> a + b;
add.apply(1, 2)
MyFunction cal = add;
cal.apply(1, 2)
- 변수
add
의 타입은MyFunction
함수형 인터페이스이다. 따라서MyFunction
형식에 맞는 람다를 대입할 수 있다. (메서드 시그니처가 일치한다) - 람다도 일반 인스턴스 참조값을 대입하는 것과 마찬가지다. 함수형 인터페이스로 선언한 변수에 람다를 대입하는 것은 람다 인스턴스의 참조값을 대입하는 것이다.
- 람다도 인터페이스(함수형 인터페이스)를 사용하므로, 람다 인스턴스의 참조값을 변수에 전달할 수 있다.
- 매개변수를 통해 메서드(함수)에 람다를 전달할 수 있다. (정확히는 람다 인스턴스의 참조값을 전달)
- 메서드가 람다를 반환할 수 있다. (정확히는 람다 인스턴스의 참조값을 반환)
- 람다도 인터페이스(함수형 인터페이스)를 사용하므로, 람다 인스턴스의 참조값을 변수에 전달할 수 있다.
- /람다를 메서드(함수)에 전달하기
MyFunction add = (a, b) -> a + b;
static void calculate(MyFunction function) { int result = function.apply(x, y);}
- 변수를 통해 람다 전달:
calculate(add);
- 람다를 직접 전달:
calculate((a, b) -> a + b);
- 람다를 직접 전달해도 람다 인스턴스가 만들어져 참조값을 전달하는 것은 동일하다.
- 변수를 통해 람다 전달:
- /람다를 반환하기
// 람다를 반환하는 메서드
static MyFunction getOperation(String operator) {
switch (operator) {
case "add":
return (a, b) -> a + b; // 람다 인스턴스를 생성하고 참조값을 반환
case "sub":
return (a, b) -> a - b; // 람다 인스턴스를 생성하고 참조값을 반환
default:
return (a, b) -> 0; // 람다 인스턴스를 생성하고 참조값을 반환
}
}
psvm {
MyFunction add = getOperation("add"); // 람다 인스턴스의 참조값을 대입
System.out.println("add.apply(1, 2) = " + add.apply(1, 2));
MyFunction xxx = getOperation("xxx"); // 람다 인스턴스의 참조값을 대입
System.out.println("xxx.apply(1, 2) = " + xxx.apply(1, 2));
}
고차 함수(Higher Order Function)
- 람다를 변수에 대입한다는 것은 람다 인스턴스의 참조값을 대입하는 것이고, 람다를 메서드(함수)의 매개변수나 반환값으로 넘긴다는 것 역시 람다 인스턴스의 참조값을 전달, 반환하는 것이다.
MyFunction add = (a, b) -> a + b;
calculate(add);
return (a, b) -> a + b;
- 고차 함수는 함수를 값처럼 다루는 함수를 뜻한다. 일반적으로 다음 두 가지 중 하나를 만족하면 고차 함수라 한다.
- 함수를 인자로 받는 함수(메서드)
- 함수를 반환하는 함수(메서드)
- "값"을 다루는 것을 넘어, "함수"라는 개념 자체를 값처럼 다룬다는 점에서 추상화의 수준(계층, order)이 한 단계 높아진다고 해서 Higher-Order(더 높은 차원의) 함수라고 부른다.
- 자바에서 함수를 주고받는다는 것은 "함수형 인터페이스를 구현한 어떤 객체(람다든 익명 클래스든)를 주고받는 것"과 동의어이다. (함수형 인터페이스는 인터페이스이므로 익명 클래스, 람다 둘 다 대입할 수 있다. 하지만 실질적으로 함수형 인터페이스에는 람다를 주로 사용한다.)
'Java > Modern Java(8~)' 카테고리의 다른 글
[Java] 62. 메서드 참조 (0) | 2025.07.04 |
---|---|
[Java] 61. 람다 vs 익명 클래스 (0) | 2025.07.04 |
[Java] 60. 람다 활용 (0) | 2025.07.04 |
[Java] 59. 함수형 인터페이스 (0) | 2025.07.04 |
[Java] 57. 람다가 필요한 이유 (0) | 2025.07.04 |