代码之家  ›  专栏  ›  技术社区  ›  VSO

Java8流懒惰在实践中是无用的吗?

  •  8
  • VSO  · 技术社区  · 7 年前

    here over here

    我们以这段代码为例:

    int[] myInts = new int[]{1,2,3,5,8,13,21};
    
    IntStream myIntStream = IntStream.of(myInts);
    
    int[] myChangedArray = myIntStream
                            .peek(n -> System.out.println("About to square: " + n))
                            .map(n -> (int)Math.pow(n, 2))
                            .peek(n -> System.out.println("Done squaring, result: " + n))
                            .toArray();
    

    这将登录控制台,因为 terminal operation ,在这种情况下 toArray()

      IntStream myChangedInts = myIntStream
        .peek(n -> System.out.println("About to square: " + n))
        .map(n -> (int)Math.pow(n, 2))
        .peek(n -> System.out.println("Done squaring, result: " + n));
    

    什么也不会被打印出来,因为地图没有发生,因为我不需要数据。直到我称之为:

      int[] myChangedArray = myChangedInts.toArray();
    

    在我调用 ,我可以传递这个“不是真正的过滤流”,但那又怎样?这是唯一的好处吗?

    这些文章似乎暗示,懒惰会带来绩效提升,例如:

    Java8StreamsAPI通过短路操作优化流处理。短路方法在满足条件后立即结束流处理。在正常的话短路操作,一旦条件得到满足只是打破所有的中间操作,躺在前面的管道。一些中间操作和终端操作都有这种行为。

    最后,在第二篇文章中有一句令人困惑的话:

    不处理过时的数据?什么?延迟加载如何防止人们处理过时的数据?


    TLDR:除了能够在以后运行filter/map/reduce/whatever操作之外,延迟加载还有什么好处吗(这对性能没有好处)?

    7 回复  |  直到 7 年前
        1
  •  19
  •   ernest_k Petronella    7 年前

    你的终端操作, toArray() ,可能支持您的论点,因为它需要流的所有元素。

    //example 1: print first element of 1000 after transformations
    IntStream.range(0, 1000)
        .peek(System.out::println)
        .mapToObj(String::valueOf)
        .peek(System.out::println)
        .findFirst()
        .ifPresent(System.out::println);
    
    //example 2: check if any value has an even key
    boolean valid = records.
        .map(this::heavyConversion)
        .filter(this::checkWithWebService)
        .mapToInt(Record::getKey)
        .anyMatch(i -> i % 2 == 0)
    

    第一个流将打印:

    0
    0
    0
    

    peek() 调用必须在所有元素上运行( 完全没有必要 因为你只对一个元素感兴趣)。中间操作可能会很昂贵(例如在第二个示例中)

    短路终端操作(其中 toArray 是不是)使这种优化成为可能。

        2
  •  7
  •   Oleksandr Pyrohov Andreas    7 年前

    惰性对于API的用户来说非常有用,尤其是当最终的结果 Stream 管道评估可能非常大!

    Files.lines 方法。如果不想将整个文件读入内存,只需要第一个文件 N 行,然后写:

    Stream<String> stream = Files.lines(path); // lazy operation
    
    List<String> result = stream.limit(N).collect(Collectors.toList()); // read and collect
    
        3
  •  4
  •   Peter Mortensen Pieter Jan Bonestroo    7 年前

    map().reduce() map().collect() ,但有一个非常明显的好处 findAny() findFirst() anyMatch() , allMatch() 基本上,任何可以短路的操作。

        4
  •  4
  •   Peter Mortensen Pieter Jan Bonestroo    7 年前

    我有一个来自我们代码库的真实示例,因为我要简化它,不完全确定您是否喜欢它或完全理解它。。。

    我们的服务需要 List<CustomService> ,我应该叫它。现在为了调用它,我将访问一个数据库(比实际情况简单得多)并获取一个 List<DBObject> ; 为了获得 列表<客户服务> 从这一点来看,需要做一些重大的转变。

    和一个 Function<DBObject, CustomService> . 这听起来微不足道,但它使 懒惰 max 通过一些属性等-因此我不需要为 所有元素 ,这是 Stream API 基于拉力的懒惰是赢家。

    在溪流出现之前,我们用 guava Lists.transform( list, function) 那也太懒了。

    这并不是溪流的基本特征,即使没有番石榴也可以做到,但这样做简单多了。这里提供的示例 findFirst 是伟大的,也是最容易理解的;这就是懒惰的全部要点,元素只在需要时才被拉出来,它们不是从一个中间操作大块地传递到另一个中间操作,而是一次从一个阶段传递到另一个阶段。

        5
  •  3
  •   Peter Mortensen Pieter Jan Bonestroo    7 年前

    问得好。

    for 和一个 stream

    考虑下面的例子。

    // Some lengthy computation
    private static int doStuff(int i) {
        try { Thread.sleep(1000); } catch (InterruptedException e) { }
        return i;
    }
    
    public static OptionalInt findFirstGreaterThanStream(int value) {
        return IntStream
                .of(MY_INTS)
                .map(Main::doStuff)
                .filter(x -> x > value)
                .findFirst();
    }
    
    public static OptionalInt findFirstGreaterThanFor(int value) {
        for (int i = 0; i < MY_INTS.length; i++) {
            int mapped = Main.doStuff(MY_INTS[i]);
            if(mapped > value){
                return OptionalInt.of(mapped);
            }
        }
        return OptionalInt.empty();
    }
    

    给定上述方法,下一个测试应该显示它们在大约相同的时间内执行。

    public static void main(String[] args) {
        long begin;
        long end;
    
        begin = System.currentTimeMillis();
        System.out.println(findFirstGreaterThanStream(5));
        end = System.currentTimeMillis();
        System.out.println(end-begin);
    
        begin = System.currentTimeMillis();
        System.out.println(findFirstGreaterThanFor(5));
        end = System.currentTimeMillis();
        System.out.println(end-begin);
    }
    

    可选项[8]

    5119

    可选项[8]

    不管怎样,我们大部分时间都呆在公园里 doStuff 方法。假设我们想在混合中添加更多线程。

    调整stream方法很简单(考虑到您的操作满足并行流的前提条件)。

    public static OptionalInt findFirstGreaterThanParallelStream(int value) {
        return IntStream
                .of(MY_INTS)
                .parallel()
                .map(Main::doStuff)
                .filter(x -> x > value)
                .findFirst();
    }
    

    在没有流的情况下实现相同的行为可能很棘手。

    public static OptionalInt findFirstGreaterThanParallelFor(int value, Executor executor) {
        AtomicInteger counter = new AtomicInteger(0);
    
        CompletableFuture<OptionalInt> cf = CompletableFuture.supplyAsync(() -> {
            while(counter.get() != MY_INTS.length-1);
            return OptionalInt.empty();
        });
    
        for (int i = 0; i < MY_INTS.length; i++) {
            final int current = MY_INTS[i];
            executor.execute(() -> {
                int mapped = Main.doStuff(current);
                if(mapped > value){
                    cf.complete(OptionalInt.of(mapped));
                } else {
                    counter.incrementAndGet();
                }
            });
        }
    
        try {
            return cf.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            return OptionalInt.empty();
        }
    }
    

    测试再次在大约相同的时间执行。

    public static void main(String[] args) {
        long begin;
        long end;
    
        begin = System.currentTimeMillis();
        System.out.println(findFirstGreaterThanParallelStream(5));
        end = System.currentTimeMillis();
        System.out.println(end-begin);
    
        ExecutorService executor = Executors.newFixedThreadPool(10);
        begin = System.currentTimeMillis();
        System.out.println(findFirstGreaterThanParallelFor(5678, executor));
        end = System.currentTimeMillis();
        System.out.println(end-begin);
    
        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.SECONDS);
        executor.shutdownNow();
    }
    

    1004

    可选项[8]

    1004

    ,尽管我们并没有从流中获得很大的性能优势(考虑到您在自己的应用程序中编写了优秀的多线程代码) 或者),代码本身更易于维护。

    与编程语言一样,更高层次的抽象( streams fors )以性能为代价使开发更容易。我们没有从汇编语言转向过程语言,而是转向面向对象语言,因为后者提供了更高的性能。我们搬家是因为它提高了我们的生产效率(以更低的成本开发同样的产品)。如果您能够从流中获得与使用 对于

        6
  •  2
  •   João Mendes    7 年前

    一个有趣的用例还没有提到,它是流上操作的任意组合,来自代码库的不同部分,响应不同种类的业务或技术需求。

    如果没有惰性流,代码的同一部分可能会过滤已经实现的完整集合,但这可能会很昂贵,因为没有真正的收益。

    或者,代码的同一部分可能希望将其筛选器附加到数据源,但现在它必须知道数据是否来自数据库,以便可以附加WHERE子句或其他源。

    因此,更好的抽象、更好的性能、更好的代码可读性和可维护性,对我来说似乎是一个胜利

        7
  •  0
  •   Konstantin Pelepelin    7 年前

    非惰性实现将处理所有输入,并将输出收集到每个操作的新集合中。显然,对于无限或足够大的源来说是不可能的,否则会消耗内存,并且在减少和短路操作的情况下会消耗不必要的内存,因此有很大的好处。