람다를 제대로 모르고 사용해보기만 하다가, 모던 자바 인 액션을 보고 정리해야겠다는 생각을 들어서 정리 해본 글입니다 :)
이 글을 통해서 알아가셨으면 하는 것은 3가지입니다.
- 람다 표현식이 어떤 것(WHAT)을 표현할 수 있는지
- 람다를 어디에(WHERE) 사용하는지
- 람다가 어떻게(HOW) 사용되고 동작하는지
메서드로 전달할 수 있는 익명 함수이며 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트 등을 가질 수 있다.
먼저 람다 표현식을 어떻게 구성되어 있는지 알아보자.
(o1, o2) -> o1.getWeight().compareTo(o2.getWeight());
람다식의 구조는 화살표로 나눌 수 있다.
화살표 이전은 파라미터 리스트, 화살표는 파라미터 리스트와 바디를 나눠준다.
그리고 화살표 이후는 람다의 바디로 메서드 코드블럭 내부처럼 동작하는 부분이다.
(List<String> list) -> list.isEmpty()
() -> new Apple(10)
(Apple apple) -> {System.out.println(a.getWeight());}
(String s) -> s.length()
(int a, int b) -> a + b
(Apple apple1, Apple apple2) -> a1.getWeight.compareTo(a2.getWeight())
위에 람다 표현식의 정의를 메서드로 전달할 수 있는 익명 함수라고 했다.
메서드는 무슨 말인지 알겠고, 함수는 input output이 있는 느낌인데 익명 함수란 무엇일까?
일단 익명에 대해서 알아보자.
public interface Comparator<T> {
int compare(T o1, T o2);
}
Comparator<Apple> byWeight = new Comparator<Apple>() { //2
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
};
2번 코드 블럭을 보면 인터페이스는 인스턴스를 가질 수 없는데 어떻게 생성자 생긴게 사용하고 있을까?
언뜻 보면 byWeight란 인스턴스가 생성자를 통해 생성하는 것 같지만, 아래 많은 코드 블럭을 볼 수 있다.
그리고 @Override라는 어노테이션을 확인할 수 있다.
우리는 @Override을 주로 상속을 받아 부모 클래스의 메서드를 수정하고, 사용하고 싶을 때 사용한다.
상속을 받기 위해서는 클래스가 필요한데, 이 클래스에는 이름이 없으므로 익명 클래스라고 한다.
이처럼 상속받는 구현체의 이름이 없다. == 익명이라고 할 수 있다.
람다가 익명 함수인 이유는 함수형 인터페이스(하나의 추상 메서드만 가지는 인터페이스) 의 문맥에서 람다를 사용할 수 있기 때문이다.
그 이유는 여러가지가 있다.
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달하여 사용할 수 있다.
예제를 보며 이해해보자!
@FunctionalInterface
public interface Runnable {
void run();
}
public Thread(Runnable target) { // Thread class의 생성자
this(null, target, "Thread-" + nextThreadNum(), 0);
}
Thread class는 생성자에서 Runnable이라는 함수형 인터페이스의 인스턴스를 받아 하나의 Thread에서 실행을 한다.
Thread thread = new Thread(() -> System.out.println("judy"));
thread.start();
동작 과정을 보면, Runnable의 추상 메서드 run의 파라미터는 없고 return type가 void이다.
그러므로 파라미터 리스트를 똑같이 빈 값으로 맞춘다.
body의 return type도 똑같이 void로 구현하면서 만약 Runnable의 인스턴스를 만들고 run 메서드를 구현한다면 그 내부에 들어갈 로직을 바디에 넣어준다.
그리고 thread.start()가 실행되면, 이 start 메서드 내부에 run 메서드가 있어서 바디의 로직이 실행된다.
그래서 Runnable 인스턴스 대신 람다식을 이용해서 추상 메서드 구현 부분을 클래스 생성 없이 전달할 수 있다.
@FunctionalInterface
public interface MoveGenerator {
boolean isMovable();
}
public class RacingCar {
private int position = 0;
private final MoveGenerator moveGenerator;
public RacingCar(final MoveGenerator moveGenerator) {
this.moveGenerator = moveGenerator;
}
public void decideMove() {
if (moveGenerator.isMovable()) {
move();
}
}
private void move() {
position++;
}
}
MoveGenerator의 추상 메서드를 보면 파라미터가 없고, retrun type는 boolean이다.
그렇다면 아래와 같이 MoveGenerator의 인스턴스를 람다식으로 전달하여 RacingCar를 생성할 수 있다.
RacingCar car = new RacingCar(() -> true);
파라미터가 없으므로 화살표 이전의 파라미터 리스트에 아무 것도 없다.
return type가 boolean이므로 화살표 이후에는 추상 메서드에서 실행할 동작인 true or false를 반환한다.
@Test
void testMovableCar() {
RacingCar car = new RacingCar(() -> true);
car.decideMove();
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void testNonMovableCar() {
RacingCar car = new RacingCar(() -> false);
car.decideMove();
assertThat(car.getPosition()).isEqualTo(0);
}
위와 같은 테스트들이 통과하는 것을 알 수 있다.
결국 우리는 람다로 함수형 인터페이스의 인스턴스를 만들어서 사용하는 것이다.
하지만 람다식만 보고는 어떤 함수형 인터페이스의 인스턴스인지 알 수 없는데, 어떻게 인스턴스를 만들 수 있는걸까?
결론부터 말하자면, 자바에서는 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수로 형식 검사를 해서 어떤 함수형 인터페이스인지 알 수 있다.
예시를 통해 형식 검사를 하는 과정을 알아보자
@FunctionalInterface
public interface Predicate<T> {//A
boolean test(T t);
}
public List<Apple> filterApples(final Predicate<Apple> p) {//B
List<Apple> result = new ArrayList<>();
for (Apple apple : apples) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
inventory.filterApples((Apple a) -> a.getWeight() > 20);//C
람다가 전달될 메서드의 파라미터나 람다가 할당되는 변수에서 기대되는 람다 표현식의 형식을 대상 형식이라고 한다.
그러므로 자바의 컴파일러는 대상 형식을 통해 파라미터의 타입을 알 수 있다.
그래서 아래와 같이 코드를 단순화 할 수 있다.
//형식 추론 전
Comparator<Apple> comparator = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
//형식 추론 후
Comparator<Apple> comparator = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
참고참고
익명 클래스보다는 람다를 사용하라