Java8学习笔记

170 阅读24分钟

Java特性学习

Java函数式编程

lambda表达式

匿名内部类的缺点

  1. 冗余

  2. this指针容易产生误解

  3. GC失败

    lambda表达式简介

    Lambda 表达式是匿名方法,利用函数式的形式简化的写法,而且还不易产生误解。

    lambda表达式的语法由参数列表->,和函数体组成。函数体可以式表达式也可以式代码块。

    • 表达式简化了return,会直接执行然后返回结果

    但是使用lambda表达式是有条件的,必须有相应的函数接口(函数接口,是指内部只有一个抽象方法的接口)。这一点跟Java是强类型语言吻合,也就是说你并不能在代码的任何地方任性的写Lambda表达式。实际上Lambda的类型就是对应函数接口的类型。

    例子:

    (int x, int y) -> x + y                      //接收x和y两个整形参数并返回他们的和
    () -> 66                                     //不接收任何参数直接返回66
    (String name) -> {System.out.println(name);} //接收一个字符串然后打印出来
    (View view) -> {view.setText("lalala");}     //接收一个View对象并调用setText方法复制代码
    
    // Lambda表达式的书写形式
    Runnable run = () -> System.out.println("Hello World");// 1
    ActionListener listener = event -> System.out.println("button clicked");// 2
    Runnable multiLine = () -> {// 3 代码块
        System.out.print("Hello");
        System.out.println(" Hoolee");
    };
    BinaryOperator<Long> add = (Long x, Long y) -> x + y;// 4
    BinaryOperator<Long> addImplicit = (x, y) -> x + y;// 5 类型推断
    

lambda表达式的类型由上下文决定

编译器利用lambda表达式所在的上下文所期待的类型来推导表达式的类型,这个被期待的类型被称为目标类型。lambda表达式只能出现在目标类型函数式接口的上下文中。

Lambda表达式的类型和目标类型的方法签名必须一致,编译器会对此做检查,一个lambda表达式要想赋值给目标类型T则必须满足下面所有的条件:

  • T是一个函数式接口
  • lambda表达式的参数必须和T的方法参数在数量、类型和顺序上一致(一一对应)
  • lambda表达式的返回值必须和T的方法的返回值一致或者是它的子类
  • lambda表达式抛出的异常和T的方法的异常一致或者是它的子类

lambda表达式的参数类型是可以从目标类型获取的。

lambda表达式的作用域

lambda表达式不会从父类中继承任何变量,也不会引入新的作用域。

例如:

public class HelloLambda {

    Runnable r1 = () -> System.out.println(this);
    Runnable r2 = () -> System.out.println(toString());

    @Override
    public String toString() {
        return "Hello, lambda!";
    }

    public static void main(String[] args) {
        new HelloLambda().r1.run();  
        new HelloLambda().r2.run();
    }
}
最终会打印出两个Hello,lambda!

lambda表达式不会掩盖任何其所在上下文的局部变量。

变量捕获

捕获的变量即为:内部类中引用的外部变量。

Java7中明确表示:如果捕获的变量没有被声明为final就会产生一个编译错误。

Java8中可以捕获只读的局部变量:对于lambda表达式和内部类,允许在其中捕获那些符合有效只读的局部变量(如果一个局部变量在初始化后从未被修改过,那么它就是有效只读)

但是仍然不可以修改捕获的变量。

方法引用(可以认为是lambda表达式的有名字的简写形式)

方法引用和lambda表达式拥有相同的特性(他们都需要一个目标类型,并且需要被转化为函数式接口的实例),不过我们不需要为方法引用提供方法体,我们可以直接通过方法名引用已有方法。

class User{

    private String userName;

    public String getUserName() {
        return userName;
    }
    ...
}

List<User> users = new ArrayList<>();
Comparator<User> comparator = Comparator.comparing(u -> u.getUserName());
Collections.sort(users, comparator);

方法引用的写法为

Comparator<User> comparator = Comparator.comparing(User::getUserName);

这里的User::getUserName被看做是lambda表达式的简写形式。尽管方法引用不一定会把代码变得更紧凑,但它拥有更明确的语义--如果我们想要调用的方法拥有一个名字,那么我们就可以通过方法名调用它。

Optional

Java8中用来解决空指针异常的类。

从本质上来说,该类属于包含可选值的封装类(wrapper class),因此它既可以包含对象也可以仅仅为空。

但是Optional并不意味着是一种避免所有类型的空指针的机制。

例如,它仍然必须测试方法和构造函数的强制输入参数。

optional简介

我们先来看一个简单的案例。在 Java 8 之前,凡涉及到访问对象方法或者对象属性的操作,无论数量多寡,都可能导致 空指针异常:

String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();

假如我们想保证上面的小示例不出现异常,我们可能需要在访问它之前对每一个值进行显式检查:

if (user != null) {
 Address address = user.getAddress();
 if (address != null) {
     Country country = address.getCountry();
     if (country != null) {
         String isocode = country.getIsocode();
         if (isocode != null) {
             isocode = isocode.toUpperCase();
         }
     }
 }
}

optional用法

  1. 创建实例 of()或者ofNullable()来创建一个optional对象。

    of():如果传null进本方法,则会抛出空指针异常。

    ofNullable():如果对象可能为null也可能为非null,那就选择ofNullable()。

  2. 访问实例的值 可以使用get()方法,但和之前类似,这种方法在值为 null 时也会抛出异常。为避免出现异常,可以选择首先检验其中是否存在值。

利用 ifPresent()也可以用来检查是否存在值。而且该方法还带有一个 Consumer 参数,在对象不为空时执行 λ 表达式:

opt.ifPresent( u -> assertEquals(user.getEmail(), u.getEmail()));

在此示例中,只有在用户对象非空时,才会执行assertion。

  1. 返回默认值

    1)orElse() :如果存在值,则返回该值,如果不存在值,则返回它收到的参数

    @Test
    public void whenEmptyValue_thenReturnDefault() {
        User user = null;
        User user2 = new User("anna@gmail.com", "1234");
        User result = Optional.ofNullable(user).orElse(user2);
    
        assertEquals(user2.getEmail(), result.getEmail());
    }
    

    user为空,所以user2最为默认值返回,不为空,则直接返回user

    2)orElseGet() :如果存在值,则方法回返该值,如果不存在,则其执行 Supplier 函数接口(作为其收到的一个参数),并返回执行结果

    User result = Optional.ofNullable(user).orElseGet( () -> user2);
    

    如果对象为空式,以上两者并无区别,但是如果对象不为空时,orElse()方法仍然会创建默认的user对象,而orElseGet()则不会再创建user对象,前者会降低密集调用时的性能

optional的主要用途

Optional的主要用途是作为一种返回类型。

在获得该类型的一个实例后,如果存在值,可以直接提取该值,如果不存在值,就可以获得一个替换值。

Optional类的一个非常有用的用例是将其与流或返回Optional值以构建流畅的API的其他方法结合。请参见下面的代码段

User user = users.stream().findFirst().orElse(new User("default", "1234"));

不应使用optional的情况

a)不要将其用作类中的字段,因为它不可序列化

如果确实需要序列化包含Optional值的对象,则Jackson库提供了将Optionals视为普通对象的支持。这意味着Jackson将空对象视为空,将具有值的对象视为包含该值的字段。可以在jackson-modules-java8项目中找到此功能。

b)不要将其用作构造函数和方法的参数,因为这会导致不必要的复杂代码。

User user = new User("john@gmail.com", "1234", Optional.empty());

Stream

使用函数式编程的优势:

  1. 代码简洁函数式编程写出的代码简洁且意图明确,使用stream接口让你从此告别for循环。
  2. 多核友好,Java函数式编程使得编写并行程序从未如此简单,你需要的全部就是调用一下parallel()方法。

stream并不是某种数据结构,它只是数据源的一种视图。这里的数据源可以是一个数组,Java容器或I/O channel等。正因如此要得到一个stream通常不会手动创建,而是调用对应的工具方法,比如:

  • 调用Collection.stream()或者Collection.parallelStream()方法
  • 调用Arrays.stream(T[] array)方法

下图就是stream的接口继承关系:

Java_stream_Interfaces

虽然大部分情况下stream是容器调用Collection.stream()方法得到的,但streamcollections有以下不同:

  • 无存储stream并不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
  • 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream
  • 惰式执行stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  • 可消费性stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations),二者的特点是:

  1. 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。
  2. 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。

下表汇总了Stream接口的部分常见方法:

操作类型接口方法
中间操作concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()
结束操作allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

区分中间操作和结束操作最简单的方法,就是看方法的返回值,返回值为stream的大都是中间操作,否则是结束操作。

List<String> myList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
    .stream() // 创建流
    .filter(s -> s.startsWith("c")) // 执行过滤,过滤出以 c 为前缀的字符串
    .map(String::toUpperCase) // 转换成大写
    .sorted() // 排序                                                     以上四个均为中间操作
    .forEach(System.out::println); // for 循环打印                         终止操作

// C1
// C2

  • :中间操作会再次返回一个流,所以,我们可以链接多个中间操作,注意这里是不用加分号的。上图中的filter 过滤,map 对象转换,sorted 排序,就属于中间操作。
  • :终端操作是对流操作的一个结束动作,一般返回 void 或者一个非流的结果。上图中的 forEach循环 就是一个终止操作。

无集合创建Stream流

在集合上调用stream()方法会返回一个普通的 Stream 流。但是, 也大可不必刻意地创建一个集合,再通过集合来获取 Stream 流,还可以通过如下这种方式:

Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);  // a1

我们可以通过Stream.of()的方式从一堆对象中创建一个Stream流。

Stream流的执行顺序

当且仅当存在终端操作时,中间操作操作才会被执行。

例子:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    })
    .forEach(s -> System.out.println("forEach: " + s));

我们可以看到输出如下:

filter:  d2
forEach: d2
filter:  a2
forEach: a2
filter:  b1
forEach: b1
filter:  b3
forEach: b3
filter:  c
forEach: c

我最开始想的应该是先将所有 filter 前缀的字符串打印出来,接着才会打印 forEach 前缀的字符串。因为这样比较符合当前的认知。

但是输出的结果却是随着链条垂直移动的。比如说,当 Stream 开始处理 d2 元素时,它实际上会在执行完 filter 操作后,再执行 forEach 操作,接着才会处理第二个元素。

这其实是为了性能的考虑,因为把最好先把filter放在第一位,以减少后边操作的重复次数,减少资源浪费。

Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase(); // 转大写
    })
    .anyMatch(s -> {
        System.out.println("anyMatch: " + s);
        return s.startsWith("A"); // 过滤出以 A 为前缀的元素
    });

// map:      d2
// anyMatch: D2
// map:      a2
// anyMatch: A2

终端操作 anyMatch()表示任何一个元素以 A 为前缀,返回为 true,就停止循环。所以它会从 d2 开始匹配,接着循环到 a2 的时候,返回为 true ,于是停止循环。

由于数据流的链式调用是垂直执行的,map这里只需要执行两次。相对于水平执行来说,map会执行尽可能少的次数,而不是把所有元素都 map 转换一遍。

如下是一个非常明显的例子,我们尽量把能削减操作次数的操作放在流操作的先执行位置。

下面的例子由两个中间操作mapfilter,以及一个终端操作forEach组成。让我们再来看看这些操作是如何执行的:

Stream.of("d2", "a2", "b1", "b3", "c")
 .map(s -> {
     System.out.println("map: " + s);
     return s.toUpperCase(); // 转大写
 })
 .filter(s -> {
     System.out.println("filter: " + s);
     return s.startsWith("A"); // 过滤出以 A 为前缀的元素
 })
 .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

// map:     d2
// filter:  D2
// map:     a2
// filter:  A2
// forEach: A2
// map:     b1
// filter:  B1
// map:     b3
// filter:  B3
// map:     c
// filter:  C

学习了上面一小节,大概应该已经知道了,mapfilter会对集合中的每个字符串调用五次,而forEach却只会调用一次,因为只有 "a2" 满足过滤条件。

如果我们改变中间操作的顺序,将filter移动到链头的最开始,就可以大大减少实际的执行次数:

Stream.of("d2", "a2", "b1", "b3", "c")
 .filter(s -> {
     System.out.println("filter: " + s)
     return s.startsWith("a"); // 过滤出以 a 为前缀的元素
 })
 .map(s -> {
     System.out.println("map: " + s);
     return s.toUpperCase(); // 转大写
 })
 .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

// filter:  d2
// filter:  a2
// map:     a2
// forEach: A2
// filter:  b1
// filter:  b3
// filter:  c

现在,map仅仅只需调用一次,性能得到了提升,这种小技巧对于流中存在大量元素来说,是非常很有用的。

接下来,让我们对上面的代码再添加一个中间操作sorted

Stream.of("d2", "a2", "b1", "b3", "c")
 .sorted((s1, s2) -> {
     System.out.printf("sort: %s; %s\n", s1, s2);
     return s1.compareTo(s2); // 排序
 })
 .filter(s -> {
     System.out.println("filter: " + s);
     return s.startsWith("a"); // 过滤出以 a 为前缀的元素
 })
 .map(s -> {
     System.out.println("map: " + s);
     return s.toUpperCase(); // 转大写
 })
 .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

sorted 是一个有状态的操作,因为它需要在处理的过程中,保存状态以对集合中的元素进行排序。

执行上面代码,输出如下:

sort:    a2; d2
sort:    b1; a2
sort:    b1; d2
sort:    b1; a2
sort:    b3; b1
sort:    b3; d2
sort:    c; b3
sort:    c; d2
filter:  a2
map:     a2
forEach: A2
filter:  b1
filter:  b3
filter:  c
filter:  d2

咦咦咦?这次怎么又不是垂直执行了。你需要知道的是,sorted是水平执行的。因此,在这种情况下,sorted会对集合中的元素组合调用八次。这里,我们也可以利用上面说道的优化技巧,将 filter 过滤中间操作移动到开头部分:

Stream.of("d2", "a2", "b1", "b3", "c")
 .filter(s -> {
     System.out.println("filter: " + s);
     return s.startsWith("a");
 })
 .sorted((s1, s2) -> {
     System.out.printf("sort: %s; %s\n", s1, s2);
     return s1.compareTo(s2);
 })
 .map(s -> {
     System.out.println("map: " + s);
     return s.toUpperCase();
 })
 .forEach(s -> System.out.println("forEach: " + s));

// filter:  d2
// filter:  a2
// filter:  b1
// filter:  b3
// filter:  c
// map:     a2
// forEach: A2

从上面的输出中,我们看到了 sorted从未被调用过,因为经过filter过后的元素已经减少到只有一个,这种情况下,是不用执行排序操作的。因此性能被大大提高了。 作者:犬小哈 链接:juejin.cn/post/684490… 来源:掘金

stream中常用的API

forEach()

我们对forEach()方法并不陌生,在Collection中我们已经见过。方法签名为void forEach(Consumer<? super E> action),作用是对容器中的每个元素执行action指定的动作,也就是对元素进行遍历。

// 使用Stream.forEach()迭代
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.forEach(str -> System.out.println(str));

由于forEach()是结束方法,上述代码会立即执行,输出所有字符串。

filter()

Stream.filter

函数原型为Stream<T> filter(Predicate<? super T> predicate),作用是返回一个只包含满足predicate条件元素的Stream

// 保留长度等于3的字符串
Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.filter(str -> str.length()==3)
    .forEach(str -> System.out.println(str));

上述代码将输出为长度等于3的字符串youtoo。注意,由于filter()是个中间操作,如果只调用filter()不会有实际计算,因此也不会输出任何信息。

distinct()

Stream.distinct

函数原型是Stream<T> distinct(),作用是返回一个去重后的Stream

Stream<String> stream = Stream.of("I", "love", "you", "too", "too");
stream.distinct()
  .forEach(str -> System.out.println(str));
sorted()

排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序,函数原型分别为Stream<T> sorted()Stream<T> sorted(Comparator<? super T> comparator)

Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.sorted((str1, str2) -> str1.length()-str2.length())
    .forEach(str -> System.out.println(str));

上述代码将输出按照长度升序排序后的字符串,结果完全在预料之中。

map()

Stream.map

函数原型为<R> Stream<R> map(Function<? super T,? extends R> mapper),作用是返回一个对当前所有元素执行执行mapper之后的结果组成的Stream。直观的说,就是对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。

Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.map(str -> str.toUpperCase())
    .forEach(str -> System.out.println(str));

上述代码将输出原字符串的大写形式。

faltMap()

Stream.flatMap

函数原型为<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper),作用是对每个元素执行mapper指定的操作,并用所有mapper返回的Stream中的元素组成一个新的Stream作为最终返回结果。说起来太拗口,通俗的讲flatMap()的作用就相当于把原stream中的所有元素都”摊平”之后组成的Stream,转换前后元素的个数和类型都可能会改变。

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
    .forEach(i -> System.out.println(i));

上述代码中,原来的stream中有两个元素,分别是两个List<Integer>,执行flatMap()之后,将每个List都“摊平”成了一个个的数字,所以会新产生一个由5个数字组成的Stream。所以最终将输出1~5这5个数字。

魔法之规约操作

规约操作(reduction operation)又被称作折叠操作(fold),是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。Stream类库有两个通用的规约操作reduce()collect(),也有一些为简化书写而设计的专用规约操作,比如sum()max()min()count()等。

reduce()

reduce()操作可以或私信啊从一组元素中生成一个值,例如sum()max()min()count()等都是reduce()操作。

reduce()的方法定义有三种重写形式:

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • T reduce(T identity, BinaryOperator<T> accumulator)
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

虽然函数定义越来越长,但语义不曾改变,多的参数只是为了指明初始值(参数identity),或者是指定并行执行时多个部分结果的合并方式(参数combiner)。reduce()最常用的场景就是从一堆值中生成一个值。

需求:求出一组单词的长度之和。这是个“求和”操作,操作对象输入类型是String,而结果类型是Integer。

// 求单词长度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)
        (sum, str) -> sum+str.length(), // 累加器 // (2)
        (a, b) -> a+b); // 部分和拼接器,并行执行时才会用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

上述代码标号(2)处将i. 字符串映射成长度,ii. 并和当前累加和相加。这显然是两步操作,使用reduce()函数将这两步合二为一,更有助于提升性能。

Stream.reduce_parameter

终极魔法之collect()

不夸张的讲,如果你发现某个功能在Stream接口中没找到,十有八九可以通过collect()方法实现。collect()Stream接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。先看几个热身的小例子:

// 将Stream转换成容器或Map
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
// Set<String> set = stream.collect(Collectors.toSet()); // (2)
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)

上述代码分别列举了如何将Stream转换成ListSetMap

接口的静态方法和默认方法

Function是一个接口,那么Function.identity()是什么意思呢?这要从两方面解释:

  1. Java 8允许在接口中加入具体方法。接口中的具体方法有两种,default方法和static方法,identity()就是Function接口的一个静态方法。
  2. Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

上面的解释是不是让你疑问更多?不要问我为什么接口中可以有具体方法,也不要告诉我你觉得t -> tidentity()方法更直观。我会告诉你接口中的default方法是一个无奈之举,在Java 7及之前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的,因为所有实现了该接口的类都要重新实现。试想在Collection接口中加入一个stream()抽象方法会怎样?default方法就是用来解决这个尴尬问题的,直接在接口中实现新加入的方法。既然已经引入了default方法,为何不再加入static方法来避免专门的工具类呢!

方法引用

诸如String::length的语法形式叫做方法引用(method references),这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类:

方法引用类别举例
引用静态方法Integer::sum
引用某个对象的方法list::add
引用某个类的方法String::length
引用构造方法HashMap::new

我们会在后面的例子中使用方法引用。

收集器

Stream.collect_parameter

收集器(Collector)是为Stream.collect()方法量身打造的工具接口(类)。考虑一下将一个Stream转换成一个容器(或者Map)需要做哪些工作?我们至少需要两样东西:

  1. 目标容器是什么?是ArrayList还是HashSet,或者是个TreeMap
  2. 新元素如何添加到容器中?是List.add()还是Map.put()

如果并行的进行规约,还需要告诉collect() 3. 多个部分结果如何合并成一个。

结合以上分析,collect()方法定义为<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三个参数依次对应上述三条分析。不过每次调用collect()都要传入这三个参数太麻烦,收集器Collector就是对这三个参数的简单封装,所以collect()的另一定义为<R,A> R collect(Collector<? super T,A,R> collector)Collectors工具类可通过静态方法生成各种常用的Collector。举例来说,如果要将Stream规约成List可以通过如下两种方式实现:

// 将Stream规约成List
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);

通常情况下我们不需要手动指定collect()的三个参数,而是调用collect(Collector<? super T,A,R> collector)方法,并且参数中的Collector对象大都是直接通过Collectors工具类获得。实际上传入的收集器的行为决定了collect()的行为

用collect()生成Collection

前面已经提到通过collect()方法将Stream转换成容器的方法,这里再汇总一下。将Stream转换成ListSet是比较常见的操作,所以Collectors工具已经为我们提供了对应的收集器,通过如下代码即可完成:

// 将Stream转换成List或Set
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)

上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过Collectors.toCollection(Supplier<C> collectionFactory)方法完成。

// 使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

上述代码(3)处指定规约结果是ArrayList,而(4)处指定规约结果为HashSet。一切如你所愿。as your wish!

使用collect()生成Map

前面已经说过Stream背后依赖于某种数据源,数据源可以是数组、容器等,但不能是Map。反过来从Stream生成Map是可以的,但我们要想清楚Mapkeyvalue分别代表什么,根本原因是我们要想清楚要干什么。通常在三种情况下collect()的结果会是Map

情况1:使用toMap()生成的收集器,这种情况是最直接的,前面例子中已提到,这是和Collectors.toCollection()并列的方法。如下代码展示将学生列表转换成由<学生,GPA>组成的Map。非常直观,无需多言。

// 使用toMap()统计学生GPA
Map<Student, Double> studentToGPA =
     students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成key
                                     student -> computeGPA(student)));// 如何生成value

情况2:使用partitioningBy()生成的收集器,这种情况适用于将Stream中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,比如男女性别、成绩及格与否等。下列代码展示将学生分成成绩及格或不及格的两部分。

// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
         .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

情况3:使用groupingBy()生成的收集器,这是比较灵活的一种情况。跟SQL中的group by语句类似,这里的groupingBy()也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key上。下列代码展示将员工按照部门进行分组:

// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));

基础用法,增强版groupingBy()见参考链接。

以下是增强版用法:

在SQL中使用group by是为了协助其他查询,比如1. 先将员工按照部门分组,2. 然后统计每个部门员工的人数。Java类库设计者也考虑到了这种情况,增强版的groupingBy()能够满足这种需求。增强版的groupingBy()允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。

// 使用下游收集器统计每个部门的人数
Map<Department, Integer> totalByDept = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment,
                                                   Collectors.counting()));// 下游收集器

上面代码的逻辑是不是越看越像SQL?高度非结构化。还有更狠的,下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。考虑将员工按照部门分组的场景,如果我们想得到每个员工的名字(字符串),而不是一个个Employee对象,可通过如下方式做到:

// 按照部门对员工分布组,并只保留员工的名字
Map<Department, List<String>> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                        Collectors.mapping(Employee::getName,// 下游收集器
                                Collectors.toList())));// 更下游的收集器

如果看到这里你还没有对Java函数式编程失去信心,恭喜你,你已经顺利成为Java函数式编程大师了。

使用collect()做字符串join(太棒啦,过目难忘)

这个肯定是大家喜闻乐见的功能,字符串拼接时使用Collectors.joining()生成的收集器,从此告别for循环。Collectors.joining()方法有三种重写形式,分别对应三种不同的拼接方式。无需多言,代码过目难忘。

// 使用Collectors.joining()拼接字符串
Stream<String> stream = Stream.of("I", "love", "you");
//String joined = stream.collect(Collectors.joining());// "Iloveyou"
//String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"

stream底层原理 Stream Pipelines流水线

ArrayList.forEach()方法为例,具体代码如下:

// ArrayList.forEach()
public void forEach(Consumer<? super E> action) {
    ...
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);// 回调方法
    }
    ...
}

我们看到ArrayList.forEach()方法的主要逻辑就是一个for循环,在该for循环里不断调用action.accept()回调方法完成对元素的遍历。Lambda表达式的作用就是相当于一个回调方法,这很好理解。

Stream API中大量使用Lambda表达式作为回调方法,但这并不是关键。理解Stream我们更关心的是另外两个问题:流水线和自动并行。使用Stream或许很容易写入如下形式的代码:

int longestStringLengthStartingWithA
        = strings.stream()
              .filter(s -> s.startsWith("A"))
              .mapToInt(String::length)
              .max();

上述代码求出以字母A开头的字符串的最大长度,一种直白的方式是为每一次函数调用都执一次迭代,这样做能够实现功能,但效率上肯定是无法接受的。类库的实现着使用流水线(Pipeline)的方式巧妙的避免了多次迭代,其基本思想是在一次迭代中尽可能多的执行用户指定的操作。为讲解方便我们汇总了Stream的所有操作。

参考技术博客链接:

juejin.cn/post/684490…

www.jianshu.com/p/63830b7cb…

github.com/CarpenterLe…

juejin.cn/post/684490…