람다 표현식(Lambda) 알고 사용하기 - 기본편

주리링·2022년 4월 29일
1

모던 자바 인 액션

목록 보기
2/3
post-thumbnail

람다를 제대로 모르고 사용해보기만 하다가, 모던 자바 인 액션을 보고 정리해야겠다는 생각을 들어서 정리 해본 글입니다 :)
이 글을 통해서 알아가셨으면 하는 것은 3가지입니다.

  • 람다 표현식이 어떤 것(WHAT)을 표현할 수 있는지
  • 람다를 어디에(WHERE) 사용하는지
  • 람다가 어떻게(HOW) 사용되고 동작하는지

람다 표현식이란?

메서드로 전달할 수 있는 익명 함수이며 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트 등을 가질 수 있다.

람다 표현식 구조

먼저 람다 표현식을 어떻게 구성되어 있는지 알아보자.

(o1, o2) -> o1.getWeight().compareTo(o2.getWeight());

람다식의 구조는 화살표로 나눌 수 있다.

화살표 이전은 파라미터 리스트, 화살표는 파라미터 리스트와 바디를 나눠준다.

그리고 화살표 이후는 람다의 바디메서드 코드블럭 내부처럼 동작하는 부분이다.

람다로 어떤 것을 표현할 수 있을까?

  1. boolean 표현식
(List<String> list) -> list.isEmpty()
  1. 객체 생성
() -> new Apple(10)
  1. 객체에서 소비
(Apple apple) -> {System.out.println(a.getWeight());}
  1. 객체에서 추출
(String s) -> s.length()
  1. 두 값을 조합
(int a, int b) -> a + b
  1. 두 객체 비교
(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을 주로 상속을 받아 부모 클래스의 메서드를 수정하고, 사용하고 싶을 때 사용한다.
상속을 받기 위해서는 클래스가 필요한데, 이 클래스에는 이름이 없으므로 익명 클래스라고 한다.

이처럼 상속받는 구현체의 이름이 없다. == 익명이라고 할 수 있다.

익명 함수

람다가 익명 함수인 이유는 함수형 인터페이스(하나의 추상 메서드만 가지는 인터페이스) 의 문맥에서 람다를 사용할 수 있기 때문이다.

그 이유는 여러가지가 있다.

  1. 기존부터 추상 메서드가 하나인 interface를 많이 사용해왔으므로, 개발자들의 거부감을 낮출 수 있었다.
  2. 타입 시스템을 복잡하게 만들지 않고, 람다식을 사용해, functional interface의 인스턴스로 변환한다면 큰 변화없이 적용할 수 있었다.
  3. 컴파일 시점에 구조적으로 람다식을 functional interface로 인식하고 치환할 수 있다

람다를 어떻게 사용할까?

람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달하여 사용할 수 있다.

예제를 보며 이해해보자!

  1. 이미 사용되고 있는 함수형 인터페이스 이용하여 동작 과정 살펴보기
@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 인스턴스 대신 람다식을 이용해서 추상 메서드 구현 부분을 클래스 생성 없이 전달할 수 있다.

  1. 직접 함수형 인터페이스 구현하며 적용하기
@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의 인스턴스를 받아, move를 할지 결정하는 클래스가 있다고 하자.

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
  1. 일단 C 코드와 같이 람다가 사용되는 코드부터 형식 검사가 시작된다.
  2. 람다가 쓰인 B 메서드의 파라미터의 함수형 인터페이스를 확인한다.
    이 때, T가 Apple로 대체된다.
  3. 확인한 함수형 인터페이스의 추상 메서드를 확인한다.
  4. 추상메서드의 파라미터와 리턴 타입이 람다식과 일치하는지 확인한다.
  5. 일치하다면 형식 검사가 완료된 것이다.

형식 추론

람다가 전달될 메서드의 파라미터나 람다가 할당되는 변수에서 기대되는 람다 표현식의 형식을 대상 형식이라고 한다.
그러므로 자바의 컴파일러는 대상 형식을 통해 파라미터의 타입을 알 수 있다.
그래서 아래와 같이 코드를 단순화 할 수 있다.

//형식 추론 전
Comparator<Apple> comparator = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
//형식 추론 후
Comparator<Apple> comparator = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

참고참고
익명 클래스보다는 람다를 사용하라

profile
코딩하는 감자

0개의 댓글