【问题标题】:How to skip even lines of a Stream<String> obtained from the Files.lines如何跳过从 Files.lines 获得的 Stream<String> 的偶数行
【发布时间】:2023-03-26 00:55:01
【问题描述】:

在这种情况下,只有奇数行具有有意义的数据,并且没有唯一标识这些行的字符。我的意图是获得与以下示例等效的内容:

Stream<DomainObject> res = Files.lines(src)
     .filter(line -> isOddLine())
     .map(line -> toDomainObject(line))

是否有任何“干净”的方式来做到这一点,而不共享全局状态?

【问题讨论】:

  • 你的意思是在最后一行使用 forEach 吗?
  • 有时只使用老式循环会让您的任务更轻松。
  • 查看stackoverflow.com/questions/29878981/… 了解一些解决方法
  • @aioobe,我没有看到 forEach 如何适合这个例子!
  • forEach 是终端。如果您的链中没有终端调用,则不会执行链中的任何链接。也就是说你不能用 map() 结束链

标签: java-8 java-stream


【解决方案1】:

不,无法使用 API 方便地执行此操作。 (基本上与为什么没有简单的方法拥有zipWithIndex 的原因相同,请参阅Is there a concise way to iterate over a stream with indices in Java 8?)。

您仍然可以使用Stream,但请使用迭代器:

Iterator<String> iter = Files.lines(src).iterator();
while (iter.hasNext()) {
    iter.next();                  // discard
    toDomainObject(iter.next());  // use
}

(不过,您可能希望在该流上使用 try-with-resource。)

【讨论】:

    【解决方案2】:

    一种干净的方法是更深入一层并实现Spliterator。在这个级别上,您可以控制对流元素的迭代,并在下游请求一个项目时简单地迭代两个项目:

    public class OddLines<T> extends Spliterators.AbstractSpliterator<T>
        implements Consumer<T> {
    
        public static <T> Stream<T> oddLines(Stream<T> source) {
            return StreamSupport.stream(new OddLines(source.spliterator()), false);
        }
        private static long odd(long l) { return l==Long.MAX_VALUE? l: (l+1)/2; }
        Spliterator<T> originalLines;
    
        OddLines(Spliterator<T> source) {
            super(odd(source.estimateSize()), source.characteristics());
            originalLines=source;
        }
    
        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            if(originalLines==null || !originalLines.tryAdvance(action))
                return false;
            if(!originalLines.tryAdvance(this)) originalLines=null;
            return true;
        }
    
        @Override
        public void accept(T t) {}
    }
    

    那么你就可以像这样使用它了

    Stream<DomainObject> res = OddLines.oddLines(Files.lines(src))
        .map(line -> toDomainObject(line));
    

    此解决方案没有副作用,并保留了 Stream API 的大部分优点,例如惰性求值。但是,应该清楚的是,它对于无序流处理没有有用的语义(注意微妙的方面,例如在对所有元素执行终端操作时使用forEachOrdered 而不是forEach)并且原则上支持并行处理,它不太可能非常有效......

    【讨论】:

    • 请注意,正如您从 stackoverflow.com/questions/43809885/… 看到的那样,在某些情况下,此解决方案的性能会比使用有状态 filter 的“不那么干净”解决方案更差
    【解决方案3】:

    作为aioobe said,没有一种方便的方法可以做到这一点,但有几种不方便的方法。 :-)

    这是另一种基于拆分器的方法。不像Holger's,它包装了另一个分离器,这个分离器自己做I/O。这可以更好地控制诸如排序之类的事情,但这也意味着它必须处理 IOException 和关闭处理。我还添加了一个Predicate 参数,可以让您了解哪些行通过了。

    static class LineSpliterator extends Spliterators.AbstractSpliterator<String>
            implements AutoCloseable {
        final BufferedReader br;
        final LongPredicate pred;
        long count = 0L;
    
        public LineSpliterator(Path path, LongPredicate pred) throws IOException {
            super(Long.MAX_VALUE, Spliterator.ORDERED);
            br = Files.newBufferedReader(path);
            this.pred = pred;
        }
    
        @Override
        public boolean tryAdvance(Consumer<? super String> action) {
            try {
                String s;
                while ((s = br.readLine()) != null) {
                    if (pred.test(++count)) {
                        action.accept(s);
                        return true;
                    }
                }
                return false;
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }
    
        @Override
        public void close() {
            try {
                br.close();
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }
    
        public static Stream<String> lines(Path path, LongPredicate pred) throws IOException {
            LineSpliterator ls = new LineSpliterator(path, pred);
            return StreamSupport.stream(ls, false)
                                .onClose(() -> ls.close());
        }
    }
    

    您可以在 try-with-resources 中使用它来确保文件已关闭,即使发生异常:

    static void printOddLines() throws IOException {
        try (Stream<String> lines = LineSpliterator.lines(PATH, x -> (x & 1L) == 1L)) {
            lines.forEach(System.out::println);
        }
    }
    

    【讨论】:

    • 这种方法在并行转动流时有效吗? (好吧,我有我的答案,我对其进行了测试,它多次打印了一些行,但由于共享的count 变量:/,我仍然很惊讶它可以工作。你认为有可能做到这一点,以便它在并行计算中工作吗?我应该说不,因为根据定义,请求的任务是连续的(除非您提前知道文件的行数),但我不确定......
    • @user2336315 如果我正确实现它,它会起作用的。 :-) 固定的。对不起,我应该更彻底地测试。 tryAdvance 的要求是(a)调用 action 零次并返回 false,或者(b)调用 action 一次并返回 true。之前的代码有这样的情况,动作被调用了零次,但它返回 true,这破坏了事情。这是一个简单的编程错误,而不是并发问题。拆分器实现本身不需要是线程安全的。
    • 那么当任务并行运行时,count 变量是如何调度的呢? trySplit 是否知道它拆分了多少元素并相应地更新它?我的意思是当你有一个有状态的条件时,我很难理解它是如何工作的:-(
    • @user2336315 AbstractSpliterator 子类一次由一个线程调用,并行性使用简单化策略。基本上trySplit 多次调用tryAdvance 将元素收集到一个批次中,然后作为拆分的结果移交该批次。该批次包含在由其他线程处理的不同拆分器对象中。其余元素保留在 this 拆分器中,并且可以再次以这种方式拆分。查看源代码hg.openjdk.java.net/jdk8/jdk8/jdk/file/jdk8-b132/src/share/…
    • @Stuart Marks:……值得注意的是,由于流的大小未知,拆分将使用相当大的块大小(afaik,从1024开始,然后增加)并且由于tryAdvance 中的过滤,它过滤单线程并仅在缓冲拆分时计算匹配行。所以你需要相当大的文件才能对parallel 产生任何影响,除了减慢一切速度。顺便说一句,您可以指定NONNULL 特征。也许将来会有一个利用它的实现......
    【解决方案4】:

    您可以使用自定义拆分器来做到这一点:

    public class EvenOdd {
        public static final class EvenSpliterator<T> implements Spliterator<T> {
            private final Spliterator<T> underlying;
            boolean even;
    
            public EvenSpliterator(Spliterator<T> underlying, boolean even) {
                this.underlying = underlying;
                this.even = even;
            }
    
            @Override
            public boolean tryAdvance(Consumer<? super T> action) {
                if (even) {
                    even = false;
    
                    return underlying.tryAdvance(action);
                }
                if (!underlying.tryAdvance(t -> {})) {
                    return false;
                }
                return underlying.tryAdvance(action);
            }
    
            @Override
            public Spliterator<T> trySplit() {
                if (!hasCharacteristics(SUBSIZED)) {
                    return null;
                }
                final Spliterator<T> newUnderlying = underlying.trySplit();
                if (newUnderlying == null) {
                    return null;
                }
                final boolean oldEven = even;
    
                if ((newUnderlying.estimateSize() & 1) == 1) {
                    even = !even;
                }
    
                return new EvenSpliterator<>(newUnderlying, oldEven);
            }
    
            @Override
            public long estimateSize() {
                return underlying.estimateSize()>>1;
            }
    
            @Override
            public int characteristics() {
                return underlying.characteristics();
            }
        }
    
        public static void main(String[] args) {
    
            final EvenSpliterator<Integer> spliterator = new EvenSpliterator<>(IntStream.range(1, 100000).parallel().mapToObj(Integer::valueOf).spliterator(), false);
            final List<Integer> result = StreamSupport.stream(spliterator, true).parallel().collect(Collectors.toList());
            final List<Integer> expected = IntStream.range(1, 100000 / 2).mapToObj(i -> i * 2).collect(Collectors.toList());
            if (result.equals(expected)) {
                System.out.println("Yay! Expected result.");
            }
        }
    }
    

    【讨论】:

    • 虽然这段代码 sn-p 可以解决问题,但including an explanation 确实有助于提高帖子的质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。
    【解决方案5】:

    遵循@aioobe 算法,这是@Holger 提出的另一种基于拆分器的方法,但更简洁,即使效率较低。

    public static <T> Stream<T> filterOdd(Stream<T> src) {
        Spliterator<T> iter = src.spliterator();
        AbstractSpliterator<T> res = new AbstractSpliterator<T>(Long.MAX_VALUE, Spliterator.ORDERED)
        {
            @Override
            public boolean tryAdvance(Consumer<? super T> action) {
                iter.tryAdvance(item -> {});    // discard
                return iter.tryAdvance(action); // use
            }
        };
        return StreamSupport.stream(res, false);
    }
    

    那么你就可以像这样使用它了

    Stream<DomainObject> res = Files.lines(src)
    filterOdd(res)
     .map(line -> toDomainObject(line))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-03-07
      • 2023-03-31
      • 2020-07-20
      • 2015-09-18
      • 1970-01-01
      • 2015-07-08
      相关资源
      最近更新 更多