[모던 자바 인 액션] Chapter3. 람다 표현식

SunYerim·2024년 1월 28일
0

언어

목록 보기
3/11
post-thumbnail

Chapter3. 람다 표현식

3.1 람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것

  • 익명: 보통의 메서드와 달리 이름이 없으므로
  • 함수: 메서드처럼 특정 클래스에 종속되지 않으므로
  • 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있으므로
  • 간결성: 익명 클래스처럼 많은 자질구레한 코드 구현할 필요가 없다.

람다 표현식은 파라미터, 화살표, 바디로 이루어진다.

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

3.2 어디에, 어떻게 람다를 사용할까?

필터 메서드에도 람다를 활용할 수 있다.

List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));

3.2.1 함수형 인터페이스

Predicate가 함수형 인터페이스인데, 이는 오직 하나의 추상 메서드만 지정하기 때문이다.

public interface Predicate<T> {
	boolean test (T t);
}

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스다.

자바 API의 함수형 인터페이스로 Comparator, Runnable등이 있다.

3.2.2 함수 디스크럽터

함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.

() → void 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미한다. (Runnable이 해당)


3.3 람다 활용 : 실행 어라운드 패턴

자원 처리에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어진다.

실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다.

public String processFile() throws IOException {
	try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return br.readLine(); // 실제 필요한 작업을 하는 행
	}
}

3.3.1 1단계: 동작 파라미터화를 기억하라

processFile의 동작을 파라미터화. 람다를 이용해서 동작을 전달할 수 있다.

다음은 BufferedReader에서 두 행을 출력하는 코드다.

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

3.2.2 2단계: 함수형 인터페이스를 이용해서 동작 전달

함수형 인터페이스 자리에 람다를 사용할 수 있다. BufferedReader → String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.

@FunctionalInterface
public interface BufferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
	...
}

3.2.3 3단계: 동작 실행

BufferedReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader → String)와 일치하는 람다를 전달할 수 있다.

public String processFile(BufferedReaderProcessor p) throws IOExpection {
	try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return p.process(br); // BufferedReader 객체 처리
	}
}

3.2.4 4단계: 람다 전달

람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.

한 행을 처리하는 코드

String oneLine = processFile((BufferedReader br) -> br.readLine());

두 행을 처리하는 코드

String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());

3.4 함수형 인터페이스 사용

함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 한다.

3.4.1 Predicate

java.util.function.Predicate<T>인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.

3.4.2 Consumer

java.util.function.Consumer<T>인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.

3.4.3 Function

java.util.function.FunctionL<T, R>인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다. 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.

자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능을 박싱이라고 하며 참조형을 기본형으로 변환하는 반대 동작을 언박싱이라고 한다.

박싱과 언박싱이 자동으로 이루어지는 오토박싱이라는 기능도 제공한다.


3.5 형식 검사, 형식 추론, 제약

3.5.1 형식 검사

람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부른다.

List<Apple> heavierThan150g = 
filter(inventory, (Apple apple) -> apple.getWeight() > 150);
  • filter 메서드의 선언을 확인한다.
  • filter 메서드는 두 번째 파라미터로 Predicate 형식(대상 형식)을 기대한다.
  • Predicate은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다.
  • test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
  • filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.

3.5.2 같은 람다, 다른 함수형 인터페이스

대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.

3.5.3 형식 추론

자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)를 사용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 즉, 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다, ⇒ 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.

3.5.4 지역 변수 사용

람다 형식에서는 익명 함수가 하는 것처럼 자유 변수 를 활용할 수 있다.

람다 캡처링이라고 한다.


3.6 메서드 참조

메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 람다 표현식보다 메서드 참조를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다.

3.6.1 요약

메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있다. 이때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.

메서드 참조는 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용할 수 있다.

Apple::getWeight는 Apple 클래스에 정의된 getWeight의 메서드 참조다.

메서드 참조는 세 가지 유형으로 구분할 수 있다.

  • 정적 메서드 참조
    • Integer의 parseInt 메서드는 Integer::parseInt로 표현할 수 있다.
  • 다양한 형식의 인스턴스 메서드 참조
    • String의 length 메서드는 String::length로 표현할 수 있다.
  • 기존 객체의 인스턴스 메서드 참조
    • Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있다면, 이를 expensiveTransaction::getValue라고 표현할 수 있다.

메서드 참조는 콘텍스트의 형식과 일치해야 한다.

3.6.2 생성자 참조

ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 이것은 정적 메서드의 참조를 만드는 방법과 비슷하다.

Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get(); // Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.

(추후 보충 예정)


3.7 람다, 메서드 참조 활용하기

사과 리스트 정렬 문제를 해결하면서 동작 파라미터화, 익명 클래스, 람다 표현식, 메서드 참조 등을 총동원한다.

3.7.1 1단계: 코드 전달

sort메서드에 어떻게 정렬 전략을 전달할 수 있을까?

void sort(Comparator<? super E> C)

Comparator 객체를 인수로 받아 두 사과를 비교한다. 객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달할 수 있다. 이제 sort의 동작은 파라미터화 되었다.라고 말할 수 있다. 즉, sort에 전달된 정렬 전략에 따라 sort의 동작이 달라질 것이다.

3.7.2 2단계: 익명 클래스 사용

한 번만 사용할 Comparator를 익명 클래스를 이용하는 것이 좋다.

inventory.sort(new Comparator<Apple>() {
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
});

3.7.3 3단계: 람다 표현식 사용

함수형 인터페이스를 기대하는 곳 어디에서나 람다 표현식을 사용할 수 있음을 배웠다. Comparator의 함수 디스크립터는 (T, T) → int다.

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight())

3.7.4 4단계: 메서드 참조 사용

메서드 참조를 이용해서 코드를 조금 더 간소화할 수 있다.

inventory.sort(comparing(Apple::getWeight));

3.8 람다 표현식을 조합할 수 있는 유용한 메서드

3.8.1 Comparator

정적 메서드 Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환할 수 있다.

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

역정렬

inventory.sort(comparing(Apple::getWeight).reversed());

3.8.2 Predicate

Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.

특정 프레디케이트를 반전시킬때는 negate메서드를 사용할 수 있으며, and 메서드를 이용해서 두 람다를 조합할 수 있다. 뿐만 아니라 or 메서드를 이용해서 다양한 조건을 만들 수 있다.

3.8.3 Function

Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.

andThen → 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.

compose → 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.

profile
내 안에 있는 힘을 믿어라.

0개의 댓글