Skip to main content

[Java] 람다식과 함수형 인터페이스

·4 mins· loading · loading ·
CS Java
Soeun Uhm
Author
Soeun Uhm
problem-solving engineer, talented in grit.
Table of Contents

Lambda 식
#

JDK1.8 부터 람다식이 추가되었다. 람다식은 메서드를 하나의 식(expression)으로 표현한 것이다. 메서드를 람다식으로 표현하면, 메서드의 이름과 반환값이 사라진다. 그래서 람다식을 익명 함수(annonymous function) 이라고도 한다.

// 더하는 메서드
int sum(int a, int b){
	return a+b;
}

// 람다식으로 표현
(int a, int b) -> {a+b}

함수형 인터페이스(Functional Interface)
#

자바에서 모든 메서드는 클래스 내에 포함되어야 한다. 그렇다면 람다식은 어떤 클래스에 포함되어 있을까? 사실 람다식이 메서드와 동등한 것이 아니라, 익명 클래스의 객체와 동일하다.

(int a, int b) -> a > b ? a : b
// 위와 동등
new Object(){
	int max(int a, int b){
		return a > b ? a : b
	}
}

그렇다면 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까? 참조변수가 있어야 객체의 메서드를 호출할 수 있으므로, 이 익명 객체의 주소를 f 라는 참조변수에 저장하자.

타입 f = (int a, int b) -> a > b ? a : b;

f 는 참조변수이므로, 타입에는 클래스 또는 인터페이스가 가능하다. 그리고 람다식과 동등한 메서드가 정의되어 있어야 한다. 그래야 참조변수로 익명 객체의 메서드를 호출할 수 있다.

interface MyFunction(){
	public abstract int max(int a, int b);
}
MyFunction f = new MyFunction() {
	public int max(int a, int b){
		return a > b ? a : b;
	}
}
MyFunction f = (int a, int b) -> a > b ? a : b; // 익명 객체 MyFunction 을 람다식으로 대체
int big = f.max(5,3); // 익명 객체의 메서드 호출

이렇게 하나의 추상 메서드(abstract method) 만 선언된 인터페이스를 함수형 인터페이스(Functional Interface) 이라고 한다.

@FunctionalInterface
interface MyFunction{
	public abstract int max(int a, int b);
}

함수형 인터페이스는 단 하나의 추상 메서드만 정의되어 있어야 한다. 그래야 람다식과 객체의 메서드가 1:1 로 연결되기 때문이다.

java.util.function 패키지
#

java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 두었다.

함수형 인터페이스메서드설명
java.lang.Runnablevoid run()매개변수X, 반환값X
Supplier<T>T get() -> T매개변수X, 반환값 O
Consumer<T>void accept(T t)매개변수O, 반환값 X
Funtion<T,R>R apply(T t) -> R일반적인 함수처럼 매개변수를 받아 결과를 반환
Predicate<T>boolean test(T t)조건식을 표현하는데 사용.
매개변수는 하나, 반환 타입은 boolean

자세히 코드로 한번 더 살펴보자.

Supplier
#

Supplier<T>

  • Supplier 는 어떠한 값을 반환한다.
  • Supplier 는 필요할 때만 실행되기 때문에, produce 하기에는 비용이 들지만 몇몇 상황에서만 필요할 때 Supplier 로 전달되어 그때 실행된다. 따라서 필요 시점에만 해당 값이나 객체를 생성할 수 있어서, 불필요한 자원 사용을 줄일 수 있다. -> Lazy Evaluation
image

Supplier 는 오직 하나의 method get() 만 있다.

public class Company{

	void createAccount(Employee e, Supplier<String> errorMsg){
		if (e.getEmail() == null){
			System.out.println(errorMsg.get());
		}
		// create Account .. 
	}

	void createAccount(){
		createAccount(new Employee, () -> "Invalid employee at" + LocalDateTime.now());
		// Counld be expensive
		// Or just something you want to do something later - Like get the time when the error really happens
		// ...both benefits of deferred execution
	}
	
}

어떠한 예외를 처리할 때도 Supplier 를 사용할 수 있다. 예를 들어, 입력을 받고 그 입력이 유효한 입력이 아닐 때 발생하는 예외를 잡고 재입력을 받게 하고 싶다고 하자. 아래 메서드는 매개변수로 Supplier<T> 를 받는다.

private static <T> T executeWithExceptionHandle(final Supplier<T> supplier) {  
    while (true) {  
        try {  
            return supplier.get();  
        } catch (IllegalArgumentException e) {  
            System.out.println(e.getMessage());  
        }  
    }  
}

실제 예외가 발생하는 입력을 받는 메서드에서는 아래와 같이 표현할 수 있다. Date.of(inputDate) 를 리턴하는 과정에서 여러 가지 비즈니스 로직 관련된 예외가 throw 될 수 있다.

private Date getDate() {  
    return executeWithExceptionHandle(() -> {  
        int inputDate = InputView.readDate();  
        return Date.of(inputDate);  
    });  
}

getDate() 안에서는 내부적으로 람다식을 사용한다. Supplier 는 매개변수가 필요없고, 반환값이 있다고 했다. 여기서 반환값은 에러가 발생했을 시 IllegalArgumentException 이 된다.

executeWithExceptionHandle 에서는, supplier.get() 이 더 이상 IllegalArgumentException 을 반환하지 않을 때까지 실행된다.

Function
#

Function<T,R>

  • Function<T,R> : T = type of input, R = type of result
  • Function<String, Integer> : maps String to Integer

image

public class Company{
	Employee generateEmployeeId(Person p, Function<Person, String> mapToId){
		Employee e = new Employee();
		e.setEmail(p.getEmail());
		e.setEmployeeId(mapToId.apply(p)); // apply() 에서 아래 선언한 실제 Function 코드 실행
	}

	private void AppleGenerateEmployee(){
		generageEmployeeId(p, pp -> pp.gerEmail().toUpperCase()); // 코드 선언
	}

	private void SamsungGenerateEmployee(){
		generageEmployeeId(p, pp -> pp.gerEmail() + pp.getName());
	}
}

Consumer
#

Consumer<T>

  • 매개변수를 받고, 그것을 가지고 어떤 동작을 수행한다.
  • 아무것도 반환하지 않는다. image
public class Company{
	// 위에 이어서..

	void logEmployee(Employee e, Consumer<Employee> logger){
		System.out.println("Printing Employee log");
		logger.accept(e);
	}

	// call the method
	void logInDetail(){
		logEmployee(new Employee(), e -> {
			System.out.println(e.getEmail());
			System.out.println(e.getEmployeeId());
		})
	}

	void logLight(){
		logEmployee(new Employee(), e -> System.out.println(e.getEmail()));
	}
	
}

Reference
#

Related

[Java] 생성자(Constructor)
·2 mins· loading · loading
CS Java
인스턴스 초기화 메서드인 생성자
[Java] 클래스 메서드 vs. 인스턴스 메서드
·2 mins· loading · loading
CS Java
static method vs. instance method 는 언제 써야 할까?
[Java] 클래스 변수 vs. 인스턴스 변수 vs. 로컬 변수
·3 mins· loading · loading
CS Java
Java 3가지 변수의 종류