이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › 람다와 스트림
람다와 함수형 인터페이스
람다에 적은 코드는 언제 실행되나
람다는 "나중에 실행할 동작"을 값처럼 변수에 담고 다른 메서드에 넘기는 문법입니다. 람다를 적은 줄에서는 그 안의 코드가 실행되지 않고, 넘겨받은 쪽이 부를 때 비로소 실행됩니다.
실행으로 대조 · JDK 21에서 서른여섯 가지 조합을 실행해 대조
이 실습에 나오는 개념
- 람다 표현식
- 이름 없는 함수를 간결하게 표현하는 문법으로 Java 8에서 도입되었다. (매개변수) -> 실행문 형태로 쓰며, 추상 메서드가 하나뿐인 함수형 인터페이스의 구현으로 사용된다. 동작(코드)을 값처럼 다른 메서드에 전달할 수 있게 해 준다.
- 함수형 인터페이스
- 추상 메서드가 하나뿐인 인터페이스로(equals처럼 Object의 메서드를 다시 선언한 것은 세지 않고, default·static 메서드는 여러 개 있어도 된다), 람다 표현식이나 메서드 참조를 대입할 수 있는 타입이다. java.util.function 패키지에 Function(입력을 받아 결과 반환), Supplier(값 공급), Consumer(값 소비), Predicate(조건 판별) 등이 미리 정의되어 있다.
- 메서드 참조
- 이미 있는 메서드를 람다 대신 이름으로 가리키는 문법이다. String::length, System.out::println처럼 클래스(또는 객체)와 메서드 이름을 ::로 연결해 쓴다. 람다가 단순히 메서드 하나를 호출하기만 할 때 더 짧게 표현할 수 있다.
- 익명 클래스와 람다의 차이
- 익명 클래스는 인터페이스나 클래스를 그 자리에서 구현하면서 이름 없이 객체를 만드는 문법으로, new 인터페이스이름() { 메서드 구현 } 형태로 쓴다. 람다는 추상 메서드가 하나뿐인 함수형 인터페이스에 한해 같은 일을 (매개변수) -> 실행문 형태로 짧게 쓰는 문법이다. 함수형 인터페이스를 구현할 때 this를 쓰지 않는 단순한 본문이면 둘 중 어느 것으로 써도 호출한 결과가 같다. 다만 익명 클래스는 메서드가 여러 개인 인터페이스도 구현할 수 있고 필드를 가질 수 있으며, 그 안의 this는 익명 클래스의 객체 자신을 가리킨다. 람다 안의 this는 람다를 적은 바깥 객체를 가리킨다.
이렇게 바꿔 보세요
- 처음 설정으로 — 람다를 만든 줄에서는 아무 일도 없고, run이 부를 때마다 본문이 실행됩니다.
- 받기만 하고 부르지 않으면 — 람다를 만들고 넘겼는데도 본문은 한 번도 실행되지 않습니다.
- 익명 클래스로 쓰면 — 코드는 길어지지만 결과는 람다와 같습니다.
- 메서드 참조로 쓰면 — 이미 있는 메서드를 이름으로 가리킵니다. 결과는 같습니다.
- Supplier를 넘겨 두고 나중에 get — 값을 만드는 일을 미뤄 두었다가 필요할 때 실행합니다.
- Predicate — 조건 검사를 넘깁니다. true 또는 false가 돌아옵니다.
- Consumer — 값을 받아 쓰기만 하고 돌려주는 것이 없습니다.
처음 설정으로 실행하면
- Function<String, Integer> f = s -> length(s): 화살표(->) 오른쪽에 적은 동작을 담은 객체를 만들어 변수 f에 담습니다. length 메서드는 아직 불리지 않습니다. 아무것도 출력되지 않습니다.
- System.out.println("만든 뒤"): "만든 뒤"가 출력됩니다. 동작을 만든 줄을 이미 지났지만 "본문 실행"은 아직 한 번도 출력되지 않았습니다.
- run(f): 변수 f에 담긴 동작을 run 메서드에 넘깁니다. 숫자나 문자열을 넘기듯이 동작을 넘기는 것입니다. 넘기는 것만으로는 실행되지 않습니다.
- System.out.println("run 시작"): run 안으로 들어와 "run 시작"이 출력됩니다.
- f.apply("hello"): 여기서 비로소 동작이 실행됩니다. length 메서드가 불려 "본문 실행: length(hello)"가 출력됩니다. 돌려받은 5가 "결과 5"로 출력됩니다.
- f.apply("hi"): 같은 동작이 한 번 더 실행됩니다. length 메서드가 불려 "본문 실행: length(hi)"가 출력됩니다. 돌려받은 2가 "결과 2"로 출력됩니다.
- System.out.println("run 끝"): "run 끝"이 출력됩니다. "본문 실행"은 모두 2번 출력되었습니다.
실행 결과
- 동작을 만든 줄에서는 아무것도 실행되지 않았고, run 안에서 apply 메서드를 부를 때마다 실행되었습니다. 람다 안의 코드는 만들 때가 아니라 불릴 때 실행됩니다.
- Function<T, R>은 T 타입 값 하나를 받아 R 타입 값을 돌려주는 동작이고, apply로 부릅니다. 값을 다른 값으로 바꾸는 일(변환)에 씁니다.
- 익명 클래스, 람다, 메서드 참조 가운데 무엇으로 쓰든 결과는 같습니다.
코드
class Demo {
static int length(String s) { // 넘길 동작이 하는 일
System.out.println("본문 실행: length(" + s + ")");
return s.length();
}
static void run(Function<String, Integer> f) { // 동작을 받아서 부르는 쪽
System.out.println("run 시작");
System.out.println("결과 " + f.apply("hello"));
System.out.println("결과 " + f.apply("hi"));
System.out.println("run 끝");
}
public static void main(String[] args) {
Function<String, Integer> f = s -> length(s); // 동작을 만든다 (아직 실행되지 않는다)
System.out.println("만든 뒤");
run(f); // 동작을 값처럼 넘긴다
}
}
이 모형의 전제
서른여섯 가지 조합의 출력 순서, 본문 실행 횟수, 돌려받은 값을 이 PC의 JDK 21에서 같은 구조의 코드를 실행한 결과와 대조했습니다. 각 인터페이스가 부르는 메서드의 이름, 받는 값의 개수, 돌려주는 타입도 실제 인터페이스를 들여다본 값과 대조했습니다. 익명 클래스, 람다, 메서드 참조는 이 실습에서 보는 결과(출력과 돌려받은 값)가 같다는 뜻이며, 내부에서 객체가 만들어지는 방법과 this가 가리키는 대상은 서로 다릅니다. 람다 안에서 바깥의 지역 변수를 쓸 때는 값이 바뀌지 않는 변수만 쓸 수 있는데, 이 규칙은 "내부 클래스와 익명 클래스" 실습에서 설명합니다.
앞 실습: 정렬과 Comparator · 다음 실습: 스트림 지연 평가