概述

为什么要学?

1.大数量下处理集合效率高 2.代码可读性高 3.消灭嵌套地狱

概念

面向对象思想需要关注用什么对象完成什么事情,而函数式编程思想就类似于我们数学中的函数,它主要是关注对数据进行了什么操作

优点

  • 代码简洁,开发快速
  • 接近自然语言,易于理解
  • 易于并发编程

Lambda表达式

Lambda 是 JDK8 中一个语法糖,可以看成是一种语法糖,它可以对某些匿名内部类的写法进行简化,它是函数式编程思想的一个重要体现,让我们不用关注什么是对象,而是关注我们对数据进行了什么操作

基本格式

1
(参数列表) -> {代码}

我们在创建线程并启动时可以使用匿名内部类的写法:

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("小吉崽汁");
}
}).start();

可以使用Lambda的格式对其进行修改:

1
2
3
new Thread(() -> {
System.out.println("小吉崽汁");
}).start();

省略规则

  • 参数类型可以省略
  • 方法体只有一行代码时大括号return和唯一一行代码的分号可以省略
  • 方法只有一个参数时小括号可以省略

Stream流

Java8 的 Stream 使用的是函数式标称模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式的操作。可以更方便的让我们对集合或数组操作

创建流

  • 单列集合:集合对象.stream()
1
2
List<Author> authors = getAuthors();
Stream<Author> atream = authors.stream();
  • 数组:Arrays.stream(数组) 或者使用 Stream.of 来创建
1
2
3
Integer[] arr = {1, 2, 3, 4, 5};
Stream<Integer> stream = Arrays.stream(arr);
Stream<Integer> stream2 = Stream.of(arr);
  • 双列集合:转换成单列集合后再创建
1
2
3
4
Map<String, Integer> map = new HashMap<>();
map.put("蜡笔小新", 19);
map.put("黑子", 17);
Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();

中间操作

  • filter:可以对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中,例如打印所年龄小于 18 的作用姓名
1
2
3
authorList.stream()
.filter(author -> author.getAge() < 18)
.forEach(author -> System.out.println(author.getName()));
  • map:可以把对流中的元素进行计算或转换,例如打印所有作家姓名,这里返回的是一个 Map,类型就是 String 类型
1
2
3
authorList.stream()
.map(author -> author.getName())
.forEach(s -> System.out.println(s));
  • distinct:可以去除流中的重复元素,例如打印所有作家的姓名,并且要求其中不能有重复元素
1
2
3
authorList.stream()
.distinct()
.forEach(System.out::println);

注意:distinct方法是依赖 Object 方法来判断是否相同对象的所有需要注意重写 equals 方法

  • sorted:可以对流中的元素排序,例如对流中的元素按年龄进行降序排序,并且不能有重复元素
1
2
3
4
authorList.stream()
.distinct()
.sorted()
.forEach(author -> System.out.println(author.getAge()));
1
2
3
4
authorList.stream()
.distinct()
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
.forEach(author -> System.out.println(author.getAge()));

注意:如果调用空参的 sorted 的方法需要流中的元素实现 Comparable 接口

  • limit:可以设置流的最大长度,超出的部分将被抛弃,例如对流中的元素按照年龄进行降序排序,并且要求不能又重复元素,然后打印其中年龄最大的两个最大的作家姓名
1
2
3
4
5
authorList.stream()
.distinct()
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
.limit(2)
.forEach(author -> System.out.println(author.getName()));
  • skip:跳过流中的前 n 个元素,返回剩下的元素,例如打印除了年龄最大的作家外的其他作家,要求不能有重复元素,并且按年龄排序
1
2
3
4
5
authorList.stream()
.distinct()
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
.skip(1)
.forEach(author -> System.out.println(author.getName()));
  • flatMap:map 只能把一个对象转换成另一个对象来作为流中的元素。而 flatMap 可以把一个对象转换成多个对象作为流中的元素,例如打印所有书籍中的名字,要求对元素去重
1
2
3
4
authorList.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.forEach(book -> System.out.println(book.getName()));

终结操作

  • forEach:对流中的元素进行遍历操作,我们通过传入的参数去指定对遍历到的元素进行什么具体操作,例如输出所有作家的名字
1
2
3
4
authorList.stream()
.map(author -> author.getName())
.distinct()
.forEach(name -> System.out.println(name));
  • count:可以用来获取当前流中元素的个数,例如打印这些作家的所出书籍的数目,注意删除重复元素
1
2
3
4
authorList.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.count();
  • max&min:可以用来获得流中的最值,例如分别获取这些作家的所出书籍的最高分和最低分并打印
1
2
3
4
authorList.stream()
.flatMap(author -> author.getBooks().stream())
.map(book -> book.getScore())
.max((o1, o2) -> o1 - o2);
  • collect:把当前流转换成一个集合,例如获取一个存放所有作者名字的 List 集合
1
2
3
authorList.stream()
.map(author -> author.getName())
.collect(Collectors.toList());
  • anyMatch:可以用来判断是否有任意字符符合匹配条件的元素,结果为 boolean 类型,例如判断是否有年龄在 29 岁以上的作家
1
2
authorList.stream()
.anyMatch(author -> author.getAge() > 29);
  • allMatch:可以用来判断是否都符合匹配条件。结果为 boolean 类型。如果都符合结果为 true,否则为 false,例如判断是否所有作家都是成年人
1
2
authorList.stream()
.allMatch(author -> author.getAge() > 18);
  • noneMatch:可以判断流中的元素是否都不符合匹配条件。如果都不符合结果为 true,否则为 false,例如判断作家是否都没有超过 100 岁的
1
2
authorList.stream()
.noneMatch(author -> author.getAge() > 100);
  • findAny:获取流中的任意一个元素。该方法没有办法保证获取的一定是流中的第一个元素,例如获取任意一个大于 18 的作家,如果存在就输出他的名字
1
2
3
4
authorList.stream()
.filter(author -> author.getAge() > 18)
.findAny()
.ifPresent(author -> System.out.println(author.getName()));
  • findFirst:获取流中的第一个元素,例如获取一个年龄最小的作家,并输出他的姓名
1
2
3
4
authorList.stream()
.sorted(((o1, o2) -> o1.getAge() - o2.getAge()))
.findFirst()
.ifPresent(author -> System.out.println(author.getAge()));
  • reduce归并:对流中的数据按照指定的计算方式计算出一个结果。reduce 的作用是把 stream 中的元素给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和在初始化值的基础上进行计算,计算结果再和后面的元素计算,例如使用 reduce 求所有的作者年龄的和
1
2
3
4
authorList.stream()
.map(author -> author.getAge())
.reduce((integer, integer2) -> integer + integer2)
.ifPresent(age -> System.out.println(age));

注意事项

  • 惰性求值(如果没有终结操作,中间操作是不会得到不会得到执行的)
  • 流是一次性的(一旦一个流对象经过一个终结操作后,这个流就不能再被使用了)
  • 不会影响原数据(我们在流中可以多数据做很多处理,但是正常情况下是不会影响原来数据的,这也是我们所期望的)

Optional

我们在编写代码的时候出现最多的就是空指针异常,所以在很多情况下我们需要做各种非空判断。尤其是对象中的属性还是一个对象的情况下,这种判断会更多,而过多的判断语句会让我们代码显得臃肿不堪,所以在 JDK8 引入了 Optional,养成使用 Optional 的习惯后你可以写出更优雅的代码来避免空指针异常,并且在很多函数式编程相关的 API 中也使用到了 Optional

创建对象

Optional 就好像是包装类,可以把我们的具体数据封装 Optional 对象内部,然后我们去使用 Optional 中封装好的方法操作封装进去的数据就可以非常优雅的避免空指针异常。我们一一般都是用 Optional 静态方法 ofNullable 来把数据封装成一个 Optional 对象,无论传入的参数是否为 NULL 都不会出现问题

1
2
Optional.ofNullable(author)
.ifPresent(author1 -> System.out.println(author1.getName()));

你可能会觉得还要加一行代码来封装数据表麻烦,但是如果改造下 getAuthor 方法,让其返回值就是封装好的 Optional 的话,我们使用时就会更方便,而且在实际开发中我们的数据往往是从数据库获取的,Mybatis 从 3.5 也开始支持 Optional 了,我们可以直接把 dao 方法的返回值类型定义为 Optional 类型,Mybatis 会自己把数据封装成 Optional 对象返回,封装的过程也不需要我们自己操作,如果你确定一个对象不是空的则可以使用 Optional 的静态方法 of 来把自己的数据封装成 Optional 对象

1
Optional.of(author);

但是一定要注意,如果使用 of 的时候传入的参数必须不为 NULL。如果一个方法的返回值类型是 Optional 类型,而如果我们经过判断发现某次计算得到的返回值为 NULL,这个时候就需要把 NULL 封装成 Optional 对象返回,这时可以使用 Optional 的静态方法 empty 来进行封装

1
Optional.empty();

安全消费值

我们获取一个 Optional 对象后肯定需要对其中的数据进行使用,这时候我们可以使用其 ifPresent 方法来消费其中的值,这个方法会判断其内部封装的数据是否为空,不为空才会执行消费代码,这样用起来更安全

获取值

如果我们想获取值自己进行处理可以使用 get 方法获取,但是不推荐,因为当 Optional 内部的数据为空时就会出现异常

安全获取值

如果我们期望安全的获取值,我们可以使用 Optional 提供的一下方法:

  • orElseGet:获取数据并且设置为空时的默认值。如果数据不为空就能获取到该对象,如果为空则根据你传入的参数来创建对象作为默认值返回
1
Optional.ofNullable(author).orElseGet(() -> new Author());
  • orElseThrow:获取数据,如果数据不为空就能获取到,如果为空则根据你传入的参数来创建异常抛出
1
Optional.ofNullable(author).orElseThrow(() -> new RuntimeException("author为空"));

过滤

我们可以使用 filter 方法对数据进行过滤,如果原本有数据,但是不符合判断,也会变成一个无数据的 Optional 对象

1
2
Optional.ofNullable(author)
.filter(author1 -> author1.getAge() > 18);

判断

我们可以使用 isPresent 方法进行是否存在数据的判断,如果为空返回值为 false,如果不为空,返回值为 true,但是这种方式并不能体现 Optional 的好处,更推荐使用 ifPresent 方法

1
2
3
if (Optional.ofNullable(author).isPresent()) {
System.out.println(Optional.ofNullable(author).get().getName());
}

数据转换

Optional 还提供了 map 可以让我们对数据进行转换,并且转换得到的数据还是被 Optional 包装的,保证了我们的使用安全

1
2
3
Optional.ofNullable(author)
.map(author1 -> author1.getBooks())
.ifPresent(books -> System.out.println(books));

函数式接口

只有一个抽象方法的接口我们称之为函数式接口。JDK 的函数式接口都加上了 @FunctionalInterface 注解进行标识,但是无论是否加上该注解还要接口中只有一个抽象方法,都是函数式接口

常见的函数式接口

  • Consumer 消费接口
  • Function 计算转换接口
  • Predicate 判断接口
  • Supplier 生产型接口

方法引用

我们在使用 lambda 时,如果方法体中只有一个方法调用的话(包括构造方法),我们可以用方法引用进一步简化代码

推荐方法

我们在使用 lambda 时不需要考虑什么时候方法引用,用哪种方法引用,方法引用的格式是什么。我们只需要在写完 lambda 方法发现方法只有一行代码,并且方法的调用使用快捷键尝试是否能够转换成方法引用即可

基本格式

类名或者对象名::方法名

引用类的静态方法

如果我们再重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有的参数都按顺序传入了这个静态方法中,这个时候我们就可以引用类的静态方法

1
2
3
authorList.stream()
.map(author -> author.getAge())
.map(String::valueOf);

引用对象的实例方法

如果我们在重写方法的时候,方法体中只有一行代码,并且这个代码是调用了某个对象的成员方法,并且我们把要重写的抽象方法中所有的参数都按顺序传入了这个成员方法中,这个时候我们就可以引用对象的实例方法

1
2
3
4
StringBuilder sb = new StringBuilder();
authorList.stream()
.map(author -> author.getName())
.forEach(sb::append);

引用类的实例方法

如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了第一个参数的成员方法,并且我们把要重写的抽象方法中所剩余的所有参数都按顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法

1
subAuthorName("小吉崽汁", String::substring);

构造器引用

如果我们在重写方法时,方法体中只有一行代码,并且这行代码是调用了某个类的构造方法,并且我们把要重写的抽象方法中的所有参数都按顺序传入了这个构造方法中,这个时候就可以引用构造器

1
2
3
authorList.stream()
.map(Author::getName)
.map(StringBuilder::new);

高级用法

基本数据类型优化

我们之前用道德很多 Stream 的方法由于都使用了泛型,所以涉及到的参数和返回值都是引用数据类型。即使我们操作的是整数小数,但是实际用的都是他们的包装类,JDK5 中引入的自动装箱拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便,但是你一定要知道装箱好拆箱肯定是要消耗时间的,虽然这个时间消耗很小,但是在大量的数据下不断重复装箱拆箱的时候,你就不能无视这个时间损耗了,所以为了让我们能够对这部分的时间消耗进行优化,Stream 还提供了很多专门针对基本数据类型的方法,例如:mapToInt、mapToLong、mapToDouble、flatMapToInt、flatMapToDouble

1
2
3
4
5
6
authorList.stream()
.mapToInt(Author::getAge)
.map(age -> age + 10)
.filter(age -> age > 18)
.map(age -> age + 2)
.forEach(System.out::println);

并行流

当流中有大量元素时,我们可以使用并行流去提高操作的效率。其实并行流就是把任务分配给多个线程去完成,如果我们自己去用代码实现的话其实会非常复杂,并且要求你对并发编程有足够的理解和认识,而如果我们使用 Stream 的话,我们只需要修改一个方法的调用就可以使用并行流来帮我们实现,从而提高效率

1
2
3
4
5
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
stream.parallel().peek(integer -> System.out.println(integer + " " + Thread.currentThread().getName()))
.filter(num -> num > 5)
.reduce(Integer::sum)
.ifPresent(System.out::println);

也可以通过 parallelStream 直接获取并行流对象

1
authorList.parallelStream()