Java 8 - Collections.groupingBy 结果顺序

4

我正在准备Java考试,其中有一道题让我花费了很长时间。尽管我努力学习,但我仍然无法找出结果顺序的确定方式。

请看一下:

class Country {

    public enum Continent {
        ASIA, EUROPE
    }
    String name;
    Continent region;

    public Country(String na, Continent reg) {
        name = na;
        region = reg;
    }

    public String getName() {
        return name;
    }

    public Continent getRegion() {
        return region;
    }
}

public class OrderQuestion {

    public static void main(String[] args) {
        List<Country> couList = Arrays.asList(
                new Country("Japan", Country.Continent.ASIA),
                new Country("Italy", Country.Continent.EUROPE),
                new Country("Germany", Country.Continent.EUROPE));
        Map<Country.Continent, List<String>> regionNames = couList.stream()
                .collect(Collectors.groupingBy(Country::getRegion,
                        Collectors.mapping(Country::getName, Collectors.toList())));
        System.out.println(regionNames);
    }
}

结果是什么?
A. {欧洲 = [意大利,德国],亚洲 = [日本]} B. {亚洲 = [日本],欧洲 = [意大利,德国]} C. {欧洲 = [德国,意大利],亚洲 = [日本]} D. {欧洲 = [德国],欧洲 = [意大利],亚洲 = [日本]}
最重要的是什么决定了具体的结果而不是其他结果?

3
请查看 Map 的 API 文档并了解其对元素顺序的说明。链接为 https://docs.oracle.com/javase/8/docs/api/java/util/Map.html - Tesseract
建议的答案是:A. {欧洲 = [意大利,德国],亚洲 = [日本]} B. {亚洲 = [日本],欧洲 = [意大利,德国]} C. {欧洲 = [德国,意大利],亚洲 = [日本]} D. {欧洲 = [德国],欧洲 = [意大利],亚洲 = [日本]}我知道这段代码产生了Java Runtime (A),但不知道为什么会是这样而不是其他的。我正在寻找理由。 - Fred Filozof
使用流,您可以在一行代码中执行许多操作。为了使问题更清晰,您可以拆分流行,将每个步骤分配给变量,并描述您感到惊讶的内容以及您期望看到什么。 - elirandav
这道考试题目对我来说似乎相当荒谬。如果我没记错的话,Arrays.asList和Collectors.groupingBy并不保证底层实现的顺序,因此元素的顺序是不确定的。即使它们确实有保证,我也不会在没有使用排序的情况下做出任何假设。 - IcedDante
在我的IDE中,{EUROPE=[意大利,德国],ASIA=[日本]} - Stéphane GRILLON
3
Arrays.asList()方法保证顺序,并且更多:返回一个由指定数组支持的固定大小列表。(对返回的列表进行的更改会直接反映到原数组中。)[...] 返回的列表是可序列化的并且实现了RandomAccess接口 - Andreas
1个回答

6
我们可以排除D,因为Map中的键需要唯一,但EUROPE不唯一。
我们可以排除C,因为在[Germany, Italy]中的顺序。 Italy在列表中先于Germany,因此它也必须按照那个顺序存储在结果列表中。
但是,我们应该如何决定是否排除BA?好吧,我们无法做出决定。
Map不保证键值对的特定顺序。某些地图允许记住放置键值对的顺序,例如LinkedHashMap,有些允许根据键排序条目,例如TreeMap,但这种行为未指定为Collectors.groupingBy
这个方法使用HashMap来实现,它基于键(此处为Country.Continent枚举)的hashCode()和已经持有的键值对的数量来排序键值对。 Enum的hashCode()实现继承自Object类,这意味着它基于可以在每次运行JVM时更改的内存位置,因此它是随机值,防止我们假设任何顺序(这证实了它是未指定的)。
因此,基于groupingBy返回的Map缺乏规范,所以条目的两个顺序都是可能的,因此A和B都是可能的答案。

4
好的分析,点赞。我认为,这道题目描述得很差,不太适合作为考试题。获得“正确”答案取决于实现特定的行为(HashMap 的排序),而不是取决于规范。 - Stuart Marks
1
@FredFilozof (a)我怀疑那个问题的作者只是运行了几次代码并得到了相同的答案,因此他认为它必须总是正确的。但是我通过在枚举使用之前创建其他对象来获得两个答案,因此它们将占用与先前授予的不同的内存,因此它们的哈希值也会改变。(b)你流中元素的顺序取决于源中元素的顺序(在couList中)。确实,HashMap不保证任何特定的顺序,但是有基于键的哈希码和成对数量的某种顺序。 - Pshemo
1
如果使用可以在每次运行中更改的属性(例如内存地址)计算密钥的哈希码,则使用此类密钥的HashMap在每次运行中可能具有半随机顺序。但是,许多类的hashCode不是基于可以在每次运行中随机的属性。例如,Integer返回其持有的int值作为hashCode,因此new Integer(1)的hashCode为1new Integer(2)的hashCode为2。String的HashCode也仅基于其所包含的字符计算。因此,如果您在HashMap中使用Integer作为key,则应对相同的键组合获得相同的顺序。 - Pshemo
1
顺便提一下,HashMap 中的顺序可能取决于放置元素的顺序。HashMap 是由类似于链表的结构数组支持的(我们称之为桶)。每个桶将保存特定范围哈希码的元素(例如,如果桶的数量为 4,则第一个桶可以收集哈希值为 0、4、8 等的元素,第二个桶可以收集哈希值为 1、5、9 等的元素,你懂的)。因此,桶中元素的顺序取决于插入的顺序。因此,如果两个键可以具有相同的哈希码,例如 "Aa""BB" 字符串(它们的哈希码都是 2112),它们将始终放置在同一个桶中。演示:https://ideone.com/GqI9hd - Pshemo
1
实际上,我所描述的是Java 8更新之前的HashMap。以前它的工作方式是这样的:https://dev59.com/lGw15IYBdhLWcg3wntKh#18492835,但如果我没记错的话,后来它被改为使用树而不是链表结构。现在我不能再说更多了,将来有机会再去学习。 - Pshemo
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接