如何使用Java 8流来查找更大值之前的所有值?

用例

通过Katas在工作中发布的一些编码,我偶然发现了这个问题,我不知道如何解决。

使用Java 8 Streams,给定正整数列表,生成一个整数列表,其中整数前面有一个更大的值。

[10, 1, 15, 30, 2, 6] 

以上输入将产生:

 [1, 15, 2] 

因为1在15之前,15在30之前,2在6之前。

非流解决方案

 public List findSmallPrecedingValues(final List values) { List result = new ArrayList(); for (int i = 0; i < values.size(); i++) { Integer next = (i + 1 < values.size() ? values.get(i + 1) : -1); Integer current = values.get(i); if (current < next) { result.push(current); } } return result; } 

我试过的

我遇到的问题是我无法弄清楚如何在lambda中访问下一个。

 return values.stream().filter(v -> v < next).collect(Collectors.toList()); 

  • 是否可以检索流中的下一个值?
  • 我应该使用map和映射到一Pair以便访问下一个吗?

使用IntStream.range

 static List findSmallPrecedingValues(List values) { return IntStream.range(0, values.size() - 1) .filter(i -> values.get(i) < values.get(i + 1)) .mapToObj(values::get) .collect(Collectors.toList()); } 

它肯定比具有大循环的命令式解决方案更好,但对于以惯用方式“使用流”的目标仍然有点meh。

是否可以检索流中的下一个值?

不,不是真的。 我知道的最好的引用是在java.util.stream包描述中 :

流的元素仅在流的生命期间访问过一次。 与Iterator一样,必须生成新流以重新访问源的相同元素。

(检索正在操作的当前元素之外的元素意味着它们可以被访问多次。)

我们还可以通过其他方式在技术上做到这一点:

  • 有条不紊地(非常meh)。
  • 使用流的iterator技术上仍然使用流。

这不是纯Java8,但最近我发布了一个名为StreamEx的小型库,它有一个完全适用于此任务的方法:

 // Find all numbers where the integer preceded a larger value. Collection numbers = Arrays.asList(10, 1, 15, 30, 2, 6); List res = StreamEx.of(numbers).pairMap((a, b) -> a < b ? a : null) .nonNull().toList(); assertEquals(Arrays.asList(1, 15, 2), res); 

pairMap操作在内部使用自定义spliterator实现。 因此,您拥有非常干净的代码,这不依赖于源是List还是其他任何东西。 当然,它也适用于并行流。

为此任务提供了一个测试用例 。

它不是单线(它是双线),但这有效:

 List result = new ArrayList<>(); values.stream().reduce((a,b) -> {if (a < b) result.add(a); return b;}); 

不是通过“查看下一个元素”解决它,而是通过“查看前一个元素, reduce()给你免费的方式来解决它。我通过注入一个填充列表的代码片段来调整其预期用途。先前元素和当前元素的比较,然后返回当前值,以便下一次迭代将其视为前一个元素。


一些测试代码:

 List result = new ArrayList<>(); IntStream.of(10, 1, 15, 30, 2, 6).reduce((a,b) -> {if (a < b) result.add(a); return b;}); System.out.println(result); 

输出:

 [1, 15, 2] 

如果流是顺序的或并行的,则接受的答案可以正常工作,但如果基础List不是随机访问,则会因为多次调用get

如果您的流是顺序的,您可能会滚动此收集器:

 public static Collector> collectPrecedingValues() { int[] holder = {Integer.MAX_VALUE}; return Collector.of(ArrayList::new, (l, elem) -> { if (holder[0] < elem) l.add(holder[0]); holder[0] = elem; }, (l1, l2) -> { throw new UnsupportedOperationException("Don't run in parallel"); }); } 

和用法:

 List precedingValues = list.stream().collect(collectPrecedingValues()); 

不过,您也可以实现一个收集器,以便顺序和并行流。 唯一的事情是您需要应用最终转换,但是您可以控制List实现,这样您就不会受到get性能的影响。

我们的想法是首先生成一对对象列表(由大小为2的int[]数组表示),其中包含由大小为2且间隔为1的窗口切片的流中的值。 当我们需要合并两个列表时,我们检查空白并将第一个列表的最后一个元素的间隙与第二个列表的第一个元素合并。 然后我们应用最终转换来仅过滤所需的值并将它们映射到具有所需的输出。

它可能不像接受的答案那么简单,但它可以是一种替代解决方案。

 public static Collector> collectPrecedingValues() { return Collectors.collectingAndThen( Collector.of(() -> new ArrayList(), (l, elem) -> { if (l.isEmpty()) l.add(new int[]{Integer.MAX_VALUE, elem}); else l.add(new int[]{l.get(l.size() - 1)[1], elem}); }, (l1, l2) -> { if (l1.isEmpty()) return l2; if (l2.isEmpty()) return l1; l2.get(0)[0] = l1.get(l1.size() - 1)[1]; l1.addAll(l2); return l1; }), l -> l.stream().filter(arr -> arr[0] < arr[1]).map(arr -> arr[0]).collect(Collectors.toList())); } 

然后,您可以在实用程序收集器方法中包装这两个收集器,检查流是否与isParallel平行,然后决定返回哪个收集器。

如果您愿意使用第三方库而不需要并行性,那么jOOλ提供SQL风格的窗口函数如下

 System.out.println( Seq.of(10, 1, 15, 30, 2, 6) .window() .filter(w -> w.lead().isPresent() && w.value() < w.lead().get()) .map(w -> w.value()) .toList() ); 

生产

 [1, 15, 2] 

lead()函数从窗口以遍历顺序访问下一个值。

免责声明:我为jOOλ背后的公司工作

你可以通过使用有界队列来存储流经流的元素来实现这一点(这是基于我在这里详细描述的想法: 是否有可能获得流中的下一个元素?

Belows示例首先定义了BoundedQueue类的实例,它将存储通过流的元素(如果您不喜欢扩展LinkedList的想法,请参阅上面提到的链接以获得替代和更通用的方法)。 稍后您只需检查两个后续元素 – 感谢辅助类:

 public class Kata { public static void main(String[] args) { List input = new ArrayList(asList(10, 1, 15, 30, 2, 6)); class BoundedQueue extends LinkedList { public BoundedQueue save(T curElem) { if (size() == 2) { // we need to know only two subsequent elements pollLast(); // remove last to keep only requested number of elements } offerFirst(curElem); return this; } public T getPrevious() { return (size() < 2) ? null : getLast(); } public T getCurrent() { return (size() == 0) ? null : getFirst(); } } BoundedQueue streamHistory = new BoundedQueue(); final List answer = input.stream() .map(i -> streamHistory.save(i)) .filter(e -> e.getPrevious() != null) .filter(e -> e.getCurrent() > e.getPrevious()) .map(e -> e.getPrevious()) .collect(Collectors.toList()); answer.forEach(System.out::println); } }