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

신현호·2022년 3월 18일
0

Java

목록 보기
1/1

간결하며 이해하기 쉬운 코드가 각광받음에 따라 객체지향 언어인 자바에서도 8버전(1.8버전)부터 화살표 함수, 즉 람다식을 지원하기 시작했다.

아래 코드는 id로 DB에서 값을 조회하여 만약 존재한다면 해당 객체의 이름을 출력하는 함수를 두 가지 방식으로 구현한 것이다. (다소 비현실적인 형태이지만 양해바람...)

public class Test {

	class Student {
		private Long id;
		private String name;

		public String getName() {
			return this.name;
		}
	}

	Optional<Student> findById(Long id) {
		// DB의 Student 객체가 담긴 테이블에서 id로 검색 후 검색된 녀석을 Student 객체로 변환하여 반납
		// 못찾으면 예외를 발생시키므로 이 함수를 사용하는 곳에서 예외처리를 해주어야 함
		// 구현은 생략
	}

	void DB에서_Student조회후_name출력_1(Long id) {
		Student student = findById(id).못찾으면이거반납(null);
		if (student != null) {
			System.out.println(student.getName());
		}
	}

	void DB에서_Student조회후_name출력_2(Long id) {
		findById(id).ifPresent(찾은거 -> System.out.println(찾은거.getName()));
	}
}

첫 번째 함수도 못 알아볼 정도는 아니지만 두 번째 함수가 더 마음에 든다. (아님 말구)

이제 두 번째 함수와 같은 코드를 쓸 수 있게 해주는 함수형 인터페이스에 대해 알아보자.

함수형 인터페이스(functional interface)

함수형 인터페이스는 추상메소드를 단 하나만 가지고 있는 인터페이스를 말한다.

그런데 이 정의만 가지고는 함수형 인터페이스가 도대체 뭘 하기 위한 녀석인지 감이 오지 않는다.

왜 추상메소드를 단 하나만 가지고 있는 인터페이스를 특정하여 '함수형 인터페이스'라는 이름까지 붙여주었는지 이해하려면 아무래도 추가 설명이 필요하다.

하고 싶은 일

  • 함수의 파라미터로 람다식을 전달했을 때 자동으로 익명 객체 만들기

해결방안

  • 난 함수A의 파라미터 자리에 람다식 하나만 줄테니 자바 네가 알아서 익명 객체 만들어서 대신 집어넣어.
    대신 함수A는 파라미터로 추상메소드가 하나뿐인 인터페이스를 받으라고 할게.
    거기에는 추상메소드가 하나뿐이니까 그 자리에 내가 준 람다식을 넣어서 익명 객체를 만들면 되겠지?

위 내용을 통해 대략적인 감을 잡고 더 구체적인 내용으로 넘어가보자.

자바는 객체지향 언어(매우매우 형식적인 언어)이다.

따라서 언어를 사용함에 있어 꽤 많은 제약이 따르는데, 여기서 주목할 것은 함수의 파라미터로 '일급 객체' 만 받을 수 있다는 사실이다.

'일급 객체'가 무엇이냐 물으면 필자도 잘 모르니 넘어가고, 지금 중요한 사실은 자바에서 함수, 즉 메소드는 일급 객체가 아니라는 사실이다. (다른 언어에서는 메소드가 일급 객체일 수도 있다)

그러면 위 코드의

~~~.ifPresent(찾은거 -> System.out.println(찾은거.getName()));

에서, ifPresent가 파라미터로 받은 람다식은 마치 메소드처럼 생겼지만 메소드가 아니라는 말이 된다.

실제로 ifPresent가 어떻게 구현되어 있는지 살펴보면 아래와 같다.

public final class Optional<T> {

	// Optional에 들어있을 것이라고 여겨지는 값
	// 위 예제처럼 DB에서 무엇인가를 조회했다면 그 값이 value로 전달. 만약 찾지 못했다면 null이 채워짐
	// 아래 두 생성자 참고
	private final T value;

	private Optional() { this.value = null; }
	private Optional() { this.value = Objects.requireNonNull(value); }

	public void ifPresent(Consumer<? super T> action) {
		if (value != null) {
			action.accept(value);
		}
	}

	// 여러가지 함수들...

}

위 코드에서 볼 수 있듯이 ifPresent는 파라미터로 Consumer라는 녀석을 받고 있다.

Consumer는 함수형 인터페이스로, 구현되지 않은 단 하나의 추상 메소드 accept를 가진다.

자바는 컴파일 시(정확하지 않음... 원문에서는 '람다식이 평가될 때') ifPresent의 파라미터로 전달된 람다식을 accept 자리에 넣어 Consumer를 구현한 익명 객체를 만든 후, 그 객체를 ifPresent의 파라미터 자리에 집어넣는다.

위 코드에서 ifPresent는 파라미터로 받은 Consumer 타입의 action 이라는 객체에서 accept 메소드를 꺼내 쓰고 있다는 것을 확인할 수 있다.

다시 처음 코드로 돌아가보자.

// findById(id)는 Optional<Student> 형
findById(id).ifPresent(찾은거 -> System.out.println(찾은거.getName()));

자바는 ifPresent의 파라미터로 전달한

찾은거 → System.out.println(찾은거.getName())

를 이용하여 Consumer<? super Student> 형의 익명 객체를 만든다.

우리가 전달한 람다식은 이 객체의 accept 라는 메소드가 되어 ifPresent 내부에서 호출된다.

이제 Consumer의 내부를 살펴보자.

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

(맨 위 @FunctionalInterface 어노테이션은 휴먼에러를 방지하기 위한 어노테이션으로, 이 어노테이션이 달린 인터페이스가 가진 추상메소드가 한 개가 아닐 경우 컴파일 에러가 발생된다)

(default 메소드는 여기서는 설명하지 않는다)

Consumer는 '소비하는 자'라는 이름답게 추상메소드 accept가 파라미터를 받지만 반환형은 void이다.

만약 파라미터를 받아서 무언가를 반환하고 싶거나, 파라미터를 받지 않고 싶다면 Consumer가 아닌 다른 함수형 인터페이스를 사용해야 한다.

다음은 자바에서 지원하는 대표적 함수형 인터페이스들이다.

입력 → 출력추상메소드
ConsumerT → voidvoid accept(T t)
FunctionT → RR apply(T t)
PredicateT → booleanboolean test(T t)
Supplier( ) → TT get()
Runnable( ) → voidvoid run()
Comparator(T, T) → intint compare(T o1, T o2)

이외에도 자바가 제공하는 함수형 인터페이스들이 더 있고 필요에 따라 사용자가 직접 만들어 사용할 수도 있다.

다만 위 목록에 있는 녀석들 만으로도 웬만한 일은 전부 해결할 수 있다.

끄읕_!

문제

1. 자바 스트림의 map, filter 메소드는 각각 파라미터로 위 목록에 있는 함수형 인터페이스중 하나를 받는다. 각 함수가 파라미터로 어떤 함수형 인터페이스를 받을지 추론하여 말하고 그 이유를 설명하시오.

힌트 _ map, filter 사용 예시

// 각각의 student를 student의 name으로 변경
~~~.map(student -> student.getName());

// student 중 점수가 80 미만인 친구들만 남기기
~~~.filter(student -> student.getScore() < 80);

2. 다음을 읽고 ${ㅁㅁㅁ}, ${ㅇㅇㅇ} 안에 들어갈 코드를 작성하시오.

class Robot {
	private String name;
	private String nickname;
	private int weight;
	private int price;

	public Robot(String name, String nickname, int weight, int price) {
		this.name = name;
		this.nickname = nickname;
		this.weight = weight;
		this.price = price;
	}
}

${ㅁㅁㅁ}<Robot> makeRobot = () -> new Robot(랜덤문자열, 랜덤문자열, 랜덤양의정수, 랜덤양의정수);
List<Robot> robots = new ArrayList<>();
for (int i = 0 ; i < 100 ; i++) {
	robots.add(makeRobot.${ㅇㅇㅇ});
}
profile
수학요정니모

0개의 댓글