Java 8 Stream混合了两个元素

我在数组列表中有许多Slot类型的对象。

槽类如下图所示 –

Slot{ int start; int end; } 

让列表List称为slots 。 插槽根据开始时间排序。 一个时隙的结束时间可以等于下一个时隙的开始时间,但它们永远不会重叠。

是否有任何可能的方法可以使用Java 8流迭代此列表,如果一个的结束时间与下一个的开始时间匹配并将它们输出到ArrayList ,则组合两个槽?

我的免费StreamEx库完美支持这种情况,它增强了标准的Stream API。 有一个intervalMap中间操作,它能够将几个相邻的流元素折叠到单个元素。 这是完整的例子:

 // Slot class and sample data are taken from @Andreas answer List slots = Arrays.asList(new Slot(3, 5), new Slot(5, 7), new Slot(8, 10), new Slot(10, 11), new Slot(11, 13)); List result = StreamEx.of(slots) .intervalMap((s1, s2) -> s1.end == s2.start, (s1, s2) -> new Slot(s1.start, s2.end)) .toList(); System.out.println(result); // Output: [3-7, 8-13] 

intervalMap方法有两个参数。 第一个是接受来自输入流的两个相邻元素的BiPredicate ,如果必须合并则返回true(这里的条件是s1.end == s2.start )。 第二个参数是BiFunction ,它从合并的系列中获取第一个和最后一个元素并生成结果元素。

请注意,如果您有例如100个相邻的插槽应该组合成一个,这个解决方案不会创建100个中间对象(比如@ Misha的答案,这仍然非常有趣),它会立即跟踪系列中的第一个和最后一个插槽忘记中间人。 当然这个解决方案是并行友好的。 如果您有数千个输入槽,使用.parallel()可以提高性能。

请注意,即使它未与任何内容合并,当前实现也将重新创建Slot 。 在这种情况下, BinaryOperator接收两次相同的Slot参数。 如果你想优化这种情况,你可以进行额外的检查,如s1 == s2 ? s1 : ... s1 == s2 ? s1 : ...

 List result = StreamEx.of(slots) .intervalMap((s1, s2) -> s1.end == s2.start, (s1, s2) -> s1 == s2 ? s1 : new Slot(s1.start, s2.end)) .toList(); 

由于这些类型的问题出现了很多,我认为编写一个可以通过谓词对相邻元素进行分组的收集器可能是一个有趣的练习。

假设我们可以将组合逻辑添加到Slot

 boolean canCombine(Slot other) { return this.end == other.start; } Slot combine(Slot other) { if (!canCombine(other)) { throw new IllegalArgumentException(); } return new Slot(this.start, other.end); } 

然后可以按如下方式使用groupingAdjacent收集器:

 List combined = slots.stream() .collect(groupingAdjacent( Slot::canCombine, // test to determine if two adjacent elements go together reducing(Slot::combine), // collector to use for combining the adjacent elements mapping(Optional::get, toList()) // collector to group up combined elements )); 

或者,第二个参数可以是toList() collectingAndThen(reducing(Slot::combine), Optional::get) ,第三个参数是toList()

这是groupingAdjacent的来源。 它可以处理null元素并且是并行友好的。 有点麻烦,使用Spliterator可以做类似的事情。

 public static  Collector groupingAdjacent( BiPredicate keepTogether, Collector inner, Collector outer ) { AI EMPTY = (AI) new Object(); // Container to accumulate adjacent possibly null elements. Adj can be in one of 3 states: // - Before first element: curGrp == EMPTY // - After first element but before first group boundary: firstGrp == EMPTY, curGrp != EMPTY // - After at least one group boundary: firstGrp != EMPTY, curGrp != EMPTY class Adj { T first, last; // first and last elements added to this container AI firstGrp = EMPTY, curGrp = EMPTY; AO acc = outer.supplier().get(); // accumlator for completed groups void add(T t) { if (curGrp == EMPTY) /* first element */ { first = t; curGrp = inner.supplier().get(); } else if (!keepTogether.test(last, t)) /* group boundary */ { addGroup(curGrp); curGrp = inner.supplier().get(); } inner.accumulator().accept(curGrp, last = t); } void addGroup(AI group) /* group can be EMPTY, in which case this should do nothing */ { if (firstGrp == EMPTY) { firstGrp = group; } else if (group != EMPTY) { outer.accumulator().accept(acc, inner.finisher().apply(group)); } } Adj merge(Adj other) { if (other.curGrp == EMPTY) /* other is empty */ { return this; } else if (this.curGrp == EMPTY) /* this is empty */ { return other; } else if (!keepTogether.test(last, other.first)) /* boundary between this and other*/ { addGroup(this.curGrp); addGroup(other.firstGrp); } else if (other.firstGrp == EMPTY) /* other container is single-group. prepend this.curGrp to other.curGrp*/ { other.curGrp = inner.combiner().apply(this.curGrp, other.curGrp); } else /* other Adj contains a boundary. this.curGrp+other.firstGrp form a complete group. */ { addGroup(inner.combiner().apply(this.curGrp, other.firstGrp)); } this.acc = outer.combiner().apply(this.acc, other.acc); this.curGrp = other.curGrp; this.last = other.last; return this; } R finish() { AO combined = outer.supplier().get(); if (curGrp != EMPTY) { addGroup(curGrp); assert firstGrp != EMPTY; outer.accumulator().accept(combined, inner.finisher().apply(firstGrp)); } return outer.finisher().apply(outer.combiner().apply(combined, acc)); } } return Collector.of(Adj::new, Adj::add, Adj::merge, Adj::finish); } 

您可以使用reduce()方法, U是另一个List ,但除了在for循环中执行它之外,它更复杂,除非需要并行处理。

请参阅测试设置的答案结尾。

这是for循环实现:

 List mixed = new ArrayList<>(); Slot last = null; for (Slot slot : slots) if (last == null || last.end != slot.start) mixed.add(last = slot); else mixed.set(mixed.size() - 1, last = new Slot(last.start, slot.end)); 

产量

 [3-5, 5-7, 8-10, 10-11, 11-13] [3-7, 8-13] 

这是流减少实现:

 List mixed = slots.stream().reduce((List)null, (list, slot) -> { System.out.println("accumulator.apply(" + list + ", " + slot + ")"); if (list == null) { List newList = new ArrayList<>(); newList.add(slot); return newList; } Slot last = list.get(list.size() - 1); if (last.end != slot.start) list.add(slot); else list.set(list.size() - 1, new Slot(last.start, slot.end)); return list; }, (list1, list2) -> { System.out.println("combiner.apply(" + list1 + ", " + list2 + ")"); if (list1 == null) return list2; if (list2 == null) return list1; Slot lastOf1 = list1.get(list1.size() - 1); Slot firstOf2 = list2.get(0); if (lastOf1.end != firstOf2.start) list1.addAll(list2); else { list1.set(list1.size() - 1, new Slot(lastOf1.start, firstOf2.end)); list1.addAll(list2.subList(1, list2.size())); } return list1; }); 

产量

 accumulator.apply(null, 3-5) accumulator.apply([3-5], 5-7) accumulator.apply([3-7], 8-10) accumulator.apply([3-7, 8-10], 10-11) accumulator.apply([3-7, 8-11], 11-13) [3-5, 5-7, 8-10, 10-11, 11-13] [3-7, 8-13] 

为并行(multithreading)处理更改它:

 List mixed = slots.stream().parallel().reduce(... 

产量

 accumulator.apply(null, 8-10) accumulator.apply(null, 3-5) accumulator.apply(null, 10-11) accumulator.apply(null, 11-13) combiner.apply([10-11], [11-13]) accumulator.apply(null, 5-7) combiner.apply([3-5], [5-7]) combiner.apply([8-10], [10-13]) combiner.apply([3-7], [8-13]) [3-5, 5-7, 8-10, 10-11, 11-13] [3-7, 8-13] 

警告

如果slots是空列表,则for循环版本将生成空列表,并且流版本结果为null值。


测试设置

以上所有代码都使用了以下Slot类:

 class Slot { int start; int end; Slot(int start, int end) { this.start = start; this.end = end; } @Override public String toString() { return this.start + "-" + this.end; } } 

slots变量定义为:

 List slots = Arrays.asList(new Slot(3, 5), new Slot(5, 7), new Slot(8, 10), new Slot(10, 11), new Slot(11, 13)); 

slots和结果mixed使用以下方式打印:

 System.out.println(slots); System.out.println(mixed); 

这是两个class轮:

 List condensed = new LinkedList<>(); slots.stream().reduce((a,b) -> {if (a.end == b.start) return new Slot(a.start, b.end); condensed.add(a); return b;}).ifPresent(condensed::add); 

如果槽的字段不可见,则必须将a.end更改为a.getEnd()


一些带有边缘情况的测试代码:

 List> tests = Arrays.asList( Arrays.asList(new Slot(3, 5), new Slot(5, 7), new Slot(8, 10), new Slot(10, 11), new Slot(11, 13)), Arrays.asList(new Slot(3, 5), new Slot(5, 7), new Slot(8, 10), new Slot(10, 11), new Slot(12, 13)), Arrays.asList(new Slot(3, 5), new Slot(5, 7)), Collections.emptyList()); for (List slots : tests) { List condensed = new LinkedList<>(); slots.stream().reduce((a, b) -> {if (a.end == b.start) return new Slot(a.start, b.end); condensed.add(a); return b;}).ifPresent(condensed::add); System.out.println(condensed); } 

输出:

 [3-7, 8-13] [3-7, 8-11, 12-13] [3-7] [] 

如果您将以下方法添加到您的Slot

 public boolean join(Slot s) { if(s.start != end) return false; end = s.end; return true; } 

您可以通过以下方式使用标准API执行整个操作

 List result = slots.stream().collect(ArrayList::new, (l, s)-> { if(l.isEmpty() || !l.get(l.size()-1).join(s)) l.add(s); }, (l1, l2)-> l1.addAll( l1.isEmpty()||l2.isEmpty()||!l1.get(l1.size()-1).join(l2.get(0))? l2: l2.subList(1, l2.size())) ); 

这遵循API的合同(与滥用reduce不同),因此可以与并行流无缝地工作(尽管您需要非常大的源列表才能从并行执行中受益)。


但是,上面的解决方案使用Slot s的就地连接,只有在您不再需要源列表/项目时才可以接受。 否则,或者如果仅使用不可变Slot实例,则必须创建表示关节槽的新Slot实例。

一种可能的解决方案如下

 BiConsumer,Slot> joinWithList=(l,s) -> { if(!l.isEmpty()) { Slot old=l.get(l.size()-1); if(old.end==s.start) { l.set(l.size()-1, new Slot(old.start, s.end)); return; } } l.add(s); }; List result = slots.stream().collect(ArrayList::new, joinWithList, (l1, l2)-> { if(!l2.isEmpty()) { joinWithList.accept(l1, l2.get(0)); l1.addAll(l2.subList(1, l2.size())); } } ); 

一个干净(并行安全)的解决方案,不需要任何新方法:

 List condensed = slots.stream().collect(LinkedList::new, (l, s) -> l.add(l.isEmpty() || l.getLast().end != s.start ? s : new Slot(l.removeLast().start, s.end)), (l, l2) -> {if (!l.isEmpty() && !l2.isEmpty() && l.getLast().end == l2.getFirst().start) { l.add(new Slot(l.removeLast().start, l2.removeFirst().end));} l.addAll(l2);}); 

这使用更合适的列表实现LinkedList ,在合并Slots时简单地删除和访问列表的最后一个元素。


 List> tests = Arrays.asList( Arrays.asList(new Slot(3, 5), new Slot(5, 7), new Slot(8, 10), new Slot(10, 11), new Slot(11, 13)), Arrays.asList(new Slot(3, 5), new Slot(5, 7), new Slot(8, 10), new Slot(10, 11), new Slot(12, 13)), Arrays.asList(new Slot(3, 5), new Slot(5, 7)), Collections.emptyList()); for (List slots : tests) { List condensed = slots.stream().collect(LinkedList::new, (l, s) -> l.add(l.isEmpty() || l.getLast().end != s.start ? s : new Slot(l.removeLast().start, s.end)), (l, l2) -> {if (!l.isEmpty() && !l2.isEmpty() && l.getLast().end == l2.getFirst().start) { l.add(new Slot(l.removeLast().start, l2.removeFirst().end));} l.addAll(l2);}); System.out.println(condensed); } 

输出:

 [[3, 7], [8, 13]] [[3, 7], [8, 11], [12, 13]] [[3, 7]] []