手记

Java 比较器接口.

要对一组对象进行排序,Java 提供了 ComparableComparator 接口。在之前的教程中,我通过示例演示了如何使用 Comparable 接口。在这次教程里,我们将通过实际例子来展示如何使用 Comparator 接口。最后,我们会比较这两个接口,并讨论什么时候该用哪个接口。

在开始本教程之前,我建议你先看看我之前的 Comparable<T> 接口教程。要学好 Java 排序,搞清楚 Comparable<T>Comparator<T> 接口的区别,并搞清楚什么时候该用哪个接口。

Java Comparable 接口 Java 语言经常处理对象,并且这些对象经常需要以某种方式排序,例如按某种标准排序……levelup.gitconnected.com](https://levelup.gitconnected.com/java-comparable-interface-3a956d543c78?source=post_page-----1e350c0c706f--------------------------------)

本教程包含许多代码示例,其中一些示例包含可优化的重复代码。我故意这么做,这样可以更好地解释其工作原理。

泛型比较器<T> 接口

Comparator<T> 接口只包含一个抽象方法,这种方法被 @FunctionalInterface 注解标注,并且 适合 通过 lambda 表达式来实现。

/**

* 比较两个对象,返回值表示它们的顺序。
 */
public int compare(对象1, 对象2);

这个界面包含几个静态方法和默认方法,我稍后会在本教程中详细讲解这些方法。请稍等一下。

compare 方法通常返回一个整数值,这个整数值具有以下含义:

  • -1 表示第一个对象小于第二个对象。
  • 0 表示第一个对象等于第二个对象。
  • 1 表示第一个对象大于第二个对象。

Java的官方文档并没有明确指出compareTo方法必须返回的具体正整数和负整数。1和-1常被作为示例使用,不过,像Integer类这样的许多标准Java实现通常使用这些值,如你所见,这样的约定与Comparable<T>.compareTo()方法类似。

该方法会遇到以下错误:

  • NullPointerException:如果参数是空的,并且这个比较器不接受空参数。
  • ClassCastException:参数类型不兼容,导致无法进行比较。

compare方法实现时必须遵循这些规则。

  • 反对称性:对于任何两个对象,如果第一个对象小于、等于或大于第二个对象,那么第二个对象必须分别大于、等于或小于第一个对象。
当 compare(obj1, obj2) 的结果为 -1 时,compare(obj2, obj1) 的结果会是 1;
当 compare(obj1, obj2) 的结果为 0 时,compare(obj2, obj1) 的结果也会是 0;
当 compare(obj1, obj2) 的结果为 1 时,compare(obj2, obj1) 的结果会是 -1。
  • 传递性 :对于任何三个对象,如果第一个对象大于第二个来说,第二个大于第三个,那么第一个也大于第三个,等等。
compare(obj1, obj2) 返回 -1; compare(obj2, obj3) 返回 -1; => compare(obj1, obj3) 返回 -1  
compare(obj1, obj2) 返回 1; compare(obj2, obj3) 返回 1; => compare(obj1, obj3) 返回 1

这确保了比较关系的传递性。

  • 一致性:如果通过compare方法判定两个对象相等,它们在与第三个对象比较时应该表现出相同的行为。
    compare(obj1, obj2) == 0; compare(obj1, obj3) == 1; => compare(obj2, obj3) == 1

注:在比较函数中,通常1表示obj1大于obj3,那么比较obj2和obj3时,obj2大于obj3应该返回1,以保持一致性。根据源代码中的逻辑,compare(obj1, obj2) == 0表示obj1和obj2相等,compare(obj1, obj3) == 1表示obj1大于obj3,因此compare(obj2, obj3) == 1表示obj2大于obj3。

  • equals() 方法的一致性:强烈建议 compare() 方法和 equals() 方法保持一致。

如果 compare(obj1, obj2) 等于 0,就表示 obj1.equals(obj2) 返回 true

开始设置

为了展示Comparator<T>接口的使用方法,下面我定义一个示例类。这个简单的POJO(普通Java对象)类代表一次购买交易。它包含几个可用于比较并最终对从该类实例化的对象进行排序的字段。

包 com.polovyi.ivan.tutorials;

导入 java.math.BigDecimal;  
导入 java.time.LocalDate;  
导入 lombok.AllArgsConstructor;  
导入 lombok.Data;  

@Data  
@AllArgsConstructor  
公共类 PurchaseTransaction {  

    私有 String id;  
    私有 String paymentType;  
    私有 BigDecimal amount;  
    私有 LocalDate createdAt;  
    私有 int cashBack;  
}
实现起来

我们先来创建一个比较函数,用来根据返现量对购买交易进行排序。

package com.polovyi.ivan.tutorials;  

import java.util.Comparator;  

public class 比较器 {  

    public Comparator<购买交易> 返现比较器 =  
            (object1, object2) -> {  
                int diff = object1.get返现() - object2.get返现();  
                return diff == 0 ?  
                        0 :  
                        diff > 0 ?  
                                1 :  
                                -1;  
            };  
    // 这个比较器用于比较购买交易的返现
}

现在我们可以试试了,

    @Test  
    public void testSortByCashBack() {  
        PurchaseTransaction transaction1 = new PurchaseTransaction("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        PurchaseTransaction transaction2 = new PurchaseTransaction("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        PurchaseTransaction transaction3 = new PurchaseTransaction("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        List<PurchaseTransaction> 交易 = new ArrayList<>();  
        交易.add(transaction1);  
        交易.add(transaction2);  
        交易.add(transaction3);  
        System.out.println("排序前的交易记录为:" + 交易);  
        Collections.sort(交易, new Comparator().cashbackComparator());  
        System.out.println("排序后的交易记录为:" + 交易);  
        List<String> idsList = 交易.stream()  
                .map(PurchaseTransaction::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(Arrays.asList("#3", "#1", "#2"), idsList);  
    }

就像你看到的,我把它当作参数传给 Collection.sort() 方法。这样一来,我们的集合就会按现金回馈从低到高排列。

你可能会觉得这个实现有点繁琐,确实如此。正如我之前提到的,Comparator<T>接口是一个函数式接口,允许我们将实现直接传递给排序函数。我们也可以利用Integer类中已有的方法来简化实现,简化实现本身。

    @Test  
    public void testSortByCashBack_Direct_Impl() {  
        购买交易1 = new 购买交易("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        购买交易2 = new 购买交易("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        购买交易3 = new 购买交易("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        List<购买交易> 交易列表 = new ArrayList<>();  
        交易列表.add(购买交易1);  
        交易列表.add(购买交易2);  
        交易列表.add(购买交易3);  
        System.out.println("在排序前的交易 = " + 交易列表);  
        Collections.sort(交易列表, (object1, object2) ->  
                Integer.compare(object1.getCashBack(), object2.getCashBack()));  
        System.out.println("在排序后的交易 = " + 交易列表);  
        List<String> id列表 = 交易列表.stream()  
                .map(购买交易::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#1", "#2"), id列表);  
    }  

    @Test  
    public void testSortByCashBack_Arrays_Sort() {  
        购买交易1 = new 购买交易("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        购买交易2 = new 购买交易("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        购买交易3 = new 购买交易("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        购买交易[] 交易 = {购买交易1, 购买交易2, 购买交易3};  
        System.out.println("在排序前的交易 = " + Arrays.toString(交易));  
        Arrays.sort(交易, new 比较器().现金回扣比较器);  
        System.out.println("在排序后的交易 = " + 交易);  
        List<String> id列表 = Arrays.stream(交易).map(购买交易::getId).collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#1", "#2"), id列表);  
    }  

    @Test  
    public void testSortByCashBack_Arrays_Sort_Direct_Impl() {  
        购买交易1 = new 购买交易("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        购买交易2 = new 购买交易("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        购买交易3 = new 购买交易("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        购买交易[] 交易 = {购买交易1, 购买交易2, 购买交易3};  
        System.out.println("在排序前的交易 = " + Arrays.toString(交易));  
        Arrays.sort(交易, (object1, object2) ->  
                Integer.compare(object1.getCashBack(), object2.getCashBack()));  
        System.out.println("在排序后的交易 = " + 交易);  
        List<String> id列表 = Arrays.stream(交易).map(购买交易::getId).collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#1", "#2"), id列表);  
    }  

    @Test  
    public void testSortByCashBack_Stream() {  
        购买交易1 = new 购买交易("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        购买交易2 = new 购买交易("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        购买交易3 = new 购买交易("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        List<购买交易> 排序列表 = Stream.of(购买交易1, 购买交易2, 购买交易3)  
                .sorted(new 比较器().现金回扣比较器)  
                .collect(Collectors.toUnmodifiableList());  
        System.out.println("在排序后的交易 = " + 排序列表);  
        List<String> id列表 = 排序列表.stream().map(购买交易::getId).collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#1", "#2"), id列表);  
    }  

    @Test  
    public void testSortByCashBack_Stream_Direct_Impl() {  
        购买交易1 = new 购买交易("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        购买交易2 = new 购买交易("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        购买交易3 = new 购买交易("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        List<购买交易> 排序列表 = Stream.of(购买交易1, 购买交易2, 购买交易3)  
                .sorted((object1, object2) ->  
                        Integer.compare(object1.getCashBack(), object2.getCashBack()))  
                .collect(Collectors.toUnmodifiableList());  
        System.out.println("在排序后的交易 = " + 排序列表);  
        List<String> id列表 = 排序列表.stream().map(购买交易::getId).collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#1", "#2"), id列表);  
    }  

    @Test  
    public void testSortByCashBack_TreeSet() {  
        购买交易1 = new 购买交易("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        购买交易2 = new 购买交易("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        购买交易3 = new 购买交易("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        Set<购买交易> 交易 = new TreeSet<>(new 比较器().现金回扣比较器);  
        交易.addAll(Set.of(购买交易1, 购买交易2, 购买交易3));  
        System.out.println("交易 = " + 交易);  
        List<String> id列表 = 交易.stream().map(购买交易::getId).collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#1", "#2"), id列表);  
    }  

    @Test  
    public void testSortByCashBack_TreeSet_Direct_Impl() {  
        购买交易1 = new 购买交易("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        购买交易2 = new 购买交易("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        购买交易3 = new 购买交易("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        Set<购买交易> 交易 = new TreeSet<>((object1, object2) ->  
                Integer.compare(object1.getCashBack(), object2.getCashBack()));  
        Set<购买交易> 未排序的交易 = Set.of(购买交易1, 购买交易2, 购买交易3);  
        System.out.println("在排序前的交易 = " + 未排序的交易);  
        交易.addAll(未排序的交易);  
        System.out.println("在排序后的交易 = " + 交易);  
        List<String> id列表 = 交易.stream().map(购买交易::getId).collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#1", "#2"), id列表);  
    }

如下面一章中所述,我们可以通过利用静态和缺省方法来更简单地简化它。

默认方法

Comparator<T>接口包含了一系列默认方法。我们一个个来看看。

    泛型 Comparator<T> 反转方法: {...}

我们从第一个方法开始,称为 reversed()。这个方法可以在比较器上使用,来反转排序顺序。它会将排序变成相反的排序。

(object1, object2) ->  
    Integer.compare(object1.getCashBack(), object2.getCashBack())

就这样:

(object1, object2) -> Integer.compare(object2.get返现(), object1.get返现())

我们现在可以试试了,

    @Test  
    public void testSortByCashBack_Reversed_Method() {  
        Transaction transaction1 = new Transaction("#1", "VISA", new BigDecimal("10"), LocalDate.now(), 2);  
        Transaction transaction2 = new Transaction("#2", "MASTER", new BigDecimal("2"), LocalDate.now(), 3);  
        Transaction transaction3 = new Transaction("#3", "AMEX", new BigDecimal("1"), LocalDate.now(), 1);  
        List<Transaction> transactions = new ArrayList<>();  
        transactions.add(transaction1);  
        transactions.add(transaction2);  
        transactions.add(transaction3);  
        System.out.println("交易 BEFORE 排序 = " + transactions);  

        Comparator<Transaction> cashbackComparator = (object1, object2) ->  
                Integer.compare(object1.getCashBack(), object2.getCashBack());  
        Collections.sort(transactions, cashbackComparator.reversed());  

        System.out.println("交易 AFTER 排序 = " + transactions);  
        List<String> idsList = transactions.stream()  
                .map(Transaction::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#2", "#1", "#3"), idsList);  
    }

下面的方法:

Comparator<T> thenComparing(Comparator<? super T> other) {...} 默认

这种方法允许你将两个比较器结合起来。首先,它用调用的比较器来比较两个对象。如果第一个比较器认为两者相等(返回0),则使用作为参数提供的第二个比较器。这种方法让你可以通过多个字段来比较对象。

在下面的测试里,我结合了两个比较器来对集合进行排序的。先按cashBack字段排序,再按createdAt字段排序的顺序。

    @Test  
    public void testSortByCashBack_ThenComparing_Method() {  
        PurchaseTransaction transaction1 = new PurchaseTransaction("#1", "VISA", BigDecimal.TEN, LocalDate.now().minus(1,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction2 = new PurchaseTransaction("#2", "MASTER", BigDecimal.TWO, LocalDate.now().minus(10,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction3 = new PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.now().minus(5,  
                ChronoUnit.DAYS), 1);  
        List<PurchaseTransaction> transactions = new ArrayList<>();  
        transactions.add(transaction1);  
        transactions.add(transaction2);  
        transactions.add(transaction3);  
        System.out.println("交易 BEFORE 排序 = " + transactions);  

        Comparator<PurchaseTransaction> cashBackComparator = (object1, object2) ->  
                Integer.compare(object1.getCashBack(), object2.getCashBack());  
        Comparator<PurchaseTransaction> purchaseTransactionComparator = cashBackComparator.thenComparing((object1, object2) ->  
                object1.getCreatedAt().compareTo(object2.getCreatedAt()));  
        Collections.sort(transactions, purchaseTransactionComparator);  

        System.out.println("交易 AFTER 排序 = " + transactions);  
        List<String> idsList = transactions.stream()  
                .map(PurchaseTransaction::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#2", "#3", "#1"), idsList);  
    }

thenComparing 方法有兩個重載版本,它們的邏輯都是一樣的。首先,使用調用它的比較器來比較兩個對象。如果第一個比較器判定兩個對象相等(返回 0),則使用作為參數傳入的第二個比較器來進行比較。

接下来的方法接受一个函数作为参数,该函数应用于比较器接收的对象。这使得可以根据提供的函数提取出的特定键来进行比较,比较器通过方法参数接收。该方法的签名如下所示:

Comparator<T> thenComparing(  
                Function<? super T, ? extends U> keyExtractor,  
                Comparator<? super U> keyComparator) {...}

实际上,它是这样操作的:

用 keyExtractor 对 comparator1 的 keyComparator 进行比较。

大概会是这样的:

比较器1.compare(对象1, 对象2);
键比较器.compare(键提取器.apply(对象1), 键提取器.apply(对象2));

假设我们需要根据 createdAt 字段中的星期几来对一个集合进行排序。为此,我们需要传递一个函数给比较函数,以从 createdAt 字段中提取星期几,并在后续的比较中使用。

    @Test  
    public void testSortByCashBack_ThenComparing_Method_2() {  
        PurchaseTransaction transaction1 = new PurchaseTransaction("#1", "visa", BigDecimal.TEN, LocalDate.of(2024,7,13).minus(1,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction2 = new PurchaseTransaction("#2", "Master", BigDecimal.TWO, LocalDate.of(2024,7,13).minus(10,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction3 = new PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.of(2024,7,13).minus(5,  
                ChronoUnit.DAYS), 1);  
        List<PurchaseTransaction> transactions = new ArrayList<>();  
        transactions.add(transaction1);  
        transactions.add(transaction2);  
        transactions.add(transaction3);  
        System.out.println("Transactions BEFORE sort = " + transactions);  

        Comparator<PurchaseTransaction> cashbackComparator = (object1, object2) ->  
                Integer.compare(object1.getCashBack(), object2.getCashBack());  
        Comparator<PurchaseTransaction> purchaseTransactionComparator = cashbackComparator  
                .thenComparing(s -> s.getCreatedAt().get(ChronoField.DAY_OF_WEEK), (createdAt1, createdAt2) ->  
                        createdAt1.compareTo(createdAt2));  

        Collections.sort(transactions, purchaseTransactionComparator);  

        System.out.println("Transactions AFTER sort = " + transactions);  
        List<String> idsList = transactions.stream()  
                .map(PurchaseTransaction::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#2", "#1"), idsList);  
    }

thenComparing的第二个重载版本接收一个函数作为参数。

    default <U extends Comparable<? super U>> Comparator<T> thenComparing(  
                Function<? super T, ? extends U> keyExtractor) {...}

实际上,它是这样工作的。

 comparator1 然后根据 keyExtractor 进行比较;

比较过程如下所示:

比较器1比较object1和object2; 键提取器应用到object1的结果与键提取器应用到object2的结果进行比较。

这种方法允许我们仅传递一个提取字段的函数,从而使前面的例子变得更简单。

    @Test  
    public void testSortByCashBack_ThenComparing_Method_3() {  
        PurchaseTransaction transaction1 = new PurchaseTransaction("#1", "visa", BigDecimal.TEN, LocalDate.of(2024,7,13).minus(1,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction2 = new PurchaseTransaction("#2", "Master", BigDecimal.TWO, LocalDate.of(2024,7,13).minus(10,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction3 = new PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.of(2024,7,13).minus(5,  
                ChronoUnit.DAYS), 1);  
        List<PurchaseTransaction> 购买交易 = new ArrayList<>();  
        购买交易.add(transaction1);  
        购买交易.add(transaction2);  
        购买交易.add(transaction3);  
        System.out.println("交易 BEFORE 排序 = " + 购买交易);  

        Comparator<PurchaseTransaction> 现金回馈比较器 = (object1, object2) ->  
                Integer.compare(object1.getCashBack(), object2.getCashBack());  
        Comparator<PurchaseTransaction> 交易比较器 = 现金回馈比较器  
                .thenComparing(s -> s.getCreatedAt().get(ChronoField.DAY_OF_WEEK));  

        Collections.sort(购买交易, 交易比较器);  

        System.out.println("排序后的交易 = " + 购买交易);  
        List<String> idsList = 购买交易.stream()  
                .map(PurchaseTransaction::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#2", "#1"), idsList);  
    }

以下方法专为处理基础数据类型而设计。

提供默认的 Comparator<T> thenComparingInt 方法,该方法接受一个 ToIntFunction<? super T> 类型的参数 keyExtractor {...}  
提供默认的 Comparator<T> thenComparingLong 方法,该方法接受一个 ToLongFunction<? super T> 类型的参数 keyExtractor {...}  
提供默认的 Comparator<T> thenComparingDouble 方法,该方法接受一个 ToDoubleFunction<? super T> 类型的参数 keyExtractor {...}

例如,我们可以使用它来按星期几对集合进行排序,因为星期可以用一个整数表示。因此,我们可以使用 thenComparingToInt 方法。

    @Test  
    public void testSortByCashBack_ThenComparing_Primitive() {  
        PurchaseTransaction transaction1 = new PurchaseTransaction("#1", "visa", BigDecimal.TEN, LocalDate.of(2024,7,13).minus(1,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction2 = new PurchaseTransaction("#2", "Master", BigDecimal.TWO, LocalDate.of(2024,7,13).minus(10,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction3 = new PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.of(2024,7,13).minus(5,  
                ChronoUnit.DAYS), 1);  
        List<PurchaseTransaction> transactions = new ArrayList<>();  
        transactions.add(transaction1);  
        transactions.add(transaction2);  
        transactions.add(transaction3);  
        System.out.println("排序前的交易 = " + transactions);  

        Comparator<PurchaseTransaction> 现金回馈比较器 = (object1, object2) ->  
                Integer.compare(object1.getCashBack(), object2.getCashBack());  
        Comparator<PurchaseTransaction> 购买交易排序比较器 = 现金回馈比较器  
                .thenComparingInt(s -> s.getCreatedAt().get(ChronoField.星期));  

        Collections.sort(transactions, 购买交易排序比较器);  

        System.out.println("排序后的交易 = " + transactions);  
        List<String> idsList = transactions.stream()  
                .map(PurchaseTransaction::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#2", "#1"), idsList);  
    }
静态函数

除了可以使用比较器的默认方法外,还有一些可以在没有比较器实例的情况下直接使用的静态方法。

Comparator<T> 接口提供了两个版本的比较方法,分别是。第一个版本接收一个函数和一个比较器。它将该函数应用于要进行比较的对象,然后根据结果进行比较。

    public static <T, U> Comparator<T> comparing(  
            Function<? super T, ? extends U> keyExtractor,  
            Comparator<? super U> keyComparator) {...}

我们可以创建一个比较器来对集合按createdAtr字段的星期几进行排序。

    @Test  
    public void testSortByCashBack_Comparing_Method() {  
        PurchaseTransaction transaction1 = new PurchaseTransaction("#1", "visa", BigDecimal.TEN, LocalDate.of(2024,7,13).minus(1,  
                ChronoUnit.DAYS), 3);  
        PurchaseTransaction transaction2 = new PurchaseTransaction("#2", "Master", BigDecimal.TWO, LocalDate.of(2024,7,13).minus(10,  
                ChronoUnit.DAYS), 1);  
        PurchaseTransaction transaction3 = new PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.of(2024,7,13).minus(5,  
                ChronoUnit.DAYS), 2);  
        List<PurchaseTransaction> transactions = new ArrayList<>();  
        transactions.add(transaction1);  
        transactions.add(transaction2);  
        transactions.add(transaction3);  
        System.out.println("交易记录 在 排序 之前 = " + transactions);  

        Comparator<PurchaseTransaction> comparator = Comparator.comparing(s -> s.getCreatedAt().get(ChronoField.DAY_OF_WEEK),  
                (createdAt1, createdAt2) ->  
                        createdAt1.compareTo(createdAt2));  
        Collections.sort(transactions, comparator);  

        System.out.println("交易记录 在 排序 之后 = " + transactions);  
        List<String> idsList = transactions.stream()  
                .map(PurchaseTransaction::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#2", "#1"), idsList);  
    }

第二个重载方法的签名如下:

定义一个静态方法,接收两个泛型类型 T 和 U(U 必须是实现了 Comparable 接口的泛型类型),该方法返回一个比较器 Comparator<T>。这个方法接收一个函数式接口 Function 的实现,其中 Function 的泛型参数是 T 和 U 的子类型。

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) {...}

// 这是一个静态方法,接收一个泛型参数,返回一个比较器。
// 这个方法接收一个函数式接口 Function 的实现,用来从类型 T 的对象中提取类型 U 的键,从而进行比较。


这种方法将接收的函数应用到对象上,然后对它们进行比较并按自然排列顺序进行排序。
@Test  
public void testSortByCashBack_Comparing_Method_2() {  
    购买记录 transaction1 = new 购买记录("#1", "visa", BigDecimal.TEN, LocalDate.of(2024,7,13).minus(1,  
            ChronoUnit.DAYS), 3);  
    购买记录 transaction2 = new 购买记录("#2", "Master", BigDecimal.TWO, LocalDate.of(2024,7,13).minus(10,  
            ChronoUnit.DAYS), 1);  
    购买记录 transaction3 = new 购买记录("#3", "AMEX", BigDecimal.ONE, LocalDate.of(2024,7,13).minus(5,  
            ChronoUnit.DAYS), 2);  
    List<购买记录> 交易 = new ArrayList<>();  
    交易.add(transaction1);  
    交易.add(transaction2);  
    交易.add(transaction3);  
    System.out.println("排序前 = " + 交易);  

    Comparator<购买记录> comparator = Comparator.comparing(s -> s.getCreatedAt().get(ChronoField.DAY_OF_WEEK));  
    Collections.sort(交易, comparator);  

    System.out.println("排序后 = " + 交易);  
    List<String> id列表 = 交易.stream()  
            .map(购买记录::getId)  
            .collect(Collectors.toUnmodifiableList());  
    assertEquals(Arrays.asList("#3", "#2", "#1"), id列表);  
}

The `reverseOrder` 方法,顾名思义,会反转排序顺序,它与默认的 `reversed` 方法功能相同。`naturalOrder` 方法定义自然排序顺序。`reverseOrder` 方法与 `naturalOrder` 方法相对。
/**
  • 返回一个逆序的比较器,该比较器根据自然顺序将元素从高到低排列。
    */
    public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {...}
    /**

  • 返回一个按照自然顺序排列的比较器,该比较器将元素从低到高排列。
    */
    public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {...}

现在,我们可以将这种方法与比较方法结合,像这样:

    测试  
    public void testSortByCashBack_ReverseOrder_Method() {  
        购买交易记录 transaction1 = new 购买交易记录("#1", "VISA", BigDecimal.TEN, LocalDate.now(), 2);  
        购买交易记录 transaction2 = new 购买交易记录("#2", "MASTER", BigDecimal.TWO, LocalDate.now(), 3);  
        购买交易记录 transaction3 = new 购买交易记录("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1);  
        List<购买交易记录> 交易列表 = new ArrayList<>();  
        交易列表.add(transaction1);  
        交易列表.add(transaction2);  
        交易列表.add(transaction3);  
        System.out.println("交易列表在排序前 = " + 交易列表);  

        Comparator<购买交易记录> 比较器 = Comparator.comparing(PurchaseTransaction::getCashBack,  
                Comparator.reverseOrder());  
        Collections.sort(交易列表, 比较器);  

        System.out.println("交易列表在排序后 = " + 交易列表);  
        List<String> idsList = 交易列表.stream()  
                .map(购买交易记录::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(Arrays.asList("#2", "#1", "#3"), idsList);
        // 测试结果与预期一致

以下方法正确处理了NULL值:第一个方法将NULL值置于首位,而最后一个方法则将NULL值置于末尾。

public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {...}  
// 返回一个比较器,将null值排在前面

public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {...}  
// 返回一个比较器,将null值排在后面

假如我们知道有些字段允许为空,就可以用这些方法了。

    @Test  
    public void testSortByCashBack_NullLast_Method() {  
        采购交易 transaction1 = new 采购交易("#1", "VISA", null, LocalDate.now(), 2);  
        采购交易 transaction2 = new 采购交易("#2", "MASTER", BigDecimal.TWO, LocalDate.now(), 3);  
        采购交易 transaction3 = new 采购交易("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1);  
        List<采购交易> 购买交易列表 = new ArrayList<>();  
        购买交易列表.add(transaction1);  
        购买交易列表.add(transaction2);  
        购买交易列表.add(transaction3);  
        System.out.println("排序前的交易 = " + 购买交易列表);  

        Comparator<采购交易> comparator = Comparator.comparing(采购交易::getAmount,  
                Comparator.nullsLast(Comparator.naturalOrder()));  
        Collections.sort(购买交易列表, comparator);  

        System.out.println("排序后的交易 = " + 购买交易列表);  
        List<String> idsList = 购买交易列表.stream()  
                .map(采购交易::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#2", "#1"), idsList);  
    }

有专门设计的函数用于比较基本数据类型。

public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {...}  
// 按照提供的整数提取器对元素进行比较
public static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor) {...}  
// 按照提供的长整数提取器对元素进行比较
public static<T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor) {...}  
// 按照提供的双精度提取器对元素进行比较

我们可以在以下情况下使用它:

    @Test  
    public void testSortByCashBack_ComparingInt() {  
        购买交易 transaction1 = new 购买交易("#1", "VISA", BigDecimal.TEN, LocalDate.now(), 2);  
        购买交易 transaction2 = new 购买交易("#2", "MASTER", BigDecimal.TWO, LocalDate.now(), 3);  
        购买交易 transaction3 = new 购买交易("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1);  
        List<购买交易> 交易列表 = new ArrayList<>();  
        交易列表.add(transaction1);  
        交易列表.add(transaction2);  
        交易列表.add(transaction3);  
        System.out.println("排序前的交易 = " + 交易列表);  

        Collections.sort(交易列表,   
                      Comparator.comparingInt(PurchaseTransaction::getCashBack));  

        System.out.println("排序后的交易 = " + 交易列表);  
        List<String> idsList = 交易列表.stream()  
                .map(PurchaseTransaction::getId)  
                .collect(Collectors.toUnmodifiableList());  
        assertEquals(List.of("#3", "#1", "#2"), idsList);  
    }
方法链:链式调用

我们可以链式调用 Comparator<T> 接口的方法,以增强代码的可读性和简洁性。通过创建一个实现了 Comparable<T> 接口的类,我们可以在实现比较方法时使用 Comparator<T> 接口的方法,如下:

    包 com.polovyi.ivan.tutorials;  

    导入 java.math.BigDecimal;  
    导入 java.time.LocalDate;  
    导入 java.util.Comparator;  
    导入 lombok.AllArgsConstructor;  
    导入 lombok.Data;  

    @Data  
    @AllArgsConstructor  
    公开 类 A_PurchaseTransaction 实现 Comparable<A_PurchaseTransaction> 接口 {  

        私有 String id;  
        私有 String paymentType;  
        私有 BigDecimal amount;  
        私有 LocalDate createdAt;  
        私有 int 现金回赠;  

        @Override  
        公开 int compareTo(A_PurchaseTransaction pt) {  
            返回 Comparator.comparingInt(A_PurchaseTransaction::getCashBack)  
                    .然后比较(A_PurchaseTransaction::getCreatedAt)  
                    .然后比较(A_PurchaseTransaction::getAmount)  
                    .然后比较(A_PurchaseTransaction::getPaymentType)  
                    .比较(this, pt);  
        }  

    //    @Override  
    //    公开 int compareTo(A_PurchaseTransaction pt) {  
    //        如果(this.getCashBack() != pt.getCashBack())  
    //            返回 Integer.compare(this.getCashBack(), pt.getCashBack());  
    //        如果(!this.getCreatedAt().equals(pt.getCreatedAt()))  
    //            返回 this.getCreatedAt().compareTo(pt.getCreatedAt());  
    //        如果(!this.getAmount().equals(pt.getAmount()))  
    //            返回 this.getAmount().compareTo(pt.getAmount());  
    //        返回 this.getPaymentType().compareTo(pt.getPaymentType());  
    //    }  

    }

我注释了这个没有使用Comparator的方法实现,这样你可以看到差异。通过调整链式调用,我们可以改变优先顺序。在这个例子中,排序将首先按cashback字段,然后按createdAt字段,等等。

可比较和流畅

你很可能在流中直接使用Comparator<T>。在这章里,我将通过一些例子来说明如何使用它。

var example1 = Stream.of(3, 2, 1)  
        .sorted()  
        .collect(Collectors.toList());  
// 示例1 = [1, 2, 3]  

var example2 = Stream.of(3, 2, 1)  
        .sorted(Comparator.reverseOrder())  // 按降序排列
        .collect(Collectors.toList());  
// 示例2 = [3, 2, 1]

这只是一个简单的例子,不需要解释太多。

以下的例子对这些字符串进行了排序。String 类包含了一个可以执行大小写不敏感排序的比较器实现。

    var example3 = Stream.of("A", "B", "C")  
            .sorted(Comparator.reverseOrder())  // 这里我们反转排序
            .collect(Collectors.toList());  
    // example3 的结果为 [C, B, A]  

    var example4 = Stream.of("A", "b", "C")  
            .sorted(String.CASE_INSENSITIVE_ORDER.reversed())  // 使用不区分大小写的逆序进行排序
            .collect(Collectors.toList());  
    // example4 的结果为 [C, b, A]

这是一个更复杂的例子,使用对象的某个字段。我们结合使用了自然排序和空值友好的比较方式。

var example5 = Stream.of(new PurchaseTransaction("#1", "visa", new BigDecimal("十"), null, 3),  
                    new PurchaseTransaction("#2", "Master", new BigDecimal("二"), LocalDate.now().minus(10,  
                            ChronoUnit.DAYS), 1),  
                    new PurchaseTransaction("#3", "AMEX", new BigDecimal("一"), LocalDate.now().minus(5,  
                            ChronoUnit.DAYS), 2))  
            .sorted(Comparator.comparing(PurchaseTransaction::getCreatedAt,  
                    Comparator.nullsLast(Comparator.naturalOrder())))  
            .map(PurchaseTransaction::getId)  
            .collect(Collectors.toList());  
//  example5 = [#2, #3, #1]

// 例子5变量用来收集经过排序和映射后的购买交易的ID,结果为列表 ['#2', '#3', '#1']。

我们甚至可以创建这样的疯狂实现方式来根据对象的多个字段来排序,不过这样做我不建议,因为这样做会显得太繁琐和冗长。尽管如此,我并不建议这样做。

var example6 = Stream.of(new PurchaseTransaction("#1", "VISA", BigDecimal.ONE,  
                            LocalDate.now(), 1),  
                    new PurchaseTransaction("#2", "MASTER", BigDecimal.ONE, LocalDate.now(), 1),  
                    new PurchaseTransaction("#3", "AMEX", BigDecimal.ONE, LocalDate.now(), 1))  
            .sorted((对象1, 对象2) -> Comparator.comparingInt(PurchaseTransaction::获取现金返还)  
                    .thenComparing(PurchaseTransaction::获取创建时间)  
                    .thenComparing(PurchaseTransaction::获取金额)  
                    .thenComparing(PurchaseTransaction::获取支付类型)  
                    .比较(对象1, 对象2))  
            .map(PurchaseTransaction::getId)  
            .collect(Collectors.toList());  
    // example6 的结果为 [#3, #2, #1]

我在這裡使用了var關鍵字。更多詳情請參閱:

Java中的局部变量类型推断功能。使用var。Java 10 引入了一个令人兴奋的新功能,让代码更整洁,提高可读性。不过,也需要注意……medium.com](https://medium.com/javarevisited/local-variable-type-inference-in-java-use-of-var-59beb4f2c764?source=post_page-----1e350c0c706f--------------------------------)

以下示例展示了如何在分组操作中使用比较器,展示了比较器在分组操作中的用法。

Java 流式处理分组汇总Java 流式处理 API 包含了各种收集器。其中最突出的是分组器。它非常实用… 可比较 vs 比较工具

最后,让我们简单总结一下 Comparable<T>Comparator<T>,两者的区别。

  • Comparable 用于自然排序,需要排序的类应当实现它。
  • Comparator 用于自定义排序,可以由单独的类实现,也可以作为一个 lambda 表达式或方法引用来提供。
  • Comparable 影响原始类,提供单一的排序方式。
  • Comparator 更灵活,可以定义多个排序方式,并且这些方式可以定义在类外部。

通过恰当使用这些接口,可以有效地管理和调整在Java应用程序中的对象排序顺序。

可以在这里找到完整的代码:

GitHub - polovyivan/java-comparator-interface在GitHub上创建帐户以加入polovyvan/java-comparator-interface的开发中 结论部分

正如你所见,Comparator 接口允许执行多种操作。由于它是一个函数式接口,我们可以在内联实现它,并且可以将多个比较器链接起来,提高代码的可读性和功能性。你可以根据不同的标准对集合进行排序,优雅地处理空值(null),并结合多个比较器以实现更复杂的排序需求。这使得 Comparator 接口成为开发人员编写清晰、高效且易于维护的 Java 代码的重要工具。

感谢您的阅读!如果您喜欢这篇帖子,欢迎点赞并关注我。如果您有任何问题或建议,欢迎在下方留言或在我的LinkedIn上与我联系。

0人推荐
随时随地看视频
慕课网APP