Back-end/이것이 자바다[신용권 한빛미디어]

람다식 표준 API의 함수적 인터페이스, 메소드 참조

Ho's log 2022. 4. 10. 23:14

표준 API의 함수적 인터페이스


자바에서 제공되는 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스들은 모두 람다식을 이용해서 익명 구현 객체로 표현이 가능하다.

예를 들어 스레드의 작업을 정의하는 Runnable 인터페이스는 매개 변수와 리턴 값이 없는 run() 메소드만 존재하기 때문에 다음과 같이 람다식을 이용해서 Runnable 인스턴스를 생성 시킬수 있다. 

 

package Chapter14;

public class RunnableExample {
    public static void main(String[] args) {
        Runnable r =  () -> {
            for(int i = 0; i < 10; i++) {
                System.out.println("Hello from thread " + Thread.currentThread().getName());
            }
        };

        Thread t = new Thread(r);
        t.start();
    }
}

Thread 생성자를 호출 할때 람다식을 매개값으로 대체 해도 된다 

Thread thead = new Thread(() -> {
	for(int i = 0; i <10; i++){
    	System.out.println('h')
    }
});

 

자바 8 부터는 빈번하게 사용되는 함수적 인터페이스 (functional interface)는 java.util.function 표준 API 패키지로 제공한다.

이 패키지에서 제공하는 함수적 인터페이스의 목적은 메소드 또는 생성자의 매개 타입으로 사용되어  람다식을 대입할수 있도록 하기 위해서 이다.

 

자바 8부터 추가되거나 변경되 API에서 이 함수적 인터페이스들을 매개 타입으로 많이 사용한다.

개발하는 메소드에도 이 함수적 인터페이스들을 매개 타입으로 사용 할 수 있다. 

java.util.function 패키지의 함수적 인터페이스는 크게 Consumer, Supplieer, Function, Operator, Predicate 로 구분된다.

구분 기준은 인터페이스에 선언된 추상 메소드의 매개값과 리턴 값이 유무이다. 

 

종류 추상 메소드 특징  
Consumer - 매개값은 있고, 리턴 값은 없음 매개값 -> ㅁ
Supplier - 매개값은 없고, 리턴 값은 있음 ㅁ -> 리턴값
Function - 매개값도 있고, 리턴 값도 있음
- 주로 매개값을 리턴 값으로 매핑(타입변환)
매개값 -> ㅁ -> 리턴값
Operator - 매개값도 있고 리턴값도 있음
- 주로 매개값을 연산하고 결과를 리턴
매개값 -> ㅁ -> 리턴값
Predicate - 매개값은 있고, 리턴 타입은 boolean
- 매개값을 조사해서 true/ false 를 리턴
매개값 -> ㅁ -> boolean

 

Consumer 함수적 인터페이스


Consumer 함수적 인터페이스의 특징은 리턴값이 없는 accept () 메소드를 가지고 있다. 

accept() 메소드는 단지 매개값을 소비하는 역할만 한다. 

여기서 소비한다는 말은 사용만 할 뿐 리턴값이 없다는 뜻이다.

 

매개값

-> [XXXConsumer]

 

매개 변수의 타입과 수에 따라서 아래와 같은 Consumer들이 있다.

 

인터페이스명 추상 메소드 설명
Consumer<T> void accept(T t) 객체 T를 받아 소비 
BiConsumer<T,U> void accept(T t, U u ) 객체 T와 U를 받아 소비
DoubleConsumer void accept(double value) double 값을 받아 소비
IntConsumer void accept(int value) int 값을 받아 소비
LongConsumer void accept(long value) long 값을 받아 소비
ObjDoubleConsumer<T> void accept(T t, double value) 객체 T와 double 값을 받아 소비
ObjintConsumer<T> void accept(T t, int value) 객체 T와 int 값을 받아 소비
ObjLongConsumer<T> void accept(T t, long value) 객체 T와 long 값을 받아 소비

 

Consumer<T> 인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다. 

accept() 메소드는 매개값으로 T 객체 하나를 가지므로 람다식도 한 개의 매개 변수를 사용한다. 

타입 파라미터 T에 String이 대입 되었기 때문에 람다식의 t의 매개 변수 타입은 String이 된다.

 

Consumer<String> consumer = t -> {t를 소비하는 실행문;};

 

BiConsumer<T , U> 인ㅌ퍼에시를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.

accept() 메소드는 매개값으로 T와 U 두개의 객체를 가지므로 람다식도 두개의 매개변수를 사용한다.

타입파라미터 T와 U에 String이 대입되었기 때문에 람다식의 t와 u 매개변수타입은 각각 String이 된다. 

BiConsumer<String, String> consume = (t, u) -> {t와 ufmf 소비하는 실행문;}

 

DoubleConsumer 인터페이스르 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.

accept()메소드는 매개값으로 double 하나를 가지므로 람다식도 한 개의 매개 변수를 사용한다.

d는 고정적으로 double타입이 된다.

DoubleConsumer consumer = d -> {d를 소비하는 실행문; }

 

accept()메소드는 매개값으로 T 객체와 int 값 두개를 가지기 때문에 람다식도 두 개의 매개변수를 사용한다. T가 String 타입이므로 람다식의 t 매개 변수 타입은 String이 되고, i는 고정적으로 int 타입이 된다

ObjIntConsumer<String> consumer = (t,i) -> {t와 i를 소비하는 실행문; }

 

Supplier 함수적 인터페이스 


Supplier 함수적 인터페이스의 특징은 매개 변수가 없고 리턴 값이 있는 getXXX() 메소드를 가지고 있다.

이 메소드들은 실행 후 호출한 곳으로 데이터를 리턴(공급)하는 역할을 한다

 

         리턴값

Supplier => 

 

리턴 타입에 따라서 아래와 같은 Supplier 함수적 인터페이스들이 있다.

인터페이스명 추상 메소드  설명
Supplier<T> T get() T 객체를 리턴
BooleanSupplier boolean getAsBoolean() boolean 값을 리턴
DoubleSupplier double getAsDouble() double 값을 리턴
IntSupplier int getAsInt() int 값을 리턴
LongSupplier long getAsLong() long 값을 리턴

 

Supplier<T> 인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.

get() 메소드가 매개값을 가지지 않으므로 람다식도 ()를 사용한다. 람다식의 중괄호 {}는 반드시 한 개의 T 객체를 리턴하도록 해야한다.

T가 String 타입이므로 람다식의 중괄호 {} 는 문자열을 리턴하도록 해야한다.

Supplier<String> supplier = () -> {...; return 문자열;}

 

IntSupplier 인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.

getAsInt() 메소드가 매개값을 가지지 않으므로 람다식도 ()를 사용한다. 람다식의 중괄호 {} 는 반드시 int 값을 리턴 하도록 해야한다

 

IntSupplier supplier = ()-> {...; return int값;}

 

Function 함수적 인터페이스


Function 함수적 인터페이스의 특징은 매개값과 리턴값이 있는 applyXXX() 메소드를 가지고 있다.

이 메소드들은 매개값을 리턴 값으로 매핑(타입 변환)하는 역할을 한다.

 

매개값               리턴값 

  =>      Function   =>

 

인터페이스명 추상메소드 설명
Function<T,R> R apply(T t) 객체 T를 객체 R로 매핑
BiFunction<T,U,R> R apply(T t, U u) 객체 T와 U를 객체 R로 매핑
DoubleFunction<R> R apply(double value) double을 객체 R로 매핑
IntFunction<R> R apply(int value) int를 객체 R 로 매핑
IntToDoubleFunction double applyAsDouble(int value) int를 double로 매핑
IntToLongFunction  long applyAsLong(int value) int를 long 으로 매핑
LongToDoubleFunction  double applyAsDouble(long value) long을 double 로 매핑
LongToIntFunction int applyAsInt(long value) long을 int로 매핑
ToDoubleBiFunction<T , U> double applyAsDouble(T t, U u) 객체 T와 U를 double 로 매핑 
ToIntBiFunction<T, U> int applyAsInt(T t , U u) 객체 T 와 U를 int로 매핑
ToIntFunction<T> int applyAsInt(T t) 객체 T 를 int 로 매핑
ToLongBiFunction<T, U> long applyAsLong(T t , U u ) 객체 T와 U를 long으로 매핑
ToLongFunction<T> long applyAsLong(T t) 객체 T를 long 으로 매핑 

 

Function<T,R> 인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성 할 수 있다.

apply()메소드는 매개값으로 T 객체 하나를 가지므로 람다식도 한 개의 매개 변수를 사용한다. 

그리고 apply() 메소드의 리턴 타입이 R 이므로 람다식 중괄호 {}의 리턴 값은 R 객체가 된다.

T가 Student 타입이고 R 이 String 타입이므로 t 매개 변수 타입은 Student가 되고,

람다식의 중괄호{}는 String 을 리턴해야 한다. t.getName()은 Student 객체의 getName() 메소드를 호출해서 학생이름 (String)을 얻는다.

return 문만 있을 경우 중괄호 {}와 return 문은 생략할 수 있다는 것을 미미 배웠다. 다음 코드는 Student 객체를 학생 이름 (String)으로 매핑 하는 것이다.

 

Function<Student, String> function = t -> {return t.getName();}

또는

Function<Student, String> function = t-> t.getName();

 

ToIntFunction<T> 인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.

appyAsInt() 메소드는 매개값으로 T 객체 하나를 가지므로 람다식도 한 개의 매개변수를 사용한다.

그리도 applyAsInt() 메소드의 리턴 타입이 int 이므로 람다식 중괄호 {} 의 리턴값은 int가 된다.

T가 Student 타입이므로 t 매개 변수 타입은 Student가 된다. 

t.getScore()는 Student 객체의 getScore() 메소드를 호출해서 학생 점수(int)를 얻는다. 

다음 코드는 Student 객체를 학생 점수(int)로 매핑하는 것이라고 볼 수 있다. 

 

ToIntFunction<Student> function = t-> {return t.getScore();}

또는

ToIntFunction<Student> function = t -> t.getScor();

 

다음 예제는 List에 저장된 학생 객체를 하나씩 거내서 이름과 점수를 출력한다.

FunctionExample1의 printString() 메소드는 Function<Student, String> 매개 변수를 가지고 있고 

printInt() 메소드는 ToIntFunction<Student> 매개 변수를 가지고 있으므로 이 메소드들을 호출 할 때 매개값으로 람다식을 사용할 수 있다 .

 

package Chapter14;

import BasicAPIClass.Student;

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.ToIntFunction;

public class FunctionExample1 {

    private final static List<Student> list = Arrays.asList(
            new Student( "2","John", 18, "F" ),
            new Student("3","Mary" , 13, "M"),
            new Student("4","Peter", 34, "M"),
            new Student("5", "Paul", 22, "M"),
            new Student("6", "Mike", 23, "M")


    );

    public static void printString(Function<Student, String> f) {
        list.forEach(s -> System.out.println(f.apply(s)));
    }

    public static void printInt(ToIntFunction<Student> f) {
        list.forEach(s -> System.out.println(f.applyAsInt(s)));


    }



    public static void main(String[] args) {
        printString(Student::getStudentNum);

        printInt(Student::getStudentAge);
    }
}

 

 

다음 예제는 List에 저장된 학생 객체를 하나씩 꺼내서 나이의 평균값을 산출한다 

FunctonExample2의 avg () 메소드는 ToIntFunction<Student> 매개 변수를 가지고 있다. 

따라서 avg()메소드를 호출할 때 매개값으로 람다식을 사용할 수 있다. 람다식은 getEnglishScore()와

getMathScore()를 호출해서 영어 점수와 수학 점수로 Student 객체를 매핑(변환)시킨다.

package Chapter14;

import BasicAPIClass.Student;

import java.util.Arrays;
import java.util.List;
import java.util.function.ToIntFunction;

public class FunctionExample2 {

    private static List<Student> list = Arrays.asList(
            new Student("John", "Doe", 2 , ""),

            new Student("John", "Doe", 4, ""),

            new Student("John", "Doe", 6 , "")
    );

    public static double avg(ToIntFunction<Student> f) {
        return list.stream()
                .mapToInt(f)
                .average()
                .getAsDouble();
    }

    public static void main(String[] args) {
        System.out.println(avg(Student::getStudentAge));
    }
}

 

Operator 함수적 인터페이스


Operator 함수적 인터페이스는 Function과 동일하게 매개 변수와 리턴값이 있는 applyXXX() 메소드를 가지고 있다. 

하지만 이 메소드들은 매개값 리턴값으로 매핑 (타입 변환)하는 역할보다는 매개값을 이용해서 연산을 수행한 후 동일한 타입으로 리턴값을 제공하는 역할을 한다. 

매개값                          리턴값

->            Operator         -> 

 

매개 변수의 타입과 수에 따라서 아래와 같은 Operator 함수적 인터페이스들이 있다. 

 

인터페이스명 추상 메소드 설명
BinaryOperator<T> T apply(T t, T t) T와 T를 연산한 후 T를 리턴
UnaryOperator<T> T apply(T t) T를 연산한 후 T를 리턴
DoubleBinaryOperator double applyAsDouble(double, double) 두 개의 double 연산
DoubleUnaryOperator double applyAsDouble(double) 한 개의 double 연산
IntBinaryOperator int applyAsInt(int, int) 두 개의 int 연산
IntUnaryOperator int applyAsInt(int) 한 개의 int 연산
LongBinaryOperator long applyAsLong(long, long) 두 개의 long 연산
LongUnaryOperator long applyAsLong(long) 한 개의 long 연산

 

 

IntBinaryOperator 인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.

applyAsInt() 메소드는 매개값으로 두 개의 int를 가지므로 람다식도 두 개의 int 매개변수 a와 b 를 사용한다.

그리고 applyAsInt() 메소드의 리턴 타입이 int 이므로 람다식의 중괄호{}의 리턴값은 int가 된다. 다음 코드는 두 개의 int를 연산해서 결과값으로 int를 리턴한다.

 

IntBinaryOperator operator = (a, b) -> {...; return int 값}

 

다음 예제 int [] 배열에서 최대값과 최소값을 얻는다 

maxOrMin() 메소드는 IntBinaryOperator 매개 변수를 가지고 있다. 따라서 maxOrMin() 메소드를 호촐할 때 람다식을 사용할수 있다.

package Chapter14;

import java.util.function.IntBinaryOperator;

public class OperatorExample {

    private static int[] scores = {92, 95, 87};

    public static int maxOrMin(IntBinaryOperator operator){
        int max = operator.applyAsInt(scores[0], scores[1]);
        for (int i = 2; i < scores.length; i++) {
            max = operator.applyAsInt(max, scores[i]);
        }
        return max;
    }

    public static void main(String[] args) {
        System.out.println("Max: " + maxOrMin(Integer::max));
        System.out.println("Min: " + maxOrMin(Integer::min));
    }
}

 

Predicate 함수적 인터페이스


Predicate 함수적 인터페이스는 매개 변수와 boolean 리턴값이 있는 testXXX() 메소드를 가지고 있다.

이 메소드들은 매개값을 조사해서 true 또는 false 를 리턴하는 역할을 한다.

 

 

매개값                   boolean

-->      Predicate    --> 

 

매개 변수 타입과 수에 따라서 아래와 같은 Predicate 함수적 인터페이스들이 있다. 

인터페이스명 추상 메소드 설명
Predicate<T> boolean test(T t) 객체 T를 조사 
BiPredicate<T , U> boolean test(T t, U u) 객체 T와 U를 비교 조사
DoublePredicate boolean test(double value) double 값을 조사
IntPredicate boolean test(int value) int 값을 조사
LongPredicate boolean test(long value) long 값을 조사 

 

Predicate<T> 인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다. test()메소드는 매개값으로 T 객체 하나를 가지므로 람다식도 한 개의 매개변수를 사용한다

그리고 test()메소드의 리턴 타입이 boolean 이므로 람다식 중괄호 {} return 값은 boolean 이 된다.

 

T 가 Student 타입이므로 t 매개 변수 타입은 Student 가 된다. t.getSex()는 Student 객체의 getSex() 메소드를 호출해서 "남자" 또는 "여자"를 얻는다. 결국 다음 코드는 String의 equals()메소드를 이용해서 남학생만 true를 리턴한다.

 

Predicate<Student> predicate = t -> {return t.getSex().equals("남자");}

또는

Predicate<Student> predicate = t -> t.getSex().equals("남자");

 

다음 예제는 List에 저장된 남자 또는 여자 학생들의 평균 점수를 출력한다. avg() 메소드는 Predicate<Student> 매개 변수를 가지고 있다. 따라서 avg() 메소드를 호출 할 때 매개값으로 람다식을 사용할 수 있다. 

package Chapter14;

import BasicAPIClass.Student;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class PredicateExample {


    private static List<Student> student = Arrays.asList(
            new Student("John", "Doe", 1, "M"),
            new Student("Jane", "Doe", 2, "F"),
            new Student("John", "Smith", 3, "M"),
            new Student("Jane", "Smith", 4, "F"),
            new Student("John", "Doe", 5, "M"),
            new Student("Jane", "Doe", 6, "F"),
            new Student("John", "Smith", 7, "M"),
            new Student("Jane", "Smith", 8, "F")
    );

    public static double avg(Predicate<Student> p) {
        int count = 0, sum = 0;
        for (Student s : student) {
            if(p.test(s)) {
                count++;
                sum += s.getStudentAge();

            }

        }
        return (double) sum / count;

    }

    public static void main(String[] args) {
        double avg = avg(s -> s.getStudentSex().equals("M"));
        System.out.println("Average age of " + avg);

    }


}

 

andThen()과 compose() 디폴트 메소드


디폴트 및 정적 메소드는 추상 메소드가 아니기 때문에 함수적 인터페이스에 선언되어도 여전히 함수적 인터페이스의 성질을 잃지 않는다. 

여기서 함수적 인터페이스 성질이란 하나의 추상 메소드를 가지고 있고,

람다식으로 익명 구현 객체를 생성할 수 있는 것을 말한다.

java.util.function 패키지의 함수적 인터페이스는 하나 이상의 디폴트 및 정적 메소드를 가지고 있다.

Consumer, Function, Operator 종류의 함수적 인터페이스는 andThen()과 compose() 디폴트 메소드를 가지고 있다.

andThen()r과 compose() 디폴트 메소드는 두 개의 함수적 인터페이스를 순차적으로 연결하고,

첫 번째 처리 결과를 두 번재 매개값으로 제공해서 최종 결과값을 얻을 때 사용한다. 

andThen()과 compose()의 차이점은 어떤 함수적 인터페이스부터 먼저 처리하느냐이다.

.

인터페이스AB = 인터페이스A.andThen(인터페이스B);
최종결과 = 인터페이스AB.method();

 

인터페이스AB의 method() 를 호출하면 우선 인터페이스A부터 처리하고 결과를 인터페이스B의 매개값으로 제공한다.

인터페이스B는 제공받은 매개값을 가지고 처리한 후 최종 결과를 리턴한다.

 

인터페이스AB.method() -> 인터페이스A(람다식) -> andThen -> 인터페이스B(람다식) -> 최종결과

 

이번에는 compose()를 살펴보자. 인터페이스AB의 method()를 호출하면 우선 인터페이스B부터 처리하고 결과를 인터페이스A의 매개값으로 제공한다. 인터페이스A는 제공받은 매개값을 가지고 처리한 후 최종 결과를 리턴 한다. 

인터페이스AB = 인터페이스A.compose(인터페이스B);
최종결과 = 인터페이스AB.method();

 

인터페이스AB.method() ->  인터페이스B(람다식) ->compose() ->인터페이스A(람다식) ->  최종결과

종류 함수적 인터페이스 andThne() compose()
Consumer Consumer<T>  
BiConsumer<T,U>  
DoubleConsumer  
IntConsumer  
LongConsumer  
Function Function<T, R>
BiFunction<T, U, R>  
Operator BinaryOperator<T>  
DoubleUnaryOperator
IntUnaryOperator
LongUnaryOperator

 

Consumer의 순차적 연결


Consumer 종류의 함수적 인터페이스는 처리 결과를 리턴하지 않기 대문에 andTher() 디폴트 메소드는 함수적 인터페이스의 호출 순서만 정한다.

 

아래는 counsumerA 를 실행후 consumerB 를 실행한다.

package Chapter14;

import BasicAPIClass.Member;

import java.util.function.Consumer;

public class ConsumerAndThenExample {

    public static void main(String[] args) {
        Consumer<Member> consumerA = (m) -> System.out.println("Consumer A" + m.toString());

        Consumer<Member> consumerB = (m) -> System.out.println("Consumer B" + m.hashCode());

        Consumer<Member> consumerAD = consumerA.andThen(consumerB);

        consumerAD.accept(new Member("John", "Doe"));
    }
}

 

Function의 순차적 연결


Function과 Operator 종류의 함수적 인터페이스는 먼저 실행한 함수적 인터페이스의 결과를 다음 함수적 인터페이스의 매개값으로 넘겨주고, 최종 처리 결과를 리턴한다.

예를들어 Functiton<Member, Address> 와 Function<Address, String>을 순차적으로 연결해서 Function<Member, String>을 생성한다고 가정해보자,

Function<Member, Address>는 매개값으로 제공되는 Member로부터 Address를 리턴한다. Function<Address, String> 은 매개값으로 제공되는 Address 로부터 String을 리턴한다. 

이 둘을 andThen()이나 compse()로 연결하면 Function<Member, Address>에서 리턴한 Address를 Function<Address, String>의 매개값으로 넘겨서 최종 String 타입을 리턴하는 Function<Member, String>을 생성해 낸다.

 

Function<Member, Address> + Function<Address, String> = Function<Member, String>

 

Address는 두 함수적 인터페이스 간의 전달 데이터이다. Address는 내부적으로 전달되기 때문에 최종 함수적 인터페이스의 형태는 입력 데이터가 Member, 출력 데이터가 String이 되는 Function<Member, String> 이 된다.

 

package Chapter14;

import java.util.function.Function;

public class FunctionAndThenComposeExample {

    public static void main(String[] args) {
        Function<Member, Address> functionA;
        Function<Address, String> functionB;
        Function<Member, String> functionAB;
        String city;

        functionA = m -> m.getAddress();
        functionB = a -> a.getCity();
        functionAB = functionA.andThen(functionB);
        city = functionAB.apply(new Member("John", "Doe", new Address("123 Main St.", "Anytown")));
        System.out.println(city);

        functionAB = functionB.compose(functionA);

        city = functionAB.apply(new Member("John", "Doe", new Address("123 Main St.", "Anytown")));
        System.out.println(city);

    }
}

 

모두 functionA 부터 실행하고 functionB를 나중에 실행한다.

 

and(), or(), negate() 디폴트 메소드 isEqual() 정적 메소드


predicate 종류의 함수적 인터페이스는 and(), or, negate() 디폴트 메소드를 가지고 있다. 

이 메소드들은 각각 논리 연산자인 &&, ||, !과  대응 된다고 볼수 있다.

and() 메소드는 두 predicate가 모두 true 를 리턴함면 최종 적으로 true 를 리턴하는 predicate를 생성한다.

or()는 두 predicate 중 하나만 true를 리턴하더라도 최종적으로 true를 리턴하는 predicate를 생성한다

negate()는 원래 predicate의 결과가 ture이면 false로, false 이면 true를 리턴하는 새로운 predicate를 생성한다.

다음은 and(), or() , negate() 디폴트 메소드를 제공하는 predicate 함수적 인터페이스들이다. 

종류 함수적인터페이스 and() or() negate()
predicate Predicate<T>
BiPredicate<T, U>
DoublePredicate
IntPredicate
LongPredicate

 

 

다음 예제는 2의 배수와 3의 배수를 조사하는 두 Predicat를 논리 연산한 새로운 Predicate를 생성한다

 

package Chapter14;

import java.util.function.IntPredicate;

public class PredicateAndOrNegateExample {

    public static void main(String[] args) {
        IntPredicate predicateA = a -> a % 2 == 0;

        IntPredicate predicateB = a -> a % 3 == 0;

        IntPredicate predicateC = a -> a % 5 == 0;

        IntPredicate predicateABC;
        boolean result;

//        and()

        predicateABC = predicateA.and(predicateB).and(predicateC);

        result = predicateABC.test(30);
        System.out.println("15 is divisible by 2, 3 and 5: " + result);

        //or()
        predicateABC = predicateA.or(predicateB).or(predicateC);
        result = predicateABC.test(5);
        System.out.println(
                "5 is divisible by 2, 3 or 5: " + result);

        //negate()
        predicateABC = predicateA.negate();
        result = predicateABC.test(5);
        System.out.println("5 is divisible by 2: " + result);


    }
}

 

Predicate<T> 함수적 인터페이스는 and(), or(), negate() 디폴트 메소드 이외에 isEqual() 정적 메소드를 추가로 제공한다.

isEqual() 메소드는 test() 매개값인 sourceObject 와 isEqual()의 매개값인 targetObject를 java.util.Objects 클래스의 equals() 매개값으로 제공하고, Object.equals(sourceObject, targetObject)의 리턴값을 얻어 새로운 Predicate<T>를 생성한다.

 

Predicate<Object> predicate = Predicate.isEqual(targetObject);

boolean result = predicate.test(sourceObject);

Objects.equals(sourceObject, targetObject) 실행

 

Objects.equals(sourceObject, targetObject)는 다음과 같은 리턴값을 제공한다.

 

sourceObject targetObject 리턴값
null null true
not null null false
null not null false
not null not null sourceObject.equals(targetObject)의 리턴값 

 

다음 예제는 두 문자열을 비교하기 위해 Predicate의 isEqual() 정적 메소드를 사용하였다. 

 

package Chapter14;

import java.util.function.Predicate;

public class PredicateIsEqualExample {

    public static void main(String[] args) {
        Predicate<String> predicate = Predicate.isEqual(null);

        System.out.println(predicate.test(null));
        System.out.println(predicate.test(""));

        Predicate<String> predicate2 = Predicate.isEqual("");
        System.out.println(predicate2.test(""));
        System.out.println(predicate2.test(null));


    }

}

 

minBy(), maxBy() 정적 메소드


BinaryOperator<T> 함수적 인터페이스는 minBy() 와 maxBy() 정적 메소드를 제공한다.

이 두 메소드는 매개값으로 제공되는 Comparator를 이용해서 최대 T 와 최소 T 를 얻는 BinaryOperator<T>를 리턴한다.

리턴 타입 정적 메소드 
BinaryOperator<T> minBy<Compator<? super T> compator>
BinaryOperator<T> maxBy<Compator<? super T> compator>

 

Comparator<T>는 다음과 같이 선언된 함수적 인터페이스이다. o1 과 o2를 비교해서 o1 이 작으면 음수를, 

o1과 o2가 동일하면 0을, o1이 크면 양수를 리턴하는 compare()메소드가 선언되어 있다. 

@FunctionalInterface
public interface Comparator<T> {
	public int compare(T o1, To2);
}

 

Comparator<T>를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다. 

(o1, o2 ) -> {...; return int 값};

 

만약 o1과 o2 가 int 타입이라면 다음과 같이 Integet.compare(int, int) 메소드를 이용 할 수 있다. 

Integer.compare()는 첫 번째 매개값이 두 번째 매개값보다 작으면 음수, 같으면 0, 크면 양수를 리턴한다. 

(o1, o2) -> Integer.compare(o1, o2);

 

다음 예제는 두 과일의 값을 비교해서 값이 낮거나 높은 과일을 얻어 낸다. 

package Chapter14;

import java.util.function.BinaryOperator;

public class OperatorMinByMaxByExample {
    public static void main(String[] args) {

        BinaryOperator<Fruit> binaryOperator = BinaryOperator.minBy((f1,f2) ->Integer.compare((f1.getPrice()),(f2.getPrice())));
        Fruit friut1 = binaryOperator.apply(new Fruit("Apple", 10), new Fruit("Orange", 20));
        System.out.println(friut1.getName());

        BinaryOperator<Fruit> binaryOperator1 = BinaryOperator.maxBy((f1,f2) ->Integer.compare((f1.getPrice()),(f2.getPrice())));
        Fruit friut2 = binaryOperator1.apply(new Fruit("Apple", 10), new Fruit("Orange", 20));
        System.out.println(friut2.getName());
    }
}

 

메소드 참조


메소드 참조(Method References)는 말 그대로 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내어, 람다식에서 불필요한 매개 변수를 제거하는 것이 목적이다. 람다식은 종종 기존 메소드를 단순히 호출만 하는 경우가 많다.

 

예를들어 두개의 값을 받아 큰 수를 리턴하는 Math 클래스의 max() 정적 메소드를 호출하는 람다식은 다음과 같다 

 

(left, right) -> Math.max(left, right);

 

람다식은 단순히 두 개의 값을 Math.max() 메소드의 매개값으로 전달하는 역할만 하기 때문에 다소 불편해 보인다. 

이 경우에는 다음과 같이 메소드 참조를 이용하면 매우 깔끔하게 처리할 수 있다. 

Math::max;

 

메소드 참조도 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 타겟타입인 인터페이스의 추상 메소드가 어떤 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라진다.

IntBinaryOperator 인터페이스는 두 개의 int 매개값을 받아 int 값을 리턴하므로 Math :: max 메소드를 참조를 대입할 수 있다. 

IntBinaryOperator operaotr = Math::max;

 

메소드 참조는 정적 또는 인스턴스 메소드를 참조할 수 있고, 생성자 참조도 가능하다. 지금부터 하나씩 살펴보기로 하자.

 

정적 메소드와 인스턴스 메소드 참조


정적(static) 메소드를 참조할 경우에는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메소드 이름을 기술하면 된다.

 

클래스::메소드

인스턴스 메소드일 경우에는 먼저 객체를 생성한 다음 참조 변수 뒤에 :: 기호를 붙이고 인스턴스 메소드 이름을 기술하면 된다.

참조변수::메소드

다음 예제는 Calculator 정적 및 인스턴스 메소드를 참조한다. 람다식이 메소드 참조로 대체되는 것을 기억해두자 

package Chapter14;

import java.util.function.IntBinaryOperator;

public class MethodReferenceExample {

    public static void main(String[] args) {
        IntBinaryOperator operator;

        // 정적 메소드 참조
        operator = (x,y) -> Calculator.staticMethod(x,y);
        System.out.println(operator.applyAsInt(10,20));

        operator = Calculator::staticMethod;
        System.out.println(operator.applyAsInt(10,20));
        // 인스턴스 메소드 참조
        Calculator c = new Calculator();
        operator = c::instanceMethod;
        System.out.println(operator.applyAsInt(10,20));

        operator = (x,y) -> c.instanceMethod(x,y);
        System.out.println(operator.applyAsInt(10,20));


    }

}

 

매개 변수의 메소드 참조


메소드는 람다식 외부의 클래스 멤버일 수도 있고, 람다식에서 제공되는 매개 변수의 멤버일 수도 있다.

이전 예제는 람다식 외부의 클래스 멤버인 메소드를 호출하였다. 그러나 다음과 같이 람다식 에서 제공되는 a 매개 변수의 메소드를 호출해서 b 매개 변수를 매개값으로 사용하는 경우도 있다. 

(a,b) -> {a.instanceMethod(b); }

 

이것을 메소드 참조로 표현하면 다음과 같다. a의 클래스 이름 뒤에 :: 기호를 붙이고 메소드 이름을 기술하면 된다.

작성 방법은 정적 메소드 참조와 동일하지만, a의 인스턴스 메소드가 참조되므로 전혀 다른 코드가 실행 된다. 

클래스::instanceMethod

다음 예제는 두 문자열이 대소문자와 상관없이 동일한 알파벳으로 구성되어 있는지 비교한다.

비교를 위해 사용된 메소드는 String의 인스턴스 메소드인 compareToIgnoreCase() 이다.

a.compareToIgnoreCase(b)로 호출 될때 사전순으로 a가 b보다 먼저 오면 음수를, 동일하면 0을, 나중에 오면 양수를 리턴한다. 사용된 함수적 인터페이스는 두 String 매개값을 받고 int 값을 리턴하는 ToIntBiFunction<String, String> 이다

 

package Chapter14;

import java.util.function.ToIntBiFunction;

public class ArgumentMethodReferencesExample {
    public static void main(String[] args) {
        ToIntBiFunction<String, String> function;

        function = (a,b) -> a.compareToIgnoreCase(b);
        print(function.applyAsInt("JAVA7", "JAVA8"));

        function = String::compareToIgnoreCase;
        print(function.applyAsInt("JAVA7", "JAVA8"));
    }

    public static void print(int order) {
        if(order < 0) {
            System.out.println("a comes before b");
        }
        else if(order == 0) {
            System.out.println("a and b are the same");
        }
        else {
            System.out.println("b comes before a");
        }
    }
}

 

생성자 참조


메소드 참조(method references)는 생성자 참조도 포함한다. 생성자를 참조한다는 것은 객체 생성을 의미한다.

단순히 메소드 호출로 구성된 람다식을 메소드 참조로 대치할 수 있듯이, 단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치 할 수있다. 

다음 코드를 보면 람다식은 단순히 객체 생성 후 리턴만 한다. 

 

(a,b)-> {return new 클래스(a,b)}

 

이 경우, 생성자 참조로 표현하면 다음과 같다. 클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술하면 된다.

생성자가 오버로딩되어 여러 개가 있을 경우, 컴파일러는 함수적 인터페이스의 추상메소드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아 실행한다.

만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생한다.

클래스::new

 

다음 예제는 생성자 참조를 이용해서 두 가지 방법으로 Member 객체를 생성한다. 하나는 Function<String, Member> 함수적 인터페이스의 Member apply(String) 메소드를 이용해서 Member 객체를 생성하였고,

다른 하나는 BiFunction<String, String, Member> 함수적 인터페이스의 Member apply(String, String) 메소드를 이용해서 Member 객체를 생성하였다.

생성자 참조는 두가지 방법 모두 동일하지만, 실행되는 Member생성자가 다름을 볼 수 있다. 

package Chapter14;

import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Function;

public class ConstructorReferenceExample {

    public static void main(String[] args) {
        Function<String, String> f = String::new;
        String s = f.apply("Hello");
        System.out.println(s);
    }
}

'Back-end > 이것이 자바다[신용권 한빛미디어]' 카테고리의 다른 글

List 컬렉션  (0) 2022.04.24
컬렉션 프레임 워크  (0) 2022.04.24
람다식  (0) 2022.04.06
[제네릭]  (0) 2022.02.12
스레드 풀  (0) 2021.12.12