在Java 8中对对象流进行习惯性枚举

4
如何使用Java 8流方法对Stream<T>进行枚举,将每个T实例映射到唯一的整数?例如,对于数组T[] values,创建一个Map<T,Integer>,其中Map.get(values[i]) == i计算结果为true
目前,我正在定义一个匿名类来增加一个int字段,以便与Collectors.toMap(..)方法一起使用。
private static <T> Map<T, Integer> createIdMap(final Stream<T> values) {
    return values.collect(Collectors.toMap(Function.identity(), new Function<T, Integer>() {

        private int nextId = 0;

        @Override
        public Integer apply(final T t) {
            return nextId++;
        }

    }));
}

然而,使用Java 8流API是否有更加简洁/优雅的方法来完成这个任务?如果可以安全地并行化,则额外得分。


1
values 流中的所有数值都是唯一的吗? - Andremoniy
在我个人的情况下是这样,但也很有趣看到一个可以处理一个对象的多个出现的解决方案。 - errantlinguist
1
@SME_Dev 绝对不行。 - Andremoniy
3个回答

5

如果存在重复元素,您的方法将失败。

除此之外,您的任务需要可变状态,因此可以使用可变减少来解决。当我们填充一个映射表时,我们可以简单地使用映射表的大小来获取未使用的ID。

更棘手的是合并操作。下面的操作只是为右侧的映射表重复赋值,这将处理潜在的重复项。

private static <T> Map<T, Integer> createIdMap(Stream<T> values) {
    return values.collect(HashMap::new, (m,t) -> m.putIfAbsent(t,m.size()),
        (m1,m2) -> {
            if(m1.isEmpty()) m1.putAll(m2);
            else m2.keySet().forEach(t -> m1.putIfAbsent(t, m1.size()));
        });
}

如果我们依赖于唯一元素,或者插入一个明确的distinct(),我们可以使用。
private static <T> Map<T, Integer> createIdMap(Stream<T> values) {
    return values.distinct().collect(HashMap::new, (m,t) -> m.put(t,m.size()),
        (m1,m2) -> { int leftSize=m1.size();
            if(leftSize==0) m1.putAll(m2);
            else m2.forEach((t,id) -> m1.put(t, leftSize+id));
        });

}

我喜欢关于 Map 大小的技巧,聪明极了。但是你为什么需要检查 if(leftSize==0) 呢?这是一个非并发的收集器,因此供应者将会在流中的所有元素上被调用,然后累加器将在空映射中放置一个元素,然后再进行合并操作。 - Eugene
1
@Eugene:merge函数将被用于部分结果。这取决于流源的分割能力(即它是否可以平衡地分割)以及中间是否存在大小更改操作(例如filterflatMap)。因此,组合器函数有可能会以空的部分结果调用。那在这里仍然不需要对空映射进行测试,因为普通的合并操作会做正确的事情。这只是一种优化,使用廉价的测试并简化该情况下的操作。 - Holger
1
三个参数的collect方法无法达到最大值,但是如果通过Collector.of创建自定义收集器,则可以在第一个映射为空时返回第二个映射,从而省略整个putAll操作。如果您对工作分割和潜在的空部分结果有更多细节方面的兴趣,可以考虑这个Q&A - Holger

4
我会这样做:
private static <T> Map<T, Integer> createIdMap2(final Stream<T> values) {
    List<T> list = values.collect(Collectors.toList());
    return IntStream.range(0, list.size()).boxed()
            .collect(Collectors.toMap(list::get, Function.identity()));
}

为了实现并行处理,它可以改为:
   return IntStream.range(0, list.size()).parallel().boxed().
                (...)

1
你说得很对,我没有注意到parallel()可以直接调用,谢谢。 - Andremoniy
3
如果你可以承受中间存储的话,你的解决方案是简单且足够的。正如所说,如果预期存在重复项,则可以使用 List<T> list = values.distinct().collect(Collectors.toList());。不过,这并非必须;无论哪种情况,id都将是唯一的,而且没有人说它们不能有间隔... - Holger
这个答案比Holger的答案更容易阅读,但是最好不要使用中间列表。另外令人遗憾的是,Collectors.toList()不能保证返回一个随机访问列表,这意味着你的解决方案的复杂性可能会有很大的差异;是否有类似于Python的enumerate(iterable)的Java标准库函数呢? - errantlinguist
@errantlinguist:原则上,你是对的,文档确实没有说明列表将具有随机访问,但另一方面,随机访问属性不在未指定的显式命名属性之内(类型、可变性、可序列化性、线程安全性)。我认为,合理地假设返回的列表永远不会有昂贵的get操作。如果这个假设不成立,这个解决方案仍然可以工作,只是更慢(由决定让toList()返回一个没有随机访问的列表的人负责)... - Holger

0
与Andremoniy提供的解决方案中首先将输入流转换为List相比,我更喜欢以不同的方式进行操作,因为我们不知道"toList()"和"list.get(i)"的成本,并且创建额外的List是不必要的,这个List可能很小或很大。
private static <T> Map<T, Integer> createIdMap2(final Stream<T> values) {
    final MutableInt idx = MutableInt.of(0); // Or: final AtomicInteger idx = new AtomicInteger(0);        
    return values.collect(Collectors.toMap(Function.identity(), e -> idx.getAndIncrement()));
}

不管问题是什么,我认为在方法中传递流作为参数是一个糟糕的设计。


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