有条件地从流中删除第一个(索引为零)元素

Remove first(with zero index) element from stream conditionally

我有以下代码:

Stream<String> lines = reader.lines();

如果第一个字符串等于 "email" 我想从流中删除第一个字符串。对于流中的其他字符串,我不需要此检查。

我该如何实现?

P.S.

我当然可以将它转换为列表,然后使用老派的 for 循环,但我还需要再次流式传输。

如果您无法获得列表而必须只使用一个 Stream,您可以使用一个变量。

问题是你只能使用 "final" 或 "effectively final" 的变量,所以你不能使用文字布尔值。您仍然可以使用 AtomicBoolean 来做到这一点:

Stream<String> stream  = Arrays.asList("test", "email", "foo").stream();

AtomicBoolean first = new AtomicBoolean(true);
stream.filter(s -> {
    if (first.compareAndSet(true, false)) {
        return !s.equals("email");
    }
    return true;
})
// Then here, do whatever you need
.forEach(System.out::println);

注意:我不喜欢在 Stream 中使用 "external variables",因为副作用在函数式编程范例中是一种不好的做法。欢迎更好的选择。

要根据索引过滤元素,您可以在处理 Stream 时使用 AtomicInteger 来存储和递增索引:

private static void filter(Stream<String> stream) {
  AtomicInteger index = new AtomicInteger();
  List<String> result = stream
      .filter(el -> {
        int i = index.getAndIncrement();
        return i > 0 || (i == 0 && !"email".equals(el));
      })
      .collect(toList());
  System.out.println(result);
}

public static void main(String[] args) {
  filter(Stream.of("email", "test1", "test2", "test3")); 
  //[test1, test2, test3]

  filter(Stream.of("test1", "email", "test2", "test3")); 
  //[test1, email, test2, test3]

  filter(Stream.of("test1", "test2", "test3")); 
  //[test1, test2, test3]
}

这种方法允许过滤任何索引处的元素,而不仅仅是第一个。

@Arnouds 回答正确。您可以为第一行创建一个流,然后如下比较,

Stream<String> firstLineStream = reader.lines().limit(1).filter(line -> !line.startsWith("email"));;
Stream<String> remainingLinesStream = reader.lines().skip(1);
Stream.concat(firstLineStream, remainingLinesStream);

为了避免检查文件每一行的条件,我只是单独阅读并检查第一行,然后 运行 其余行的管道而不检查条件:

String first = reader.readLine();
Stream<String> firstLines = Optional.of(first)
        .filter(s -> !"email".equals(s))
        .map(s -> Stream.of(s))
        .orElseGet(() -> Stream.empty());

Stream<String> lines = Stream.concat(firstLines, reader.lines());

在 Java 9+ 上更简单:

Stream<String> firstLines = Optional.of(first)
        .filter(s -> !"email".equals(s))
        .stream();

Stream<String> lines = Stream.concat(firstLines, reader.lines());

有点复杂,从this snippet中得到一些启发。

您可以创建一个 Stream<Integer> 来表示索引,并 "zip" 它与您的 Stream<String> 一起创建一个 Stream<Pair<String, Integer>>

然后使用索引过滤并将其映射回 Stream<String>

public static void main(String[] args) {
    Stream<String> s = reader.lines();
    Stream<Integer> indexes = Stream.iterate(0, i -> i + 1);

    zip(s, indexes)
        .filter(pair -> !(pair.getKey().equals("email") && pair.getValue() == 0))
        .map(Pair::getKey)
        .forEach(System.out::println);
}

private static Stream<Pair<String,Integer>> zip(Stream<String> stringStream, Stream<Integer> indexesStream){
    Iterable<Pair<String,Integer>> iterable = () -> new ZippedWithIndexIterator(stringStream.iterator(), indexesStream.iterator());
    return StreamSupport.stream(iterable.spliterator(), false);
}

static class ZippedWithIndexIterator implements Iterator<Pair<String, Integer>> {
    private final Iterator<String> stringIterator;
    private final Iterator<Integer> integerIterator;

    ZippedWithIndexIterator(Iterator<String> stringIterator, Iterator<Integer> integerIterator) {
        this.stringIterator = stringIterator;
        this.integerIterator = integerIterator;
    }
    @Override
    public Pair<String, Integer> next() {
        return new Pair<>(stringIterator.next(), integerIterator.next());
    }
    @Override
    public boolean hasNext() {
        return stringIterator.hasNext() && integerIterator.hasNext();
    }
}

虽然 reader 在您从中构建线条流后将处于未指定状态,但在您构建之前它处于明确定义的状态。

所以你可以做到

String firstLine = reader.readLine();
Stream<String> lines = reader.lines();
if(firstLine != null && !"email".equals(firstLine))
    lines = Stream.concat(Stream.of(firstLine), lines);

我认为这是最干净的解决方案。请注意,这与 Java 9 的 dropWhile 不同,如果它们匹配,后者会丢弃不止一行。

这里是 Collectors.reducing 的例子。但最终还是创建了一个列表。

Stream<String> lines = Arrays.asList("email", "aaa", "bbb", "ccc")
        .stream();

List reduceList = (List) lines
        .collect(Collectors.reducing( new ArrayList<String>(), (a, v) -> {
                    List list = (List) a;
                    if (!(list.isEmpty() && v.equals("email"))) {
                        list.add(v);
                    }
                    return a;
                }));

reduceList.forEach(System.out::println);

试试这个:

MutableBoolean isFirst = MutableBoolean.of(true);
lines..dropWhile(e -> isFirst.getAndSet(false) && "email".equals(e))