람다, 메소드 참조 활용하기
https://poloopy.tistory.com/50
람다 표현식
람다 표현식은 메소드로 전달할 수 있는 익명 함수를 단순화한 것이다. 특징익명보통의 메소드와 달리 이름이 없다.함수메소드처럼 특정 클래스에 종속되지 않아 함수라고 부른다.전달람다 표
poloopy.tistory.com
https://poloopy.tistory.com/51
메소드 참조
메소드 참조는 람다 표현식을 더 간결하게 작성할 수 있는 기능이다.람다 표현식이 단순 기존 메소드를 호출하는 경우 사용할 수 있다. 메소드 참조의 기본 구조 클래스 또는 객체 이름이중 콜
poloopy.tistory.com
람다와 메소드 참조에 대해 이론적으로 익혔다면 어떤식으로 활용할지 알아보자.
좋은 예시로 List API의 sort() 메소드가 있다. sort 메소드는 정렬 기능을 제공해줘 직접 구현할 수고를 덜어주는 고마운 기능이다. 하지만 정렬 기능을 편집하고 싶다면 어떻게 해야할까? 그리고 이것을 간단하게 만들고 싶다면 어떻게 해야할까?
1단계 : 코드 전달
sort는 다음과 같은 시그니처를 가진다.
void sort(Comparator<? super E> c)
이제 sort의 동작을 파라미터화 시켜보자.
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
2단계 : 익명 클래스 사용
한번만 사용할 Comparator를 위 코드처럼 구현하는 것은 불필요한 것 같다. 익명 클래스를 활용해보자.
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
3단계 : 람다 표현식 사용
람다 표현식을 배우며 익명 클래스를 람다 표현식으로 바꾸는 것을 배웠다. 더 간결하게 변경해보자.
Comparator의 함수 디스크립터는 (T, T) -> int이다.
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
);
여기서 람다 표현식을 쓸 경우 파라미터의 타입을 추론하므로 생략하면
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
Comparator는 Comparable 키를 추출해서 Comparator 객체로 만드는 Function 함수를 인수로 받는 정적 메소드 comparing을 포함한다. 다음처럼 comparing 메소드를 사용할 수 있다.
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
이제 static import를 하면 다음처럼 간소화할 수 있다.
inventory.sort(comparing(apple -> apple.getWeight()));
4단계 : 메소드 참조 사용
위의 코드에 메소드 참조를 더하면
inventory.sort(comparing(Apple::getWeight));
자 이제 처음 코드와 비교해보자
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
코드가 매우 짧아졌다.
그 뿐만 아니라 전에 비해 'Apple을 weight 별로 비교하여 inventory를 정렬하라'는 의미가 명확해졌다.
람다 표현식을 조합하는 메소드
몇몇 함수형 인터페이스는 다양한 유틸리티 메소드를 포함한다. 예를 들어, Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메소드를 제공한다. 하지만 함수형 인터페이스는 분명 추상 메소드 하나만 가지고 있어야한다고 정의했다. 어떻게 이런 일이 가능할까?
여기서 등장하는 것이 디폴트 메소드다. 디폴트 메소드는 추상 메소드가 아니므로 함수형 인터페이스의 정의에 어긋나지 않는다.
디폴트 메소드란?
기존에는 인터페이스에 새로운 메소드를 추가하면 그 인터페이스를 구현한 모든 클래스에 해당 메소드를 구현해야 했다. 하지만 디폴트(default) 메소드는 구현이 필요없다. 그래서 인터페이스에 새로 추가해도 하위 클래스에서 구현할 필요가 없다는 것이다. 물론 필요 시엔 메소드 오버라이딩을 통해 재정의가 가능하다.
따라서 디폴트 메소드는 추상 메소드처럼 필수적으로 정의해주지 않아도 되기 때문에 추상 메소드와 별도로 볼 수 있는 것이고, 람다 표현식을 조합하는 역할을 할 수 있는 것이다.
Comparator 조합
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
위의 예시에서 사과의 무게를 내림차순으로 정렬하고 싶다면 어떻게 해야할까? 다른 Comparator 인스턴스를 만들 필요가 없다. 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메소드를 제공하기 때문이다.
inventory.sort(comparing(Apple::getWeight).reversed(); // 무게를 내림차순으로 정렬
이번엔 무게가 같을 때 다른 조건을 넣고 싶은 경우를 가정하자. 이럴 땐 비교 결과를 더 다듬을 수 있는 두 번째 Comparator를 만들 수 있다. thenComparing 메소드로 두 번째 비교자를 만들 수 있다. thenComparing은 함수를 인수로 받아 첫번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달한다.
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry)); // 두 사과의 무게가 같으면 국가별로 정렬
Predicate 조합
Predicate 인터페이스는 negate, and, or 세 가지 메소드를 제공한다.
- negate
해당 결과를 제외한 나머지 결과
Predicate<Apple> notRedApple = redApple.negate();
- and
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
- or
Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));
Function 조합
Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메소드를 제공한다.
- andThen
외부 함수를 먼저 실행한 다음에 그 결과를 내부 함수의 인수로 제공한다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1); // 4 반환
- compose
내부 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1); // 3 반환
이런 식으로 람다 표현식을 조합하면 람다 표현식만으로 코드를 작성하는 것보다 훨씬 간결하고 가독성이 좋은 코드가 완성된다. 또한 한 람다 표현식 안에 하나의 계산을 담당할 수 있기 때문에 유지보수 면에서도 강점이 있다.