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

seony·2023년 2월 28일
0

모던 자바 인 액션

목록 보기
3/6
post-thumbnail
  • 람다 표현식은 익명 클래스처럼 1. 이름이 없는 함수면서, 2. 메서드를 인수로 전달할 수 있다.
  • 3장에서는 람다 표현식을 어떻게 만드는지, 어떻게 사용하는지, 어떻게 코드를 간략하게 만들 수 있는지 설명하고 자바 8 API에 추가된 중요한 인터페이스형식 추론 등의 기능도 확인하고 메서드 참조에 대해 공부한다.

3.1 람다란 무엇인가?

  • 익명
    • 이름이 없으므로 익명이라 표현한다.
  • 함수
    • 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다.
    • 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
  • 전달
    • 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성
    • 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.

<기존 코드>

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

<람다 이용한 코드>

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
  • (Apple a1, Apple a2) : 람다 파라미터
    • Comparator의 compare 메서드 파라미터(사과 두 개)
  • -> : 화살표
    • 화살표(->)는 람다의 파라미터 리스트와 바디를 구분한다.
  • a1.getWeight().compareTo(a2.getWeight()); : 람다 바디
    • 두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식이다.

<자바 8의 유용한 다섯 가지 람다 표현식 예제>

(String s) -> s.length();
(Apple a) -> a.getWeight() > 150;
(int x, int y) -> {
	System.out.println("Result: ");
    System.out.println(x + y);
}
() -> 42;
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

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

1. 함수형 인터페이스

함수형 인터페이스란?

정확히 하나의 추상 메서드를 지정하는 인터페이스

  • 2장에서 만든 Predicate<T> 인터페이스로 필터 메서드를 파리미터화할 수 있었음
  • 바로 Predicate<T>가 함수형 인터페이스
    • Predicate<T>오직 하나의 추상 메서드만 지정하기 때문!

람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.
(정확히는 함수형 인터페이스를 구현한 클래스의 인스턴스)

<예시>

public interface Predicate<T> {
	int compare(T o1, T o2);
}

public interface Runnable {
	void run();
}

@FunctionalInterface

  • 함수형 인터페이스임을 가리키는 어노테이션
  • @FunctionalInterface 선언했지만 실제로 함수형 인터페이스가 아니라면 컴파일 에러 발생

2. 함수 디스크립터

  • 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처와 같다.
  • 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.
  • 예) Runnable 인터페이스의 유일한 추상 메서드 run
    • 인수와 반환값이 없음 (void 반환)
      () -> void

참고!
왜 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있을까?

  • 언어를 더 복잡하게 만들지 않는 현재 방법을 선택
  • 대부분의 자바 프로그래머가 하나의 추상 메서드를 갖는 인터페이스(예를 들면 이벤트 처리 인터페이스)에 이미 익숙

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

  • 자원 처리(e.g. 데이터베이스의 파일 처리)에 사용하는 순환 패턴은 1. 자원을 열고, 2. 처리한 다음에, 3. 자원을 닫는 순서로 이뤄진다.
    • 즉, 실제 자원을 처리하는 코드를 설정정리 두 과정이 둘러싸는 형태를 갖는다.

<그림 3-2 실행 어라운드 패턴>


3.4 함수형 인터페이스 사용

  • 함수형 인터페이스
    • 오직 하나의 추상 메서드를 지정
  • 함수 디스크립터
    • 함수형 인터페이스의 추상 메서드 시그니처를 의미

1. Predicate

(T) → boolean

@FuncationalInterface
public interface Predicate<T> {
	boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
	List<T> results = new ArrayList<>();
    for (T t: list) {
    	if (p.test(t)) {
			results.add(t);
        }
    }
}

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
//List<String> nonEmpty2 = filter(listOfStrings, (String s) -> !s.isEmpty());

2. Consumer

(T) -> void

@FunctionalInterface
public interface Consumer<T> {
	void accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> c) {
	for (T t: list) {
    	c.accept(t);
    }
}

forEach(
	Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i) // Consumer의 accpet를 구현
);

3. Function

(T) -> R

@FunctionalInterface
public interface Function<T, R> {
	R apply(T t);
}
public <T, R> List<R> map(List<T> list, Function<T, R> f) {
	List<R> result = new ArrayList<>();
    for (T t: list) {
    	result.add(f.apply(t));
    }
    return result;
}

List<Integer> l = map(
	Arrays.asList("lambdas", "in", "action"),
    (String s) -> s.length(); // Function의 apply 메서드를 구현하는 람다
);

기본형 특화

  • 제네릭 파라미터(예를 들면 Consumer<T> 의 T)에는 참조형만 가능
  • IntegerDouble은 변환 없이 바로 제네릭에 바로 사용이 가능하지만 intdouble 같은 형태는 오토 박싱 과정에서 비용이 발생한다.
  • 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 IntPredicate와 같은 특별한 인터페이스를 제공한다.
public interface IntPredicate {
	boolean test(int t);
}
  • 일반적으로 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에서는 DoublePredicate, IntConsumer 등 처럼 형식명이 붙는다.

참고!
Bi가 앞에 붙는다면 인수를 2개 받을 수 있다.

  • BiPredicate<L, R> : (T, U) -> boolean
  • BiConsumer<T, U> : (T, U) -> void
  • BiFucntion<T, U, R> : (T, U) -> R

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

1. 형식 검사

  • 람다가 사용되는 콘텍스트(Context)를 이용해서 람다의 형식(Type) 추론 가능
  • 대상 형식
    • 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수와 같은 컨텍스트(Context)에서 기대되는 람다 표현식 형식

<filter 함수 람다 표현식 적용>
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>
    • test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다!
  4. test
    • Apple을 받아 boolean을 반환하는 함수다!
  5. 함수 디스크립터는 Apple -> boolean이므로 람다의 시그니처와 일치한다!

특별한 void 호환 규칙

  • 람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환된다.
    예) Listadd 메서드는 Consumer 콘텍스트(T -> void)지만 boolean을 반환해도 호환

<예시>

// Predicate는 불리언 반환값을 갖는다.
Predicate<String> p = s -> list.add(s);
// Consumer은 void 반환값을 갖지만 호환 가능
Consumer<String> b = s -> list.add(s);

2. 형식 추론

  • 자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.
    ➜ 람다 표현식의 파라미터 형식에 접근할 수 있으므로 이를 생략 가능
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); // 형식을 추론 X
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); // 형식을 추론 O

3. 지역 변수 사용 및 제약

람다 캡처링
람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

지역 변수 제약

  • 지역변수는 명시적으로 final로 선언되어 있거나 한 번만 할당할 수 있는 지역 변수만을 사용할 수 있다.

    Why❓

    • 인스턴스 변수 -> 에 저장
    • 지역 변수 -> 스택에 저장
    • 지역 변수가 스레드에서 실행되다가 해당 스레드가 해제되어도 람다를 실행하는 스레드에서 해당 변수에 접근하려 할 수 있음
    • 원래 변수에 접근을 허용하는 것(사라질 수도 있기 때문)이 아니라 자유 지역 변수의 복사본을 제공
      따라서 복사본의 값이 바뀌지 않아야 한다.

3.6 메서드 참조

  • 메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.

<기존 코드>

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

<메서드 참조 사용 - java.util.Comparator.comparing 활용>

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

1. 요약

  • 메서드를 어떻게 호출해야 하는지 설명을 참조하기보다는 명시적으로 메서드명을 직접 참조하는 것이 편리

장점

  • 가독성을 높일 수 있음

사용 방법

  • 메서드명 앞에 구분자(::)를 붙임
  • 예) 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

메서드 참조 만드는 방법 3가지

  1. 정적 메서드 참조
  • (String s) -> Integer.parseInt(s)Integer::parseInt
  1. 다양한 형식의 인스턴스 메서드 참조
  • (String s) -> s.length()String::length
  1. 기존 객체의 인스턴스 메서드 참조
  • Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있으면 ➜ expensiveTransaction::getValue
방법람다메서드 참조 단축 표현
방법 1ToIntFunction<String> stringToInt = (String s) -> Integer.parseInt(s);Function<String, Integer> stringToInteger = Integer::parseInt;
방법 2BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);BiPredicate<List<String>, String> contains = List::contains;
방법 3Predicate<String> startsWithNumber = (String string) -> this.startsWithNumber(string);Predicate<String> startsWithNumber = this::startsWithNumber;

2. 생성자 참조

  • ClassName::new처럼 클래스명new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.

<기본 생성자>

  • Supplier의 () -> Apple 과 같은 시그니처를 갖는 생성자가 있다고 가정
// 원래 형태
// Supplier<Apple> c1 = () -> new Apple();
Supplier<Apple> c1 = Apple::new;
Apple apple = c1.get();

<인자 1개 - 무게>

// 원래 형태
// Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Function<Integer, Apple> c2 = Apple::new;
Apple apple c2.apply(110);

<인자 2개 - 색상, 무게>

// 원래 형태
// BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
BiFunction<Color, Integer, Apple> c2 = Apple::new;
Apple apple c3.apply(GREEN, 110);

<생성자를 전달받는 함수(map) 예시>

List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new);

public List<Apple> map(List<Integer> list, Function<Integer, Apple> f) {
	List<Apple> result = new ArrayList<>();
    for (Integer i: list) {
    	result.add(f.apply(i));
    }
    return result;
}

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

1. Comparator 조합, 연결

  • comparing(Function<U, V>))
    • Function 함수를 인수로 받아 Comparable 키를 추출해서 Comparator 객체로 만듦
    • 예) Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
  • reversed()
    • 역정렬(내림차순 정렬) inventory.sort(Comparator.comparing(Apple::getWeight).reversed());
  • thenComparing(Function<U, V>)
    • 첫 번째 비교 결과가 같은 경우, 다음 Comparator
    • 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달함.
inventory.sort(comparing(Apple::getWeight)
			   .reversed()
               .thenComparing(Apple::getCountry));

2. Predicate

  • 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공
// 기존 프레디케이트 객체 redApple의 결과를 반전시킨 객체를 만듦
Predicate<Apple> notRedApple = redApple.negate(); 
// 두 프레디케이트를 연결해서 새로운 프레디케이트 객체를 만듦
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);

3. Function

  • Function 인스턴스를 반환하는 andThen compose 두 가지 디폴트 메서드를 제공

andThen

  • 주어진 함수를 실행하고 그 결과를 다른 함수의 입력으로 전달

compose

  • 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 입력으로 전달

마치며

  • 람다 표현식은 익명 함수의 일종이다.
  • 함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스다.
  • 람다 표현식함수형 인터페이스를 기대하는 곳에서만 사용할 수 있다.
  • 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
  • 메서드 참조를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.

0개의 댓글