람다 표현식으로 Comparator 구현하기
함수형 인터페이스의 정의 덕분에 JDK 2에 도입된 기존의 Comparator<T> 인터페이스가 기능성을 갖추게 되었습니다. 따라서 람다 표현식을 사용하여 비교기를 구현할 수 있습니다.
다음은 Comparator<T> 인터페이스의 유일한 추상 메서드입니다:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}comparator의 내용은 다음과 같습니다:
o1 < o2이면compare(o1, o2)는 음수를 반환해야 합니다.o1 > o2이면compare(o1, o2)는 양수를 반환해야 합니다.- 모든 경우에
compare(o1, o2)와compare(o2, o1)는 반대 부호를 가져야 합니다.
o1.equals(o2)가 true인 경우 o1과 o2의 비교에서 0을 반환해야 하는 것은 strictly 필요하지 않습니다.
자연 정렬을 구현하는 정수 비교기를 만들려면 어떻게 해야 할까요? 이 튜토리얼의 시작 부분에서 보았던 방법을 사용하면 됩니다:
Comparator<Integer> comparator = (i1, i2) -> Integer.compare(i1, i2);이 람다 표현식은 이러한 방식으로 매우 멋진 바인딩 메서드 참조로 작성할 수도 있다는 것을 눈치챘을 것입니다:
Comparator<Integer> comparator = Integer::compare;이 비교기를
(i1 - i2)로 구현하지 마세요. 이 패턴이 작동하는 것처럼 보이더라도 올바른 결과를 생성하지 않는 코너 케이스가 있습니다.
이 패턴은 comparator의 내용을 따르기만 하면 비교해야 하는 모든 것으로 확장할 수 있습니다.
The Comparator API는 한 단계 더 나아가 훨씬 더 읽기 쉬운 방식으로 comparators 를 생성하는 데 매우 유용한 API를 제공합니다.
팩토리 메서드를 사용하여 Comparator 생성
가장 짧은 문자열이 가장 긴 문자열보다 작다는 자연스럽지 않은 방식으로 문자열을 비교하는 comparator를 만들어야 한다고 가정해 보겠습니다.
이러한 comparator는 이러한 방식으로 작성할 수 있습니다:
Comparator<String> comparator =
(s1, s2) -> Integer.compare(s1.length(), s2.length());이전 파트에서 람다 표현식을 연결하고 구성하는 것이 가능하다는 것을 배웠습니다. 이 코드는 이러한 구성의 또 다른 예시입니다. 실제로 그런 식으로 다시 작성할 수 있습니다:
Function<String, Integer> toLength = String::length;
Comparator<String> comparator =
(s1, s2) -> Integer.compare(
toLength.apply(s1),
toLength.apply(s2));이제 이 Comparator의 코드가 toLength라는 Function에만 의존하고 있음을 알 수 있습니다. 따라서 이 함수를 인수로 받아 해당 Comparator<String>을 반환하는 팩토리 메서드를 만들 수 있게 됩니다.
여전히 toLength 함수의 반환 타입에는 비교 가능해야 한다는 제약 조건이 있습니다. 여기에서는 항상 정수를 자연 정렬로 비교할 수 있기 때문에 잘 작동하지만, 이 점을 염두에 두어야 합니다.
이러한 팩토리 메서드는 JDK에 존재하며, Comparator 인터페이스에 직접 추가되었습니다. 따라서 이전 코드를 이 방법으로 작성할 수 있습니다:
Comparator<String> comparator = Comparator.comparing(String::length);이 comparing() 메서드는 Comparator 인터페이스의 정적 메서드입니다. 이 메서드는 인자로 Function을 받으며, Comparable의 확장인 타입을 반환해야 합니다.
getName() 게터가 있는 User 클래스가 있고 이름에 따라 사용자 목록을 정렬해야 한다고 가정해 보겠습니다. 작성해야 하는 코드는 다음과 같습니다:
List<User> users = ...; // this is your list
Comparator<User> byName = Comparator.comparing(User::getName);
users.sort(byName);
Comparators 체이닝
귀하가 근무하는 회사는 현재 귀하가 제공한 Comparable<User>에 매우 만족하고 있습니다. 하지만 버전 2에는 새로운 요구 사항이 있습니다. 이제 User 클래스에는 firstName과 lastName이 있으며, 이 변경 사항을 처리하려면 새로운 Comparator를 생성해야 합니다.
각 comparator를 작성하는 것은 이전 comparator와 동일한 패턴을 따릅니다:
Comparator<User> byFirstName = Comparator.comparing(User::getFirstName);
Comparator<User> byLastName = Comparator.comparing(User::getLastName);이제 필요한 것은 Predicate 또는 Consumer의 인스턴스를 연결한 것처럼 이들을 연결할 수 있는 방법입니다. comparator API는 이를 위한 솔루션을 제공합니다:
Comparator<User> byFirstNameThenLastName =
byFirstName.thenComparing(byLastName);thenComparing() 메서드는 다른 comparator를 인수로 받아 새 comparator를 반환하는 Comparator 인터페이스의 기본 메서드입니다. 두 사용자에게 적용하면 비교기는 먼저 byFirstName 비교기를 사용하여 두 사용자를 비교합니다. 결과가 0이면 byLastName comparator를 사용하여 비교합니다. 간단히 말해서 예상대로 작동합니다.
비교기 API는 한 단계 더 나아가 byLastName이 User::getLastName 함수에만 의존하기 때문에, 이 함수를 인수로 받는 thenComparing() 메서드의 오버로드가 API에 추가되었습니다. 따라서 패턴은 다음과 같이 됩니다:
Comparator<User> byFirstNameThenLastName =
Comparator.comparing(User::getFirstName)
.thenComparing(User::getLastName);람다 표현식, 메서드 참조, 연쇄, 구성으로 comparators를 만드는 것이 그 어느 때보다 쉬워졌습니다!
특화된 Comparators
박싱 및 언박싱 또는 원시 타입은 comparators에서도 발생할 수 있으며, java.util.function 패키지의 함수 인터페이스의 경우와 동일한 성능 저하를 초래할 수 있습니다. 이 문제를 해결하기 위해 comparing() 팩토리 메서드와 thenComparing() 기본 메서드의 특수 버전이 추가되었습니다.
다음을 사용하여 Comparator<T>의 인스턴스를 만들 수도 있습니다:
comparingInt(ToIntFunction<T> keyExtractor);comparingLong(ToLongFunction<T> keyExtractor);comparingDouble(ToDoubleFunction<T> keyExtractor).
원시 타입인 프로퍼티를 사용하여 객체를 비교해야 하고 이 원시 타입의 박싱/언박싱을 피해야 하는 경우 이 메서드를 사용합니다.
Comparator<T>를 연결하는 해당 메서드도 있습니다:
thenComparingInt(ToIntFunction<T> keyExtractor);thenComparingLong(ToLongFunction<T> keyExtractor);thenComparingDouble(ToDoubleFunction<T> keyExtractor).
이러한 메서드를 사용하면 박싱/언박싱으로 인한 성능 저하 없이 원시 타입을 반환하는 특수 함수에 기반한 비교기로 비교를 체인화할 수 있다는 점은 동일합니다.
자연 정렬(오름차순)을을 사용하여 비교 가능한 객체 비교하기
이 튜토리얼에서는 간단한 comparators를 만드는 데 도움이 되는 몇 가지 팩토리 메서드를 소개합니다.
JDK의 많은 클래스, 그리고 아마도 여러분의 애플리케이션에서도 JDK의 특별한 인터페이스인 Comparable<T> 인터페이스를 구현하고 있을 것입니다. 이 인터페이스에는 하나의 메서드가 있습니다: int를 반환하는 compareTo(T other) 메서드가 있습니다. 이 메서드는 Comparator<T> 인터페이스의 계약에 따라 T의 이 인스턴스를 other와 비교하는 데 사용됩니다.
JDK의 많은 클래스가 이미 이 인터페이스를 구현하고 있습니다. 원시 타입의 모든 래퍼 클래스(Integer, Long 등), String 클래스, 날짜 및 시간 API의 날짜 및 시간 클래스가 이에 해당합니다.
이러한 클래스의 인스턴스는 자연 정렬을 사용하여, 즉 이 compareTo() 메서드를 사용하여 비교할 수 있습니다. 비교기 API는 Comparator.naturalOrder() 팩토리 클래스를 제공합니다. 이 클래스가 빌드하는 비교기는 Comparable 객체를 compareTo() 메서드를 사용하여 비교하는 작업을 수행합니다.
이러한 팩토리 메서드가 있으면 비교자를 연결해야 할 때 매우 유용할 수 있습니다. 다음은 문자열의 길이를 비교한 다음 자연 정렬을 비교하는 예제입니다(이 예에서는 가독성을 높이기 위해 naturalOrder() 메서드에 대한 정적 임포트를 사용합니다):
Comparator<String> byLengthThenAlphabetically =
Comparator.comparing(String::length)
.thenComparing(naturalOrder());
List<String> strings = Arrays.asList("one", "two", "three", "four", "five");
strings.sort(byLengthThenAlphabetically);
System.out.println(strings);이 코드를 실행하면 다음과 같은 결과가 생성됩니다:
[one, two, five, four, three]
Comparator 반전하기(내림차순)
비교자의 주요 용도 중 하나는 물론 객체 목록을 정렬하는 것입니다. JDK 8에서는 특히 이를 위한 메서드가 List 인터페이스에 추가되었습니다: List.sort(). 이 메서드는 비교자를 인자로 받습니다.
이전 목록을 역순으로 정렬해야 하는 경우, Comparator 인터페이스의 멋진 reversed() 메서드를 사용할 수 있습니다.
List<String> strings =
Arrays.asList("one", "two", "three", "four", "five");
strings.sort(byLengthThenAlphabetically.reversed());
System.out.println(strings);이 코드를 실행하면 다음과 같은 결과가 생성됩니다:
[three, four, five, two, one]
Null 값 처리하기
널 객체를 비교하면 코드를 실행하는 동안 불쾌한 NullPointerException이 발생할 수 있으며, 이는 피하고 싶은 문제입니다.
정수 목록을 정렬하기 위해 정수의 널 안전 비교기를 작성해야 한다고 가정해 봅시다. 여러분이 따르기로 결정한 규칙은 모든 널 값을 목록의 끝에 밀어 넣는 것, 즉 널 값이 다른 널이 아닌 값보다 크다는 것을 의미합니다. 그런 다음 null이 아닌 값을 자연스러운 순서대로 정렬하려고 합니다.
다음은 이 동작을 구현하기 위해 작성할 수 있는 코드의 종류입니다:
Comparator<Integer> comparator =
(i1, i2) -> {
if (i1 == null && i1 != null) {
return 1;
} else if (i1 != null && i2 == null) {
return -1;
} else {
return Integer.compare(i1, i2);
}
};이 코드를 이 부분의 시작 부분에서 작성한 첫 번째 비교기와 비교해 보면 가독성이 크게 떨어졌음을 알 수 있습니다.
nullsLast
다행히도 이 비교기를 작성하는 훨씬 더 쉬운 방법이 있는데, Comparator 인터페이스의 다른 팩토리 메서드를 사용하는 것입니다.
Comparator<Integer> naturalOrder = Comparator.naturalOrder();
Comparator<Integer> naturalOrderNullsLast =
Comparator.nullsLast(naturalOrder());nullsLast()와 그 형제 메서드 nullsFirst()는 Comparator 인터페이스의 팩토리 메서드입니다. 둘 다 비교기를 인수로 받아 널 값을 처리하거나, 끝으로 밀거나, 정렬된 목록에서 가장 먼저 배치하는 등의 작업을 수행합니다.
다음은 예제입니다:
List<String> strings =
Arrays.asList("one", null, "two", "three", null, null, "four", "five");
Comparator<String> naturalNullsLast =
Comparator.nullsLast(naturalOrder());
strings.sort(naturalNullsLast);
System.out.println(strings);이 코드를 실행하면 다음과 같은 결과가 생성됩니다:
[five, four, one, three, two, null, null, null]