简体   繁体   English

Java 8流混合两个元素

[英]Java 8 Stream mixing two elements

I have a many objects of type Slot in an array list. 我在数组列表中有很多Slot类型的对象。

Slot class is as shown below- 插槽类别如下所示-

Slot{
   int start;
   int end;
}

let the list of type List<Slot> be called slots . List<Slot>类型的List<Slot>称为slots The slots are sorted based on start time. 插槽基于开始时间进行排序。 End time of one slot may be equal to start time of next slot, but they would never overlap. 一个时隙的结束时间可以等于下一时隙的开始时间,但是它们永远不会重叠。

Is there any possible way in which I can iterate over this list using Java 8 streams, and combine two slots if end time of one matches start time of next and output them into an ArrayList ? 有没有一种方法可以使用Java 8流在此列表上进行迭代,并且如果一个的结束时间与下一个的开始时间匹配,然后合并两个插槽并将它们输出到ArrayList

Since these types of questions come up a lot, I thought it might be an interesting exercise to write a collector that would group adjacent elements by a predicate. 由于这些类型的问题很多,因此我认为编写一个收集器以谓词对相邻元素进行分组可能是一个有趣的练习。

Assuming we can add combining logic to the Slot class 假设我们可以向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);
}

the groupingAdjacent collector can then be used as follows: 然后可以按以下方式使用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
    ));

Alternatively, second parameter can be collectingAndThen(reducing(Slot::combine), Optional::get) and the third argument be toList() 另外,第二个参数可以是collectingAndThen(reducing(Slot::combine), Optional::get) ,第三个参数是toList()

Here's the source for groupingAdjacent . 这是groupingAdjacent的来源。 It can handle null elements and is parallel-friendly. 它可以处理null元素,并且是并行友好的。 With a bit more hassle, a similar thing can be done with a Spliterator . 麻烦一点的话,可以使用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);
}

Such scenario is perfectly supported by my free StreamEx library which enhances standard Stream API. 我的免费StreamEx库完全支持这种情况, 库增强了标准Stream API。 There's an intervalMap intermediate operation which is capable to collapse several adjacent stream elements to the single element. 有一个intervalMap中间操作,它可以将几个相邻的流元素折叠为单个元素。 Here's complete example: 这是完整的示例:

// 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]

The intervalMap method takes two parameters. intervalMap方法采用两个参数。 The first is a BiPredicate accepting two adjacent elements from the input stream and returns true if they must be merged (here the condition is s1.end == s2.start ). 第一个是BiPredicate接受来自输入流的两个相邻元素,如果必须合并它们,则返回true(此处的条件是s1.end == s2.start )。 The second parameter is a BiFunction which takes the first and the last elements from the merged series and produces the resulting element. 第二个参数是BiFunction ,它从合并的序列中获取第一个和最后一个元素,并生成结果元素。

Note that if you have, for example 100 adjacent slots which should be combined into one, this solution does not create 100 intermediate objects (like in @Misha's answer, which is nevertheless very interesting), it tracks first and last slot in the series immediately forgetting about intermediate onces. 请注意,如果您有例如100个相邻的插槽应合并为一个,则此解决方案不会创建100个中间对象(如@Misha的回答,尽管如此非常有趣),它会立即跟踪系列中的第一个和最后一个插槽忘记中间一次。 Of course this solution is parallel friendly. 当然,该解决方案是并行友好的。 If you have many thousands of input slots, using .parallel() may improve the performance. 如果您有成千上万个输入插槽,则使用.parallel()可能会提高性能。

Note that current implementation will recreate the Slot even if it's not merged with anything. 请注意,即使当前实现未与任何内容合并,它也会重新创建Slot In this case the BinaryOperator receives the same Slot parameter twice. 在这种情况下, BinaryOperator会两次收到相同的Slot参数。 If you want to optimize this case, you can make additional check like s1 == s2 ? s1 : ... 如果要优化这种情况,可以进行其他检查,例如s1 == s2 ? s1 : ... s1 == s2 ? s1 : ... : 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();

You can do it using the reduce() method with U being another List<Slot> , but it's a lot more convoluted than just doing it in a for loop, unless parallel processing is required. 您可以使用reduce()方法(其中U是另一个List<Slot> 来执行此操作,但是它比仅在for循环中执行时要复杂得多,除非需要并行处理。

See end of answer for test setup. 有关测试设置,请参见答案结尾。

Here is the for loop implementation: 这是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));

Output 输出量

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

Here is the stream reduce implementation: 这是流减少实现:

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;
});

Output 输出量

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]

Changing it for parallel (multi-threaded) processing: 对其进行更改以进行并行(多线程)处理:

List<Slot> mixed = slots.stream().parallel().reduce(...

Output 输出量

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]

Caveat 警告

If slots is an empty list, the for loop version results in an empty list, and the streams version results is a null value. 如果slots是一个空列表,则for循环版本将导致一个空列表,而流版本的结果将是一个null值。


Test Setup 测试设置

All the above code used the following Slot class: 以上所有代码均使用以下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;
    }
}

The slots variable was defined as: 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));

Both slots and the result mixed are printed using: 两个slotsmixed结果mixed使用以下命令打印:

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

It's a two-liner: 这是两条线:

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);

If the fields of slot are not visible, you will have to change a.end to a.getEnd() , etc 如果slot的字段不可见,则必须将a.end更改为a.getEnd()


Some test code with some edge cases: 一些带有一些极端情况的测试代码:

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);
}

Output: 输出:

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

If you add the following method to your Slot class 如果将以下方法添加到Slot类中

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

you can perform the entire operation using the standard API the following way 您可以通过以下方式使用标准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()))
);

This obeys the contract of the API (unlike abusing reduce ) and will therefore works seamlessly with parallel streams (though you need really large source lists to benefit from parallel execution). 这遵守了API的约定(不同于滥用reduce ),因此将与并行流无缝地工作(尽管您需要非常大的源列表才能从并行执行中受益)。


However, the solution above uses in-place joining of Slot s which is only acceptable if you don't need the source list/items anymore. 但是,上面的解决方案使用Slot的就地连接,仅当您不再需要源列表/项目时,才可以接受。 Otherwise, or if you use immutable Slot instances only, you have to create new Slot instance representing joint slots. 否则,或者如果仅使用不可变的Slot实例,则必须创建代表联合插槽的新Slot实例。

One possible solution looks like 一种可能的解决方案看起来像

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()));
        }
    }
);

A clean (parallel safe) solution that doesn't require any new methods: 不需要任何新方法的干净(并行安全)解决方案:

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);});

This uses the more appropriate list implementation LinkedList to simplify removing and accessing the last element of the list when merging Slots. 这将使用更合适的列表实现LinkedList来简化合并插槽时删除和访问列表的最后一个元素的过程。


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);
}

Output: 输出:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM