본문 바로가기
Language/Java

[Java 8] 람다(Lambda) - 3

by ocwokocw 2021. 2. 11.

- 출처: 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

댓글