람다 표현식이란?
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화 한 것.
- 람다 표현식은 익명 클래스처럼 이름이 없는 함수면서 메서드를 인수로 전달할 수 있으므로 익명 클래스와 비슷하다고 일단 생각하며 이해를 하나씩 해보자
- 람다 표현식은 이름은 가질 수 없지만 파라미터, 바디, 리턴 타입, 예외 리스트는 가질 수 있다.
- 람다는 기술적으로 자바8 이전의 자바로 할 수 없었던 일을 제공하는 것이 아니라는 점을 명심하자, 코드가 간결해지고 유연해지는 것!
람다의 특징
- 익명 : 이름이 없으므로 익명이라 표현하고 구현해야할 코드가 적고 간결하다.
- 함수 : 메서드처럼 특정 클래스에 종속되지 않고 독립적인 기능을 하기 때문에 함수라고 불린다.
- 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
- 간결성 : 익명클래스처럼 로직과 필요없는 코드를 구현할 필요가 없다.
람다 사용법
파라미터 + 화살표 + 바디
로 이루어진다
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 람다 파라미터
// 화살표
// 람다 바디
(parameters) -> expression
또는 블록스타일
(parameters) -> { statements; }
그렇다면 람다 표현식은 어디에 사용할 수 있을까??
람다 표현식은 함수형 인터페이스 문맥에서 사용할 수 있다.
함수형 인터페이스
함수형 인터페이스는 오직 하나의 추상메서드만 지정하는 인터페이스이다.
- java.util.function, 기본제공 함수형 인터페이스
- 함수형 인터페이스로 뭘 할 수 있을까?
- 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로
전체 표현식을 함수형 인터페이스를
구현한클래스의 인스턴스
라고취급
할 수 있다. - 람다를 이해하기 위해 가장 중요한 개념
- 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로
interface Runnable {
void run();
}
public class Main {
public static void process(Runnable r){
r.run();
}
public static void main(String[] args)
{
Runnable r1 = () -> System.out.println("Hello, r1");
Runnable r2 = new Runnable() {
@Override
public void run() {
System.out.println("Hello, r2");
}
};
r1.run(); // hello, r1
r2.run(); // hello, r2
process(() -> System.out.println("Hello, r3")); // hello, r3
}
}
함수 디스크립터(function descriptor)
- 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다.
- 람다 표현식의 시그니처를 서술하는 메서드를
함수 디스크립터
라고 부른다.- 예를들어 MyFunc 인터페이스의 함수 디스크립터는
(int, int) → int
이다.
- 예를들어 MyFunc 인터페이스의 함수 디스크립터는
- 람다 표현식은
함수형 인터페이스
를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있다.
람다 활용 : 실행 어라운드 패턴
- 자원 처리(예를 들면 데이터베이스의 파일 처리)에 사용하는
순환 패턴(recurrent pattern)
은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어 진다. - 설정(setup)과 정리(cleanup) 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다.
- 설정 - 작업 a - 정리
- 이와 같은 형식의 코드를
실행 어라운드 패턴
이라고 한다.
public String processFile() throws IOException {
try ( BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine(); // 실제 작업 코드
}
}
1단계 : 동작(메서드) 파라미터화를 기억해보자
- 현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있다.
- 만약 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까?
- 기존의 설정, 정리 코드는 재사용 하고 싶고 processFile() 메서드만 다른 동작을 수행하도록 바꾸고 싶다.
- 바로 processFile()을 동작 파라미터화 시키는 것이다.
- 한 번에 두 줄 출력
- BufferedReader를 인수로 받아서 String을 반환하는 람다 사용
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
2단계 : 함수형 인터페이스를 이용해서 동작 전달
- 함수형 인터페이스 자리에 람다를 사용할 수 있다.
- 따라서 BufferedReader -> String 과 IOException을 던질(throw) 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.
- 이 인터페이스를 BufferedReaderProcessor라고 정의하자.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
// ...
}
3단계 : 동작 실행
- 이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader -> String)와 일치하는 람다를 전달할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedREader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
4단계 : 람다 전달
이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.
String oneLine = processFile((BufferedReaer br) -> br.readLine());
String twoLine = processFile((BufferedReaer br) -> br.readLine() + br.readLine());
함수형 인터페이스 사용
자바8 라이브러리 설계자들은 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공
Predicate<T>
- 조건식을 표현하는데 사용
- T -> boolean
Supplier<T>
- void를 받아 제네릭 형식 T로 반환
- () -> T
Consumer<T>
- Supplier 와 반대
- T -> ()
Function<T, R>
- 일반적인 함수, T를 받아 R을 반환
- T -> R
기본형 특화
- 자바의 모든 형식은 참조형 혹은 기본형
- 하지만 제네릭은 내부 구현상 어쩔 수 없이 참조형만 사용 가능하다.
- 그래서 박싱(기본형 -> 참조형)과 언박싱(참조형->기본형) 제공한다.
- 박싱한 값은 기본형을 감싸는 래퍼이며 힙에 저장된다.
- 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.
- 오토박싱으로 저 과정은 자동으로 해주지만, 자원을 소모하게 된다.
- 그래서 오토박싱을 피할 수 있는 버전의 함수형 인터페이스 제공
- IntConsumer, LongConsumer...
형식 검사, 형식 추론, 제약
람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다.
따라서 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다.
대상형식
- 어떤 컨텍스트에서 기대되는 람다 표현식의 형식
- 람다 표현식이 예외를 던질 수 있다면, 추상 메소드도 같은 예외를 던질 수 있어야 함.
형식 검사
- 람다가 사용되는
콘텍스트(context)
를 이용해서 람다의형식(type)
을 추론할 수 있다.
List<Apple> heavierThan150g = filter(apples, (Apple apple) -> apple.getWeight() > 150**);
1. 람다가 사용된 콘텍스트는 무엇이지? filter의 정의 확인
-> filter(List<Apple> apples, Predicate<Apple> p)
2. 대상 형식은 Predicate<Apple>이다.
3. Predicate<Apple> 인터페이스의 추상메서드는 무엇이지?
-> boolean test(Apple apple)
4. test 메소드의 Apple -> boolean 함수 디스크립터 묘사
5. 찾은 함수의 디스크립터가 전달된 람다 표현식과 일치하는지 확인
6. 형식 검사 성공적으로 완료
형식 추론
- 자바 컴파일러는 람다 표현식이 사용된 컨텍스트(대상 형식)를 이용해서 함수 디스크립터를 알 수 있으므로 람다의 시그니처도 추론할 수 있다.
- 따라서 컴파일러는 람다 표현식의 파라미터 형식을 추론할 수 있으므로 람다 문법에서 생략할 수 있다.
// 형식 추론 하지 않음
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 형식 추론 함
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
자유변수(free variable) 와 람다 캡처링(capturing lambda)
- 지금까지 살펴본 모든 람다 표현식은 인수를 자신의 바디 안에서만 사용했다.
- 하지만 람다 표현식에서는 익명 함수가 하는 것처럼
자유 변수
(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다.- 이와 같은 동작을
람다 캡처링
이라고 한다.
- 이와 같은 동작을
- 다음은 portNumber 변수를 캡처하는 람다 예제이다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
- 하지만 자유 변수에도 제약이 있다.
- 람다는
인스턴스 변수
와정적 변수
를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록) 할 수 있다. - 하지만 그러러면
지역 변수
는명시적으로 final
로 선언되어 있어야 하거나,실질적으로 final로 선언된 변수와 똑같이 사용(effectively final)
되어야 한다. - 즉
한번만 할당할 수 있는 지역 변수
를 캡처 가능
- 람다는
- 아래는 portNumber에 값을 두 번 할당하므로 컴파일 할 수 없는 코드이다.
int portNumber = 1337;
// error
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
class CapturingTest {
private int a = 1;
public void test() {
final int b = 2;
int c = 3;
int d = 4;
final Runnable r = () -> {
// 인스턴스 변수 a는 final로 선언돼있을 필요도, final처럼 재할당하면 안된다는 제약조건도 적용되지 않는다.
a = 123;
System.out.println(a);
};
r.run();
// 지역변수 b는 final로 선언돼있기 때문에 OK
final Runnable r2 = () -> System.out.println(b);
r2.run();
// 지역변수 c는 변수에 값을 재할당하지 않았으므로 OK
final Runnable r3 = () -> System.out.println(c + " " + b);
r3.run();
// 지역변수 d는 final이 아니고 effectively final도 아니다.
d = 12;
final Runnable r4 = () -> System.out.println(d);
}
}
이유가 무엇일까??
- 람다 표현식은 여러 쓰레드에서 사용할 수 있다.
- 힙 영역에 저장되는 인스턴스 변수와 달리 스택 영역에 저장되는 지역 변수는 외부 쓰레드에서 접근 불가능하다.
- 지역 변수에 바로 접근할 수 있다는 가정하에 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다.
- 따라서 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공하는데. 이를 람다 캡쳐링이라고 한다.
- 복사본은 원본의 값이 바뀌어도 알 수 없기 때문에 쓰레드 동기화를 위해 지역 변수는
final
또는effectively final
상태여야 한다.
즉 가변 지역 변수를 새로운 스레드에서 캡쳐할 수 있다면 안전하지 않은 동작이 수행될 가능성이 생기기 때문!!!
메서드 참조
메서드 참조는 특정 람다 표현식을 축약한 것이라고 생각하면 좋다.
- 메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.
- 람다식이 하나의 메서드만 호출하는 경우에는 메서드명을 참조함으로써 가독성을 높일 수 있다.
// (Apple apple) -> apple.getWeight()
Apple::getWeight
// () -> Thread.currentThread().dumpStack()
Thread.currentThread()::dumpStack()
// (str, i) -> str.substring(i)
String::substring
// (String s) -> System.out.println(s)
System.out::println
// (String s) -> this.isValidName(s)
this::isValidName
메서드 참조의 세 가지 유형
메서드 참조는 세 가지 유형으로 구분할 수 있다.
- 정적 메서드 참조
- 람다 : (x) → ClassName.method(x)
- 메서드 참조 : ClassName::method
- ex)
Integer::parseInt
- 인스턴스 메서드 참조
- 람다 : (obj, x) → obj.method(x)
- 메서드 참조 : ClassName::method
- ex)
String::length
- 기존 객체의 인스턴스 메서드 참조
- 이미 생성된 객체의 메서드를 람다식에서 사용하는 경우
MyClass obj = new MyClass();
(x) -> obj.equals(x);
obj::equals
- 람다 : (x) → obj.method(x)
- 메서드 참조 : obj::method
- 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷하게 함수형 인터페이스와 호환하는지 확인한다.
- 즉, 메서드 참조는 콘텍스트의 형식과 일치해야 한다.
생성자 참조
생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.
Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s = MyClass::new;
MyClass obj1 = s.get();
- 매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하거나 새로 정의해서 사용하면 된다.
Function<Integer, MyClass> f = (i) -> new MyClass(i);
Function<Integer, MyClass> f2 = MyClass::new;
Function<Integer, int[]> ff = x -> new int[x];
Function<Integer, int[]> ff2 = int[]::new;
요약
- 람다 표현식은 익명 함수의 일종이다.
- 함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스이다.
- 함수형 인터페이스를 기대하는 곳에서 람다 표현식을 사용할 수 있으며, 추상 메서드를 즉석으로 제공할 수 있다. 이 때 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급한다.
- 메서드 참조를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.
부족했던 부분(TODO)
Function의 합성과 Predicate의 결합
- Function의 addThen, compose 디폴트 메서드 코드 이해 및 활용 해보기
- Predicate의 and, or, negate 디폴트 메서드 코드 이해 및 활용 해보기
왜 람다에서 지역변수는 final or effectively final?
- https://vagabond95.me/posts/lambda-with-final/
- https://www.baeldung.com/java-lambda-effectively-final-local-variables
참고 문헌
'java' 카테고리의 다른 글
[모던 자바 인 액션] 스트림 활용 (0) | 2024.07.07 |
---|---|
[모던 자바 인 액션] 스트림 소개 (0) | 2024.07.07 |
[모던 자바 인 액션] 동작 파라미터화 (0) | 2024.07.07 |
[모던 자바 인 액션] 자바 8, 9, 10, 11에서 일어난 일 (0) | 2024.07.07 |
static inner vs non-static inner class (0) | 2024.07.07 |