使用TreeMap的流返回不连贯的结果

时间:2017-01-13 10:02:33

标签: java collections java-8 java-stream

我正在尝试解决以下练习来自" Core Java for the Impatient"作者:Cay Horstmann:

  

当具有部分Unicode覆盖的Charset的编码器无法编码时   它用一个默认值替换它 - 通常但不总是编码"?"。   查找支持编码的所有可用字符集的所有替换。使用   获取编码器的newEncoder方法,并调用其replacement方法获取   更换。对于每个唯一结果,请报告字符集的规范名称   使用它。

为了教育,我决定使用流媒体API解决这个庞大的单线程练习,尽管 - 在我看来 - 一个更清洁的解决方案会将计算分成若干步骤,其中包含中间变量 - 之间(当然它会简化调试)。不用多说,这里有一个我创建的代码怪物:

   Charset.availableCharsets().values().stream().filter(charset -> charset.canEncode()).collect(
            Collectors.groupingBy(
                    charset -> charset.newEncoder().replacement(),
                    () -> new TreeMap<>((arr1, arr2) -> Arrays.equals(arr1, arr2) == true ? 0 : Integer.compare(arr1.hashCode(), arr2.hashCode())),
                    Collectors.mapping( charset -> charset.name(), Collectors.toList()))).
            values().stream().map(list -> list.stream().collect(Collectors.joining(", "))).forEach(System.out::println);

基本上,我们只考虑canEncode的字符集;使用Map作为键创建replacement,并将规范名称列表作为值;因为分组对于使用groupingBy的默认实现HashMap的数组不起作用,所以我决定使用TreeMap。然后,我们使用Lists规范名称,用逗号和打印将它们连接起来。

不幸的是,我发现它会给出不连贯的结果。如果我在同一个程序中运行该函数两次,则第一个实例返回包含23 Strings的结果,第二个实例返回结果 - 仅为21 Strings。我怀疑这与Comparator TreeMap的执行不当有关,其定义如下:

((arr1, arr2) -> Arrays.equals(arr1, arr2) == true ? 0 : Integer.compare(arr1.hashCode(), arr2.hashCode()))

如果这是原因,在这种情况下应该适当的Comparator?除此之外,单线班机能否以任何方式得到改善?

我也很好奇是否在专业程序中遇到了我编写的代码这样错综复杂的结构?也许只有我发现它不可读?

1 个答案:

答案 0 :(得分:2)

无法保证两个不同实例的哈希码不同。这将是一个理想的情况,但永远不会得到保证。恰恰相反:如果两个对象相等,则它们具有相同的哈希码。

因此,如果您创建一个比较器,当它们具有相同的哈希码时将对象视为相同,则可以认为任意对象是相同的。由于byte[]返回的replacement()数组是防御性副本,因此读取临时对象时,结果可能会在此代码的每次运行中发生变化。

此外,由于数组的哈希码与其内容无关,因此比较器违反了传递规则:两个内容相等的数组应该是相同的,但是因为它们可能/很可能具有不同的哈希码,与第三个数组进行比较时,它们具有不同的关系,不具有相同的内容a == b,而是a < cb > c。这就是为什么即使是由Arrays.equals进行比较的相等数组也可能最终出现在不同的组中,因为TreeSet在与其他键进行比较时无法找到现有的键。

如果希望按值比较数组,可以使用:

Charset.availableCharsets().values().stream().filter(Charset::canEncode).collect(
    Collectors.groupingBy(
            charset -> charset.newEncoder().replacement(),
            () -> new TreeMap<>(Comparator.comparing(ByteBuffer::wrap)),
            Collectors.mapping(Charset::name, Collectors.joining(", "))))
    .values().forEach(System.out::println);

ByteBufferComparable并且一致地评估包装数组的内容。

我将Collectors.joining收藏家移动到grouping收藏家,以避免创建临时List,无论如何你将加入其中的内容。

顺便说一下,永远不要使用像expression == true这样的代码。没有理由追加== true因为expression已经足够了。

由于您只对值感兴趣,换句话说,不需要键是某种类型的键,您可以预先包装所有数组,简化操作,甚至使其效率稍高:

Charset.availableCharsets().values().stream().filter(Charset::canEncode).collect(
    Collectors.groupingBy(
            charset -> ByteBuffer.wrap(charset.newEncoder().replacement()),
            TreeMap::new,
            Collectors.mapping(Charset::name, Collectors.joining(", "))))
    .values().forEach(System.out::println);

如果不需要一致的迭代顺序,此更改甚至允许求助于散列:

Charset.availableCharsets().values().stream().filter(Charset::canEncode).collect(
    Collectors.groupingBy(
            charset -> ByteBuffer.wrap(charset.newEncoder().replacement()),
            Collectors.mapping(Charset::name, Collectors.joining(", "))))
    .values().forEach(System.out::println);

这很有效,因为ByteBuffer还实现了equalshashCode