1. 概述
Java流API提供了多种方法来操作和处理元素序列。然而,如果我们只想处理部分流,例如每隔N个元素,这可能会很有用,比如当我们处理代表CSV文件或数据库表的原始数据流,并且只想处理特定列时。
我们将探讨两种类型的流:有限流和无限流。对于有限流,我们可以将其转换为列表,从而允许索引访问。而对于无限流,我们需要不同的方法。在这篇教程中,我们将学习如何使用各种技术解决这个挑战。
2. 测试设置
我们将使用参数化测试来检查解决方案的正确性。将有一些与相应的N值和预期结果相关的案例:
Arguments.of(
Stream.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"),
List.of("Wednesday", "Saturday"), 3),
Arguments.of(
Stream.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"),
List.of("Friday"), 5),
Arguments.of(
Stream.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"),
List.of("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"), 1)
现在,我们可以深入研究从流中处理第N个元素的不同方法。
3. 使用filter()
在第一种方法中,我们可以创建一个只包含我们想要处理的元素索引的单独流。我们可以使用一个*过滤器(断言函数)*来创建这样的数组:
void givenListSkipNthElementInListWithFilterTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
final List<String> sourceList = input.collect(Collectors.toList());
final List<String> actual = IntStream.range(0, sourceList.size())
.filter(s -> (s + 1) % n == 0)
.mapToObj(sourceList::get)
.collect(Collectors.toList());
assertEquals(expected, actual);
}
如果我们的数据结构允许索引访问,如列表,这种方法将奏效。所需的元素可以收集到一个新的List中,或者使用*forEach(消费器)*进行处理。
4. 使用iterate()
这种方法类似于前一种,但需要允许索引访问的数据结构。然而,我们不会像过滤掉不需要的索引那样,而是仅生成我们一开始想要使用的索引:
void givenListSkipNthElementInListWithIterateTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
final List<String> sourceList = input.collect(Collectors.toList());
int limit = sourceList.size() / n;
final List<String> actual = IntStream.iterate(n - 1, i -> (i + n))
.limit(limit)
.mapToObj(sourceList::get)
.collect(Collectors.toList());
assertEquals(expected, actual);
}
在这个例子中,我们使用了*IntStream.iterate(int, IntUnaryOperator),它允许我们创建步长为n*的整数序列。
5. 使用subList()
这个方法利用了Stream.iterate
,类似于前一个方法,但它创建的是以nk索引开始的列表流:
void givenListSkipNthElementInListWithSublistTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
final List<String> sourceList = input.collect(Collectors.toList());
int limit = sourceList.size() / n;
final List<String> actual = Stream.iterate(sourceList, s -> s.subList(n, s.size()))
.limit(limit)
.map(s -> s.get(n - 1))
.collect(Collectors.toList());
assertEquals(expected, actual);
}
我们应该取这些列表的第一个元素以获取所需的结果。
6. 使用自定义Collector
作为更高级且透明的解决方案,我们可以实现一个自定义的收集器,它只收集所需的元素:
class SkippingCollector {
private static final BinaryOperator<SkippingCollector> IGNORE_COMBINE = (a, b) -> a;
private final int skip;
private final List<String> list = new ArrayList<>();
private int currentIndex = 0;
private SkippingCollector(int skip) {
this.skip = skip;
}
private void accept(String item) {
int index = ++currentIndex % skip;
if (index == 0) {
list.add(item);
}
}
private List<String> getResult() {
return list;
}
public static Collector<String, SkippingCollector, List<String>> collector(int skip) {
return Collector.of(() -> new SkippingCollector(skip),
SkippingCollector::accept,
IGNORE_COMBINE,
SkippingCollector::getResult);
}
}
这种方法更复杂,需要一些编码。同时,这种解决方案不支持并行化,并且在技术上甚至可能在顺序流上失败,因为组合是实现细节,可能会在未来版本中更改:
public static List<String> skipNthElementInStreamWithCollector(Stream<String> sourceStream, int n) {
return sourceStream.collect(SkippingCollector.collector(n));
}
然而,可以使用* Spliterators*使这种方法适用于并行流,但这应该有充分的理由。
7. 简单循环
所有之前的解决方案都能工作,但总的来说,它们过于复杂,而且往往误导人。解决问题的最好方式往往是使用尽可能简单的实现。这就是我们如何使用一个*for*循环来达到相同的效果:
void givenListSkipNthElementInListWithForTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
final List<String> sourceList = input.collect(Collectors.toList());
List<String> result = new ArrayList<>();
for (int i = n - 1; i < sourceList.size(); i += n) {
result.add(sourceList.get(i));
}
final List<String> actual = result;
assertEquals(expected, actual);
}
然而,有时我们需要直接与流一起工作,这将不允许我们直接通过索引访问元素。在这种情况下,我们可以使用带有while循环的*Iterator*:
void givenListSkipNthElementInStreamWithIteratorTestShouldFilterNthElement(Stream<String> input, List<String> expected, int n) {
List<String> result = new ArrayList<>();
final Iterator<String> iterator = input.iterator();
int count = 0;
while (iterator.hasNext()) {
if (count % n == n - 1) {
result.add(iterator.next());
} else {
iterator.next();
}
++count;
}
final List<String> actual = result;
assertEquals(expected, actual);
}
这些解决方案更清晰,更容易理解,同时解决了相同的问题。
8. 总结
Java流API是一个强大的工具,有助于使代码更具声明性和可读性。此外,流可以通过利用参数化来实现更好的性能。然而,渴望在任何地方都使用流可能不是使用此API的最佳方式。
尽管在不适合的地方应用流操作的思维锻炼可能会很有趣,但也可能导致“聪明代码”。通常,最简单的结构,如循环,可以用较少且更易理解的代码实现相同的结果。
如往常一样,本文中使用的所有代码均可在GitHub上找到。