2025. 1. 16. 18:40ㆍBook/모던 자바 인 액션
목차.
1. 람다란 무엇인가?
2. 실행 어라운드 패턴
3. 함수형 인터페이스, 형식 추론
4. 메서드 참조
5. 람다 만들기
1. 람다란 무엇인가?
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 반생할 수 있는 예외 리스트는 가질 수 있다. 람다의 특징을 살펴보자
* 익명
- 보통의 메서드와 달리 이름이 없어 익명이라고 표현한다. 구현해야 할 코드에 대한 걱정거리가 줄어든다.
* 함수
- 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 메서드처럼 파라미터 리스트, 바디, 반환 형식 가능한 예외 리스트를 포함한다.
* 전달
- 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
* 간결성
- 익명 클래스처럼 자질구레한 코드를 구현할 필요가 없다.
람다 표현식을 사용하면 동작 파라미터 형식의 코드를 더 쉽게 구현할 수 있다.
//익명 클래스
Comparator<Apple> byWeight = new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
};
// 람다 코드
Comparator<Apple> byWeight2 = (o1, o2) -> o1.getWeight().compareTo(o2.getWeight());
(o1, o2)
=> 파라미터 리스트
->
=> 화살표 : 파라미터리스트, 람다 바디를 구분
o1.getWeight().compareTo(o2.getWeight())
=> 람다 바디 : 람다의 반환값
2. 실행 어라운드 패턴
초기화/준비 코드 => 작업A => 정리/마무리 코드
초기화/준비 코드 => 작업B => 정리/마무리 코드
실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태
public String processFile() throws IOException {
try(BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
람다를 활용해서 좀 더 유연하게 위 메서드를 수정해보자
만약 한 번에 한 줄이 아닌 두 줄을 읽어들이게 수정하려면 메서드를 수정해야 한다.
이전 챕터에서 했던대로 동작을 파라미터화 시켜서 한다면 기존의 준비, 마무리 코드는 변경하지 않은채로 작업부만 변경 할 수 있을 것이다.
@FunctionalInterface
public interface BufferedReaderProcess {
String process(BufferedReader br) throws IOException;
}
public String processFile(BufferedReaderProcess p) throws IOException {
try(BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
System.out.println(processFile((br -> br.readLine() + "\n" + br.readLine())));
3. 형식 검사, 형식 추론, 제약
* 형식 검사
List<Apple> heavierThan150g =
filter(inventory, (Apple apple) -> apple.getWeight() > 150);
1. 람다가 사용된 콘텍스는 무엇인가? -> filter 정의 확인
filter(List<Apple> inventory, Predicate<Apple> p)
2. 대상 형식은 Predicate<Apple>이다.
3. Predicate<Apple> 의 추상 메서드 시그니쳐(함수 디스크립터) 확인해보자
4. Apple을 인수로 받아 boolean 을 반환한다.
boolean test(Apple apple)
위 예제에서는 Apple을 인수로 받고 boolean을 반환하므로 유효하다
* 같은 람다, 다른 함수형 인터페이스
대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다
Comparator<Apple> c1 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
* 형식 추론
자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 컨텍스트를 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다.
Comparator<Apple> c1 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c1 =
(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향싱시킬 때도 있다.
* 지역 변수 사용
지금까지 람다 표현식은 인수를 자신의 바디 안에서만 사용했다. 하지만 람다 표현식에서는 익명 함수가 하느 것처럼 자여 뷴셔를 활용할 수 있다. 이와 같은 동작을 람다 캡처링이라고 부른다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
하지만 자유 변수에도 약간의 제약이 있다. 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처할 수 있다. 하지마 ㄴ그러려면 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
// 만약 변수 값이 변경된다면 에러 발생
portNumber = 1;
왜 지역 변수에 이런 제약이 필요하냐면 인스턴스 변수와 지역 변수의 차이 때문이다. 지역 변수는 메서드 내부에 선언되어 메서드가 실행 될 때 메모리를 할당받는 반면 인스턴스 변수는 클래스 내부에 선언되며 클래스가 생성될 때 메모리를 할당받는다. 이 말은 인스턴스 변수는 힙 메모리 영역에 저장되고 지역 변수는 스택 메모리에 저장되는 것이다.
람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드2에서 실행된다면 변수를 할당한 스레드1(스택은 스레드마다 가지고 있음)가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드2에서는 해당 변수에 접근하려 할 수 있다. 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 하는 제약이 생긴 것이다.
4. 메서드 참조
메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 때로는 람다 표현식보다 메서드 참조를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다.
inventory.sort((Apple a1, Apple a2) ->
a1.getWeight().compareTo(a2.getWeight()));
inventory.sort(comparing(Apple::getWeight));
* 지역 변수 사용
메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다.
메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있다. 이때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다. 메서드 참조는 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용할 수 있다. 실제로 메서드를 호출하는 것은 아니므로 괄호는 필요 없다. 결괄적으로 메서드 참조는 람다 표현식 (Apple a) -> a.getWeight() 를 축약한 것이다.
* 메서드 참조를 하는 다양한 방법
정적 메서드 참조 | (x) -> ClassName.method(x) | ClassName::method |
인스턴스 메서드 참조 | (x) -> obj.method(x) | obj::method |
매개변수의 메서드 참조 | (obj, x) -> obj.method(x) | ClassName::method |
생성자 참조 | (x, y) -> new ClassName(x, y) | ClassName::new |
* 메서드 참조를 활용하기
사과 리스트를 다양한 정렬 기법으로 정렬하는 문제를 간결하게 해결해보자
- 1단계 : 코드 전달
List API 에서 sort 메서드를 제공하므로 직접 구현할 필요는 없다. sort 메서드에 정렬 전략을 전달하기 위해 시그니처를 찾아보자
void sort(Comparator<? super E> c)
이 코드는 Comparator 객체를 인수로 받아 두 사과를 비교한다. 객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달할 수 있다.
public class AppleComparator implements Comparator<Apple> {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
inventory.sort(new AppleComparator());
- 2단계 : 익명 클래스 사용
//2단계 익명 클래스 사용
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
- 3단계 : 람다 표현식 사용
inventory.sort((Apple o1, Apple o2) -> o1.getWeight().compareTo(o2.getWeight()));
//형식 추론
inventory.sort((o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));
이 코드의 가독성을 더 향상시킬 수 있다. Comparator는 Comparable 키를 추출해서 Comparator 객체로 만드는 Function 함수를 인수로 받는 정적 메서드 Comparing을 포함한다.
Comparator<Apple> c = Comparator.comparing((a) -> a.getWeight());
inventory.sort(c);
inventory.sort(Comparator.comparing((a) -> a.getWeight()));
- 4단계 : 메서드 참조 사용
inventory.sort(Comparator.comparing(Apple::getWeight));
'Book > 모던 자바 인 액션' 카테고리의 다른 글
CHAPTER2 - 동작 파라미터화 코드 전달 (0) | 2025.01.07 |
---|