【问题标题】:Outer Join on list of Objects对象列表的外部连接
【发布时间】:2022-01-16 08:46:51
【问题描述】:

我有以下物品:

class A {
   int id;
   String propA;
}

class B {
   int id;
   String propB;
}

class C {
   int id;
   String propA;
   String propB;
}

给定对象 A 和 B 的列表,我想计算对象 C 的列表,这将是通过 id 将对象 A 与 B 连接起来创建的对象;但是,如果 id 存在于对象 A 而不是 B 或 B 而不是 A 的列表中,我想将其包含在对象 C 的列表中,但缺少属性。换句话说,在以下列表中加入:

List<A> listA = new ArrayList<>();
listA.add(new A(1, "example1"));
listA.add(new A(2, "example2"));

List<B> listB = new ArrayList<>();
listB.add(new B(2, "another example2"));
listB.add(new B(3, "another example3"));

应该相当于创建如下列表:

List<C> listC = new ArrayList<>();
listC.add(new C(1, "example1", null));
listC.add(new C(2, "example2", "another example2"));
listC.add(new C(3, null, "another example3"));

我已经看到使用 Stream API 解决了类似的问题,但它始终是片面的外连接;无论哪种方式,我都试图使其适应我的需求。

我的尝试如下:

//creating maps of ids to list elements, so I can access it from the final stream easily
Map<Integer, A> listAbyId = listA.stream().collect(Collectors.toMap(A::getId, Function.identity()));
Map<Integer, B> listBbyId = listB.stream().collect(Collectors.toMap(B::getId, Function.identity()));

//creating set of all ids that are in both lists
Set<Integer> set = listA.stream().map(a -> a.getId()).collect(Collectors.toSet());
Set<Integer> set2 = listB.stream().map(b -> b.getId()).collect(Collectors.toSet());
HashSet<Integer> setc = new HashSet<>();
setc.addAll(set);
setc.addAll(set2);

//final stream that creates list of C objects.
List<C> listC = setc.stream().map(c -> new C(c,
                                            listAbyId.get(c) == null ? null : listAbyId.get(c).getPropA(),
                                            listBbyId.get(c) == null ? null : listBbyId.get(c).getPropB()))
                            .collect(Collectors.toList());

它按预期工作,但似乎有点矫枉过正。有没有更简单的方法?

【问题讨论】:

    标签: java java-stream


    【解决方案1】:

    类似这样的:

    Map<Integer, A> map = listA.stream().collect(Collectors.toMap(A::getId, item -> new C(item.getId(), item.getPropA(), null)));
    listB.stream().forEach(item -> map.containsKey(item.getId()) ? map.get(item.getId()).setPropB(item.getPropB()) : map.put(item.getId(), item);
    

    然后转换为列表。

    【讨论】:

    • 这也很有趣,但是: 1. 您正在创建地图 ,但用 填充它; 2.据我所知,你需要将Consumer放入forEach,所以你的三级算子的返回类型应该是void,而Map.put(K key, V value)返回的是插入到map中的值,但是我可以通过插入一个带有常规 if-else 表达式的块来绕过它。不过还是谢谢! :-)
    • 哦,另外,当您处理从 listB 创建的流时,您正尝试将 B 类型的对象插入到该映射中 - 应该有一些新创建的 C 类型的对象。
    【解决方案2】:

    你的开始已经很好了,但你是对的,MapOfA/B 有点矫枉过正

    
    public static void main(String[] args) {
    
        List<A> listA = new ArrayList<>();
        listA.add(new A(1, "texta"));
        listA.add(new A(2, "textA"));
    
        List<B> listB = new ArrayList<>();
        listB.add(new B(2, "textb"));
        listB.add(new B(3, "textB"));
        
        Set<Integer> ids = listA.stream().map(a -> a.id).collect(Collectors.toSet());
        listB.forEach(b -> ids.add(b.id));
        List<C> listC = ids.stream()
                .map(id -> new C(id, listA.stream().filter(a -> a.id == id).findAny().orElse(new A(0, null)).propA,
                        listB.stream().filter(b -> b.id == id).findAny().orElse(new B(0, null)).propB))
                .toList();
    
        }
    
    

    编辑:我昨天睡觉有点沮丧。我不知道如何让它变得更简单^^因为我仍然有点过分回答这个问题......

    所以我改进了它:D

    List<A> listA = new ArrayList<>();
    listA.add(new A(1, "texta"));
    listA.add(new A(2, "textA"));
    
    List<B> listB = new ArrayList<>();
    listB.add(new B(2, "textb"));
    listB.add(new B(3, "textB"));
    
    List<C> listC = Stream.of(listA, listB).flatMap(List::stream)
            .collect(Collectors.toMap(ab -> (ab instanceof A a) ? a.id : ((B) ab).id, ab -> {
                C c = new C();
                if (ab instanceof A a) {
                    c.textA = a.text;
                } else {
                    c.textB = ((B) ab).text;
                }
                return c;
            }, (c, newc) -> {
                if (c.textA == null) {
                    c.textA = newc.textA;
                } else {
                    c.textB = newc.textB;
                }
                return c;
            })).entrySet().stream().map(e -> {
                C c = e.getValue();
                c.id = e.getKey();
                return c;
            }).toList();
    

    我创建了一个 Stream.of listalistb 给了我 [lista, listb]

    为了提取listalistb 的元素,我使用flatMap(List::stream) 将流转换为[lista[0], lista[1], listb[0], listb[1]]

    从他们那里,我使用collector(Collectors.toMap(keyFunction, valueFunction, mergeFunction)) 来收集 id 作为键和 C 的实例作为值。

    使用ab instanceof A a,我可以决定它是instanceof A 还是B,还可以自动将ab 转换为A 并将其存储在a 中。

    现在我有一个Map&lt;Interger, C&gt;,其中 id 没有存储在 C 中,而是作为键存储。要将密钥映射为 C.id,我使用 .entrySet().stream() 遍历地图并做到了这一点;)

    请注意,对于低于 16 的 Java 版本,您需要将 .toList() 替换为 .collect(Collectors.toList)

    【讨论】:

    • 这还不错,绝对比我的好,尽管我的 IDE 尖叫 Stream 没有 toList() 方法。但是,如果您使用.collect(Collectors.toList());,它会完美运行。
    • 我想我只是有一个更高的 java 版本 :D 我的 SonarLint 说我应该使用 .toList() 而不是 .collect(Collectors.toList())
    • 哦,你是对的。我需要一个老的。过失:-)
    【解决方案3】:

    您可以使用Stream#concat,然后使用toMap 收集器,如下所示:

    如果id重复,合并两个对象(c,c2)。

     Stream.concat(
                        listA.stream().map(a -> new C(a.id, a.getPropA(), null)),
                        listB.stream().map(b -> new C(b.id, null, b.getPropB()))
                  )
            .collect(Collectors.toMap(C::getId, Function.identity(), 
                   (c, c2) -> {
                      c.setPropB(c2.getPropB());
                      return c;
             })).values(); 
    

    【讨论】:

      猜你喜欢
      • 2021-08-23
      • 1970-01-01
      • 2014-04-06
      • 1970-01-01
      • 1970-01-01
      • 2020-12-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多