람다란 무엇인가?
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.
람다라는 용어는 람다 미적분학 학계에서 개발한 시스템에서 유래했다.
람다 표현식에는 이름은 없지만 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다.
람다의 특징을 하나씩 살펴보자.
익명
보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구현해야 할 코드에 대한 걱정거리가 줄어든다.
함수
람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
전달
람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
간결성
익명 클래스처럼 많은 코드를 구현할 필요가 없다.
아래 기존 코드와 람다 표현식으로 구현한 메서드를 비교해보자.
// 기존 코드
Comparator<Product> byWeight = new Comparator<Product>() {
public int compare(Product p1, Product p2) {
return p1.getWeight().compareTo(p2.getWeight());
}
}
// 람다 표현식
Comparator<Product> byWeight =
(Product p1, Product p2) -> p1.getWeight().compareTo(p2.getWeight());
람다 표현식의 코드가 훨씬 간단해졌다. 이처럼 코드를 구축하는 과정에서 자질구레한 코드를 제거할 수 있고, 가독성 또한 높아진다.
위 람다 표현식을 설명하자면
(Product p1, Product p2) -> p1.getWeight().compareTo(p2.getWeight());
람다 파라미터 리스트, 화살표, 람다 바디 세 부분으로 이루어진다.
- 람다 파라미터 리스트 : Comparator의 compare 메서드 파라미터
- 화살표 : 화살표는 람다 파라미터 리스트와 바디를 구분한다.
- 람다 바디 : 두 상품의 무게를 비교한다. 람다의 반환값에 해당하는 표현식이다.
Java 8에서 지원하는 람다 표현식을 살펴보자.
return 문 생략
람다 표현식에는 return이 함축되어 있으므로 return 문을 명식적으로 사용하지 않아도 된다.
(String s) -> s.length()
(Product p) -> p.getWeight() > 150
여러행 문장 포함
아래 예제에서 볼 수 있듯이 람다 표현식은 여러 행의 문장을 포함할 수 있다.
(int x, int y) -> {
System.out.println("Result: ");
System.out.println(x + y);
}
여러행 문장 포함 후 반환
람다 표현식은 여러 행의 문장을 실행한 후 반환할 수 있다.
(int x, int y) -> {
int result = x + y;
System.out.println("Result: ");
System.out.println(result);
return result;
}
어디에, 어떻게 람다를 사용할까?
그래서 정확히 어디에서 람다를 사용할 수 있다는 건가? 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.
함수형 인터페이스? 그게 뭔디?
함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스다.
// java.util.Comparator
public interface Caparator<T> {
int compare(T o1, T o2);
}
// java.lang.Runnable
public interface Runnable {
void run();
}
// java.util.concurrent.Callable
public interface Callable<V> {
V call() throws Exception;
}
위에 자바에서 자공하는 인터페이스들인데, 함수형 인터페이스의 몇몇 예시이다.
디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스인다.
이제 함수형 인터페이스가 무엇인지 감이 잡히는가?
@FunctionalInterface 어노테이션
@FunctionalInterface는 함수형 인터페이스임을 가리키는 어노테이션이다.
@FunctionalInterface으로 인터페이스를 선언했지만 실제로 함수형 인터페이스가 아니라면(추상 메서드가 1개 이상 등) 컴파일러가 에러를 발생시킨다.
함수형 인터페이스로 뭘 할 수 있을까?
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다. 함수형 인터페이스보다는 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있다.
Runnable r1 = () -> System.out.println("Hello World1"); // 람다 사용
Runnable r2 = new Runnable() { // 익명 클래스 사용
public void run() {
System.out.println("Hello World2");
}
}
public static void process(Runnable r) {
r.run();
}
process(r1); // Hello World1
process(r2); // Hello World2
// 람다 표현식을 직접 전달
process(() -> System.out.println("Hello World3")); // Hello World3
함수 디스크립터(function descriptor)
함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다.
람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크럽터(function descriptor)라고 부른다.
예를 들어 Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로(void) Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있다.
process(() -> System.out.println("Hello World3")); // Hello World3
위 코드를 실행하면 "Hello World3이 출력된다. () -> System.out.println("Hello World3")은 인수가 없으며 void를 반환하는 람다 표현식이다. 이는 Runnable 인터페이스의 run 메서드 시그니처와 같다.
람다 활용 : 실행 어라운드 패턴
실행 어라운드 패턴이란?
대부분의 패턴은 자원을 열고, 작업을 처리한 다음에, 자원을 닫는 순서로 이루어진다.
이처럼 작업을 처리하는 코드를 설정과 정리 두 과장이 둘러싸는 형태를 실행 어라운드 패턴이라고 한다.
이런 실행 어라운드 패턴에서 처음과 끝, 즉 설정과 정리를 하는 두 과정은 재사용하고 작업 부분만 다른 동작을 수행하도록 명령할 수 있다면 좋을 것이다.
그럼 바구니에서 과일을 꺼내는 로직을 예제로 사용해보겠다.
@Getter
@AllArgsConstructor
public class Fruit { // 과일
private String name;
private String color;
private String gram;
}
// 바구니에서 과일 꺼내기
public Fruit TakeOutFruitInTheBasket(List<Fruit> fruits) {
Optional<Fruit> fruit = fruits.stream().filter(f -> f.getColor().equals("Red")).findFirst();
return fruit.orElse(null);
}
현재는 color가 Red인 과일을 꺼내고 있다. 하지만 요구사항이 바뀌어 300g 이상인 과일을 꺼내야 한다고 해보자.
조건별로 if문을 사용하거나, 조건별로 여러 메서드들을 생산할 것인가?
위에 실행 어라운드 패턴에서 얘기했던것 처럼 설정과 정리를 하는 두 과정은 재사용하고 작업 부분만 다른 동작을 수행하도록 변경해보겠다.
// 바구니에서 과일 꺼내기
public Fruit TakeOutFruitInTheBasket(List<Fruit> fruits, TakeOutFruit t) {
return t.takeOut(fruits);
}
// 함수형 인터페이스
@FunctionalInterface
public interface TakeOutFruit {
Fruit takeOut(List<Fruit> fruits);
}
// 빨간색 과일 꺼내기
Fruit redFruit = TakeOutFruitInTheBasket(fruits, (fs) -> {
Optional<Fruit> fruit = fs.stream().filter(f -> f.getColor().equals("Red")).findFirst();
return fruit.orElse(null);
});
// 무거운 과일 꺼내기
Fruit heavyFruit = TakeOutFruitInTheBasket(fruits, (fs) -> {
Optional<Fruit> fruit = fs.stream().filter(f -> f.getGram() >= 300).findFirst();
return fruit.orElse(null);
});
위 코드처럼 함수형 인터페이스를 생성해 필요한 동작을 람다 표현식으로 구현하고 전달해준다.
이처럼 함수형 인터페이스를 사용해 인자를 전달(동작 파라미터화)해주면 동작(작업) 구현만 다르게 만들 수 있다.
지역 변수 사용
위에서 살펴본 모든 람다 표현식은 인수를 자신의 바디 안에서만 사용했다.
하지만 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(free variable)를 정의할 수 있다. 자유 변수란 파라미터로 넘겨진 변수가 아닌 외부에서 정의 된 변수를 뜻한다.
이와 같은 동작을 람다 캡처링(capturing lambda)이라고 부른다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
하지만 자유 변수에는 약간의 제약이 있다.
람다가 사용하는 지역변수는 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다. 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처(참조)할 수 있다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 1338; // 에러 발생
위 코드처럼 람다에서 참조 후 변수값을 변경한다면 지역 변수는 인스턴스 변수와는 달리 스택에 위치하기 때문에, 람다가 스레드에서 실행중에 변수가 바뀌고, 람다가 그 바뀐 변수값을 참조하려 하기 때문에 문제가 발생할 수 있다.
또한 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴에 제동을 걸 수 있다.
'Java' 카테고리의 다른 글
[모던 자바] Java 8 메서드 참조와 생성자 참조란? (0) | 2022.03.02 |
---|---|
[모던 자바] Java 8 API에서 지원하는 함수형 인터페이스 (0) | 2022.02.28 |
[모던 자바] 동작 파라미터화란 무엇인가? (2) | 2022.02.27 |
JitPack을 이용한 Java 패키지 배포 (0) | 2021.12.10 |
Java로 Slack 메세지 발송하기 (0) | 2021.11.23 |