使用 Comparator.comparing(HashMap::get) 作为比较器时的意外行为

Posted

技术标签:

【中文标题】使用 Comparator.comparing(HashMap::get) 作为比较器时的意外行为【英文标题】:Unexpected behavior when using Comparator.comparing(HashMap::get) as a comparator 【发布时间】:2020-09-23 18:21:15 【问题描述】:

在https://java-programming.mooc.fi/part-10/2-interface-comparable 上进行“文学”练习时,我在尝试对 HashMap 中的键值对进行排序时发现了一种非常奇怪的行为,而没有将任何内容复制到 TreeMap。我应该通过创建 Book 类并将它们添加到 List 来添加书籍。但是我想尝试不创建新类,所以选择了 HashMap。我的代码如下:

public class MainProgram 

public static void main(String[] args) 
    Scanner scanner = new Scanner(System.in);

    Map<String, Integer> bookshelf = new HashMap<>();
    while (true) 


        System.out.println("Input the name of the book, empty stops: ");
        String bookName = scanner.nextLine();
        if (bookName.equals("")) 
            break;
        
        System.out.println("Input the age recommendation: ");
        int age = Integer.valueOf(scanner.nextLine());

        bookshelf.put(bookName, age);
    

    System.out.println(bookshelf.size() + " book" + (bookshelf.size() > 1 ? "s" : "") + " in total.");

    System.out.println("Books:");

    bookshelf.keySet().stream().sorted(Comparator.comparing(bookshelf::get)).forEach((key) -> System.out.println(key + " (recommended for " + bookshelf.get(key) + " year-olds or older)"));



使用.sorted(Comparator.comparing(bookshelf::get)) 是我按照推荐年龄对它们进行排序的想法,这很有效。

但是,存在一个意外行为,即当书名是单个字符(“A”、“b”)时,程序还会按字母顺序对键进行排序,就好像我做了一个比较器,如 Comparator.comparing(bookshelf::get).thenComparing(/*keys in keyset*/),但有时会也排序为aAbB

AA bb give unsorted results
AAA bbb give semi-sorted results in one or two buckets
AAAA bbbb give semi- or completely sorted results
AAAAA bbbbb and onward give unsorted results.

任何人都可以在编译器级别解释这里发生了什么,或者让我理解一下吗?

【问题讨论】:

不是你的问题的答案,但你为什么不写一本书类并添加一个compareTo() 方法?充分利用面向对象! 您仅根据键对键集进行了排序。如果要排序映射,请使用 entryset() 以避免在迭代时调用。 【参考方案1】:
bookshelf.keySet().stream().sorted(Comparator.comparing(bookshelf::get))

从您示例中的上述 sn-p 中,我们可以看到您正在尝试按其各自的值对 bookshelf 的键进行排序。

这样做的问题是,两个书名可能会映射到同一个年龄推荐。因为您只有一个 Comparator 并且因为 HashMap 没有指定一致的顺序,所以您有机会最终获得相同输入的不同结果。

为了改善这种情况,您可以使用thenComparing 来处理遇到重复值映射的情况:

bookshelf.entrySet()
         .stream()
         .sorted(Map.Entry.<String, Integer>comparingByValue().thenComparing(Map.Entry.comparingByKey()))
         .forEach(entry -> System.out.println(entry.getKey() + " (recommended for " + entry.getValue() + " year-olds or older)"));

【讨论】:

虽然我理解正确的方法,但有没有解释为什么它有点适用于 n=1 并且几乎适用于 n=3 和 n = 4? @MilosCupara 如果您将密钥的长度称为n,那么它适用这些长度只是一个巧合。 HashMap 没有指定对其条目的排序;但是,在您的情况下,当键的长度为134 时,顺序可能相同。但是,如果您继续向键长度相同的Map(超过16)添加更多条目,您将看到它们被重新排序。 附带说明,HashMap 不仅具有不可预测的迭代顺序,它还通过拆分器特性告诉流它是无序的,这允许流实现使用不稳定的排序算法当它认为它有益时。因此,即使在某个时间点感知到的特定迭代顺序也不能保证保留在流中。虽然,afaik,当前的流实现总是使用相同的(稳定的)排序算法。【参考方案2】:

构建 Entry 的 Comparator 并使用Entry::getValueEntry::getKey 按值排序,然后按键

Comparator<Entry<String, Integer>> cmp = Comparator.comparing(Entry::getValue);

bookshelf.entrySet()
         .stream()
         .sorted(cmp.thenComparing(Entry::getKey))
         .forEach(entry -> System.out.println(entry.getKey() + " (recommended for " + entry.getValue() + " year-olds or older)"));

【讨论】:

【参考方案3】:

发生这种情况是因为您只使用“键”进行比较。您应该通过“键”和“值”来比较它们。这应该可以正常工作:

bookshelf.entrySet()
        .stream()
        .sorted(Map.Entry.<String,Integer>comparingByValue()
                .thenComparing(Map.Entry.comparingByKey()))
        .map(e -> e.getKey())
        .forEach((key) -> System.out.println(key + " (recommended for " + bookshelf.get(key) + " year-olds or older)"));

【讨论】:

如果您计划使用bookshelf.get(key) 访问forEach 语句中的值,则map 操作是多余的。你可以简化为.forEach(entry -&gt; use entry.getKey and entry.getValue)

以上是关于使用 Comparator.comparing(HashMap::get) 作为比较器时的意外行为的主要内容,如果未能解决你的问题,请参考以下文章

Stream流的排序用法

Stream流的排序用法

Comparator 排序报 空指针异常

List lambda 排序

java8List.sort()排序常用方法

nullsLast处理比较器Comparator的空值安全问题