在 Java 中按多個字段對對象集合進行排序
一、概述
在編程時,我們經常需要對對象集合進行排序。如果我們想要對多個字段上的對象進行排序,排序邏輯有時會變得難以實現。在本教程中,我們將討論解決該問題的幾種不同方法,以及它們的優缺點。
2. 示例Person
類
讓我們定義一個Person
類,它有兩個字段, name
和age
。在整個示例中,我們將首先根據name
比較Person
對象,然後根據age
比較:
public class Person {
@Nonnull private String name;
private int age;
// constructor
// getters and setters
}
在這裡,我們添加了一個@Nonnull
註釋以保持示例簡單。但是在生產代碼中,我們可能需要處理可空字段的比較。
3.使用Comparator.compare()
Java 提供了Comparator
接口來比較兩個相同類型的對象。我們可以使用自定義邏輯實現其compare(T o1, T o2)
方法以執行所需的比較。
3.1.不同的字段一一檢查
讓我們逐個比較字段:
public class CheckFieldsOneByOne implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
int nameCompare = o1.getName().compareTo(o2.getName());
if(nameCompare != 0) {
return nameCompare;
}
return Integer.compare(o1.getAge(), o2.getAge());
}
}
在這裡,我們使用String
類的compareTo()
方法和Integer
類的compare()
方法依次比較name
和age
字段。
這需要大量的輸入,有時還需要處理許多特殊情況。因此,當我們有更多的字段要比較時,很難維護和擴展。通常,不建議在生產代碼中使用此方法。
3.2.使用 Guava 的ComparisonChain
鏈
首先,讓我們將 Google Guava 庫依賴項添加到我們的pom.xml
中:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
我們可以使用這個庫中的ComparisonChain
類來簡化邏輯:
public class ComparisonChainExample implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return ComparisonChain.start()
.compare(o1.getName(), o2.getName())
.compare(o1.getAge(), o2.getAge())
.result();
}
}
在這裡,我們分別使用 ComparisonChain 中的compare(int left, int right)
和compare(Comparable<?> left, Comparable<?> right)
方法來ComparisonChain
name
和age
。
這種方法隱藏了比較細節,只暴露了我們關心的內容——我們想要比較的字段以及它們應該被比較的順序。此外,我們應該注意,我們不需要任何額外的null
處理邏輯,因為庫方法會處理它。因此,它變得更容易維護和擴展。
3.3.使用 Apache Commons 的CompareToBuilder
進行排序
首先,讓我們將 Apache Commons 的依賴項添加到pom.xml
中:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
與前面的示例類似,我們可以使用 Apache Commons 中的CompareToBuilder
來減少所需的樣板代碼:
public class CompareToBuilderExample implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return new CompareToBuilder()
.append(o1.getName(), o2.getName())
.append(o1.getAge(), o2.getAge())
.build();
}
}
這種方法與 Guava 的ComparisonChain
非常相似——它也隱藏了比較細節並且易於維護和擴展.
4. 使用Comparator.comparing()
和 Lambda 表達式
從 Java 8 開始, Comparator
接口中添加了幾個static
方法,可以採用 lambda 表達式來創建Comparator
對象。我們可以使用它的comparing()
方法來構造我們需要的Comparator
:
public static Comparator<Person> createPersonLambdaComparator() {
return Comparator.comparing(Person::getName)
.thenComparing(Person::getAge);
}
這種方法更加簡潔和可讀,因為它直接採用Person
類的 getter 。
它還保留了我們之前看到的方法的可維護性和可擴展性特徵。此外,與之前的方法中的立即評估相比,這裡的 getters 是惰性評估的。因此,它的性能更好,更適合需要大量大數據比較的對延遲敏感的系統。
此外,這種方法僅使用核心 Java 類,不需要任何第三方庫作為依賴項。總的來說,這是最推薦的方法。
5. 檢查比較結果
讓我們測試我們看到的四個比較器並檢查它們的行為。所有這些比較器都可以以相同的方式調用並且應該產生相同的結果:
@Test
public void testComparePersonsFirstNameThenAge() {
Person person1 = new Person("John", 21);
Person person2 = new Person("Tom", 20);
// Another person named John
Person person3 = new Person("John", 22);
List<Comparator<Person>> comparators =
Arrays.asList(new CheckFieldsOneByOne(),
new ComparisonChainExample(),
new CompareToBuilderExample(),
createPersonLambdaComparator());
// All comparators should produce the same result
for(Comparator<Person> comparator : comparators) {
Assertions.assertIterableEquals(
Arrays.asList(person1, person2, person3)
.stream()
.sorted(comparator)
.collect(Collectors.toList()),
Arrays.asList(person1, person3, person2));
}
}
在這裡, person1
與 person3 同名(“John”) person3,
但更年輕(21 < 22),而person3的名字(“John”)在字典序上小於person2的名字(“Tom”)。所以,最終的順序是person1
、 person3
、 person2
。
此外,我們應該注意,如果我們在類變量name
上沒有@Nonnull
註釋,我們需要添加額外的邏輯來處理所有方法中的 null 情況,除了 Apache Commons 的CompareToBuilder
(它具有原生 null處理內置) 。
六,結論
在本文中,我們學習了在對對象集合進行排序時在多個字段上進行比較的不同方法。
與往常一樣,這些示例的源代碼可在 GitHub 上獲得。