【问题标题】:Conditional mapping to new objects with a Java Stream使用 Java 流条件映射到新对象
【发布时间】:2015-07-22 23:17:03
【问题描述】:

我有一个对象流(一个列表),并希望从该流创建新对象,以插入到集合中。但是,传入 List 中的两个或多个对象可能会散列到 Set 中的同一个 Key,在这种情况下,我想将第 n 个 List 对象中的 String 附加到 Set 中已经存在的对象上,而不是创建一个新对象。

类似的东西,但是是函数式的:

HashSet<ClassB> mySet = new HashSet<>();
for (ClassA instanceA : classAList) {
    if (mySet.contains(ClassB.key(instanceA))) { //static method call to find the key
        mySet.get(instanceA).appendFieldA(instanceA.getFieldA());
    } else {
        mySet.add(new ClassB(instanceA));
    }
}
return mySet;

在功能形式上,我想创建这样的东西:

List classAList = new ArrayList<>();
classAList.stream()
.map(instanceA -> new ClassB(instanceA))
.collect(Collectors.toSet());

但是,这当然会忽略哈希图,并且我无法将我的多个 ClassA 实例的字段组合起来,这些实例都将解析为同一个 ClassB。我不知道如何把它放在那里。我是否需要忽略 map() 调用并创建自定义收集器来完成这项工作?似乎有不止一种方法可以做到这一点,但我是 Streams 的新手。

【问题讨论】:

  • 嗯,整个想法的问题在于,如果行为是基于状态的(无论是否有冲突的哈希对象在 Set 中),那么你就不再是在功能上做事了。跨度>
  • mySet.get(instanceA) 应该是什么意思?我认为您混淆了地图和场景。你能粘贴你的实际代码吗?

标签: java java-8 java-stream


【解决方案1】:

很难理解您真正想要什么,因为您的代码示例根本不起作用。问题是 Set 不像 Map 那样工作,你不能向它询问包含的等效对象。除此之外,您正在为您的contains(…) 和get(…) 调用使用不同的对象。另外,不清楚ClassB.key(instanceA)和new ClassB(instanceA)有什么区别。

让我们尝试重新定义它:

假设我们有一个键类型Key 和一个方法Key.key(instanceA) 来定义候选组。然后我们有一个ClassB,它是结果类型,通过new ClassB(instanceA)为单个(或主要ClassA实例)创建,有一个.appendFieldA(…)方法在合并两个组时接收另一个ClassA实例的值成员。然后,原始(Java 8 之前)代码将如下所示:

HashMap<Key, ClassB> myMap = new HashMap<>();
for(ClassA instanceA: classAList) {
    Key key=Key.key(instanceA);
    if(myMap.containsKey(key)) {
        myMap.get(key).appendFieldA(instanceA.getFieldA());
    } else {
        myMap.put(key, new ClassB(instanceA));
    }
}

然后,myMap.values() 为您提供ClassB 实例的集合。如果它必须是Set,您可以通过

创建它
Set<ClassB> result=new HashSet<>(myMap.values());

请注意,当Key 和ClassB 与您的代码中看起来相同时,这也有效,但您可能会问自己,您是否真的需要两者,通过.key(instanceA) 创建的实例和一个通过new ClassB(instanceA)创建...


这可以通过 Java 8 API 简化为:

for(ClassA instanceA: classAList) {
    myMap.compute(Key.key(instanceA), (k,b)-> {
        if(b==null) b=new ClassB(instanceA);
        else b.appendFieldA(instanceA.getFieldA());
        return b;
    });
}

或者,如果你想让它看起来更实用:

classAList.forEach(instanceA ->
    myMap.compute(Key.key(instanceA), (k,b)-> {
        if(b==null) b=new ClassB(instanceA);
        else b.appendFieldA(instanceA.getFieldA());
        return b;
    })
);

对于流解决方案,存在一个问题,即合并函数将获得两个相同类型的实例,此处为 ClassB,并且无法像我们对compute 上面的解决方案。对于流解决方案,我们需要ClassB 中的一个方法,它返回第一个ClassA 实例,我们将它传递给它的构造函数,比如getFirstInstanceA()。然后我们可以使用:

Map<Key, ClassB> myMap = classAList.stream()
    .collect(Collectors.toMap(Key::key, ClassB::new, (b1,b2)->{
        b1.appendFieldA(b2.getFirstInstanceA().getFieldA());
        return b1;
    }));

【讨论】:

  • 谢谢你,这完全正确,正如你对我的例子哪里出错的解释一样。在我努力简化问题陈述的过程中,我弄乱了原始代码示例;对于所有注意到原始代码没有多大意义的人,我深表歉意。
【解决方案2】:

您可以将条目分组到一个映射中,该映射将散列键映射到元素列表,然后再次调用 map 将该映射转换为您所追求的集合。像这样的:

List classAList = new ArrayList<>();
classAList.stream()
.collect(Collectors.groupingBy(instanceA -> ClassB.key(instanceB)))
.entrySet()
.map(entry -> entry.getValue().stream()
   .map(instanceA -> new ClassB(instanceA))
   .reduce(null, (a,b) -> a.appendFieldA(b)))
.collect(Collectors.toSet());

【讨论】:

  • 但是appendFieldA 不接受ClassB 类型的对象。除此之外,将null 作为标识值传递给reduce,意味着它可能(当前实现将)尝试在null 引用上调用appendFieldA。
  • 啊,是的,你是对的。我没有意识到问题是将classb放在集合中,但在classa上调用appendFieldA。为了使它起作用,您需要某种身份类别a
猜你喜欢
  • 2018-08-24
  • 1970-01-01
  • 2013-01-26
  • 1970-01-01
  • 2017-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-13
相关资源
最近更新 更多