본문 바로가기
Book/모던 자바 인 액션

[정리] 모던 자바 인 액션 - 람다 표현식 (Lambda Expression)(2)

by 따라쟁이개발자 2023. 1. 5.

람다 표현식이 함수형 인터페이스의 인스턴스로 동작할 수 있다고 하였는데,

실제 람다 표현식에는 이 람다가 어떤 함수형 인터페이스를 구현하고 있는지에 대한 정보가 나와있지 않다.

 

람다의 형식 검사, 형식 추론, 제약

람다가 어떤 함수형 인터페이스를 구현하고 있는지 직접적으로 나와 있진 않지만,

람다의 문법을 보고 람다의 형식을 추론할 수는 있다.

람다가 전달될 메서드의 파라미터, 람다가 할당되는 변수 등에서 람다 표현식의 형식을 추론해볼 수 있는데 이를 대상형식이라 한다.

List<Apple> heavierThan150g =
	filter(inventory, (Apple apple) -> apple.getWeight() > 150);

1. filter 메서드를 확인한다.
2. filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 받으려 한다.

3. Predicate<Apple>은 test 라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스이다.

4. test 메서드는 Apple을 받아 boolean을 변환하는 함수 디스크립터를 묘사한다.

5. filter 메서드로 전달된 람다는 위와 같은 요구사항을 만족시켜야 한다.

 

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

// Callable과 PrivilegedAction은 모두 인수를 받지 않고 제네릭 T 를 반환한다.
// 아래 두 코드는 모두 유효한 코드다
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;

* 람다 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환된다. 실제 List의 add 메서드는 boolean을 반환하지만 

Consumer<String> b = s -> list.add(s); 의 경우 void 반환을 기대하지만 boolean이 반환되고 있음에도 유효한 코드가 될 수 있다. 

 

대상 형식을 이용해 함수 디스크립터를 추론했던 것처럼, 컴파일러는 람다의 시그니처도 추론할 수 있다.

즉, 컴파일러는 람파 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다. 

Comparator<Apple> c = 
	(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); // 형식 추론하지 않음
    
Comparator<Apple> c = 
	(a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); // 형식 추론함

 

이제까지 살펴본 예제에선 람다 표현식 바디안에서 받아온 인수만 사용했지만, 

람다 표현식에서는 익명 함수가 하는 것처럼 파라미터로 넘어온 값이 아닌 외부에서 넘어온 변수(자유변수)도 활용할 수 있다.

이를 람다 캡쳐링(capturing lambda)라고 한다.

 

하지만, 약간의 제약이 있다.

람다는 인스턴스 변수와 정적 변수를 자유롭게 참조할 수 있지만, 지역 변수는 명시적으로 final 선언되거나 

final로 선언된 변수처럼 사용되어야 한다. 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수만 캡쳐할 수 있다.

String hello = "Hello!";
Consumer<String> c = name -> System.out.println(hello + name);

// hello에 값을 재할당하려 할 경우, 위 Consumer body의 hello 부분에서 오류가 발생
hello = "Hi!";

 

메서드 참조

메서드 참조는 특정 메서드만 호출하는 람다의 축약형이라고 생각할 수 있다. 

특정 메서드를 호출하기 위해 람다 표현식의 인수, 바디를 활용할 수도 있겠지만 ( (Apple apple) -> apple.getWeight();)

이 방법 외에 메서드를 직접 명시하여 참조함으로써 가독성을 높일 수 있다. (Apple::getWeight();)

 

아래는 람다를 메서드 참조로 변형한 예제이다.

람다 메서드 참조 단축 표현
(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

 

* 메서드 참조 외에도 생성자 참조라는 것이 있다. 
   생성자 참조란 new 키워드를 이용하여 기존 생성자의 참조를 만드는 것이다. (ClassName::new)

 

처음으로 돌아가 람다, 메서드 참조를 사용해 사과 리스트 정렬을 다시 구현해 보자.

// 1단계 : 코드 전달
public class AppleComparator implements Comparator<Apple> {
	public int compare(Apple a1, Apple a2) {
    	return a1.getWeight().compareTo(a2.getWeight());
    }
}
inventory.sort(new AppleComparator());

// 2단계 : 익명클래스 사용
inventory.sort(new Comparator<Apple> {
	public int compare(Apple a1, Apple a2) {
    	return a1.getWeight().compareTo(a2.getWeight());
    }
});

// 3단계 : 람다 표현식 사용
inventory.sort((Apple a1, Apple a2) -> 
	a1.getWeight().compareTo(a2.getWeight())
);

// 4단계 : 메서드 참조 사용
inventory.sort(comparing(Apple::getWeight));

 

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

자바 8의 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메서드를 포함한다.

함수형 인터페이스에 구현된 디폴트 메서드를 활용해 람다 표현식을 조합할 수 있도록 기능을 제공한다는 것이다.

 

Comparator 조합

// comparing을 이용해 비교에 사용할 키를 추출. Function 기반의 Comparator 반환
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

// 내림차순으로 정렬하고 싶을 경우, 비교자 순서를 뒤바꾸는 reversed 메소드 사용.
inventory.sort(comparing(Apple::getWeight).reversed());

// 첫 번째 비교 기준이 동등값이 나올 경우, thenComparing 사용하여 두 번째 비교 기준 정의 가능.
inventory.sort(comparing(Apple::getWeight)
	.reversed()
    .thenComparing(Apple::getCountry));

 

Predicate 조합

// 특정 Predicate를 반전시킬 때 negate 메서드 사용 (ex. 빨간색이 아닌 사과)
Predicate<Apple> notRedApple = redApple.negate();

// and 메서드를 사용하여 두 Predicate 조합 가능
Predicate<Apple> redAndHeavyApple =
	redApple.and(apple -> apple.getWeight() > 150);

// or 조건도 사용 가능. and, or 모두 왼쪽에서 오른쪽으로 연결
// a.or(b).and(c) = (a || b) && c 
Predicate<Apple> redAndHeavyApple =
	redApple.and(apple -> apple.getWeight() > 150)
    		.or(apple -> GREEN.equals(a.getColor()));

 

Function 조합

// 먼저 주어진 함수 결과를 다음 함수의 입력으로 전달하고자 할 경우, andThen 메서드 사용
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);	// 4 반환

// 메서드 인수로 주어진 함수를 먼저 실행한 다음 외부 함수의 인수로 제공하고자 할 경우, compose 메서드 사용
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);	// 3 반환

 

 

마치며

  • 람다 표현식은 익명 함수의 일종이다.
  • 함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스이다.
  • 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다.
  • 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
  • java.util.function 패키지는 다양한 함수형 인터페이스를 제공한다. (Predicate, Function, Consumer, Supplier 등)
  • 자바 8은 제네릭 함수형 인터페이스와 관련한 박싱 동작을 피할 수 있는 기본형 특화 인터페이스도 제공한다.
  • 메서드 참조를 이용하면 기존 메서드 구현을 재사용하고 직접 전달할 수 있다.
  • Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공한다.

댓글