Java 8 Stream混合了两个元素

时间:2015-09-24 21:43:51

标签: java java-8 java-stream

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

Slot类如下所示 -

Slot{
   int start;
   int end;
}

List<Slot>类型列表调用slots。插槽根据开始时间排序。一个时隙的结束时间可能等于下一个时隙的开始时间,但它们永远不会重叠。

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

6 个答案:

答案 0 :(得分:5)

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

假设我们可以将组合逻辑添加到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<Slot> 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
    ));

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

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

public static <T, AI, I, AO, R> Collector<T, ?, R> groupingAdjacent(
        BiPredicate<? super T, ? super T> keepTogether,
        Collector<? super T, AI, ? extends I> inner,
        Collector<I, AO, R> 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);
}

答案 1 :(得分:5)

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

// Slot class and sample data are taken from @Andreas answer
List<Slot> slots = Arrays.asList(new Slot(3, 5), new Slot(5, 7), 
                new Slot(8, 10), new Slot(10, 11), new Slot(11, 13));

List<Slot> 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 : ...

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

答案 2 :(得分:3)

您可以使用reduce()方法将O(n^2)作为另一个U来实现,但除了在List<Slot>循环中执行此操作之外,它更复杂,除非并行处理是必需的。

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

以下是for循环实现:

for

输出

List<Slot> 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<Slot> mixed = slots.stream().reduce((List<Slot>)null, (list, slot) -> {
    System.out.println("accumulator.apply(" + list + ", " + slot + ")");
    if (list == null) {
        List<Slot> 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]

输出

List<Slot> 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<Slot> 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

答案 3 :(得分:2)

这是两个班轮:

App\User
App\Http\Controllers\Auth
Config\Auth

如果无法看到广告位的字段,则必须将List<Slot> 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<List<Slot>> 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<Slot> slots : tests) {
    List<Slot> 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);
}

答案 4 :(得分:2)

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

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

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

List<Slot> 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不同),因此可以与并行流无缝协作(尽管您需要真正的大型源列表才能从并行执行中受益)。 / p>

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

一种可能的解决方案如

BiConsumer<List<Slot>,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<Slot> 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()));
        }
    }
);

答案 5 :(得分:2)

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

List<Slot> 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<List<Slot>> 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<Slot> slots : tests) {
    List<Slot> 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]]
[]