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

假设我们有这个流

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

我想在地图中保存相邻字符串的对,其中第一个以“错误”开头。

我想到的是这样的

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

但出于两个主要原因,我并不完全满意

  1. 我在“滥用” reducefunction。 在Stream API中,每个函数都有其明确,明确的目的: max应该计算最大值, filter应该根据条件进行过滤, reduce应该产生递增累积值等等。
  2. 这样做会阻止我使用Streams强大的机制:如果我想将搜索限制为前两个结果怎么办?

在这里,我使用reduce因为(据我所知)它是唯一可以让你比较几个值的函数,你可以以某种方式返回类似于“当前值”和“下一个值”概念的东西。

有更简单的方法吗? 是否允许您为每次迭代考虑多个值来迭代流?

编辑

我正在考虑的是一些机制,给定当前元素,允许您为每次迭代定义要考虑的“元素窗口”。

就像是

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

代替

  Stream map(Function mapper); 

这将是对当前API的强大“升级”。

EDIT2

我很欣赏人们提出解决方案的努力,但问题不在于算法本身。 通过将流,索引,临时变量放在一起来存储以前的值,有不同的方法来实现我的目标…但我想知道Stream API中是否有一些方法是为处理除当前元素以外的元素而设计的没有打破“流范式”。 像这样的东西

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

鉴于答案,我认为除非使用StreamEx库,否则没有“清晰快速”的解决方案

您可以为此任务构建自定义收集器。

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

有:

 private static final class MappingErrors { private Map 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 finish() { return map; } public static Collector> collector() { return Collector.of(MappingErrors::new, MappingErrors::accept, MappingErrors::combine, MappingErrors::finish); } } 

在这个收集器中,保留了两个运行元素。 每次接受String ,它们都会更新,如果第一个以"err"开头,则会将两个元素添加到地图中。


另一种解决方案是使用StreamEx库,该库提供了一个pairMap方法,该方法将给定函数应用于此流的每个相邻元素对。 在下面的代码中,如果第一个元素以"err"开头,则操作返回一个String数组,该数组由该对​​的第一个和第二个元素组成,否则为null 。 然后过滤掉null元素,并将Stream收集到地图中。

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

您可以编写自定义收集器,或使用更简单的流式传输列表索引:

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

这假设您的数据位于随机访问友好列表中,或者您可以暂时将其转储为一个。

如果您无法随机访问数据或将其加载到列表或数组中进行处理,您可以随时创建自定义pairing收集器,以便您可以编写

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

这是collections家的来源。 它是并行友好的,在其他情况下可能派上用场:

 public static  Collector pairing(BiPredicate filter, BiFunction map, Collector 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); } 

如果您的输入位于随机访问列表中,事情会更容易。 这样你可以使用这样的旧的List.subList方法:

 List list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j"); Map 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))); 

使用已经提到过的StreamEx库(由我编写)可以用更短的方式完成同样的事情:

 List list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j"); Map map = StreamEx.ofSubLists(list, 2, 1) .mapToEntry(l -> l.get(0), l -> l.get(1)) .filterKeys(key -> key.startsWith("err")) .toMap(); 

虽然如果你不想要第三方依赖,那么糟糕的Stream API解决方案看起来也不是很糟糕。

这是一个使用现成的收集器的简单的衬垫:

 Stream stream = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j"); Map 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])); 

这里的“技巧”是首先将所有术语连接成一个字符串,然后将它分成对的字符串,例如"a,b""err1,c"等。一旦你有一对对象,处理就是直截了当。