- 출처: Java 8 in action
- 메서드 레퍼런스
메서드 레퍼런스는 메서드를 변수에 저장한것처럼 사용할 수 있게 한다. 람다와 메서드 레퍼런스중 꼭 어느것이 더 좋다거나 가독성이 더 좋다고 말할 수는 없다. 상황에 따라서 메서드 레퍼런스가 때로는 람다가 가독성이 더 좋으므로 상황에 맞게 잘 사용하자.
메서드 레퍼런스는 아래처럼 사용한다.
public class People {
private String name;
private Integer age;
public People(String name, Integer age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
}
List<People> persons = Arrays.asList(
new People("A", 3),
new People("B", 1),
new People("C", 4),
new People("D", 2));
persons.sort((People p1, People p2) ->
p1.getAge() - p2.getAge());
persons.sort(Comparator.comparing(People::getAge));
sort 에 익명함수를 사용할 수도 있고, 10번째 줄 처럼 Comparator.comparing을 이용하여 People::getAge 메서드 레퍼런스(Key Extractor로 사용)를 사용할 수도 있다. People::getAge는 (People p1) -> p1.getAge() 람다식을 축약한 형태라고 볼 수 있다.
메소드 레퍼런스의 형태는 3 가지가 있다.
- Reference to a static method: ContainingClass::staticMethodName
- Reference to an instance method of a particular object containingObject::instanceMethodName
- Reference to an instance method of an arbitrary object of a particular type ContainingType::methodName
위 목록은 오라클 문서 Method References 에서 발췌하였다. 우선 첫째로 static method 를 메소드 레퍼런스로 사용할 수 있다. 그리고 인스턴스가 가지는 메소드를 참조할 수 있으며, 타입을 이용하여 메소드를 참조할 수도 있다.
People.java에 아래 코드를 추가하자.
public static int comapreByAge(People p1, People p2) {
return p1.getAge().compareTo(p2.getAge());
}
persons.sort(People::comapreByAge);
persons.sort(Comparator.comparing(People::getAge));
메서드 레퍼런스로 static 메소드인 compareByAge와 People 타입의 getAge 메소드 두 가지 형태로 사용하였다.
- 생성자 레퍼런스
메서드뿐만 아니라 생성자도 People::new 처럼 레퍼런스를 만들 수 있다. 좀 헷갈릴 수 있지만 천천히 살펴보자.
People.java 에 아래처럼 기본 생성자를 추가하자.
public People() {}
Supplier<People> emptyPeople = People::new;
People p1 = emptyPeople.get();
BiFunction<String, Integer, People> initializedPeople = People::new;
People p2 = initializedPeople.apply("a", 3);
emptyPeople 에 사용한 생성자 레퍼런스 People::new 는 인자가 없고 People을 생성(반환)하는 기본 생성자 public People() 이 된다. People을 생성하려면 .get() 메소드를 이용하여 사용한다.
initializedPeople에 사용한 생성자 레퍼런스 People::new 는 이름과 나이를 인자로 받아 People을 생성(반환)하는 기본 생성자 public People(String name, Integer age)가 된다. People을 생성하려면 .apply() 를 사용한다.
여기서 좀 헷갈릴 수 있다. 어떻게 내가 의도하는 생성자를 해석하는걸까? 생성자 레퍼런스를 참조할땐 꼭 Supplier와 BiFunction 를 사용해야 하는걸까? 인터페이스를 한번 살펴보자.
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
@FunctionalInterface
public interface BiFunction<T, U, R> {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);
Supplier<T>는 인자가 없으며 T를 반환, BiFunction은 T, U를 인자로 받아 R을 반환한다. 생성자 레퍼런스를 참조하기 위한 인터페이스로 꼭 Supplier나 BiFunction을 사용해야하는것이 아니고, 해당 생성자의 인자와 반환형이 맞은 함수형 인터페이스를 사용했을 뿐이다. 만약 커스텀 함수형 인터페이스를 정의하여 생성자 레퍼런스를 사용한다면 사용할 수 있다.
@FunctionalInterface
public interface CustomPeopleNewFunction<T, U, R> {
R makePeople(T t, U u);
}
위와 같이 새로운 함수형 인터페이스를 정의하고 아래처럼 사용하면 된다.
CustomPeopleNewFunction<String, Integer, People> peopleCreator = People::new;
People p3 = peopleCreator.makePeople("b", 4);
- 람다 표현식의 조합 및 실제 사용 예제
Comparator, Function, Predicate 같은 함수형인터페이스는 람다 표현식을 조합하는 유틸리티 메소드를 제공한다. 즉 여러개의 람다 표현식을 조합해서 복잡한 람다 표현식으로 만들 수 있다는 얘기이다. Predicate를 2개 조합하면 OR 연산을 수행하는 Predicate를 만들 수 있다.
펑셔널 인터페이스는 1개의 추상메소드를 가진다고 하였는데 어떻게 추가적인 기능을 제공하는것일까? 여기서 디폴트 메소드가 등장한다. 디폴드 메소드는 추후에 자세히 다루고 디폴트 메소드가 어떤것인지만 이해하도록 하자.
- Comparator 조합
앞에서 보았듯이 Comparator.comparing은 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환한다.
Comparator<People> ageComparator = Comparator.comparing(People::getAge);
내림차순으로 정렬하려면 새로운 Comparator를 만들 필요 없이 reversed() 메소드(디폴트 메소드)를 이용하면 된다.
Comparator<People> ageDescComparator =
Comparator.comparing(People::getAge).reversed();
나이가 같은 사람이 존재할 때 그 다음 우선순위를 주려면 어떻게 해야할까?
Comparator<People> ageDescComparator =
Comparator.comparing(People::getAge).reversed()
.thenComparing(People::getName);
- Predicate 조합
Predicate 는 negate, and, or 3 가지 메소드를 제공한다. negate는 반전인경우에 사용한다.
List<People> persons = Arrays.asList(
new People("a", 1),
new People("b", 4),
new People("c", 2),
new People("d", 3));
Predicate<People> personHasNameA = (People p1) -> "a".equals(p1.getName());
Predicate<People> personNameisNotA = personHasNameA.negate();
List<People> nameAPerson = persons.stream()
.filter(personHasNameA)
.collect(Collectors.toList());
List<People> exceptNameAPerson = persons.stream()
.filter(personNameisNotA)
.collect(Collectors.toList());
System.out.println("nameAPerson: " + nameAPerson);
System.out.println("exceptNameAPerson: " + exceptNameAPerson);
위의 코드 처럼 "a" 라는 이름을 가진 People을 찾는 Predicate와 그에대한 negate로 반전시킨 Predicate를 선언후 filter로 조건을 주고 출력한 결과는 아래와 같다. (People class 에서 toString() 메소드를 자동완성으로 구현한 결과이다.)
nameAPerson: [People [name=a, age=1]]
exceptNameAPerson: [People [name=b, age=4], People [name=c, age=2], People [name=d, age=3]]
and와 or 조건을 조합할 수도 있다. and 와 or 를 조합해서 사용할 때 우선순위 연산자에 따라서 먼저 조합되는 개념인것이 아니라 조합을 사용한 순서대로 결과가 반환된다. 아래 default 메소드를 참조 하여 People 예제를 잘 살펴보자
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
List<People> persons = Arrays.asList(
new People("a", 1),
new People("b", 4),
new People("c", 2),
new People("d", 3));
Predicate<People> personHasNameA = (People p1) -> "a".equals(p1.getName());
Predicate<People> a1OrGreaterEqualThan3 =
personHasNameA.and((People p1) -> p1.getAge().compareTo(1) == 0)
.or((People p1) -> p1.getAge().compareTo(3) >= 0);
List<People> filteredPerson = persons.stream()
.filter(a1OrGreaterEqualThan3)
.collect(Collectors.toList());
System.out.println("filteredPerson: " + filteredPerson);
위의 코드는 이름이 "a" 이고(and) 나이가 1살이거나 나이가 3살이상인 사람들을 찾는 a1OrGreaterEqualThan3 Predicate 로 필터링한 코드이다. ("a" 이름 조건 && 1살) || 2살 이상
filteredPerson: [People [name=a, age=1], People [name=b, age=4], People [name=d, age=3]]
- Function 조합
Function 인터페이스는 Function 인스턴스를 반환하는 andThen, compose 두 가지 디폴트 메소드를 제공한다.
f(x) = x + 1, g(x) = x * 2 이라는 함수가 있다고 가정하자. andThen은 먼저 적용한 결과를 다음 함수의 입력으로 제공한다. 수학적으로 표현하면 g(f(x)), f(x)를 수행한 결과가 g(x)의 인자로 전달된 방식이 된다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
System.out.println(h.apply(1));
input 1에 f 가 먼저 적용되어 1이 증가된 2의 결과가 나왔고, 이 결과가 다음 함수 g의 input 으로 전달되어 2*2 = 4 의 결과를 출력한다. 반면 compose는 인자의 함수가 먼저 수행된 후 앞의 인자가 수행되는 즉 f(g(x))가 된다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
System.out.println(h.apply(1));
위의 코드의 결과는 3이 되는데, g가 먼저 적용되고 1*2가 되고 이에 f가 적용되어 1이 증가된다.
default 메소드를 직접 보면 이해가 더 쉬워진다. 헷갈릴까봐 인자의 이름까지 after, before로 적용 순서를 말해준다.
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
'Language > Java' 카테고리의 다른 글
[Java 8] Stream - 2 (기본 연산) (0) | 2021.02.11 |
---|---|
[Java 8] Stream - 1 (Stream 개요) (0) | 2021.02.11 |
[Java 8] 람다(Lambda) - 2 (0) | 2021.02.10 |
[Java 8] 람다(Lambda) - 1 (0) | 2021.02.10 |
[Java 8] 객체로서의 함수 (0) | 2021.02.10 |
댓글