JAVA/이것이 자바다

Chapter .14-2 람다식

개념원리 2023. 12. 30. 10:59

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

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

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

 

Thread 생성자를 호출할 때 다음과 같이 람다식을 매개값으로 대입해도 된다.

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

 

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

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

 

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

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

 

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

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

 

 

14.5.1 Consumer 함수적 인터페이스

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

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

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

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

 

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

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

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

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

 

 

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

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

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

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

 

 

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

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

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

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

 

 

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

accpt() 메소드는 매개값으로 T 객체와 int 값 두 개를 가지기 때문에 람다식도 두 개의 매개 변수를 사용한다.

T가 String 타입이므로 람다식의 t 매개 변수 타입은 String이 되고, i 는 고정적으로 int 타입이 된다.

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

 

 

14.5.2 Supplier 함수적 인터페이스

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

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

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

 

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

get() 메소드가 매개값을 가지지 않으므로 람다식도 ( ) 를 사용한다.

람다식의 중괄호 { } 는 반드시 한개의 T객체를 리턴하도록 해야한다.

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

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

 

 

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

getAsInt() 메소드가 매개값을 가지지 않으므로 람다식도 ( ) 를 사용한다.

람다식의 중괄호 {} 는 반드시 int 값을 리턴하도록 해야 한다.

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

 

다음 예제는 주사위의 숫자를 랜덤하게 공급하는 IntSupplier 인터페이스를 타겟 타이븡로 하는 람다식이다.

import java.util.function.IntSupplier;

public class SupplierExample {
	public static void main(String[] args) {
		IntSupplier intSupplier = () -> {
			int num = (int) (Math.random() * 6) + 1;
			return num;
		};
		
		int num = intSupplier.getAsInt();
		System.out.println("눈의 수: " + num);
	}
}

 

 

14.5.3 Function 함수적 인터페이스

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

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

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

 

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> 인터페이스를 타켓 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.

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

 

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

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

 

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

 

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

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

public class FunctionExample1 {
	private static List<Student> list = Arrays.asList(
		new Student("홍길동", 90, 96),
		new Student("신용권", 95, 93)
	);
	
	public static void printString(Function<Student, String> function) {
		for(Student student : list) {
			System.out.print(function.apply(student) + " ");
		}
		System.out.println();
	}
	
	public static void printInt(ToIntFunction<Student> function) {
		for(Student student : list) {
			System.out.print(function.applyAsInt(student) + " ");
		}
		System.out.println();
	}
	
	public static void main(String[] args) {
		System.out.println("[학생 이름]");
		printString( t -> t.getName() );
		
		System.out.println("[영어 점수]");
		printInt( t -> t.getEnglishScore() );
		
		System.out.println("[수학 점수]");
		printInt( t -> t.getMathScore() );
	}
}
public class Student {
	private String name;
	private int englishScore;
	private int mathScore;
	
	public Student(String name, int englishScore, int mathScore) {
		this.name = name;
		this.englishScore = englishScore;
		this.mathScore = mathScore;
	}

	public String getName() { return name; }
	public int getEnglishScore() { return englishScore; }
	public int getMathScore() { return mathScore; }
}

 

 

다음 에제는 List에 저장된 학생 객체를 하나씩 꺼내서 영어 점수와 수학 점수의 평균값을 산출한다.

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

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

 

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

package chap14.src.sec05.exam04_function;

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("홍길동", 90, 96),
		new Student("신용권", 95, 93)
	);
	
	public static double avg(ToIntFunction<Student> function) {
		int sum = 0;
		for(Student student : list) {
			sum += function.applyAsInt(student);
		}
		double avg = (double) sum / list.size();
		return avg;
	}
	
	public static void main(String[] args) {
		double englishAvg = avg( s -> s.getEnglishScore() );
		System.out.println("영어 평균 점수: " + englishAvg);
		
		double mathAvg = avg( s -> s.getMathScore() );
		System.out.println("수학 평균 점수: " + mathAvg);
	}
}

 

 

 

14.5.4 Operator 함수적 인터페이스

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

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

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

 

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

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

 

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

다음 코드는 두 개의 int 를 연산해서 결과값으로 int를 리턴한다.

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

 

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

maxorMin() 메소드는 IntBinaryOperator 매개 변수를 가지고 있다.

 

따라서 maxOrMin() 메소드를 호출할 때 람다식을 사용 할 수 있다.

 

import java.util.function.IntBinaryOperator;

public class OperatorExample {
	private static int[] scores = { 92, 95, 87 };
	
	public static int maxOrMin(IntBinaryOperator operator) {
		int result = scores[0];
		for(int score : scores) {
			result = operator.applyAsInt(result, score);
		}
		return result;
	}
	
	public static void main(String[] args) {
		//최대값 얻기
		int max = maxOrMin(
			(a, b) -> {
				if(a>=b) return a;
				else return b;
			}
		);
		System.out.println("최대값: " + max);
		
		//최소값 얻기
		int min = maxOrMin(
			(a, b) -> {
				if(a<=b) return a;
				else return b;
			}
		);
		System.out.println("최소값: " + min);
	}
}

 

 

 

14.5.5 Predicate 함수적 인터페이스

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

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

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

 

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

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

 

그리고 test() 메소드의 리턴 타입이 boolean 이므로 ㄹ마다식 중괄호 { } 의 리턴값은 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() 메소드를 호출할 때 매개값으로 람다식을 사용 할 수 있다.

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

public class PredicateExample {
	private static List<Student> list = Arrays.asList(
			new Student("홍길동", "남자", 90),
			new Student("김순희", "여자", 90),
			new Student("감자바", "남자",  95),
			new Student("박한나", "여자", 92)
	);
		
	public static double avg(Predicate<Student> predicate) {
		int count = 0, sum = 0;
		for(Student student : list) {
			if(predicate.test(student)) {
				count++;
				sum += student.getScore();
			}
		}
		return (double) sum / count;
	}
		
	public static void main(String[] args) {
		double maleAvg = avg( t ->  t.getSex().equals("남자") );
		System.out.println("남자 평균 점수: " + maleAvg);
			
		double femaleAvg = avg( t ->  t.getSex().equals("여자") );
		System.out.println("여자 평균 점수: " + femaleAvg);
	}
}
public class Student {
	private String name;
	private String sex;
	private int score;
	
	public Student(String name, String sex, int score) {
		this.name = name;
		this.sex = sex;
		this.score = score;
	}

	public String getSex() { return sex; }
	public int getScore() { return score; }
}

 

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

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

여기서 함수적 인터페이스 성질이란 하나의 추상 메소드를 가지고 있고, 람다식으로 익명 구현 객체를 생성할 수 있는 것을 말한다.

 

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

 

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

andThen()과 compose() 디폴트 메소드는 두 개의 함수적 인터페이스를 순차적으로 연결하고, 첫 번째 처리 결과를 두 번째 매개값으로 제공해서 최종 결과값을 얻을 때 사용한다.

 

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

다음 코드를 보면서 andThen() 부터 살펴보자.

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

 

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

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

 

 

 

이번에는 compose()를 살펴보자.

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

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

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

 

 

 

다음은 andThen()과 compose() 디폴트 메소드를 제공하는 java.util.function 패키지의 함수적 인터페이스들이다.

 

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

 

 

Consumer의 순차적 연결

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

 

다음 예제는 Consumer<Member> 함수적 인터페이스 두 개를 순차적으로 연결해서 실행한다.

첫 번째 Consumer<Member>는 이름을 출력하고

두번째 Consumer<Member>는 아이디를 출력한다.

import java.util.function.Consumer;

public class ConsumerAndThenExample {
	public static void main(String[] args) {
		Consumer<Member> consumerA = (m) -> { 
			System.out.println("consumerA: " + m.getName()); 
		};
		
		Consumer<Member> consumerB = (m) -> { 
			System.out.println("consumerB: " + m.getId()); 
		};
		
		Consumer<Member> consumerAB = consumerA.andThen(consumerB);
		consumerAB.accept(new Member("홍길동", "hong", null));
	}
}

 

public class Member {
	private String name;
	private String id;
	private Address address;
	
	public Member(String name, String id, Address address) {
		this.name = name;
		this.id = id;
		this.address = address;
	}

	public String getName() { return name; }
	public String getId() { return id; }
	public Address getAddress() { return address; }
}
public class Address {
    private String country;
    private String city;
    
    public Address(String country, String city) {
    	this.country = country;
    	this.city = city;
    }
    
    public String getCountry() { return country; }
	public String getCity() { return city; }
}

 

 

Function의 순차적 연결

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

 

예를들어 Function<Member, Address>와 Function<Address,String> 을 순차적으로 연결해서

Function <Member,String>을 생성한다고 가정 해보자

 

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

 

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

 

다음 그림을 보면서 이해해보자.

 

Address는 두 ㅎ마수적 인터페이스의 간의 전달 데이터이다.

 

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

 

다음 예제는 Member 객체의 필드인 Address에서 city 정보를 얻어내기 위해 두 Function 함수적 인터페이스를 andThen()과 compose()를 이용해서 순차적으로 연결했다.

 

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("홍길동", "hong", new Address("한국", "서울"))
		);
		System.out.println("거주 도시: " + city);
		
		functionAB = functionB.compose(functionA);
		city = functionAB.apply(
			new Member("홍길동", "hong", new Address("한국", "서울"))
		);
		System.out.println("거주 도시: " + city);
	}
}

 

예제 코드에서 andThen() 메소드를 호출한 함수적 인터페이스는 functionA이고, compose() 메소드를 호출한 함숮거 인터페이스는 functionB 이다. 

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

 

 

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

 

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

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

 

  • and() 메소드는 두 Predicate가 모두 True를 리턴하면 최종적으로 true를 리턴하는 Predicate를 생성한다.
  • or() 는 두 Predicate 중 하나만 true를 리턴하더라도 최종적으로 true를 리턴하는 Predicate를 생성한다.
  • negate() 는 원래 Predicate의 결과가 true이면 false로, false이면 true를 리턴하는 새로운 Predicate를 생성한다.

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

 

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

 

import java.util.function.IntPredicate;

public class PredicateAndOrNegateExample {
	public static void main(String[] args) {
		//2의 배수 검사
		IntPredicate predicateA = a -> a%2 == 0;
		
		//3의 배수 검사
		IntPredicate predicateB = (a) -> a%3 == 0;
		
		IntPredicate predicateAB;
		boolean result;
		
		//and()
		predicateAB = predicateA.and(predicateB);
		result = predicateAB.test(9);
		System.out.println("9는 2와 3의 배수입니까? " + result);
		
		//or()
		predicateAB = predicateA.or(predicateB);
		result = predicateAB.test(9);
		System.out.println("9는 2 또는 3의 배수입니까? " + result);
		
		//negate()
		predicateAB = predicateA.negate();
		result = predicateAB.test(9);
		System.out.println("9는 홀수입니까? " + result);
	}
}

 

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

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

 

 

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

 

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

import java.util.function.Predicate;

public class PredicateIsEqualExample {
	public static void main(String[] args) {
		Predicate<String> predicate;
		
		predicate = Predicate.isEqual(null);
		System.out.println("null, null: " + predicate.test(null));
		
		predicate = Predicate.isEqual("Java8");
		System.out.println("null, Java8: " + predicate.test(null));
		
		predicate = Predicate.isEqual(null);
		System.out.println("Java8, null: " + predicate.test("Java8"));
		
		predicate = Predicate.isEqual("Java8");
		System.out.println("Java8, Java8: " + predicate.test("Java8"));
		
		predicate = Predicate.isEqual("Java8");
		System.out.println("Java7, Java8: " + predicate.test("Java7"));
	}
}

 

 

 

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

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

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

 

Comparator<T>는 다음과 같이 선언된 함수적 인터페이스이다.

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

 

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

 

Comparetor<T>를 타겟 타입으로하는 람다식은 아래와 같이 작성할 수 있다.

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

 

 

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

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

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

 

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

import java.util.function.BinaryOperator;

public class OperatorMinByMaxByExample {
	public static void main(String[] args) {
		BinaryOperator<Fruit> binaryOperator;
		Fruit fruit;
		
		binaryOperator = BinaryOperator.minBy((f1,f2)->Integer.compare(f1.price, f2.price));
		fruit = binaryOperator.apply(new Fruit("딸기", 6000), new Fruit("수박", 10000));
		System.out.println(fruit.name);
		
		binaryOperator = BinaryOperator.maxBy((f1,f2)->Integer.compare(f1.price, f2.price));
		fruit = binaryOperator.apply(new Fruit("딸기", 6000), new Fruit("수박", 10000));
		System.out.println(fruit.name);
	}
}

 

 

 

 

14.6 메소드 참조

메소드 참조(Method References)는 말 그대로 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내어, 람다식에서 불필요한 매개 변수를 제거하는 것이 목적이다.

 

람다식은 종종 기존 메소드를 단순히 호출만하는 경우가 많다.

 

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

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

 

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

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

 

Math :: max; //메소드 참조

 

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

 

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

IntBinaryOperator operator = Math :: max;  //메소드 참조

 

메소드 참조는 정적 메소드 또는 인스턴스 메소드를 참조할 수 있고, 생성자 참조도 가능하다.

지금부터 하나씩 살펴보자.

 

 

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

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

클래스 :: 메소드

 

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

 

참조변수 :: 메소드

 

다음 에제는 Calculator의 정적 및 인스턴스 메소드를 참조한다.

람다식이 메소드 참조로 대체되는 것을 기억해두자.

public class Calculator {
	public static int staticMethod(int x, int y) {
		return x + y;
	}
	
	public int instanceMethod(int x, int y) {
		return x + y;
	}
}
import java.util.function.IntBinaryOperator;

public class MethodReferencesExample {
	public static void main(String[] args) {
		IntBinaryOperator operator;
		
		//정적 메소드 참조 ---------------------------------
		operator = (x, y) -> Calculator.staticMethod(x, y);
		System.out.println("결과1: " + operator.applyAsInt(1, 2));

		operator = Calculator :: staticMethod;
		System.out.println("결과2: " +  operator.applyAsInt(3, 4));
		
		//인스턴스 메소드 참조 ---------------------------
		Calculator obj = new Calculator();
		operator = (x, y) -> obj.instanceMethod(x, y);
		System.out.println("결과3: " + operator.applyAsInt(5, 6));

		operator = obj :: instanceMethod;
		System.out.println("결과4: " + operator.applyAsInt(7, 8));
	}
}

 

 

14.6.2 매개 변수의 메소드 참조

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

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

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

 

이것을 메소드 참조로 표현하면 다음과 같다.

a 클래스 이름 뒤에 :: 기호를 붙이고 메소드 이름을 기술하면 된다.

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

클래스 :: instanceMethod

 

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

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

a.compareToIgnoreCase(b)로 호출될 떄 사전 순으로 a 가 b 보다 먼저 오면 음수를, 동일하면 0, 나중에 오면 양수를 리턴한다.

 

사용된 함수적 인터페이스는 두 String 매개값을 받고 int 값을 리턴하는 ToIntBiFunction<String,String> 이다.

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("Java8", "JAVA8"));
		
		function = String :: compareToIgnoreCase;
		print(function.applyAsInt("Java8", "JAVA8"));
	}
	
	public static void print(int order) {
		if(order<0) { System.out.println("사전순으로 먼저 옵니다."); }
		else if(order == 0) { System.out.println("동일한 문자열입니다."); }
		else { System.out.println("사전순으로 나중에 옵니다."); }
	}
}

 

 

14.6.3 생성자 참조

 

메소드 참조(method reference)는 생성자 참조도 포함된다.

생성자를 참조한다는 것은 객체 생성을 의미한다.

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

 

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

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

 

이 경우, 생성자 참조로 표현하면 다음과 같다.

클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술하면 된다.

 

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

 

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

클래스 :: new

 

다음 예제는 생성잘 참조를 이용해서 두 가지 방법으로 Member 객체를 생성한다.

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

 

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

 

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

public class ConstructorReferencesExample {
	public static void main(String[] args) {
		Function<String, Member> function1 = Member :: new;
		Member member1 = function1.apply("angel");
		
		BiFunction<String, String, Member> function2 = Member :: new;
		Member member2 = function2.apply("신천사", "angel");
	}
}
public class Member {
	private String name;
	private String id;
	
	public Member() {
		System.out.println("Member() 실행");
	}
	public Member(String id) { 
		System.out.println("Member(String id) 실행");
		this.id = id; 
	}
	public Member(String name, String id) {
		System.out.println("Member(String name, String id)");
		this.name = name;
		this.id = id;
	}
	
	public String getId() { return id; }
}