CodeGym /Java 博客 /随机的 /Java 的比较器接口
John Squirrels
第 41 级
San Francisco

Java 的比较器接口

已在 随机的 群组中发布
懒惰的人并不是唯一用 Java 编写比较器和比较的人。我并不懒惰,所以请喜欢并抱怨另一种解释。我希望这不会是多余的。是的,这篇文章就是对这个问题的回答:“你能从记忆中写出一个比较器吗? ”我希望每个人在阅读这篇文章后都能从记忆中写出一个比较器。 Javas 比较器接口 - 1

介绍

如您所知,Java 是一种面向对象的语言。因此,习惯于在 Java 中操作对象。但迟早,您将面临根据某些特征比较对象的任务。 例如:假设我们有Message类描述的一些消息:

public static class Message {
    private String message;
    private int id;
        
    public Message(String message) {
        this.message = message;
        this.id = new Random().nextInt(1000);
    }
    public String getMessage() {
        return message;
    }
    public Integer getId() {
        return id;
    }
    public String toString() {
        return "[" + id + "] " + message;
    }
}
将此类放入Tutorialspoint Java 编译器中。不要忘记添加导入语句:

import java.util.Random;
import java.util.ArrayList;
import java.util.List;
在该main方法中,创建几条消息:

public static void main(String[] args){
    List<Message> messages = new ArrayList();
    messages.add(new Message("Hello, World!"));
    messages.add(new Message("Hello, Sun!"));
    System.out.println(messages);
}
我们想一想,如果我们要比较它们,我们会怎么做?比如我们想按id排序。为了创建一个顺序,我们需要以某种方式比较对象,以便了解哪个对象应该先出现(即较小的),哪个对象应该在后面(即较大的)。让我们从像java.lang.Object这样的类开始。我们知道所有的类都隐式继承了Object类。这是有道理的,因为它反映了“一切都是对象”的概念,并为所有类提供了共同的行为。这个类规定每个类都有两个方法:→hashCodehashCode方法返回一些数字(int) 对象的表示。这意味着什么?这意味着如果您创建一个类的两个不同实例,那么它们应该具有不同的hashCodes。该方法的描述尽可能多:“在相当实用的情况下,类 Object 定义的 hashCode 方法确实为不同的对象返回不同的整数”。也就是说,对于两个不同的instances,应该有不同的hashCodes。也就是说,这种方法不适合我们的比较。→ equals。该equals方法回答了“这些对象是否相等?”的问题。并返回一个boolean." 默认情况下,此方法具有以下代码:

public boolean equals(Object obj) {
    return (this == obj);
}
也就是说,如果这个方法没有被覆盖,它本质上是说对象引用是否匹配。这不是我们想要的消息,因为我们对消息 ID 感兴趣,而不是对象引用。即使我们覆盖了这个equals方法,我们最多也只能希望知道它们是否相等。这还不足以让我们确定顺序。那我们需要什么呢?我们需要比较的东西。比较的是a Comparator。打开Java API并找到Comparator。确实有java.util.Comparator接口 java.util.Comparator and java.util.Comparable 如您所见,存在这样的接口。实现它的类说:“我实现了一个比较对象的方法。” 您真正需要记住的唯一一件事是比较器合约,其表达如下:

Comparator returns an int according to the following rules: 
  • It returns a negative int if the first object is smaller
  • It returns a positive int if the first object is larger
  • It returns zero if the objects are equal
现在让我们写一个比较器。我们需要导入java.util.Comparator. 在 import 语句之后,将以下内容添加到方法中mainComparator<Message> comparator = new Comparator<Message>(); 当然,这行不通,因为Comparator是一个接口。{}所以我们在括号后加上大括号。在大括号内写下如下方法:

public int compare(Message o1, Message o2) {
    return o1.getId().compareTo(o2.getId());
}
您甚至不需要记住拼写。比较器是执行比较的人,也就是说,它进行比较。为了指示对象的相对顺序,我们返回一个int. 基本上就是这样。好,易于。从例子中可以看出,除了 Comparator 之外,还有一个接口—— java.lang.Comparable,需要我们实现该compareTo方法。这个接口说,“一个实现我的类可以比较类的实例。” 例如ToInteger的实现compare如下:

(x < y) ? -1 : ((x == y) ? 0 : 1)
Java 8 引入了一些不错的变化。如果您仔细查看界面Comparator,您会看到@FunctionalInterface其上方的注释。此注释仅供参考,告诉我们此接口是可用的。也就是说这个接口只有1个抽象方法,也就是没有实现的方法。这给了我们什么?现在我们可以像这样编写比较器的代码:

Comparator<Message> comparator = (o1, o2) -> o1.getId().compareTo(o2.getId());
我们在括号中命名变量。Java会看到,因为只有一个方法,那么需要输入参数的个数和类型就很清楚了。然后我们使用箭头运算符将它们传递给这部分代码。更重要的是,多亏了 Java 8,我们现在在接口中有了默认方法。当我们实现一个接口时,这些方法默认出现。界面Comparator有几个。例如:

Comparator moreImportant = Comparator.reverseOrder();
Comparator lessImportant = Comparator.naturalOrder();
还有另一种方法可以使您的代码更清晰。看看上面的示例,我们在其中定义了我们的比较器。它有什么作用?这很原始。它只是获取一个对象并提取一些“可比较”的值。例如,Integerimplements comparable,因此我们能够对消息 id 字段的值执行 compareTo 操作。这个简单的比较器函数可以这样写:

Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
换句话说,我们有一个Comparator这样比较的对象:它获取对象,使用getId()方法从对象中获取 a Comparable,然后使用对象compareTo进行比较。并且没有更多可怕的结构。最后,我想再提一个特点。比较器可以链接起来。例如:

Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
comparator = comparator.thenComparing(obj -> obj.getMessage().length());

应用

声明一个比较器被证明是合乎逻辑的,你不觉得吗?现在我们需要看看如何以及在哪里使用它。→Collections.sort(java.util.Collections) 当然,我们可以用这种方式对集合进行排序。但不是每一个集合,只有列表。这里没有什么不寻常的,因为列表是一种集合,您可以在其中通过索引访问元素。这允许第二个元素与第三个元素交换。这就是为什么下面的排序方法只适用于列表:

Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
Collections.sort(messages, comparator);
Arrays.sort(java.util.Arrays) 数组也很容易排序。同样,出于同样的原因——它们的元素是通过索引访问的。→Descendants of java.util.SortedSet and java.util.SortedMap 你会记得,Set并且Map不保证元素存储的顺序。但是,我们有特殊的实现来保证顺序。如果集合的元素没有实现java.util.Comparable,那么我们可以将 a 传递Comparator给它的构造函数:

Set<Message> msgSet = new TreeSet(comparator);
Stream API 在 Java 8 中出现的 Stream API 中,比较器让您可以简化流元素的工作。例如,假设我们需要一个从 0 到 999 的随机数序列,包括:

Supplier<Integer> randomizer = () -> new Random().nextInt(1000);
Stream.generate(randomizer)
    .limit(10)
    .sorted(Comparator.naturalOrder())
    .forEach(e -> System.out.println(e));
我们可以到此为止,但还有更有趣的问题。例如,假设您需要准备一个Map,其中 key 是消息 id。此外,我们要对这些键进行排序,因此我们将从以下代码开始:

Map<Integer, Message> collected = Arrays.stream(messages)
                .sorted(Comparator.comparing(msg -> msg.getId()))
                .collect(Collectors.toMap(msg -> msg.getId(), msg -> msg));
我们实际上得到了HashMap这里。正如我们所知,它不保证任何顺序。结果,我们按 id 排序的元素完全失去了它们的顺序。不好。我们必须稍微改变我们的收集器:

Map<Integer, Message> collected = Arrays.stream(messages)
                .sorted(Comparator.comparing(msg -> msg.getId()))
                .collect(Collectors.toMap(msg -> msg.getId(), msg -> msg, (oldValue, newValue) -> oldValue, TreeMap::new));
代码开始看起来有点吓人,但现在问题已经正确解决了。在此处阅读有关各种分组的更多信息: 您可以创建自己的收集器。在此处阅读更多内容:“在 Java 8 中创建自定义收集器”。您将受益于阅读此处的讨论:“Java 8 list to map with stream”

陷阱

Comparator并且Comparable很好。但是您应该记住一个细微差别。当一个类执行排序时,它期望你的类可以转换为一个Comparable. 如果不是这种情况,那么您将在运行时收到错误。让我们看一个例子:

SortedSet<Message> msg = new TreeSet<>();
msg.add(new Message(2, "Developer".getBytes()));
似乎这里没有什么问题。但实际上,在我们的示例中,它会失败并出现错误: java.lang.ClassCastException: Message cannot be cast to java.lang.Comparable 这一切都是因为它试图对元素进行排序(SortedSet毕竟它是一个 )...但不能。使用SortedMapand时不要忘记这一点SortedSet

更多阅读:

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION