Published on

Chapter 3. 람다 표현식

Chapter 3. 람다 표현식

익명 클래스로 다양한 동작을 구현할 수 있지만, 너무 많은 코드가 필요하고 깔끔하지 않습니다. 깔끔하지 못한 코드는 동작 파라미터를 실전에 적용하는 것을 막는 요소가 됩니다.

Java 8에 생긴 Lambda Expresstion을 이용하면 익명 클래스처럼 메소드를 인수로 전달할 수 있습니다.

Lambda메소드 참조를 같이 사용하면 훨씬 간결하게 코드를 작성할 수 있습니다.

3.1 람다란 무엇인가?

Lambda Expression은 메소드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있습니다.
Lambda Expression은 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있습니다.

Lambda는 다음과 같은 특징이 있습니다.

  • 익명: 이름이 없으므로 익명이라 표현합니다. 구현 할 코드가 줄어듭니다.
  • 함수: 특정 메소드에 종속적이지 않으므로 함수라고 부릅니다.
  • 전달: 메소드 인수로 전달하거나 변수로 저장할 수 있습니다.
  • 간결성: 익명 클래스처럼 많은 코드를 구현할 필요가 없습니다.

💡 Lambda라는 용어는 람다 미적분학 학계에서 개발한 시스템에서 유래했습니다.

LambdaJava 8 이전에 할 수 없었던 일들을 제공하는 것이 아닙니다.
다만, 동작 파라미터를 이용할 때 익명 클래스 처럼 많은 코드를 작성할 필요가 없습니다.

결과적으로는 코드가 간결하고 유연해진다는 장점이 있습니다.

class Foo {
	public static void main(String[] args) {
		Comparator<Apple> byWeight = new Comparator<Apple>() {
			@Override
			public int compare(Apple o1, Apple o2) {
				return o1.getWeight().compareTo(o2.getWeight());
			}
		};
	}
}

위의 기존 코드를 Lambda로 구현하면 아래와 같이 작성할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Comparator<Apple> byWeight = (Apple o1, Apple o2) -> o1.getWeight().compareTo(o2.getWeight());
	}
}
  • 파라미터 리스트 (Apple o1, Apple o2): Comparatorcompare 메소드의 파라미터
  • 화살표 ->: 파라미터와 바디를 구분합니다.
  • 람다 바디: 반환값에 해당하는 표현식입니다.

Java 설계자는 C#이나 스칼라 같은 비슷한 기능을 가진 다른 언어와 비슷한 문법을 Java에 적용하기로 했습니다.

Lambda의 기본 문법은 아래와 같습니다.

(parameters) -> expression  // `바디`가 한줄 이면 `return` 키워드를 생략할 수 있습니다.

또는 아래 처럼 {}안에 작성할 수 있습니다.

(parameters) -> { statements; } // 이 경우에는 return 을 명시적으로 적어주어야 합니다.

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

Lambda함수형 인터페이스에서 사용할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN == a.getColor());
	}
}

위의 예제는 filter 메소드가 함수형 인터페이스Predicate<T>를 두 번째 인자로 받기 때문에, Lambda로 값을 넘겨준 것입니다.

3.2.1 함수형 인터페이스

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

위의 예제에서 살펴 본 Predicate<T>boolean test(T t) 메소드 하나만 추상 메소드이기 떄문에 함수형 인터페이스입니다.

public interface Predicate<T> {
	boolean test(T t);
}
public interface Comparator<T> {
	int compare(T o1, T o2);
}
public interface Runnable {
	void run();
}
public interface ActionListener extends EventListener {
	void actionPerformed(ActionEvent e);
}
public interface Callable<V> {
	V call() throws Exception;
}
public interface PrivilegedAction<T> {
	T run();
}

위의 interface들은 하나의 추상 메소드만 가지므로 함수형 인터페이스 입니다.

💡 default 메소드가 있더라도, 추상 메소드가 오직 하나면 함수형 인터페이스 입니다.

함수형 인터페이스Lambda expresstion으로 구현하여 사용할 수 있습니다. 그렇게 되면 기존 처럼 해당 interface를 상속받아 구현하거나, 지저분한 익명 클래스를 사용하지 않아도 됩니다.

3.2.2 함수 디스크립터

함수형 인터페이스추상 메소드 시그니처Lambda expresstion시그니처를 가리킵니다.

💡 Lambda expression시그니처를 서술하는 메소드를 함수 디스크립터 라고 부릅니다.

() -> void파라미터 리스트가 없고 void를 반환하는 함수를 의미합니다.
(Apple, Apple) -> intApple객체 두개를 전달 받아 int를 반환하는 함수를 의미합니다.

Lambda expression을 사용할 때, 바디가 한 줄이라면 {}return 키워드를 생략할 수 있다고 했습니다. void처럼 반환 값이 없는 함수{}를 생략하고 작성할수 있습니다.

💡 @FunctionalInterface는 무엇인가?

함수형 인터페이스를 보면 @FunctionalInteface가 추가되어 있는걸 볼 수 있습니다.

해당 어노테이션이 붙어있으면 함수형 인터페이스라고 생각하고, 그에 맞지 않게 구현이 되면 컴파일 에러가 발생합니다.

@Override가 붙어있으면 override할 메소드와 시그니처가 다르면 컴파일 에러가 발생하는것과 비슷합니다.

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

자원 처리에 사용되는 순환 패턴은 자원을 열고, 처리하고, 자원를 닫는 순서로 이루어집니다.

설정정리과정은 대부분 비슷하므로 실제 처리 코드설정정리에 둘러 쌓인 형태를 갖습니다. 이러한 형식의 코드를 실행 어라운드 패턴이라고 합니다.

class Foo {
	public String processFile() throws IOException {
		try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
			return br.readLine(); // 실제 처리 코드
		}
	}
}

Java 7에 추가된 try-with-resources구문을 사용하면 자원을 명시적으로 닫을 필요가 없으므로 조금 더 간결하게 작성이 가능합니다.

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

현재 위의 코드는 한 번에 한줄만 읽는 코드지만 요구사항으로 한 번에 두 줄을 읽어야 하거니, 가장 자주 사용되는 단어를 반환해야 한다면 실제 처리 코드변경되면 됩니다.

실제 처리 코드동작 파라미터화한다면 유연하게 대처가 가능합니다.

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

우선, processFile 메소드에서 실행할 동작을 전달해야 합니다.

BufferedReader -> StringIOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 합니다.


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

processFile 메소드의 인자로 BufferedReaderProcessor를 받을 수 있도록 processFile 메소드를 변경합니다.

class Foo {
	public String processFile(BufferedReaderProcessor processor) throws IOException {
        ...
	}
}

3.3.3 3단계 : 동작 실행

processFile에서 전달 받은 동작을 실행 하도록 메소드를 변경합니다.

class Foo {
	public String processFile(BufferedReaderProcessor processor) throws IOException {
		try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
			return processor.process(br); // 전달 받은 동작을 수행
		}
	}
}

3.3.4 4단계 : 람다 전달

이제 Lambda를 이용해서 원하는 동작을 processFile 메소드에 전달하면 됩니다.

class Foo {
	public static void main(String[] args) {
		processFile(br -> br.readLine());
		processFile(br -> br.readLine() + br.readLine());
	}
}

💡 Lambda파라미터 리스트에서 타입을 지정해주지 않아도 타입 추론으로 타입이 지정되기 때문에 생략할 수 있습니다.

이제 변화하는 요구사항에 유연하게 대처할 수 있습니다.

3.4 함수형 인터페이스 사용

함수형 인터페이스추상 메소드Lambda expression시그니처를 묘사합니다.

💡 함수형 인터페이스추상 메소드 시그니처함수 디스크립터라고 합니다.

다양한 Lambda expression을 사용하려면 공통된 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요합니다.

Java 8에서는 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공합니다.

3.4.1 Predicate

java.util.function.Predicate<T>는 제네릭 형식의 T 객체를 전달받아 boolean 타입을 반환 하는 test 추상 메소드 제공합니다.

💡 예제


@FunctionalInterface
public interface Predicate<T> {
	boolean test(T t);
}
class Foo {
	public static List<T> filter(List<T> list, Predicate<T> p) {
		List<T> result = new ArrayList<>();
		for (T t : inventory) {
			if (p.test(t)) {
				result.add(t);
			}
		}
		return result;
	}
}
class Foo {
	public static void main(String[] args) {
		List<String> nonEmpty = filter(listOfStrings, (s) -> !s.isEmplty());
	}
}

3.4.2 Consumer

java.util.function.Consumer<T>는 제네릭 형식의 T 객체를 전달 받아 void를 반환하는 accpet 추상 메소드를 제공합니다.

💡 예제


@FunctionalInterface
public interface Consumer<T> {
	void accept(T t);
}
public interface Iterable<T> {
	default void forEach(Consumer<? super T> action) {
		Objects.requireNonNull(action);
		for (T t : this) {
			action.accept(t);
		}
	}
}
class Foo {
	public static void main(String[] args) {
		List<Integer> integers = List.of(1, 2, 3, 4, 5);
		integers.forEach(number -> System.out.println(number));
	}
}

3.4.3 Function

java.util.function.Function<T, R>는 제네릭 형식의 T를 객체를 전달 받아 R 객체를 반환하는 apply 추상 메소드를 제공합니다.

💡 예제


@FuncationalInterface
public interface Function<T, R> {
	R apply(T t);
}
class Foo {
	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;
	}
}
class Foo {
	public static void main(String[] args) {
		List<Integer> integers = List.of(1, 2, 3, 4, 5);
		map(integers, (number -> number + 1)); // 2, 3, 4, 5, 6
	}
}

💡 기본형 특화

제네릭 파라미터참조형만 사용할 수 있습니다. 그렇기 때문에 기본형 타입을 사용하면 참조형 타입으로 박싱 하게 됩니다.

하지만, 박싱기본 타입참조 타입으로 변한하면 Heep 영역에 저장됩니다. 따라서 박싱한 값은 메모리를 더 소모하며 기본형을 가져올 떄도 메모리를 탐색하는 과정이 필요하므로 비용이 많이 듭니다.

Java 8에서는 기본형을 입출력 사용하는 상황에서 오토박싱을 피할 수 있도록 특화된 함수형 인터페이스를 제공합니다.


@FunctionalInterface
public interface IntPredicate {
	boolean test(int value);
}
class Foo {
	public static void main(String[] args) {
		IntPredicate intPredicate = (i -> i % 2 == 0);
		intPredicate.test(1000); // true
	}
}

위의 IntPredicate기본형 타입을 그대로 사용하기 때문에 오토박싱이 일어나지 않아 기존의 Predicate<T>에 비해 효율적입니다.

이름에서 알수 있겠지만, IntPredicate외에도 DoublePredicate, LongPredicate 등 기본 타입을 모두 정의해 두었습니다.

다른 함수형 인터페이스 (Function, Consumer 등)들도 모두 같은 네이밍 규칙을 가지고 있습니다. (ex: IntFunction<T, R>)

💡 에외, 람다, 함수형 인터페이스의 관계

함수형 인터페이스에서는 확인된 예외를 던지는 동작을 허용하지 않습니다.

예외를 던지는 Lambda expression을 만들려면 함수형 인터페이스에 직접 정의하거나, try ~ catch 블록으로 감싸야합니다.


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

위의 함수형 인터페이스IOException을 던지고 있습니다.

java.util.function.Function<T, R>에 위의 함수형 인터페이스를 사용하고 싶은 경우, Function<T, R>에 에외를 정의할 수 없으니 try ~ catch 블록으로 감싸서 처리 해야합니다.

class Foo {
	public static void main(String[] args) {
		Function<BufferedReader, String> f = (b) -> {
			try {
				return b.readLine();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		};
	}
}

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

Lambda함수형 인터페이스인스턴스를 만들 수 있다고 했습니다. Lambda expresstion 자체에는 어떤 함수형 인터페이스를 구현하는지에 대한 정보가 없습니다.

Lambda expresstion을 제대로 이해하려면 Lambda의 실제 형식을 파악해야 합니다.

3.5.1 형식 검사

Lambda가 사용되는 Context를 이용해서 형식을 추론할 수 있습니다.

💡 어떤 Context에서 기대되는 Lambda expression의 형식을 대상 형식 (target type)이라고 합니다.

class Foo {
	public static void main(String[] args) {
		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의 두 번째 파라미터로 전달 된 인수는 이와 같은 요구사항을 만족해야 함

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

대상 형식(target type)이라는 특징 떄문에 같은 Lambda expression이더라도 호환되는 추상 메소드를 가진 다른 함수형 인터페이스로 사용될 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Callable<Integer> c = () -> 42;
		PrivilegedAction<Integer> p = () -> 42;
	}
}

CallablePrivilegedAction 모두 인자를 받지 않고 T 객체를 반환하는 함수형 인터페이스입니다. 따라서 같은 Lambda expression이지만 두 개 모두 유효합니다.

💡 다이아몬드 연산자

이미 Java 7에서 추가된 다이아몬드 연산자(<>)Context에 따른 제네릭 형식을 추론할 수 있습니다.

class Foo {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();
    }
}

이때, 인스턴스 표현식형식 인수Context에 의해 추론됩니다.

💡 특별한 void 호환 규칙

Lambdabody에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환이 됩니다. (파라미터 리스트도 동일해야 함)

class Foo {
    public static void main(String[] args) {
        Predicate<String> p = s -> list.add(s);
        Consumer<String> c = s -> list.add(s);

    }
}

Predicateboolean을 반환받으므로 유효하고, Consumervoid를 반환받기 때문에 호환이 되어 유효합니다.

💡 함수 디스크립터가 동일한 경우


@FunctionalInterface
class Foo {
	public void execute(Runnable runnable) {
		runnable.run();
	}

	public void execute(Action action) {
		action.act();
	}
}

@FuncationalInterface
public interface Action {
	void act();
}

위와 같이 정의되어 있을 떄, execute(() -> {})라는 Lambda expression이 있다면 두 메소드의 함수 디스크립터가 동일하므로 어떤 메소드를 가리키는지 명확하지 않습니다.

이런 경우, execute((Action) () -> {}) 처럼 캐스트를 하여 사용하면 명확해집니다.

3.5.3 형식 추론

자바 컴파일러Lambda expression이 사용된 Context를 이용해서 관련된 함수형 인터페이스를 추론합니다.

대상 형식(target type)을 이용해서 함수 디스크립터를 알 수 있으므로 Lambda시그니처도 추론이 가능합니다.

즉, 파라미터에 타입을 생략할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Comparator<Apple> c1 = (Apple a1, Apple a2) -> ...
		Comparator<Apple> c2 = (a1, a2) -> ...
	}
}

두 번째 로직 처럼 단순하게 작성이 가능합니다.

💡 꼭 생략하는것이 좋은것은 아닙니다. 상황에 따라 명시적으로 형식을 포함하는게 가독성에 더 좋을수도 있으므로, 작성하는 개발자가 결정하면 됩니다.

3.5.4 지역 변수 사용

Lambda expression에서도 익명 클래스 처럼 자유 변수 (파라미터로 넘겨진 변수가 아닌 외부에 정의 된 변수)를 사용할 수 있습니다.

이와 같은 동작을 람다 캡처링 이라고 합니다.

class Foo {
	public static void main(String[] args) {
		int portNumber = 1;
		Runnable r = () -> System.out.println(portNumber);
	}
}

💡 지역 변수의 제약

지역 변수를 사용함에 있어, 약간의 제약이 존재합니다.

지역 변수를 사용하려면 해당 변수는 final로 선언되어 있어야 하거나, 그와 동일한 의미로 사용되어야합니다. 즉, 변경되면 안됩니다.

그 이유는, 인스턴스 변수Heep 영역에 저장되는 반면 지역 변수Stack 영역에 저장됩니다.
만약 Lambda지역 변수에 바로 접근이 가능하다면 LambdaThread에서 실행 될 경우 변수를 할당한 Thread가 사라져서 변수 할당이 해제 되었음에도 Lambda를 실행하는 Thread에서는 해당 변수에 접근하려고 할 수 있기 때문입니다.

따라서, Java에서는 해당 변수에 바로 접근을 허용하지 않고, 복사본을 제공합니다. 그렇기 때문에 지역 변수는 변경되면 안되므로 final로 선언하거나 그와 동일하게 사용되어야 하는 것 입니다.

이런 제약으로 인해 외부 변수를 변경시키는 일반적인 명령형 프로그래밍 패턴에 제동을 걸 수 있습니다.

인스턴스 변수는 Thread가 공유하는 Heep 영역에 존재하기 때문에 상관이 없습니다.

💡 클로저

클로저함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킵니다.

클로저클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수도 있습니다.

Java 8Lambda익명 클래스클로저와 비슷한 동작을 수행합니다. 다만, Lambda익명 클래스는 외부의 값을 변경할 수 없다는데서 클로저와 차이가 있습니다.

덕분에 Lambda변수가 아닌 에 국한되어 동작을 수행한다는 사실이 명확해집니다.

가변 지역 변수를 새로운 Thread에서 캡처할 수 있다면 안전하지 않은 동작을 수행할 가능성이 생깁니다.

3.6 메서드 참조

메소드 참조를 이용하면 기존의 메소드 정의를 재활용하여 Lambda처럼 전달할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
	}
}

위의 코드를 메소드 참조를 이용하면 아래처럼 작성할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		inventory.sort(Comparator.comparing(Apple::getWeight));
	}
}

3.6.1 요약

메소드 참조Lamnda의 축약형이라고 생각할 수 있습니다.

예를 들어 Lambda`에서 해당 메소드를 실행해야 한다면, 해당 메소드를 어떻게 실행시키는지 설명을 참조하는 것보다 메소드명을 직접 참조 시키는것이 편리합니다.

메소드 참조를 이용하면 기존의 메소드로 Lambda를 만들 수 있습니다. 이때, 명시적으로 메소드 명을 참조함으로써 가독성을 높일 수 있습니다.

💡 메소드 참조클래스명::메소드명 형식으로 사용할 수 있습니다.

메소드 참조는 새로운 기능이 아니라 하나의 메소드를 참조하는 Lambda르 편리하게 표현할 수 있는 문법입니다.

💡 메소드 참조를 만드는 방법

메소드 참조를 만드는 방법은 3가지가 있습니다.

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

3번 째의 경우 비공개 헬퍼 메소드를 정의한 상황에서 유용하게 사용될 수 있습니다.

class Foo {
	private boolean isValidName(String str) {
		return Character.isUpperCase((String.charAt(0)));
	}
}

해당 메소드를 아래처럼 Predicate<String>을 필요로 하는 상황에 적절하게 사용할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		filter(words, this::isValidName);
	}
}

컴파일러Lambda expression을 검사하던 방식과 비슷한 과정으로 메소드 참조가 주어진 함수형 인터페이스와 호환이 가능한지 확인합니다.

💡 즉, 메소드 참조Context형식과 일치해야 합니다.

3.6.2 생성자 참조

ClassName::new처럼 기존 생성자의 참조를 만들 수 있습니다. 이것은 정적 메소드 참조를 만드는 방법과 비슷합니다.

  • 예를 들어 Supplier() -> Apple 시그니처를 갖는 생성자가 있다고 가정하면 아래 처럼 만들 수 있습니다. 아래 두 코드는 동일합니다.
class Foo {
	public static void main(String[] args) {
		Supplier<Aple> c1 = Apple::new;
		Apple a1 = c1.get();
	}
}
class Foo {
	public static void main(String[] args) {
		Supplier<Apple> c1 = () -> new Apple();
		Apple a1 = c1.get();
	}
}
  • 인수가 하나인 Apple(Integer weight)라는 시그니처를 가진 생성자Function<T, R>의 형식과 동일하므로 아래처럼 작성할 수 있습니다. 아래 두 코드는 동일합니다.
class Foo {
	public static void main(String[] args) {
		Function<Integer, Apple> c2 = Apple::new;
		Apple a2 = c2.apply(100);
	}
}
class Foo {
	public static void main(String[] args) {
		Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
		Apple a2 = c2.apply(100);
	}
}
  • 인수를 두 개 받는 시그니차를 가진 생성자의 경우 BiFunction<T, U, R>의 형식과 동일하므로 아래 처럼 작성할 수 있습니다. 아래 두 코드는 동일합니다.
class Foo {
	public static void main(String[] args) {
		BiFunction<Color, Integer, Apple> c3 = Apple::new;
		Apple a3 = c3.apple(GREEN, 100);
	}
}
class Foo {
	public static void main(String[] args) {
		BiFunction<Color, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
		Apple a3 = c3.apple(GREEN, 100);
	}
}

위에서 볼 수 있듯, 메소드 참조를 이용하면 훨씬 간결하게 코드를 작성할 수 있습니다.

💡 인수가 3개 이상일 경우

위에서는 인수가 1개인 경우와 2개인 경우에 대해서 다뤘습니다. 하지만 인수를 3개 이상 받는 생성자의 경우는 정의 된 함수형 인터페이스가 없기 때문에 사용할 수가 없습니다.

그렇기 때문에 시그니처가 일치하는 함수형 인터페이스를 직접 만들어 사용해야 합니다.

class TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}
class Foo {
    public static void main(String[] args) {
        TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
    }
}

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

처음에 다룬 사과 리스트를 동작 파라미터화, 익명 클래스, Lambda expression, 메소드 참조를 모두 사용하여, 더 간결하고 세련되게 만들어 보겠습니다.

  • 최종 코드
class Foo {
	public static void main(String[] args) {
		inventory.sort(Comparator.comparing(Apple::getWeight));
	}
}

3.7.1 1단계 : 코드 전달

Java 8Listsort 메소드를 제공합니다. 우리는 sort 메소드에 정렬 전략만 전달하면 됩니다.

sort 메소드의 시그니처는 아래와 같습니다.

public interface List<E> extends Collection<E> {
	default void sort(Comparator<? super E> c) {
        ...
	}
}

Comparator 객체를 인수로 받아 두 사과를 비교합니다.

객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달할 수 있습니다.

즉, sort에 전달된 정렬 전략에 따라 sort의 동작이 달라질 것입니다.

  • 아래처럼 작성할 수 있습니다.
public class AppleComparator implements Comparator<Apple> {
	@Override
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
}
class Foo {
	public static void main(String[] args) {
		inventory.sort(new AppleComparator());
	}
}

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

한 번만 사용된다면, 익명 클래스를 이용하는 것이 더 좋습니다.

class Foo {
	public static void main(String[] args) {
		inventory.sort(new Comparator<Apple>() {
			@Override
			public int compare(Apple a1, Apple a2) {
				return a1.getWeight().compareTo(a2.getWeight());
			}
		});
	}
}

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

앞서 익명 클래스보다는 Lambda expression을 사용하는 것이 훨씬 더 간결하다는 것을 공부했습니다.

함수형 인터페이스를 기대하는 곳에는 어디든 Lambda expression을 사용할 수 있습니다.

Comparator<T>함수 디스크립터(T, T) -> int 이기 때문에 아래처럼 Lambda를 작성할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
	}
}

Comparator<T>에는 Comparable 키를 추출하여 Comparator객체로 만드는 Function함수를 인수로 받는 정적 메소드 comparing를 포함하고 있습니다.

해당 메소드를 활용하면 위의 코드를 아래처럼 더 가독성 좋게 변경할 수 있습니다.


class Foo {
	public static void main(String[] args) {
		inventory.sort(comparing(a -> a.getWeight()));
	}
}

static import를 사용하여 Comparator도 생략하면 더 가독성이 좋습니다.

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

메소드 참조를 이용하면 Lambda expression의 인수를 더 깔끔하게 전달할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		inventory.sort(comparing(Apple::getWeight));
	}
}

이렇게 앞서 배운 모든 내용을 동원하여 최적의 코드를 완성했습니다.

💡 이것은 단순히 코드만 짧아진 것이 아니라, 코드 자체로 'Appleweight별로 비교해서 inventorysort하라' 라는 코드의 의미도 명확해졌습니다.

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

Java 8 API의 몇몇 함수형 인터페이스는 다양한 유틸리티 메소드를 포함합니다.

예를 들어 두 개의 Predicate를 조합하여 or 연산을 수행하는 큰 Predicate를 만들거나, 한 함수결과가 다른 함수입력이 되도록 조합할 수도 있습니다.

함수형 인터페이스는 하나의 추상 메소드만 제공해야 하는데 추가로 메소드를 제공한다는 것이 이상하게 생각될 수도 있습니다.

하지만, default method를 사용하면 추상 메소드가 아니므로 가능합니다.

3.8.1 Comparator 조합

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

class Foo {
	public static void main(String[] args) {
		Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
	}
}

💡 역정렬

지금까지는 방법으로는 오름차순으로 밖에 정렬을 할 수 없었습니다. 내림차순으로 정렬을 하기 위해선 Comparator 자체에 내장된 reversed 메소드를 이용하면 됩니다.

class Foo {
	public static void main(String[] args) {
		inventory.sort(comparing(Apple::getWeight).reversed());
	}
}

💡 Comparator 연결

동일한 무게를 가진 사과가 있다면 추가적으로 정렬이 필요할 수 있습니다.

이런 경우 thenComparing메소드를 사용하여 두 번째 비교자를 만들 수 있습니다.

class Foo {
	public static void main(String[] args) {
		inventory.sort(comparing(Apple::getWeight)
				.reversed()
				.thenComparing(Apple::getCountry)
		);
	}
}

💡 thenComparing 메소드는 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객채를 전달 합니다.

3.8.2 Predicate 조합

복잡한 Predicate를 만들 수 있도록 negate, and, or 세 가지 메소드를 제공합니다.

  • negate: Predicate를 반전 시킬 때 사용
class Foo {
	public static void main(String[] args) {
		Predicate<Apple> notRedApple = redApple.negate();
		// 빨간색인 사과 추출에서 빨간색이 아닌 사과 추출로 변경
	}
}
  • and: 두 Predicate를 조합하여 and 조건으로 사용
class Foo {
	public static void main(String[] args) {
		Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
		// 빨간색이면서 무게가 150이 넘는 사과 추출
	}
}
  • or: 두 Predicate를 조합하여 or 조건으로 사용
class Foo {
	public static void main(String[] args) {
		Predicate<Apple> redAndHeavyApple = redApple
				.and(a -> a.getWeight() > 150)
				.or(a -> GREEN == a.getColor());

		// 빨간색 이면서 무게가 150이 넘는 사과 또는 그냥 녹색 사과 추출
	}
}

3.8.3 Function 조합

Function 인스턴를 반환하는 andThen, compose 두 가지 default method를 제공합니다.

  • andThen: 주어진 함수를 먼저 적용한 뒤, 결과를 다른 함수입력으로 사용
    • 수학적으로는 (g(f(x)))으로 표현
class Foo {
	public static void main(String[] args) {
		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: 주어진 함수를 먼저 실행한 뒤, 그 결과외부 함수인수로 제공
    • 수학적으로는 (f(g(x)))으로 표현
class Foo {
	public static void main(String[] args) {
		Function<Integer, Integer> f = (x -> x + 1);
		Function<Integer, Integer> g = (x -> x * 2);
		Function<Integer, Integer> h = f.compose(g);
		int result = h.apply(1); // 3
	}
}

예를 들어 헤더추가한 다음, 철자 검사를 하고, 마지막에 푸터추가하는 식으로 Pipeline을 만들 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Function<String, String> addHeader = Letter::addHeader;
		Function<String, String> transformationPipeline = addHeader
				.andThen(Letter::checkSpelling)
				.anethen(Letter::addFooter);
	}
}

3.10 마치며

  • Lambda expression익명 함수의 일종입니다. 이름은 없지만 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있습니다.
  • Lambda expression로 간결한 코드를 구현할 수 있습니다.
  • 함수형 인터페이스는 하나의 추상 메소드만을 정의한는 인터페이스입니다.
  • 함수형 인터페이스를 기대하는 곳에만 Lambda expression을 사용할 수 있습니다.
  • Lambda expression 전체가 함수형 인터페이스의 인스턴스로 취급됩니다.
  • java.util.function 패키지는 자주 사용하는 다양한 함수형 인터페이스를 제공합니다.
  • Java 8박싱 동작을 피할수 있는 IntPredicate같은 기본형 특화 인터페이스를 제공합니다.
  • 실행 어라운드 패턴Lambda와 활용하면 유연성재사용성을 추가로 얻을 수 있습니다.
  • Lambda expression의 기대 형식을 대상 형식 (target type)이라고 합니다.
  • 메소드 참조를 이용하면 기존의 메소드 구현을 재사용하고 직접 전달할 수 있습니다.
  • Comparator, Predicate, Function 같은 함수형 인터페이스Lambda expression을 조합할 수 있는 다양한 default method를 제공합니다.