龙空技术网

Java8 Stream 详解——面试必备

米修的生活 569

前言:

如今同学们对“thejavatutorial”大概比较关心,朋友们都想要知道一些“thejavatutorial”的相关内容。那么小编也在网上网罗了一些对于“thejavatutorial””的相关文章,希望你们能喜欢,朋友们快快来了解一下吧!

最近有面试几家公司,Java Stream是一个常被问到的Java基础问题。

Java 8为我们提供了Stream API,延迟序列数据管道的函数式支持。 它不是作为数据结构或通过直接更改其元素来实现的。 它只是一个笨拙的管道,提供了可操作的脚手架,使其真正成为了智能管道。

基本概念

流背后的基本概念很简单:我们获得了一个数据源,执行了零次或更多次中间操作,并获得了结果。

流的各部分可以分为三组:

source 源工作 (各种中间操作)得到结果 (各种终结操作)获得源

第一步是获取流。 JDK的许多数据结构已经支持提供流:

java.util.Collection#stream()java.util.Arrays#stream(T[] array)java.nio.file.Files#list(Path dir)java.nio.file.Files#lines(Path path)

或者我们可以通过将java.util.stream.Stream.of(T ... values)与我们的值一起使用来创建一个。 类java.util.StreamSupport还提供了许多用于创建流的静态方法。

工作(中间操作)

java.util.Stream接口提供许多不同的操作。

过滤(filtering)filter(Predict predict)distinct()Mappingmap(Function mapper)mapToInt(ToIntFunction mapper)mapToLong(ToLongFunction mapper)mapToDouble(ToDoubleFunction mapper)flatMap(Function mapper)

// flatMap(Function mapper){ {1,2}, {3,4}, {5,6} } -> flatMap -> {1,2,3,4,5,6}​{ {'a','b'}, {'c','d'}, {'e','f'} } -> flatMap -> {'a','b','c','d','e','f'}​String[][] data = new String[][]{{"a", "b"}, {"c", "d"}, {"e", "f"}};​//Stream<String>, GOOD!Stream<String> stringStream = temp.flatMap(x -> Arrays.stream(x));​Stream<String> stream = stringStream.filter(x -> "a".equals(x.toString())); 
flatMapToInt(Function mapper)
// flatMapToInt(Function mapper) List<String> list = Arrays.asList("1", "2", "3",  "4", "5");  // Using Stream flatMapToInt(Function mapper)  list.stream().flatMapToInt(num -> IntStream.of(Integer.parseInt(num))).forEach(System.out::println);
flatMapToLong(Function mapper)flatMapToDouble(Function mapper)大小Sizing / 排序 Sortingskip(long n)limit(long maxSize)sorted()sorted(Comparator comparator)
//sorted(Comparator comparator)List<User> sortedList = users.stream()   .sorted(Comparator.comparingInt(User::getAge))   .collect(Collectors.toList());
调试 Debuggingpeek(Consumer action)
peed(Consumer action)Stream.of("one", "two", "three", "four")  .filter(e -> e.length() > 3)  .peek(e -> System.out.println("Filtered value: " + e))  .map(String::toUpperCase)  .peek(e -> System.out.println("Mapped value: " + e))  .collect(Collectors.toList());

得到结果(终结操作)

在流元素上执行操作很棒。 但是在某些时候,我们希望从数据管道中获得结果。 终端操作正在启动惰性管道来完成实际工作,并且不会返回新的流。

聚集得到新的集合/arrayR collect(Collector collector)R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)

//R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) List<Integer> list = Arrays.asList(1, 3, 5, 11, 14);​ Supplier<AtomicInteger> supplier = AtomicInteger::new; BiConsumer<AtomicInteger, Integer> accumulator = (AtomicInteger a, Integer i) -> { a.set(a.get() + i);                }; BiConsumer<AtomicInteger, AtomicInteger> combiner = (a1, a2) -> { a1.set(a1.get() + a2.get());                };​ AtomicInteger result = list.stream().collect(supplier, accumulator, combiner);
Object[] toArray()A[] toArray(IntFunction generator)Reduce到单值T reduce(T identity, BinaryOperator accumulator)Optional reduce(BinaryOperator accumulator)U reduce(U identity, BiFunction accumulator, BinaryOperator combiner)
//U reduce(U identity, BiFunction accumulator, BinaryOperator combiner)String result = letters.stream().reduce( "", (partialString, element) -> partialString.toUpperCase() + element.toUpperCase());
计算Optional min(Comparator comparator)Optional max(Comparator comparator)long count()匹配boolean allMatch(Predicate predicate)boolean anyMatch(Predicate predicate)boolean noneMatch(Predicate predicate)查找Optional findAny()Optional findFirst()消费void forEach(Consumer action)void forEachOrdered(Consumer action)Stream 特点

流不仅仅是牛逼的循环。 当然,我们可以用循环表示任何流,而大多数循环都可以用流表示。 但这并不意味着它们相等,或者一个总是比另一个更好。

惰性(Laziness)

与循环相比,流最显着的优势是懒惰。 除非我们在流上调用终端操作,否则任何工作都不会完成。 我们可以随着时间的推移建立处理流水线,并且仅在我们希望的确切时间运行它。 不仅管道的建设是懒惰的。 大多数中间操作也是惰性的。 元素仅在需要时使用。

无状态(Stateless)

大部分情况是这样的。

函数式编程的主要支柱之一是不可变状态。 除distinct(),sorted(),limit()和skip()外,大多数中间操作都是无状态的。 即使Java允许构建有状态的lambda,我们也应始终努力将其设计为无状态的。 任何状态都可能对安全性和性能产生严重影响,并可能带来意想不到的副作用。

优化

由于(大部分)是无状态的,流可以非常有效地优化自身。 无状态的中间操作可以合并到一个合并的使用者。 多余的操作可能会被删除。 而且某些管道路径可能会短路。 JVM也将优化传统循环。 但是由于其多操作设计,流是更容易实现的目标,并且大多是无状态的。

不可复用(unreusable)

只是愚蠢的管道,无法重复使用流。 但是它们不会更改原始数据源-我们始终可以从该源创建另一个流。

更少的样板代码

流通常更易于阅读和理解。 这是一个带有for循环的简单数据处理示例:

List<Album> albums = ...;List<String> result = new ArrayList<>();for (Album album : albums) {    if (album.getYear() != 1999) {        continue;    }    if (album.getGenre() != Genre.ALTERNATIVE) {        continue;    }    result.add(album.getName());    if (result.size() == 5) {        break    }}Collections.sort(result);

这是实现同样功能的流:

List<String> result =     albums.stream()          .filter(album -> album.getYear == 1999)          .filter(album -> album.getGenre() == Genre.ALTERNATIVE)          .limit(5)          .map(Album::getName)          .sorted()          .collect(Collectors.toList());

我们的代码块更短,操作更清晰,没有循环样板,也没有额外的临时变量。 全部打包在流畅的API中。 这样,我们的代码可以反映出什么,而我们不再需要关心实际的迭代过程,如何做。

更容易并发

并发很难做到正确,容易出错。 流支持并行执行(forkJoin),如果我们自己做的话,可以消除很多开销。 可以通过调用中间操作parallel()将流并行化,并通过调用sequence()将其转换为顺序流。 但是并不是每个流管道都适合并行处理。 源必须足够大并且操作必须足够昂贵才能证明多个线程的开销是合理的。 上下文切换很昂贵。 我们不应该仅仅因为可以并行化流。

处理基本类型

与Funcational Interface一样,流具有专门的类来处理基元,从而避免自动装箱/拆箱:

java.util.stream.IntStreamjava.util.stream.LongStreamjava.util.stream.DoubleStream

流最佳用法和警告小操作

如果用花括号将Lambda打包成简单的单行代码或庞大的代码块。 为了保持简单明了,我们应该将自己限制在以下两个用例中:

单行操作 -- .filter(album -> album.getYear() > 4)方法引用 -- filter(this::myFilterCriteria)

通过使用方法引用,我们可以进行更复杂的操作,重用操作逻辑,甚至可以更轻松地对其进行单元测试。

方法引用

使用方法引用不仅会影响简洁性。 在字节码级别上也有含义。 lambda和方法引用之间的字节码略有不同-方法引用产生的字节码更少。 Lambda可能会转换为调用正文的匿名类,从而创建超出所需数量的代码。 此外,通过使用方法引用,我们会丢失lambda的视觉噪音:

source.stream()      .map(s -> s.length())      .collect(Collectors.toList());// VS.source.stream()      .map(String::length)      .collect(Collectors.toList());
类型转换和类型检查

不要忘记Class <T>也是一个对象,它提供了许多有用的方法:

source.stream()      .filter(String.class::isInstance)      .map(String.class::cast)      .collect(Collectors.toList());
返回值或者检查null

中间操作应返回一个值或在下一个操作中处理null。 添加一个简单的.filter(Objects :: nonNull)可能足以确保没有NPE。

代码格式化

通过将每个流水线步骤放到新的一行中,我们可以提高可读性:

List<String> result = albums.stream().filter(album -> album.getReleaseYear == 1999)    .filter(album -> album.getGenre() == Genre.ALTERNATIVE).limit(5)    .map(Album::getName).sorted().collect(Collectors.toList());// VS.List<String> result =     albums.stream()          .filter(album -> album.getReleaseYear == 1999)          .filter(album -> album.getGenre() == Genre.ALTERNATIVE)          .limit(5)          .map(Album::getName)          .sorted()          .collect(Collectors.toList());

它还允许我们在正确的流水线步骤中设置断点。

不是每次迭代都是stream

如前所述,我们不应该替换每个循环。 仅仅因为迭代就不能使其成为基于流的处理的有效目标。 通常,与在流上使用forEach()相比,传统循环可能是更好的选择。

有效的final

我们可以在中间操作之外访问变量,只要它们在范围内并且有效地是最终变量即可。 这意味着不允许在初始化后进行更改,但不需要显式的final修饰符。 有时,这种限制看起来很麻烦,并且只要变量是final,我们就可以更改有效的final对象的状态。 但是这样做破坏了不变性的概念,并引入了意想不到的副作用。

检查异常

Streams和Exceptions不会结合,但我将尝试对其进行总结。 该代码无法编译:

List<Class> classes =     Stream.of("java.lang.Object",              "java.lang.Integer",              "java.lang.String")          .map(Class::forName)          .collect(Collectors.toList());

因为Class.forName(String className) 有一个检查异常, ClassNotFoundException, 必须要处理,这样代码就很丑:

List<Class> classes =    Stream.of("java.lang.Object",              "java.lang.Integer",              "java.lang.String")          .map(className -> {            try { return Class.forName(className); }            catch (ClassNotFoundException e) { return null; }        })        // additional filter step required, to deal with null        .filter(Objects::nonNull)        .collect(Collectors.toList());

通过将className转换重构为专用方法,我们可以重塑流的简单性:

Class toClass(String className) {    try {        return Class.forName(className);    }    catch (ClassNotFoundException e) {        return null;    }}List<Class> classes =    Stream.of("java.lang.Object",              "java.lang.Integer",              "java.lang.String")          .map(this::toClass)          .filter(Objects::nonNull)          .collect(Collectors.toList());

我们仍然需要处理可能的null值,但流代码中看不到已检查的异常。 处理已检查的异常的另一种解决方案是将中间操作包装在使用者/函数等中,以捕获已检查的异常并将其重新抛出为未检查。 但是,我认为,这更像是一个丑陋的骇客,而不是有效的解决方案。 如果一个操作抛出一个检查异常,我们应该将其重构为一个方法并相应地处理其异常。

未检查异常

即使我们处理所有已检查的异常,由于未检查的异常,我们的流仍然会爆炸。 就像其他任何代码中一样,没有一种通用的解决方案可以防止异常。 开发人员纪律可以大大降低风险。 使用定义明确的小型操作,并进行足够的检查和验证。 这样,我们至少可以将风险降到最低。

调试

流可以像其他任何流畅的调用一样进行调试。 如果一行中只有一个操作,则断点将相应地停止。 但是,为lambda创建匿名类可能会导致真正令人困惑的堆栈跟踪。 在开发过程中,我们还可以利用中间操作peek(Consumer <?super T> action)来拦截元素。 该操作主要用于调试目的,不应以流的最终形式使用。 IntelliJ提供了 visual debugger, 可以尝试一下。

操作的顺序

例子:

Stream.of("ananas", "oranges", "apple", "pear", "banana")      .map(String::toUpperCase)       // 1. Process      .sorted(String::compareTo)      // 2. Sort      .filter(s -> s.startsWith("A")) // 3. Filter      .forEach(System.out::println);

该代码将运行map五次,sort八次,filter五次,forEach两次。 这意味着总共要进行20个操作才能输出两个值。 如果我们对管道部件进行重新排序,则可以在不更改实际结果的情况下大大减少总操作数:

Stream.of("ananas", "oranges", "apple", "pear", "banana")      .filter(s -> s.startsWith("a")) // 1. Filter first      .map(String::toUpperCase)       // 2. Process      .sorted(String::compareTo)      // 3. Sort      .forEach(System.out::println);

首先过滤,我们将其他操作限制在最低限度:filter5次,map2次,sort1次和forEach 2次,总共为我们节省了10次操作。

Java9 增加的功能dropWhile(Predicate predicate) 删除元素,直到遇到第一个错误predict。takeWhile(Predicate predicate) 接受元素,直到遇到第一个错误predict。Stream ofNullable(T t) 如果nullable不为空,则返回单元素流;否则为空流。iterate(T seed, Predicate hasNext, UnaryOperator next) 产生终结流,等于forloop, 比如,Stream.iterate(0, i -> i < 10, i -> i + 1)

参考Stream package documentation for Java SE 8 (Oracle)“The Java 8 Stream API Tutorial” (Baeldung)

个人博客:

标签: #thejavatutorial