비교기 인터페이스 구현
Comparator 인터페이스는 검색부터 정렬, 역방향 작업 등에 이르기까지 JDK 라이브러리의 모든 곳에서 볼 수 있습니다. Java 8에서는 기능적 인터페이스가 되므로 스트리밍 구문을 사용하여 비교기를 구현할 수 있다는 장점이 있습니다.
새로운 구문의 가치를 확인하기 위해 여러 가지 방법으로 Comparator를 구현해 보겠습니다. 익명의 내부 클래스를 구현하지 않아도 되므로 많은 키 입력이 절약됩니다.
비교기를 사용하여 정렬
다음 예에서는 다양한 비교 방법을 사용하여 사람들 그룹을 정렬합니다. 먼저 Person JavaBean을 생성해 보겠습니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 사람 {
비공개 최종 문자열 이름;
비공개 최종 정수 연령;
public Person(final String theName, final int theAge) {
이름 = 이름;
나이 = 나이;
}
공개 문자열 getName() { 반환 이름 }
공개 int getAge() { 나이 반환 }
public int ageDifference(최종인 기타) {
반환 연령 - 기타.나이;
}
공개 문자열 toString() {
return String.format("%s - %d", 이름, 나이);
}
}
Person 클래스를 통해 Comparator 인터페이스를 구현할 수 있지만 이 방법에서는 하나의 비교 메서드만 사용할 수 있습니다. 우리는 이름, 나이 또는 이들의 조합과 같은 다양한 속성을 비교할 수 있기를 원합니다. 유연하게 비교를 수행하기 위해 비교가 필요할 때 Comparator를 사용하여 관련 코드를 생성할 수 있습니다.
먼저 이름과 나이가 다른 Person 목록을 만들어 보겠습니다.
다음과 같이 코드 코드를 복사합니다.
최종 List<Person> people = Arrays.asList(
새로운 사람("John", 20),
새로운 사람("사라", 21),
새로운 사람("제인", 21),
new Person("그렉", 35));
이름이나 나이를 기준으로 사람들을 오름차순이나 내림차순으로 정렬할 수 있습니다. 일반적인 방법은 익명의 내부 클래스를 사용하여 Comparator 인터페이스를 구현하는 것입니다. 이런 식으로 작성하면 관련성이 더 높은 코드만 의미가 있고 나머지는 형식에 불과합니다. 람다 식을 사용하면 비교의 본질에 집중할 수 있습니다.
먼저 나이를 기준으로 가장 어린 것부터 나이가 많은 것까지 정렬해 보겠습니다.
이제 List 객체가 있으므로 해당 sort() 메서드를 사용하여 정렬할 수 있습니다. 그러나 이 방법에도 문제가 있습니다. 이는 void 메소드입니다. 즉, 이 메소드를 호출하면 목록이 변경됩니다. 원본 목록을 유지하려면 먼저 복사본을 만든 다음 sort() 메서드를 호출해야 합니다. 노력이 너무 많았어요. 이제 Stream 클래스에 도움을 요청해야 합니다.
List에서 Stream 객체를 가져온 다음 해당 sorted() 메서드를 호출할 수 있습니다. 원본 컬렉션을 수정하는 대신 정렬된 컬렉션을 반환합니다. 이 방법을 사용하면 비교기의 매개변수를 쉽게 구성할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
List<Person> 오름차순Age =
people.stream()
.sorted((사람1, 사람2) -> 사람1.나이차이(사람2))
.collect(toList());
printPeople("나이를 기준으로 오름차순으로 정렬: ", climbingAge);
먼저 stream() 메서드를 통해 목록을 Stream 객체로 변환합니다. 그런 다음 sorted() 메서드를 호출합니다. 이 메소드는 Comparator 매개변수를 승인합니다. Comparator는 기능적 인터페이스이므로 람다 식을 전달할 수 있습니다. 마지막으로 Collect 메소드를 호출하고 결과를 목록에 저장하도록 합니다. Collect 메소드는 반복 프로세스 중에 객체를 특정 형식이나 유형으로 출력할 수 있는 리듀서입니다. toList() 메서드는 Collectors 클래스의 정적 메서드입니다.
Comparator의 추상 메소드인 CompareTo()는 비교할 객체인 두 개의 매개변수를 전달받아 int형의 결과를 반환한다. 이와 호환되도록 람다 식은 두 개의 매개 변수, 즉 두 개의 Person 객체를 받습니다. 이 유형은 컴파일러에 의해 자동으로 추론됩니다. 비교된 객체가 동일한지 여부를 나타내는 int 유형을 반환합니다.
연령별로 정렬하려고 하므로 두 객체의 연령을 비교한 후 비교 결과를 반환하겠습니다. 크기가 같으면 0을 반환합니다. 그렇지 않은 경우, 첫 번째 사람이 어린 경우 음수가 반환되고, 첫 번째 사람이 나이가 많을 경우 양수가 반환됩니다.
sorted() 메서드는 대상 컬렉션의 각 요소를 순회하고 지정된 Comparator를 호출하여 요소의 정렬 순서를 결정합니다. sorted() 메서드의 실행 방법은 앞서 언급한 Reduce() 메서드와 다소 유사합니다. Reduce() 메서드는 목록을 점차적으로 결과로 축소합니다. sorted() 메서드는 비교 결과를 기준으로 정렬합니다.
정렬을 마친 후에는 결과를 인쇄해야 하므로 printPeople() 메서드를 호출하여 이 메서드를 구현해 보겠습니다.
다음과 같이 코드 코드를 복사합니다.
공공 정적 무효 printPeople(
최종 문자열 메시지, 최종 List<Person> 명) {
System.out.println(메시지);
people.forEach(System.out::println);
}
이 방법에서는 먼저 메시지를 인쇄한 다음 목록을 순회하여 그 안에 있는 각 요소를 인쇄합니다.
sorted() 메서드를 호출하여 목록에 있는 사람들을 가장 어린 것부터 가장 오래된 것 순으로 정렬하는 방법을 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
연령별로 오름차순으로 정렬됩니다.
존 - 20
사라 - 21
제인 - 21
그렉 - 35
개선을 위해 sorted() 메서드를 다시 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
.sorted((사람1, 사람2) -> 사람1.나이차이(사람2))
전달된 람다 표현식에서는 이 두 매개변수를 간단히 라우팅합니다. 첫 번째 매개변수는 ageDifference() 메서드의 호출 대상으로 사용되고 두 번째 매개변수는 해당 매개변수로 사용됩니다. 하지만 이렇게 작성할 수는 없지만 사무실 공간 모드를 사용합니다. 즉, 메소드 참조를 사용하고 Java 컴파일러가 라우팅을 수행하도록 합니다.
여기에 사용된 매개변수 라우팅은 이전에 본 것과 약간 다릅니다. 앞서 인수가 호출 대상 또는 호출 매개변수로 전달되는 것을 확인했습니다. 이제 두 개의 매개 변수가 있으며 이를 두 부분으로 분할하려고 합니다. 하나는 메서드 호출의 대상이고 두 번째는 매개 변수입니다. 걱정하지 마십시오. Java 컴파일러가 "내가 처리하겠습니다."라고 말할 것입니다.
이전 sorted() 메서드의 람다 식을 짧고 간결한 ageDifference 메서드로 바꿀 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
people.stream()
.sorted(사람::나이차이)
이 코드는 Java 컴파일러에서 제공하는 메소드 참조 덕분에 매우 간결합니다. 컴파일러는 두 사람 인스턴스 매개변수를 수신하고 첫 번째를 ageDifference() 메서드의 대상으로 사용하고 두 번째를 메서드 매개변수로 사용합니다. 코드를 직접 작성하는 대신 컴파일러가 이 작업을 수행하도록 합니다. 이 메소드를 사용할 때 첫 번째 매개변수가 참조된 메소드의 호출 대상이고 나머지 매개변수가 메소드의 입력 매개변수인지 확인해야 합니다.
재사용 비교기
목록에 있는 사람들을 가장 어린 사람부터 가장 나이 많은 사람으로 정렬하는 것도 쉽고, 가장 나이 많은 사람부터 가장 어린 사람으로 정렬하는 것도 쉽습니다. 한번 시도해 봅시다.
다음과 같이 코드 코드를 복사합니다.
printPeople("연령별 내림차순으로 정렬: ",
people.stream()
.sorted((사람1, 사람2) -> 사람2.나이차이(사람1))
.collect(toList()));
이전 예제와 마찬가지로 sorted() 메서드를 호출하고 Comparator 인터페이스에 맞는 람다 식을 전달합니다. 유일한 차이점은 이 람다 식의 구현입니다. 비교 대상의 순서를 변경했습니다. 결과는 연령에 따라 가장 나이가 많은 것부터 가장 어린 것까지 정렬되어야 합니다. 살펴 보겠습니다.
다음과 같이 코드 코드를 복사합니다.
연령별로 내림차순으로 정렬:
그렉 - 35
사라 - 21
제인 - 21
존 - 20
단지 비교 논리를 바꾸는 것에는 많은 노력이 필요하지 않습니다. 그러나 매개변수의 순서가 메소드 참조에 대한 매개변수 라우팅 규칙을 따르지 않기 때문에 이 버전을 메소드 참조로 재구성할 수 없습니다. 첫 번째 매개변수는 메소드의 호출 대상으로 사용되지 않고 메소드 매개변수로 사용됩니다. 노력의 중복을 줄이는 동시에 이 문제를 해결할 수 있는 방법이 있습니다. 방법을 살펴보겠습니다.
우리는 이전에 두 개의 람다 표현식을 만들었습니다. 하나는 나이를 기준으로 작은 것부터 큰 것까지 정렬하는 것이고, 다른 하나는 큰 것에서 작은 것까지 정렬하는 것입니다. 그렇게 하면 코드 중복 및 중복이 발생하고 DRY 원칙을 위반하게 됩니다. 정렬 순서만 조정하려는 경우 JDK는 특수 메서드 수정자 default가 있는 reverse 메서드를 제공합니다. 이에 대해서는 77페이지의 기본 메서드에서 논의하겠습니다. 여기서는 먼저 reversed() 메서드를 사용하여 중복성을 제거합니다.
다음과 같이 코드 코드를 복사합니다.
Comparator<Person> CompareAscending =
(사람1, 사람2) -> 사람1.나이차이(사람2);
Comparator<Person> CompareDescending = CompareAscending.reversed();
우리는 먼저 사람들을 나이에 따라 가장 어린 것부터 가장 나이 많은 것까지 정렬하기 위해 비교기인 CompareAscending을 만들었습니다. 비교 순서를 바꾸려면 이 코드를 다시 작성하는 대신 첫 번째 Comparator의 reversed() 메서드를 호출하여 두 번째 Comparator 객체를 얻으면 됩니다. reversed() 메서드 내부에서는 비교된 매개변수의 순서를 바꾸는 비교기를 생성합니다. 이는 reversed도 고차 방법임을 보여줍니다. 즉, 부작용 없이 함수를 생성하고 반환합니다. 우리는 코드에서 이 두 비교기를 사용합니다.
다음과 같이 코드 코드를 복사합니다.
printPeople("연령별 오름차순으로 정렬: ",
people.stream()
.sorted(비교오름차순)
.collect(toList())
);
printPeople("연령별 내림차순으로 정렬: ",
people.stream()
.sorted(비교내림차순)
.collect(toList())
);
Java8의 이러한 새로운 기능으로 인해 코드의 중복성과 복잡성이 크게 줄어들었음을 코드에서 분명히 알 수 있지만, JDK에서 탐색할 수 있는 무한한 가능성이 기다리고 있습니다.
이미 연령순으로 정렬할 수 있고, 이름순으로도 쉽게 정렬할 수 있습니다. 마찬가지로 이름별로 사전순으로 정렬해 보겠습니다. 마찬가지로 람다 식의 논리만 변경하면 됩니다.
다음과 같이 코드 코드를 복사합니다.
printPeople("이름을 기준으로 오름차순으로 정렬되었습니다: ",
people.stream()
.sorted((사람1, 사람2) ->
person1.getName().compareTo(person2.getName()))
.collect(toList()));
출력 결과는 이름을 기준으로 사전순으로 정렬됩니다.
다음과 같이 코드 코드를 복사합니다.
이름을 기준으로 오름차순으로 정렬:
그렉 - 35
제인 - 21
존 - 20
사라 - 21
지금까지는 나이순이나 이름순으로 정렬해봤습니다. 람다 식의 논리를 더 스마트하게 만들 수 있습니다. 예를 들어, 나이와 이름을 동시에 기준으로 정렬할 수 있습니다.
목록에서 가장 어린 사람을 고르자. 먼저 연령별로 가장 작은 것부터 큰 것 순으로 정렬한 다음 결과에서 첫 번째 것을 선택할 수 있습니다. 하지만 실제로는 작동하지 않습니다. Stream에는 이를 달성하기 위한 min() 메서드가 있습니다. 이 메서드는 Comparator도 허용하지만 컬렉션에서 가장 작은 개체를 반환합니다. 그것을 사용하자.
다음과 같이 코드 코드를 복사합니다.
people.stream()
.min(사람::나이차이)
.ifPresent(막내 -> System.out.println("막내: " + 막내));
min() 메서드를 호출할 때 ageDifference 메서드 참조를 사용했습니다. 목록이 비어 있고 목록에 가장 어린 사람이 한 명 이상 있을 수 있으므로 min() 메서드는 Optinal 개체를 반환합니다. 그런 다음 Optinal의 ifPrsend() 메소드를 통해 가장 어린 사람을 구하고 그의 세부 정보를 인쇄합니다. 출력을 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
막내: 존 - 20세
가장 오래된 것을 내보내는 것도 매우 간단합니다. 이 메서드 참조를 max() 메서드에 전달하면 됩니다.
다음과 같이 코드 코드를 복사합니다.
people.stream()
.max(사람::나이차이)
.ifPresent(가장 오래된 것 -> System.out.println("가장 오래된 것: " + 가장 오래된 것));
맏형의 이름과 나이를 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
장남: 그렉 - 35세
람다 식과 메서드 참조를 사용하면 비교기 구현이 더 간단하고 편리해집니다. 또한 JDK는 Compararor 클래스에 여러 가지 편리한 메서드를 도입하여 아래에서 볼 수 있듯이 보다 원활하게 비교할 수 있도록 해줍니다.
다중 비교 및 스트리밍 비교
Comparator 인터페이스에서 제공하는 편리한 새 메서드를 살펴보고 이를 사용하여 여러 속성을 비교해 보겠습니다.
이전 섹션의 예제를 계속 사용해 보겠습니다. 이름별로 정렬하면 위에서 작성한 내용은 다음과 같습니다.
다음과 같이 코드 코드를 복사합니다.
people.stream()
.sorted((사람1, 사람2) ->
person1.getName().compareTo(person2.getName()));
지난 세기의 내부 클래스 작성 방법과 비교하면 이 작성 방법은 너무 단순합니다. 그러나 Comparator 클래스의 일부 함수를 사용하면 더 간단해질 수 있습니다. 이러한 함수를 사용하면 목적을 더 원활하게 표현할 수 있습니다. 예를 들어, 이름을 기준으로 정렬하려면 다음과 같이 작성할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
final Function<Person, String> byName = 사람 -> person.getName();
people.stream()
.sorted(비교(이름별));
이 코드에서는 Comparator 클래스의 정적 메서드 비교()를 가져왔습니다. 비교() 메서드는 전달된 람다 식을 사용하여 Comparator 개체를 생성합니다. 즉, 함수를 입력 매개변수로 받아들이고 다른 함수를 반환하는 고차 함수이기도 합니다. 구문을 더욱 간결하게 만드는 것 외에도 이러한 코드는 우리가 해결하려는 실제 문제를 더 잘 표현할 수도 있습니다.
이를 통해 여러 비교가 더 원활해질 수 있습니다. 예를 들어, 이름과 나이를 기준으로 비교하는 다음 코드는 모든 것을 말해줍니다.
다음과 같이 코드 코드를 복사합니다.
final Function<Person, Integer> byAge = 사람 -> person.getAge();
final Function<Person, String> byTheirName = 사람 -> person.getName();
printPeople("나이와 이름을 기준으로 오름차순으로 정렬되었습니다: ",
people.stream()
.sorted(비교(연령별).then비교(TheirName))
.collect(toList()));
먼저 두 개의 람다 표현식을 만들었습니다. 하나는 지정된 사람의 나이를 반환하고 다른 하나는 그의 이름을 반환합니다. sorted() 메서드를 호출할 때 여러 속성을 비교할 수 있도록 이 두 표현식을 결합합니다. Compare() 메서드는 나이를 기준으로 Comparator를 생성하고 반환합니다. 그런 다음 반환된 Comparator에서 thenComparing() 메서드를 호출하여 나이와 이름을 비교하는 결합된 비교기를 만듭니다. 아래 출력은 먼저 연령을 기준으로 정렬한 다음 이름을 기준으로 정렬한 결과입니다.
다음과 같이 코드 코드를 복사합니다.
나이와 이름을 기준으로 오름차순으로 정렬됩니다.
존 - 20
제인 - 21
사라 - 21
그렉 - 35
보시다시피 Comparator 구현은 람다 식과 JDK에서 제공하는 새로운 도구 클래스를 사용하여 쉽게 결합할 수 있습니다. 아래에서 컬렉터를 소개하겠습니다.