简体   繁体   English

Java Stream:有没有办法一次迭代两个元素而不是一个?

[英]Java Stream: is there a way to iterate taking two elements a time instead of one?

Let's say we have this stream假设我们有这个流

Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j");

and I want to save in a map the couples of adjacent strings in which the first one starts with "err".我想在地图中保存一对相邻的字符串,其中第一个字符串以“err”开头。

What I thought of is something like this我想到的是这样的

Map<String, String> map = new HashMap<>();

Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.reduce((acc, next) -> {
    if (acc.startsWith("err"))
        map.put(acc,next);
    if (next.startsWith("err"))
        return next;
    else
        return "";
});

But I'm not totally satisfied with it for two main reasons但我对它并不完全满意,主要有两个原因

  1. I'm "misusing" reduce function.我在“滥用” reduce功能。 In Stream API every function has its clear, well defined purpose: max is supposed to calcuate max value, filter is supposed to filter based on a condition, reduce is supposed to produce an incrementally accumulated value and so on.在 Stream API 中,每个函数都有其明确定义的用途: max应该计算最大值, filter应该根据条件进行过滤, reduce应该产生增量累加值等等。
  2. Doing that prevents me from using Streams powerful mechanisms: what if I wanted to limit my search to the first two results?这样做会阻止我使用 Streams 强大的机制:如果我想将搜索限制为前两个结果怎么办?

Here I used reduce because (as far as I know) it's the only function that lets you compare couple of values that you can, somehow, lead back to something similar to "current value" and "next value" concepts.在这里我使用reduce是因为(据我所知)它是唯一可以让你比较几个值的函数,你可以以某种方式返回类似于“当前值”和“下一个值”概念的东西。

Is there a more straightforward way?有没有更直接的方法? Something that allows you to iterate the stream considering more than one value for each iteration?允许您在每次迭代中考虑多个值来迭代流的东西?

EDIT编辑

What I'm thinking about is some mechanism that, given the current element, allows you to define a "window of elements" to consider, for each iteration.我正在考虑的是某种机制,在给定当前元素的情况下,允许您为每次迭代定义一个“元素窗口”以供考虑。

Something like就像是

<R> Stream<R> mapMoreThanOne(
    int elementsBeforeCurrent,
    int elementsAfterCurrent,
    Function<List<? super T>, ? extends R> mapper);

instead of代替

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

That would be a powerful "upgrade" to current API.这将是对当前 API 的强大“升级”。

EDIT2编辑2

I appreciate the effort of people proposing their solution, but the issue is not the algorithm per se.我感谢人们提出解决方案的努力,但问题不在于算法本身。 There are different ways to achieve my goal by putting together streams, indexes, temp variables to store previous values... but I was wondering if there was some method in Stream API that was designed for the task of dealing with elements other than the current without breaking the "stream paradigm".通过将流、索引、临时变量放在一起来存储以前的值,有不同的方法可以实现我的目标......但我想知道 Stream API 中是否有一些方法是为处理当前元素以外的元素而设计的不打破“流范式”。 Something like this像这样的东西

List<String> list =
        Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
        .filterFunctionImWonderingIfExist(/*filters couples of elements*/)
        .limit(2)
        .collect(Collectors.toList());

Given the answers, I think there's no "clear and quick" solution, unless using StreamEx library鉴于答案,我认为没有“清晰而快速”的解决方案,除非使用 StreamEx 库

You can build a custom Collector for this task.您可以为此任务构建自定义Collector

Map<String, String> map = 
    Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
          .collect(MappingErrors.collector());

with:和:

private static final class MappingErrors {

    private Map<String, String> map = new HashMap<>();

    private String first, second;

    public void accept(String str) {
        first = second;
        second = str;
        if (first != null && first.startsWith("err")) {
            map.put(first, second);
        }
    }

    public MappingErrors combine(MappingErrors other) {
        throw new UnsupportedOperationException("Parallel Stream not supported");
    }

    public Map<String, String> finish() {
        return map;
    }

    public static Collector<String, ?, Map<String, String>> collector() {
        return Collector.of(MappingErrors::new, MappingErrors::accept, MappingErrors::combine, MappingErrors::finish);
    }

}

In this collector, two running elements are kept.在这个收集器中,保留了两个运行元素。 Each time a String is accepted, they are updated and if the first starts with "err" , the two elements are added to a map.每次接受String ,它们都会更新,如果第一个以"err"开头,则将两个元素添加到映射中。


Another solution is to use the StreamEx library which provides a pairMap method that applies a given function to the every adjacent pair of elements of this stream.另一种解决方案是使用StreamEx库,它提供了一个pairMap方法,该方法将给定的函数应用于此流的每个相邻元素对。 In the following code, the operation returns a String array consisting of the first and second element of the pair if the first element starts with "err" , null otherwise.在以下代码中,如果第一个元素以"err"开头,则该操作返回一个 String 数组,该数组由该对​​的第一个和第二个元素组成,否则返回null null elements are then filtered out and the Stream is collected into a map.然后过滤掉null元素,并将 Stream 收集到映射中。

Map<String, String> map = 
    StreamEx.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
            .pairMap((s1, s2) -> s1.startsWith("err") ? new String[] { s1, s2 } : null)
            .nonNull()
            .toMap(a -> a[0], a -> a[1]);

System.out.println(map);

You can write a custom collector, or use the much simpler approach of streaming over the list's indexes:您可以编写自定义收集器,或使用更简单的方法来流式传输列表的索引:

Map<String, String> result = IntStream.range(0, data.size() - 1)
        .filter(i -> data.get(i).startsWith("err"))
        .boxed()
        .collect(toMap(data::get, i -> data.get(i+1)));

This assumes that your data is in a random access friendly list or that you can temporarily dump it into one.这假设您的数据位于随机访问友好列表中,或者您可以暂时将其转储到一个列表中。

If you cannot randomly access the data or load it into a list or array for processing, you can always make a custom pairing collector so you can write如果您不能随机访问数据或将其加载到列表或数组中进行处理,您可以随时制作自定义pairing收集器,以便您可以编写

Map<String, String> result = data.stream()
        .collect(pairing(
                (a, b) -> a.startsWith("err"), 
                AbstractMap.SimpleImmutableEntry::new,
                toMap(Map.Entry::getKey, Map.Entry::getValue)
        ));

Here's the source for the collector.这是收集器的来源。 It's parallel-friendly and might come in handy in other situations:它是并行友好的,在其他情况下可能会派上用场:

public static <T, V, A, R> Collector<T, ?, R> pairing(BiPredicate<T, T> filter, BiFunction<T, T, V> map, Collector<? super V, A, R> downstream) {

    class Pairing {
        T left, right;
        A middle = downstream.supplier().get();
        boolean empty = true;

        void add(T t) {
            if (empty) {
                left = t;
                empty = false;
            } else if (filter.test(right, t)) {
                downstream.accumulator().accept(middle, map.apply(right, t));
            }
            right = t;
        }

        Pairing combine(Pairing other) {
            if (!other.empty) {
                this.add(other.left);
                this.middle = downstream.combiner().apply(this.middle, other.middle);
                this.right = other.right;
            }
            return this;
        }

        R finish() {
            return downstream.finisher().apply(middle);
        }
    }

    return Collector.of(Pairing::new, Pairing::add, Pairing::combine, Pairing::finish);
}

Things would be easier if your input is located in the random-access list.如果您的输入位于随机访问列表中,事情会更容易。 This way you can utilize good old List.subList method like this:通过这种方式,您可以像这样使用旧的List.subList方法:

List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e", 
     "f", "g", "h", "err3", "i", "j");

Map<String, String> map = IntStream.range(0, list.size()-1)
    .mapToObj(i -> list.subList(i, i+2))
    .filter(l -> l.get(0).startsWith("err"))
    .collect(Collectors.toMap(l -> l.get(0), l -> l.get(1)));

The same thing could be done with already mentioned StreamEx library (written by me) in a little bit shorter manner:同样的事情可以用已经提到的 StreamEx 库(由我编写)以更短的方式完成:

List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e", 
     "f", "g", "h", "err3", "i", "j");

Map<String, String> map = StreamEx.ofSubLists(list, 2, 1)
    .mapToEntry(l -> l.get(0), l -> l.get(1))
    .filterKeys(key -> key.startsWith("err"))
    .toMap();

Though if you don't want third-party dependency, the poor Stream API solution looks also not very bad.虽然如果你不想要第三方依赖,糟糕的 Stream API 解决方案看起来也不是很糟糕。

Other approach with Collector.of and List<List<String>> as structure to collect pairs.使用Collector.ofList<List<String>>作为收集对的结构的其他方法。 First collect to List<List<String>> :首先收集到List<List<String>>

List<List<String>> collect = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
        .collect(
                Collector.of(
                        LinkedList::new,
                        (a, b) -> {
                            if (b.startsWith("err"))
                                a.add(new ArrayList<>(List.of(b)));
                            else if (!a.isEmpty() && a.getLast().size() == 1)
                                a.getLast().add(b);
                        },
                        (a, b) -> { throw new UnsupportedOperationException(); }
                )
        );

Then it can be transform to map然后它可以转换为映射

Map<String, String> toMap = collect.stream().filter(l -> l.size() == 2)
        .collect(Collectors.toMap(
                e -> e.get(0),
                e -> e.get(1))
        );

Or all in one with Collectors.collectingAndThen或者与Collectors.collectingAndThen合而为一

Map<String, String> toMap = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
        .collect(Collectors.collectingAndThen(
                Collector.of(
                        LinkedList<List<String>>::new,
                        (a, b) -> {
                            if (b.startsWith("err"))
                                a.add(new ArrayList<>(List.of(b)));
                            else if (!a.isEmpty() && a.getLast().size() == 1)
                                a.getLast().add(b);
                        },
                        (a, b) -> { throw new UnsupportedOperationException(); }
                ), (x) -> x.stream().filter(l -> l.size() == 2)
                        .collect(Collectors.toMap(
                                e -> e.get(0),
                                e -> e.get(1))
                        )
        ));

Here's a simple one liner using an off-the-shelf collector:这是一个使用现成收集器的简单衬里:

Stream<String> stream = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j");

Map<String, String> map = Arrays.stream(stream
        .collect(Collectors.joining(",")).split(",(?=(([^,]*,){2})*[^,]*$)"))
    .filter(s -> s.startsWith("err"))
    .map(s -> s.split(","))
    .collect(Collectors.toMap(a -> a[0], a -> a[1]));

The "trick" here is to first join all terms together into a single String, then split it into Strings of pairs, eg "a,b" , "err1,c" , etc. Once you have a stream of pairs, processing is straightforward.这里的“技巧”是首先将所有术语连接到一个字符串中,然后将其拆分为成对的字符串,例如"a,b" , "err1,c"等。一旦你有了一个对流,处理是直截了当。

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

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