devseop08 님의 블로그

[API] 람다와 스트림: Modern Java - 3. 람다 표현식 본문

Language/Java

[API] 람다와 스트림: Modern Java - 3. 람다 표현식

devseop08 2025. 5. 30. 21:27
  • 자바 8부터는 익명 클래스가 아닌 람다를 이용하여 동작 파라미터화를 쉽게 사용할 수 있게 되었다.
  • 동작 파라미터화는 계속해서 변화하고 추가되는 요구사항에 유연하게 대응하는 코드를 구현 할 수 있도록 도와준다.
  • 동작 파라미터화는 정의한 코드 블록을 다른 메서드로 전달할 수 있도록 한다. => 동작 파라미터화를 이용하면 더 유연하고 재사용할 수 있는 코드를 만들 수 있다.
  • 익명 클래스를 통해서도 동작 파라미터화를 구현할 수 있지만, 코드가 깔끔하지 않다.

3.1. 람다란 무엇인가?

  • 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것.
  • 람다 표현식에 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다.
  • 람다의 특징
    • 익명 : 보통의 메서드와 달리 이름이 없으므로 익명
    • 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수
    • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
    • 간결성 : 익명 클래스처럼 자질구레한 코드를 구현할 필요가 없다.
  • 람다 표현식
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight));
  • (Apple a1, Apple a2) : 람다 파라미터, 파라미터 리스트
  • 화살표 : 람다의 파라미터 리스트와 람다 바디를 구분
  • a1.getWeight().compareTo(a2.getWeight) : 람다 바디/람다의 반환값

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

람다 표현식은 함수형 인터페이스라는 문맥에서 사용 가능하다!

3.2.1 함수형 인터페이스

  • 함수형 인터페이스는 정확히 하나의 추상 메서드만을 지정하는 인터페이스이다.
public interface Predicate<T>{
    boolean test (T t);
}

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

public interface Runnable<T>{
    void run();
}

public interface ActionListener extends EventListener{
    void actionPerformed(ActionEvent e)
}

public interface Callable<V>{
    V call () throws Exception;
}
  • 인터페이스의 디폴트 메소드가 존재하더라도 추상 메서드가 하나만 존재한다면, 함수형 인터페이스이며, 상속받은 추상 메소드를 포함해서 하나의 추상 메서드만 가져야 한다.
인터페이스의 디폴트 메소드는 해당 인터페이스를 구현한 클래스의 객체가 오버라이드
하지 않고 사용할 수 있도록 인터페이스에서 구현된 메서드를 말한다.
  • 함수형 인터페이스로 무엇을 할 수 있을까?
    • 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급(함수형 인터페이스를 구현한 클래스의 인스턴스로 취급)할 수 있다.
Runnable r1 = () -> System.out.println("Hello World 1");

Runnable r2 = new Runnable(){
    public void run(){
        System.out.println("Hello World 2");
    }
}

public static void process(Runnable r){
    r.run();
}

process(r1);
process(r2);
process(() -> System.out.println("Hello World 3"));

3.2.2 함수 디스크립터

  • 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.
  • 람다 표현식의 유효성은 람다 표현식의 시그니처가 추상 메서드의 시그니처와 일치하는지를 검사하여 확인할 수 있다.
  • 즉, 추상 메서드로부터 기인한 함수 디스크립터와 람다 표현식의 시그니처가 일치하는지를 검사해야 한다.
함수형 인퍼테이스 함수 디스크립터
Predicate<T> T -> boolean
Consumer<T> T -> void
Function<T, R> T -> R
Supplier<T> () -> T
UnaryOperator<T> T -> T
BinaryOperator<T> (T, T) -> T
  • @FunctionalInterface는 무엇인가?
    • 인터페이스가 함수형 인터페이스임을 가리키는 어노테이션
    • @FunctionalInterface로 선언했지만 함수형 인터페이스 형식에 맞지 않으면, 컴파일 레벨에서 에러가 발생한다.

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

실행 어라운드 패턴 : 파일과 같은 자원 처리에 사용되는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어 진다. 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다.

public String processFile() throws IOException{
    try(
        BufferedReader br = new BuffredReader(new FileReader("data.txt"));        
    ){
        return br.readLine();  // 이 구간을 파라미터화
    }
}

String result = processFile();

3.3.1 동작 파라미터화를 기억하라

  • processFile의 동작을 파라미터화 할 수 있다.
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

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

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

public String processFile(BufferedReaderInterface p) throws IOException{
    ...
}

3.3.3 동작 실행

public String processFile(BufferedReaderProcessor p) throws IOException{
    try(
        BufferedReader br 
                = new BuffredReader(new FileReader("data.txt"));    
    ){
        return p.process(br);
    }
}

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

3.3.4 람다 전달

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

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

3.4 함수형 인터페이스 사용

java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다.

3.4.1 Predicate

  • java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며,test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환
@FunctionalInterface
public interface Prediacate<T>{
    boolean test(T t);
}

public <T> List<T> filter(List<T> list,  Predicate<T> p){
    List<T> results = list;

    for(T t : list){
        if(p.test(t))
            results.add(t)
    }

    return results;
}

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

3.4.2 Consumer

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

3.4.3 Function

  • java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다.
  • 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.
@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("lamdas", "in", "action"), (String s) -> s.length);

기본형 특화

  • 자바의 모든 데이터 형식은 참조형 아니면 기본형에 해당
  • 제네릭 파라미터의 타입은 참조형만 가능하기 때문에 제네릭 함수형 인터페이스에 입력되는 데이터가 기본형 데이터인 경우, 오토박싱이 일어난다.
  • 오토박싱은 비용이 소모된다. 오토박싱한 값은 기본형을 감싸는 래퍼며, 힙에 저장되기 때문에 메모리를 소모한다.
  • 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 상황을 피하도록 특별한 함수형 인터페이스를 제공한다.
  • IntPredicate, LongPredicate, IntConsumer, IntFunction과 같이 기존 인터페이스명에 기본 자료형 이름이 붙는 형식의 명칭을 갖는 인터페이스들이 그러한 인터페이스들이다.
@FunctionalInterface
public interface IntPredicate{
    boolean test(int t);
}

IntPredicate evenNumbers = (int i) -> i % 2 == 0;

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0; 

boolean b1 = evenNumbers.test(1000); => 오토박싱이 일어나지 않는다. 

boolean b2 = oddNumbers.test(1000); => 오토박싱이 일어난다. 

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

함수형 인터페이스는 확인된 예외를 던지는 동작(throw new Exception();)을 허용하지 않는다. 즉, 예외를 던지는 람다 표현식을 만들려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 이미 만들어진 함수형 인터페이스를 사용해야 해서 확인된 예외를 선언할 수 없다면, 람다를 try/catch 블록으로 감싸야 한다.

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

Function<BufferedReader, String> f = (BufferedReader b) -> {
    try{
        return b.readLine();
    }catch(IOException e){
        throw new RuntimeException(e);
    }
};

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

  • 람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다.
  • 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다.

3.5.1 형식 검사

  • 람다가 사용되는 컨텍스트를 이용해서 람다의 형식을 추론할 수 있다.

  • 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등을 컨텍스트라 한다.

  • 어떤 컨텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라 한다.

  • 람다 표현식의 형식 검사 과정

      1. 람다가 사용되는 컨텍스트를 확인한다.

        Predicate<Apple> p = (Apple a) -> a.getWeight() > 150;    

        // 컨텍스트는 Predicate 인터페이스 타입 변수에 대한 할당

      1. 대상 형식을 확인한다. 예문에서 대상 형식은 Predicate<Apple> 이다.
      1. Predicate<Apple>의 추상 메서드를 확인한다.
      @FunctionalInterface
      public interface Predicate<T> {
        boolean test(T t){};
      }
      1. 추상 메서드가 묘사하는 함수 디스크립터를 확인한다. 예문에서 test 메서드가 묘사하는
        함수 디스크립터는 Apple -> boolean 이다.
      1. 함수 디스크립터와 람다의 시그니처가 일치하는지 확인한다. 람다 표현식
        (Apple a) -> a.getWeight() > 150의 시그니처가 Apple -> boolean이므로 함수 디스크립터와 람다의 시그니처가 일치하여 람다 표현식(Apple a) -> a.getWeight() > 150는 유효한 코드이다.

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

  • 대상 형식이라는 특징으로 인해 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
  • 특별한 void 호환 규칙
    • 람다의 바디에 일반 표현식이 있으면, 해당 람다의 시그니처는 void를 반환하는 함수 디스크립터와 호환된다.(파라미터 리스트도 당연히 호환되어야 한다.)
// Predicate는 불리언 반환값을 갖는다.
Predicate<String> p = (String s) -> list.add(s);
// Consumer는 void 반환값을 갖는다.
Consumer<String> c = (String s) -> list.add(s); // 호환
  • 다이아몬드 연산자
    • 다이아몬드 연산자(<>)를 이용하여 콘텍스트에 따른 제네릭 형식을 추론할 수 있다.
    • 클래스 인스턴스 표현식을 두 개 이상의 다양한 콘텍스트에 사용할 수 있다.
    • 인스턴스 표현식의 타입 인수는 콘텍스트에 의해 추론된다.
List<String> listOfStrings = new ArrayList<>();
List<Integer> listOfIntegers = new ArrayList<>();
  • 람다의 타입 캐스트
    • 같은 함수형 디스크립터를 갖는 두 함수형 인터페이스를 파라미터로 하는 메서드를 오버로딩할 때 람다의 타입 캐스트를 이용할 수 있다.
@FunctionalInterface
public interface Runnable {
    void run();
}

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

public void execute(Runnable runnable){
    runnable.run();
}

public void execute(Action action){      // 메서드 오버로딩!
    action.act();
}

execute(()->{});      // Runnable의 run() 메서드를 호출할지, 
                      // Action의 act() 메서드를 호출할지 알수 없다.

execute((Action)()->{});  // 무엇을 호출할 지 알 수 있다. 

3.5.3 형식 추론

  • 자바 컴파일러는 람다 표현식이 사용된 컨텍스트와 대상 형식을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론하고, 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론 가능하다.
  • 자바 컴파일러는 추론한 람다의 시그니처를 바탕으로 람다 표현식의 파라미터 타입을 추론할 수 있게 되어, 람다 표현식 작성 시, 람다의 파라미터 타입을 생략하여 작성 가능하다.
Comparator<Apple> c = (Apple a1, Apple a2)
-> a1.getWeight().compareTo(a2.getWeight); // 타입을 추론하지 않음

Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight);
// 타입을 추론함

3.5.4 지역 변수 사용

  • 지금까지 살펴본 모든 람다 표현식은 인수를 자신의 바디 안에서만 사용했다.
  • 하지만 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수, 즉 파라마터로 넘겨진 변수가 아닌 외부에서 정의한 변수를 활용할 수 있다. 이와 같은 동작을 람다 캡처링이라 한다.
  • 자유 변수에는 제약이 있다.
    • 인스턴스 변수와 정적 변수는 자유롭게 캡처할 수 있다.
    • 하지만 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다. 즉, 람다 표현식은 한 번만 할당할 수 있는 지역변수를 캡처할 수 있다.

지역변수의 제약

  • 왜 인스턴스 변수와 정적 변수는 자유롭게 캡처할 수 있지만, 지역변수는 final로 선언되거나 최소한 final로 선언된 것처럼 사용되어야 할까?
  • 인스턴스 변수는 힙 영역에 저장된 객체에 내장되어 새로운 스레드의 생성과 소멸에는 영향을 받지 않고 자신이 속한 객체가 소멸되지 않는 한 힙 영역에 남아있게 된다. 그렇기 때문에 부모 스레드에서 다루는 인스턴스 변수가 람다 표현식에 캡처되어 새로운 스레드에서 그 인스턴스 변수를 처리하더라도 부모 스레드와 새로 생성된 자식 스레드가 동일한 힙 영역의 인스턴스 변수를 인식하기 때문에 자유롭게 람다 표현식에서 인스턴스 변수를 캡처할 수 있다.
  • 정적 변수, static 변수는 JVM 메모리 공간에서 메소드 영역에 저장되는데 메소드 영역은 힙영역과 마찬가지로 공유 영역이기 때문에 인스턴스 변수와 마찬가지로 자유롭게 람다 표현식에 캡처될 수 있다.
  • 다만 람다 표현식에 인스턴스 변수와 정적 변수를 캡처하여 여러 스레드를 실행시킬 경우에는 동기화 이슈를 고려해야 한다는 점은 유의하라.
  • 지역 변수는 메서드 호출에 의해 생성된 스택 영역에 저장되는데, 문제는 이 스택이 스레드마다 할당되고 스레드에 따라 소멸된다는 데 있다.
  • 만약 람다 표현식에 메서드 지역 변수 캡처가 가능하다고 하는 경우, 이 람다 표현식으로 정의된 동작을 실행시키는 자식 스레드에서 람다 표현식으로 정의된 그 동작을 실행시키는 시점에 부모 스레드의 스택에 저장된, 람다 표현식이 캡처한 그 지역변수를 찾게 한다고 해보자
  • 부모 스레드 종료와 함께 그 부모 스레드의 스택이 소멸되면서 람다가 캡처한 그 지역변수도 소멸되어 자식 스레드에서 그 지역변수를 찾지 못하는 상황이 발생할 수 있다.
  • 이런 상황을 방지하기 위해 자식 스레드는 람다 표현식에 캡처한 부모 스레드의 지역변수의 복사본을 자신에게 할당된 스택에 저장해둔다. 따라서 람다 표현식이 정의한 동작을 수행하는 자식 스레드에서 바로 이 "복사본"을 자신의 스택에 저장해두고 람다가 캡처한 지역변수를 처리하는 것이 된다.
  • 따라서 부모 스레드 스택에 저장된 원본과 자식 스레드 스택에 저장된 복사본이 서로 달라지지 않게 하기 위해 람다가 캡처하는 메서드 지역 변수는 final로 선언되거나 final처럼 사용돼야 한다는 것이다.
// 1. portNumber가 인스턴스 변수일 때

class InstancePort{
    private int portNumber;

    public Port(int portNumber){
        this.portNumber = portNumber;
    }

    public int getPortNumber(int portNumber){
        return this.portNumber;
    }

    public void setPortNumber(int portNumber){
        this.portNumber = portNumber;
    }

    public void multiThreadPrint(){   // 부모 스레드만의 스택을 갖는다.
        Runnable r = () -> System.out.println(this.portNumber);
        this.portNumber = 1000; // 스레드 소멸과 관련없는 힙 영역에 저장된 객체의
                                // 필드 portNumber이기 때문에 가능

        Thread thread = new Thread(r);   // 자식 스레드만의 스택을 갖는다.
        thread.start();    
        System.out.println(this.portNumber);
        // 부모 스레드와 자식 스레드 모두 동일한 포트 넘버 1000 출력

    }
}

public Class LamdaCaptureTest {
    public static void main(String[] args){
        InstancePort instancePort = new Port(100);

        instancePort.multiThreadPrint();
    }
}
// 2. portNumber가 static 변수일 때

class StaticPort{
    private static int portNumber = 100;

    private static Runnable r;

    public StaticPort(int portNumber){
        this.portNumber = portNumber;
    }

    public int getPortNumber(int portNumber){
        return this.portNumber;
    }

    public void setPortNumber(int portNumber){
        this.portNumber = portNumber;
    }

    public static void multiThreadPrint(){   // 부모 스레드만의 스택을 갖는다.
        r = () -> System.out.println(portNumber);
        portNumber = 1000; // 스레드 소멸과 관련없는 메소드 영역에 저장된 객체의
                           // 필드 portNumber이기 때문에 가능

        Thread thread = new Thread(r);   // 자식 스레드만의 스택을 갖는다.
        thread.start();    
        System.out.println(portNumber);
        // 부모 스레드와 자식 스레드 모두 동일한 포트 넘버 1000 출력

    }
}

public Class LamdaCaptureTest {
    public static void main(String[] args){

        StaticPort.multiThreadPrint();
    }
}
// 3.portNumber가 지역변수일 때
class LocalPort{

    public void multiThreadPrint(int portNumber){// 부모 스레드만의 스택을 갖는다.
        Runnable r = () -> System.out.println(portNumber);
        portNumber = 1000; // 스레드 소멸과 관련있는 스택 영역에 저장된 지역변수 
                           // portNumber이기 때문에 자식 스레드에선 portNumber의
                           // 복사본을 저장해둘 것이고, 원본과 복사본이 달라지면 안
                           // 되기 때문에 portNumeber 초기화 이후 변경은 불가능  

        Thread thread = new Thread(r);   // 자식 스레드만의 스택을 갖는다.
        thread.start();    
        System.out.println(portNumber);

    }
}

public Class LamdaCaptureTest {
    public static void main(String[] args){
        LocalPort localPort = new LocalPort();

        localPort.multiThreadPrint(100);
    }
}

3.6 메서드 참조

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

3.6.1 요약

  • 람다가 어떤 메서드를 어떻게 호출해야하는지 설명하기보다는 메서드명을 직접 참조하는 것이 편리하다.
  • 실제로 메서드 참조를 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있다.
  • 이 때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.

메서드 참조를 만드는 방법

    1. 정적 메서드 참조
      (args) -> ClassName.staticMethod(args)
      => ClassName::staticMethod
    1. 람다 인수의 인스턴스 메서드 참조
      (usedArg, rest) -> usedArg.instanceMethod(rest)
      => ClassName::instanceMethod
    1. 람다 인수가 아닌 람다 외부 객체(지역변수)의 인스턴스 메서드 참조
      (args) -> externalExpr.instanceMethod
      =>externalExpr::instanceMethod

3.6.2 생성자 참조

  • new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.
  • 이것은 정적 메서드의 참조를 만드는 것과 비슷하다.
  • Supplier<Apple> c = () -> new Apple()
    => Supplier<Apple> c = Apple::new
  • Function<Integer, int[]> f = (i) -> new int[i]
    => Function<Integer, int[]> f = int[]::new

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

  • 자바 8 API의 몇몇 함수형 인터페이스는 다양한 유티리티 메서드를 포함한다.
  • Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공한다.
  • 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있는데, 이 메서드들은 default 메서드로, 추상 메서드가 아니기 때문에, 함수형 인터페이스 규칙에 위반되지 않는다.
  • 인터페이스는 추상 메서드, static 메서드, default 메서드를 멤버로 가질 수 있다.
  • static 메서드는 상속에서 제외되지만, 추상 메서드는 상속이 되며, 오버라이딩이 필요하고, default 메서드는 상속이 되며, 다중 상속 시 인터페이스의 디폴트 메서드 시그니처 간에 충돌이 발생하면, 재정의(오버라이딩)가 필요하다.

    3.8.1 Comparator 조합

  • comparing() (static 메서드)
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

inventory.sort(Comparator.comparing(Apple::getWeight)
                    .reversed() // 역정렬 
                    .thenComparing(Apple::getCountry)); // Comparator 연결

3.8.2 Predicate 조합

  • and(), or(), negate()로 두 Predicate를 하나로 결합(default 메서드)
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i%2 == 0;

Predicate<Integer> notP = p.negate(); 
// i >= 100
Predicate<Integer> all = notP.and(q).or(r); 
// i >= 100 && (i -> i < 200) || (i % 2 == 0)
Predicate<Integer> all2 = notP.and(q.or(r));
// i >= 100 && ((i -> i < 200) || (i % 2 == 0))

notP.test(101); // true
all.test(101); // true
all2.test(101); // false
  • 등가비교를 위한 Predicate의 작성에 isEqual() 사용(static 메서드)
Predicate<String> p = Predicate.isEqual(str1);

Boolean result = p.test(str2);

result = Predicate.isEqual(str1).test(str2);

3.8.3 Function 조합

  • Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메서드를 제공한다.
  • andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.
  • compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인자로 제공한다.
Function<Integer, Integer> f1 = x -> x + 1;
Function<Integer, Integer> g1 = x -> x + 2;
Function<Integer, Integer> h1 = f1.andThen(g1);
h1.apply(1); => 4 반환

Function<Integer, Integer> f2 = x -> x + 1;
Function<Integer, Integer> g2 = x -> x * 2;
Function<Integer, Integer> h2 = f2.compose(g2);

h2.apply(1); => 3 반환

함수형 인터페이스를 이용하는 컬렉션 프레임웍 메서드

  • Collection
    • boolean removeIf(Predicate<E> filter)
  • List
    • void replaceAll(UnaryOperator<E> operator)
  • Map
    • void forEach(BiConsume<K,V> action)
    • void replaceAll(BiFunction<K,V,V> f)