CHAPTER3 - 람다 표현식

2025. 1. 16. 18:40Book/모던 자바 인 액션

목차.

 

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