Java8 函数式编程

马铃薯头 马铃薯头 | 407 | 2022-11-21

1. 函数试编程思想

1.1. 概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿数据做操作”
面对对象思想强调“必须通过对象的形式来做事情”
函数式思想强调则金量忽略面向对象的复杂语句:“强调做什么,而不是以什么形式去做”

1.2. 优点

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

2. Lambda表达式

2.1. 概述

Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码
块)。 Lambda 表达式(Lambda expression)可以看作是一个匿名函数,基于数学中的λ演算得名,也可称为闭包(Closure)

2.2. 核心原则

可推导可省略

2.3. 基本格式

(parameters) -> expression 或 (parameters) ->{ statements; }

2.4. 省略规则

  • 参数类型可以省略
  • 方法体只有一句代码时,大括号、return和唯一一语句代码的分号可以省略
  • 方法只有一个参数时,小括号可以省略
  • IDEA:Alt+Enter

3. Strem流

3.1. 概述

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

3.2. 创建流

3.2.1. 单列创建流

集合对象.stream()。

List创建流:

List<String> list = new ArrayList<>();
list.add("野原新之助");
list.add("野原美冴");
list.add("野原广志");

Stream<String> stream = list.stream();

3.2.2. 数组创建流

Arrays.stream(数组)或Stream.of(数组)。

Array创建流的两种方式:

Integer[] ints = {1, 2, 3, 4, 5};

Stream<Integer> stream1 = Arrays.stream(ints);

Stream<Integer> stream2 = Stream.of(ints);

3.2.3. 双列集合创建流

转换成单列集合后再创建。

HashMap创建流:

Map<String, Integer> map = new HashMap<>(3);
map.put("野原新之助", 5);
map.put("野原美冴", 29);
map.put("野原广志", 35);

Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();

3.3. 中间操作

创建集合并插入数据:

@Data
@AllArgsConstructor
class User {
    private Long id;
    private String name;
    private Integer age;
}

List<User> list = new ArrayList<>();
list.add(new User(1L, "野原新之助", 5));
list.add(new User(2L, "风间彻", 5));
list.add(new User(3L, "樱田妮妮", 5));
list.add(new User(4L, "佐藤正男", 5));
list.add(new User(5L, "野原美冴", 29));
list.add(new User(6L, "野原广志", 35));

3.3.1. filter

对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。

打印所有年龄大于10的用户姓名:

list.stream()
        .filter(user -> user.getAge() > 10)
        .forEach(user -> System.out.println(user.getName()));

运行结果:

野原美冴
野原广志

3.3.2. map

对流中的元素进行计算或转换。

打印所有用户的姓名并格式化:

list.stream()
        .map(User::getName)
        .map(name -> "姓名:" + name)
        .forEach(System.out::println);

运行结果:

姓名:野原新之助
姓名:风间彻
姓名:樱田妮妮
姓名:佐藤正男
姓名:野原美冴
姓名:野原广志

**mapToInt(mapper)**** **mapToLong(mapper)** **mapToDouble(mapper)**:**

返回包含 IntStream & LongStream & DoubleStream 将给定函数应用于此流的元素的结果。

打印所有年龄大于10岁的用户年龄,并转换成Double类型:

list.stream()
        .mapToDouble(User::getAge)
        .filter(age -> age > 10)
        .forEach(System.out::println);

运行结果:

29.0
35.0

3.3.3. distinct

去除流中的重复元素。

打印所有用户姓名,并且不能有重复元素:

list.add(new User(1L, "野原新之助", 5));
list.add(new User(1L, "野原新之助", 5));

list.stream()
        .distinct()
        .forEach(user -> System.out.println(user.getName()));

运行结果:

野原新之助
风间彻
樱田妮妮
佐藤正男
野原美冴
野原广志

3.3.4. sorted

对流中的元素进行排序。

打印所有用户姓名,按照年龄降序排序:

 list.stream()
         .sorted(((o1, o2) -> o2.getAge() - o1.getAge()))
         .forEach(user -> System.out.println(user.getName()));

运行结果:

野原广志
野原美冴
野原新之助
风间彻
樱田妮妮
佐藤正男

3.3.5. limit

对流的长度进行限制,超出的部分将被抛弃。

打印年龄最小的4位用户姓名:

list.stream()
        .sorted(((o1, o2) -> o1.getAge() - o2.getAge()))
        .limit(4)
        .forEach(user -> System.out.println(user.getName()));

运行结果:

野原新之助
风间彻
樱田妮妮
佐藤正男

3.3.6. skip

跳过流中的前n个元素,返回剩下的元素。

打印除了年龄最大的两个用户的其他用户姓名:

list.stream()
        .sorted(((o1, o2) -> o2.getAge() - o1.getAge()))
        .skip(2)
        .forEach(user -> System.out.println(user.getName()));

运行结果:

野原新之助
风间彻
樱田妮妮
佐藤正男

3.3.7. flatMap

把一个对象转换成多个对象作为流中的元素。

创建集合并插入数据:

@Data
@AllArgsConstructor
class Role {
    private Long id;
    private String name;
}

@Data
@AllArgsConstructor
class User {
    private Long id;
    private String name;
    private Integer age;
    private List<Role> roles;
}

Role role1 = new Role(1L, "学生,子女");
Role role2 = new Role(3L, "部长,家长");
Role role3 = new Role(4L, "主妇,家长");

List<Role> roleList1 = new ArrayList<>();
roleList1.add(role1);
List<Role> roleList2 = new ArrayList<>();
roleList2.add(role2);
List<Role> roleList3 = new ArrayList<>();
roleList3.add(role3);

List<User> list = new ArrayList<>();
list.add(new User(1L, "野原新之助", 5, roleList1));
list.add(new User(2L, "风间彻", 5, roleList1));
list.add(new User(3L, "樱田妮妮", 5, roleList1));
list.add(new User(4L, "佐藤正男", 5, roleList1));
list.add(new User(5L, "野原美冴", 29, roleList2));
list.add(new User(6L, "野原广志", 35, roleList3));

打印所有角色的名字并去重,要求按照","分割:

list.stream()
        .flatMap(user -> user.getRoles().stream())
        .distinct()
        .flatMap(role -> Arrays.stream(role.getName().split(",")))
        .distinct()
        .forEach(System.out::println);

运行结果:

学生
子女
部长
家长
主妇

**flatMapToInt(mapper)**** **flatMapToLong(mapper)** **flatMapToDouble(mapper)**:**

返回一个 IntStream & LongStream & DoubleStream 包含将此流的每个元素,
替换为通过将提供的映射函数应用于每个元素而生成的映射流的内容的结果。

打印所有年龄大于10岁的用户年龄,并转换成Double类型:

list.stream()
        .flatMapToDouble(user -> DoubleStream.of(user.getAge()))
        .filter(age -> age > 10)
        .forEach(System.out::println);

运行结果:

29.0
35.0

3.3.8. peek

forEach 是一个最终操作。除此之外,peek 和 forEach 再无其他不同。
如果想对流经的每个元素应用一个函数,从而改变某些状态,那么请用 forEach;
如果想打印流经的每个元素的状态(日志或 debug),这时应该用 peek。

打印所有角色的名字并去重,要求按照","分割,使用peek 打印中间过程:

list1.stream()
        .flatMap(user -> user.getRoles().stream())
        .peek(role -> System.out.println("first flatMap peek: " + role.getName()))
        .distinct()
        .flatMap(role -> Arrays.stream(role.getName().split(",")))
        .peek(name -> System.out.println("second flatMap: " + name))
        .distinct()
        .forEach(name -> System.out.println("result: " + name));

运行结果:

first flatMap peek: 学生,子女
second flatMap: 学生
result: 学生
second flatMap: 子女
result: 子女
first flatMap peek: 学生,子女
first flatMap peek: 学生,子女
first flatMap peek: 学生,子女
first flatMap peek: 部长,家长
second flatMap: 部长
result: 部长
second flatMap: 家长
result: 家长
first flatMap peek: 主妇,家长
second flatMap: 主妇
result: 主妇
second flatMap: 家长

3.4. 终结操作

创建集合并插入数据:

@Data
@AllArgsConstructor
class User {
    private Long id;
    private String name;
    private Integer age;
}

List<User> list = new ArrayList<>();
list.add(new User(1L, "野原新之助", 5));
list.add(new User(2L, "风间彻", 5));
list.add(new User(3L, "樱田妮妮", 5));
list.add(new User(4L, "佐藤正男", 5));
list.add(new User(5L, "野原美冴", 29));
list.add(new User(6L, "野原广志", 35));

3.4.1. forEach

对流中的元素进行遍历操作。

打印所有用户姓名:

list.stream().forEach(user -> System.out.println(user.getName()));

运行结果:

野原新之助
风间彻
樱田妮妮
佐藤正男
野原美冴
野原广志

3.4.2. count

获取流中元素的个数。

打印所有年龄大于10的用户姓名:

val count = list.stream()
        .filter(user -> user.getAge() > 10)
        .count();

System.out.println(count);

运行结果:

2

3.4.3. max&min

获取流中的最大值&最小值。

打印所有用户中年龄最大&最小的年龄数:

// 打印所有用户中年龄最大的年龄数
list.stream()
        .map(User::getAge)
        .max((o1, o2) -> o1 - o2)
        .ifPresent(System.out::println);

// 打印所有用户中年龄最小的年龄数
list.stream()
        .map(User::getAge)
        .max((o1, o2) -> o2 - o1)
        .ifPresent(System.out::println);

运行结果:

35
5

3.4.4. sum

返回一个 OptionalDouble 收集器,此流元素的总和。
如果此流为空,则返回空的可选值。
如果任何流元素是 NaN 或总和在任何时候都是 NaN,则总和将是 NaN。

计算所有用户年龄总和:

long sum = list.stream().mapToInt(User::getAge).sum();
System.out.println(sum);

运行结果:

84

3.4.5. average

返回一个 OptionalDouble 收集器,此流元素的算术平均值。
如果此流为空,则返回空的可选值。
如果任何记录的值是 NaN 或总和在任何时候都是 NaN,则平均值将是 NaN。

计算所有用户年龄平均数:

list.stream().mapToInt(User::getAge).average().ifPresent(System.out::println);

运行结果:

14.0

3.4.6. collect

把当前流转换成一个集合。

获取一个存放所有用户姓名的List集合:

List<String> nameList = list.stream()
        .map(User::getName)
        .collect(Collectors.toList());

System.out.println(nameList);

运行结果:

[野原新之助, 风间彻, 樱田妮妮, 佐藤正男, 野原美冴, 野原广志]

3.4.7 Collectors

根据不同的策略将元素收集归纳起来

3.4.7.1. 类型归纳

  • toList

将输入元素累积到一个新的List中

获取一个存放所有用户姓名的List集合:

List<String> nameList = list.stream()
        .map(User::getName)
        .collect(Collectors.toList());

System.out.println(nameList);

运行结果:

[野原新之助, 风间彻, 樱田妮妮, 佐藤正男, 野原美冴, 野原广志]

  • toSet

将输入元素累积到一个新的Set中

获取一个所有用户姓名的Set集合:

Set<String> nameSet = list.stream()
        .map(User::getName)
        .collect(Collectors.toSet());

System.out.println(nameSet);

运行结果:

[野原广志, 风间彻, 野原美冴, 樱田妮妮, 佐藤正男, 野原新之助]

  • toMap

将元素累积到Map中,其键和值是将提供的映射函数应用于输入元素的结果。

获取一个所有key值为姓名,value为年龄的Map集合:

Map<String, Integer> map = list.stream()
        .collect(Collectors.toMap(User::getName, User::getAge));

System.out.println(map);

运行结果:

{野原广志=35, 风间彻=5, 野原美冴=29, 樱田妮妮=5, 佐藤正男=5, 野原新之助=5}

  • toCollection

按遇见顺序将输入元素累积到一个新的Collection收集器中。
Collection收集器由提供的工厂创建。

获取年龄小于10岁的用户姓名并存到 LinkedList 中,然后打印集合中第一个元素:

LinkedList<String> users = list.stream()
        .filter(user -> user.getAge() < 10)
        .map(User::getName)
        .collect(Collectors.toCollection(LinkedList::new));

System.out.println(users.getFirst());
  • toConcurrentMap

将元素累积到ConcurrentMap中。
如果映射的键包含重复项(根据 Object.equals(Object));
则将值映射函数应用于每个equal元素,并使用提供的合并函数合并结果。

获取一个所有key值为年龄,value为姓名的Map集合,年龄重复时名字用","分隔:

ConcurrentMap<Integer, String> map = list.stream()
        .collect(Collectors.toConcurrentMap(User::getAge,
                User::getName,
                (name1, name2) -> name1 + "," + name2));

System.out.println(map);

运行结果:

{35=野原广志, 5=野原新之助,风间彻,樱田妮妮,佐藤正男, 29=野原美冴}

3.4.7.2. joining

joining() 按遇见顺序将输入元素连接成 String 。
joining(delimiter) 按遇到顺序连接输入元素,由指定的分隔符分隔。
joining(delimiter,prefix,suffix) 按顺序连接输入元素(由指定的分隔符分隔)与指定的前缀和后缀。

将所有用户的姓名拼接为字符串:

String names1 = list.stream().map(User::getName).collect(Collectors.joining());
System.out.println(names1);

String names2 = list.stream().map(User::getName).collect(Collectors.joining(","));
System.out.println(names2);

String names3 = list.stream().map(User::getName).collect(Collectors.joining(",", "[", "]"));
System.out.println(names3);

运行结果:

野原新之助风间彻樱田妮妮佐藤正男野原美冴野原广志
野原新之助,风间彻,樱田妮妮,佐藤正男,野原美冴,野原广志
[野原新之助,风间彻,樱田妮妮,佐藤正男,野原美冴,野原广志]

3.4.7.3. mapping

通过在累积之前对每个输入元素应用映射函数,将类型的接受元素适应 Collector 一个接受类型的 UT 元素。

统计各年龄的用户姓名,并有序输出:

TreeMap<Integer, String> map = list.stream().collect(Collectors.groupingBy(User::getAge, TreeMap::new, Collectors.mapping(User::getName, Collectors.joining(","))));
System.out.println(map);

运行结果:

{5=野原新之助,风间彻,樱田妮妮,佐藤正男, 29=野原美冴, 35=野原广志}

3.4.7.4. collectingAndThen

调整 以 Collector 执行其他精加工转换。

例如,可以调整 toList() 收集器以始终生成不可变列表:

List<User> userList = list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
userList.add(new User());

运行结果:

java.lang.UnsupportedOperationException

以指定字符串Average age of users is 输出所有学生平均年龄:

String str = list.stream().collect(
        Collectors.collectingAndThen(
                Collectors.averagingInt(User::getAge), avgAge -> "Average age of users is " + avgAge));
System.out.println(str);

运行结果:

Average age of users is 14.0

3.4.7.5. groupingBy(Function)

Function – 将输入元素映射到键的分类器函数
返回一个Collector收集器对T类型的输入元素执行 group by 操作,
根据分类函数对元素进行分组,并将结果返回到Map。

统计各年龄的用户姓名:

Map<Integer, List<User>> map = list.stream().collect(Collectors.groupingBy(User::getAge));
System.out.println(map);

运行结果:

{35=[User(id=6, name=野原广志, age=35)], 5=[User(id=1, name=野原新之助, age=5), User(id=2, name=风间彻, age=5), User(id=3, name=樱田妮妮, age=5), User(id=4, name=佐藤正男, age=5)], 29=[User(id=5, name=野原美冴, age=29)]}

3.4.7.6. groupingBy(Function, Collector)

Function – 将输入元素映射到键的分类器函数
Concurrent – downstreamCollector– 实施下游减排
返回一个Collector收集器,对T类型的输入元素执行级联 group by 操作,
根据分类函数对元素进行分组,使用指定的下游Collector收集器对与给定键关联的值执行缩减操作。

统计各年龄的用户人数:

Map<Integer, Long> map = list.stream().collect(Collectors.groupingBy(User::getAge, Collectors.counting()));
System.out.println(map);

运行结果:

{35=1, 5=4, 29=1}

3.4.7.7. groupingBy(Function, Supplier, Collector)

Function – 将输入元素映射到键的分类器函数
Supplier – 一个函数,当调用时,会产生所需类型的新空
Concurrent – downstreamCollector– 实施下游减排
返回一个Collector收集器,对T类型的输入元素执行级联 group by 操作,
根据分类函数对元素进行分组,使用指定的下游Collector收集器对与给定键关联的值执行缩减操作。
收集器生成的Map是使用提供的工厂函数创建的。

统计各年龄的用户人数,并有序输出:

TreeMap<Integer, Long> map = list.stream().collect(Collectors.groupingBy(User::getAge, TreeMap::new, Collectors.counting()));
System.out.println(map);

3.4.7.8. groupingByConcurrent

**groupingByConcurrent(Function)****: **

Function – 将输入元素映射到键的分类器函数
返回一个并发Collector收集器对T类型的输入元素执行 group by 操作,
根据分类函数对元素进行分组。

**groupingByConcurrent(Function, Collector)****: **

Function – 将输入元素映射到键的分类器函数
Concurrent – downstreamCollector– 实施下游减排
返回一个并发Collector收集器,对T类型的输入元素执行级联 group by 操作,
根据分类函数对元素进行分组,使用指定的下游Collector收集器对与给定键关联的值执行缩减操作。

**groupingByConcurrent(Function, Supplier, Collector)****: **

Function – 将输入元素映射到键的分类器函数
Supplier – 一个函数,当调用时,会产生所需类型的新空
Concurrent – downstreamCollector– 实施下游减排
返回一个并行Collector收集器,对T类型的输入元素执行级联 group by 操作,
根据分类函数对元素进行分组,使用指定的下游Collector收集器对与给定键关联的值执行缩减操作。
收集器生成的ConcurrentMap是使用提供的工厂函数创建的。

3.4.7.9. partitioningBy

**partitioningBy(predicate)**

predicate – 用于对输入元素进行分类的谓词
根据 Collector 对 Predicate输入元素进行分区,
并将它们组织成 Map<Boolean, List>.不保证返回的类型 Map 、可变性、可序列化性或线程安全性。

列出所有用户中大于10岁和小于10岁的用户信息:

Map<Boolean, List<User>> map = list.stream().collect(Collectors.partitioningBy(user -> user.getAge() > 10));
System.out.println(map);

运行结果:

{false=[User(id=1, name=野原新之助, age=5), User(id=2, name=风间彻, age=5), User(id=3, name=樱田妮妮, age=5), User(id=4, name=佐藤正男, age=5)], true=[User(id=5, name=野原美冴, age=29), User(id=6, name=野原广志, age=35)]}

**partitioningBy(predicate, downstreamCollector)**

predicate – 用于对输入元素进行分类的谓词
downstreamCollector– 实施下游减排
根据Predicate对输入元素进行分区,
根据另一个Collector收集器减少每个分区中的值,并将它们组织成Map,其值是下游减少的结果。

列出所有用户中大于10岁和小于10岁的用户姓名:

Map<Boolean, String> map = list.stream().collect(Collectors.partitioningBy(user -> user.getAge() > 10, Collectors.mapping(User::getName, Collectors.joining(","))));
System.out.println(map);

运行结果:

{false=野原新之助;风间彻;樱田妮妮;佐藤正男, true=野原美冴;野原广志}

3.4.7.10. reducing

**reducing(BinaryOperator)**

BinaryOperator– BinaryOperator 用于减少映射
BinaryOperator.maxBy(Comparator) & BinaryOperator.minBy(Comparator)
返回一个Collector收集器,它在指定的BinaryOperator下执行其输入元素的缩减。

输出年龄最高的用户姓名:

list.stream().collect(Collectors.reducing(BinaryOperator.maxBy(Comparator.comparingInt(User::getAge))))
        .ifPresent(user -> System.out.println(user.getAge()));
// 可以直接用 reduce 归并
list.stream().reduce(BinaryOperator.maxBy(Comparator.comparingInt(User::getAge)))
        .ifPresent(user -> System.out.println(user.getAge()));

运行结果:

35
35

**reducing(identity, BinaryOperator)**

identity – 归约的标识值(也是没有输入元素时返回的值)
BinaryOperator– BinaryOperator 用于减少映射
返回一个Collector收集器,它使用提供的标识在指定的BinaryOperator下执行其输入元素的缩减。

**reducing(identity, Function, BinaryOperator)**

identity – 归约的标识值(也是没有输入元素时返回的值)
mapper –应用于每个输入值的映射功能
BinaryOperator– BinaryOperator 用于减少映射
返回一个Collector收集器,它在指定的映射函数和BinaryOperator下执行其输入元素的缩减。
这是对reducing(identity, BinaryOperator)的概括,它允许在缩减之前转换元素。

统计所有用户年龄:

Integer ageSum = list.stream().map(User::getAge).collect(Collectors.reducing(0, Integer::sum));
System.out.println(ageSum);
Integer ageSum1 = list.stream().collect(Collectors.reducing(0, User::getAge, Integer::sum));
System.out.println(ageSum1);
// 可以直接用 reduce 归并
Integer ageSum2 = list.stream().map(User::getAge).reduce(0, Integer::sum);
System.out.println(ageSum2);

运行结果:

84
84
84

3.4.7.11. maxBy&minBy

该元素根据给定 Comparator的 产生最大&最小元素,描述为 Optional.

输出年龄最大&最小的用户:

list.stream().collect(Collectors.maxBy(Comparator.comparingInt(User::getAge))).map(User::getName).ifPresent(System.out::println);
// 年龄最小的用户
list.stream().collect(Collectors.minBy(Comparator.comparingInt(User::getAge))).map(User::getName).ifPresent(System.out::println);

运行结果:

野原广志
野原新之助

3.4.7.12. averaging

averagingInt(mapper) averagingLong(mapper) averagingDouble(mapper)
返回一个 Collector ,该函数生成应用于输入元素的整数值函数的算术平均值。
如果不存在任何元素,则结果为 0。
double格式可以表示 -2⁵³ 到 2⁵³ 范围内的所有连续整数。
如果管道的值超过 2⁵³ 个,则平均计算中的除数将在 2⁵³ 处饱和,从而导致额外的数值误差。

计算所有用户年龄的平均值:

Double avgAge = list.stream().collect(Collectors.averagingInt(User::getAge));
System.out.println(avgAge);

运行结果:

14.0

3.4.7.13. summing

summingInt(mapper) summingLong(mapper) summingDouble(mapper)
返回一个Collector收集器,它生成应用于输入元素的double值函数的总和。如果没有元素,则结果为0。

计算所有用户年龄的总和:

Long ageSum = list.stream().collect(Collectors.summingLong(User::getAge));
System.out.println(ageSum);

运行结果:

84

3.4.7.14. summarizing

summarizingInt(mapper) summarizingLong(mapper) summarizingDouble(mapper)
返回一个Collector收集器,它将double生成映射函数应用于每个输入元素,并返回结果值的摘要统计信息。

输出所有用户年龄统计信息:

LongSummaryStatistics summary = list.stream().collect(Collectors.summarizingLong(User::getAge));
System.out.println(summary);

运行结果:

LongSummaryStatistics{count=6, sum=84, min=5, average=14.000000, max=35}

3.4.8. 查找与匹配

3.4.8.1. anyMatch

用来判断流中是否有任意符合匹配条件的元素。

判断是否有未成年用户:

val flag = list.stream().anyMatch(user -> user.getAge() < 18);

System.out.println(flag);

运行结果:

true

3.4.8.2. allMatch

用来判断流中是否都符号匹配条件。

判断是否全部的用户都是成年人:

val flag = list.stream().allMatch(user -> user.getAge() >= 18);

System.out.println(flag);

运行结果:

false

3.4.8.3. noneMatch

用来判断流中是否都不符号匹配条件。

判断是否没有未成年用户:

val flag = list.stream().allMatch(user -> user.getAge() >= 18);

System.out.println(flag);

运行结果:

false

3.4.8.4. findAny

获取流中的任何一个元素(但不一定是第一个元素)。

打印一个未成年用户的姓名:

list.stream()
        .filter(user -> user.getAge() <= 18)
        .findAny()
        .ifPresent(user -> System.out.println(user.getName()));

运行结果:

野原新之助

3.4.8.5. findFirst

获取流中的第一个元素。

打印年龄最大的用户姓名:

list.stream()
        .sorted((o1, o2) -> o2.getAge() - o1.getAge())
        .findFirst()
        .ifPresent(user -> System.out.println(user.getName()));

运行结果:

野原广志

3.4.9. reduce归并

对流中的数据按照你指定的计算方式计算出一个结果。(缩减操作)

reduce的作用是把stream中的元素给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和在初始化值的基础上进行计算,计算结果再和后面的元素计算。

3.4.9.1. reduce两个参数的重载形式内部的计算方式:

第一个参数 identity 为默认值
第二个参数 (result, element) 分别为结果与元素

T result = identity;
for (T element : this stream)
	result = accumulator.apply(result, element)
return result;

其中identity就是我们可以通过方法参数传入的初始值
accumulator的apply具体进行什么计算也是我们通过方法参数来确定的。

打印所有用户的年龄总和:

val sum = list.stream()
        .distinct()
        .map(User::getAge)
        .reduce(0, Integer::sum);

System.out.println(sum);

运行结果:

84

打印所有用户的年龄最大值:

val sum = list.stream()
        .distinct()
        .map(User::getAge)
        .reduce(Integer.MIN_VALUE, (result, element) -> result < element ? element : result);

System.out.println(sum);

运行结果:

35

3.4.9.2. reduce一个参数的重载形式内部的计算

先把第一个元素赋为初始化值
然后再拿后面的元素跟变量进行计算

boolean foundAny = false;
T result = null;
for (T element : this stream) {
    if (!foundAny) {
        foundAny = true;
        result = element;
    }
    else
        result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();

打印所有用户的年龄最小值:

list.stream()
        .distinct()
        .map(User::getAge)
        .reduce((result, element) -> result > element ? element : result)
        .ifPresent(System.out::println);

运行结果:

5

3.5. 注意事项

  • 惰性求值(如果没有终结操作,中间操作是不会得到执行的)
// 不会有任何输出结果
list.stream()
        .filter(user -> user.getAge() > 10)
        .peek(user -> System.out.println(user.getName()));
  • 流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用)
Stream<User> stream = list.stream();
stream.forEach(user -> System.out.println(user.getName()));
// error info:java.lang.IllegalStateException: stream has already been operated upon or closed
stream.forEach(user -> System.out.println(user.getName()));
  • 不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元素的。这往往也是我们期望的)
list.stream()
        .map(User::getAge)
        .map(age -> age + 10)
        .forEach(System.out::println);
// 一般情况下,集合内的值并没有发生改变
list.forEach(user -> System.out.println(user.getAge()));

list.stream()
        .peek(user -> user.setAge(user.getAge() + 10))
        .map(User::getAge)
        .forEach(System.out::println);
// 二般情况下会发生改变
list.forEach(user -> System.out.println(user.getAge()));

4. Optional

4.1. 概述

我们在编写代码的时候出现最多的就是空指针异常,所以在很多情况下我们需要做各种非空的判断。

User user = new User();
if (user != null) {
    System.out.println(user.getName());
}

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

4.2. 使用

4.2.1. 创建对象

Optional 就好像是包装类,可以把我们的具体数据封装 Optional 对象内部。
然后我们去使用Optional中封装好的方法操作封装进去的数据就可以非常优雅的避免空指针异常。

getUser()

private User getUser() {
    return new User(1L, "野原新之助", 5);
}

我们一般使用 Optional.ofNullable() 来把数据封装成一个 Optional 对象。
无论传入的参数是否为null都不会出现问题。

User user = getUser();
Optional<User> userOptional = Optional.ofNullable(user);
// 输出结果:野原新之助
System.out.println(user1.getName());

你可能会觉得还要加一行代码来封装数据比较麻烦。
但是如果改造下getUser(),让其的返回值就是封装好的 Optional 的话,我们在使用时就会方便很多。

private Optional<User> getOptionalUser() {
    User user = new User(1L, "野原新之助", 5);
    return Optional.ofNullable(user);
}

而且在实际开发中我们的数据很多是从数据库获取的。
Mybatis从3.5 版本可以也已经支持 Optional 了。
我们可以直接把 dao 方法的返回值类型定义成 Optional 类型
MyBastis 会自己把数据封装成 Optional 对象返回。封装的过程也不需要我们自己操作。

如果你确定一个对象不是空时则可以使用 Optional.of() 来把数据封装成 Optional 对象。

User user = getUser();
Optional<User> userOptional = Optional.of(user);
// 输出结果:野原新之助
System.out.println(user.getName());

但是一定要注意,如果使用of的时候传入的参数必须不为null。

// 如果传入null,则会报空指针异常
Optional<User> userOptional = Optional.of(null);

如果一个方法的返回值类型是 Optional 类型。
而如果我们经判断发现某次计算得到的返回值为 null,这个时候就需要把 null 封装成 Optional 对象返回。
这时则可以使用 Optional.empty() 来进行封装。

4.4.2. 安全消费值

我们获取到一个 Optional 对象后肯定需要对其中的数据进行使用。
这时候我们可以使用其 ifPresent() 方法对来消费其中的值。
这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码,这样使用起来就更加安全了。
例如,以下写法就优雅的避免了空指针异常:

User user = getUser();
Optional<User> userOptional = Optional.ofNullable(user);
// 输出结果:野原新之助
userOptional.ifPresent(user1 -> System.out.println(user1.getName()));

4.4.3. 获取值

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

Optional<User> userOptional = Optional.ofNullable(null);
// error info: java.util.NoSuchElementException: No value present
User user = userOptional.get();

4.4.4. 安全获取值

如果我们期望安全的获取值。我们不推荐使用 get() 方法,而是使用 Optional 提供的以下方法。

4.4.4.1. orElseGet()

获取数据并且设置数据为空时的默认值,如果数据不为空就能获取到该数据。
如果为空则根据你传入的参数来创建对象作为默认值返回。

Optional<User> userOptional = Optional.ofNullable(null);
User user = userOptional.orElseGet(User::new);

4.4.4.2. orElseThrow()

获取数据,如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建异常抛出。

Optional<User> userOptional = Optional.ofNullable(null);
// error info: java.lang.RuntimeException: the data is null.
User user = userOptional.orElseThrow(() -> new RuntimeException("the data is null."));

4.4.5. 过滤

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

Optional<User> userOptional = getOptionalUser();
Optional<User> optional = userOptional.filter(user -> user.getAge() < 10);
User user = optional.orElseGet(User::new);
// 输出结果:野原新之助
System.out.println(user.getName());

Optional<User> optional1 = userOptional.filter(user1 -> user1.getAge() > 10);
User user1 = optional1.orElseGet(User::new);
// 输出结果:null
System.out.println(user1.getName());

4.4.6. 判断

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

Optional<User> userOptional = getOptionalUser();
boolean flag = userOptional.isPresent();
// 输出结果:true
System.out.println(flag);

Optional<User> userOptional1 = Optional.ofNullable(null);
boolean flag1 = userOptional1.isPresent();
// 输出结果:false
System.out.println(flag1);

4.4.7. 数据转换

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

前置操作:

@Data
@NoArgsConstructor
@AllArgsConstructor
class User {
    private Long id;
    private String name;
    private Integer age;
    private List<Role> roles;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
    private Long id;
    private String name;

}

private Optional<User> getOptionalUserRole() {
    Role role1 = new Role(1L, "学生");
    Role role2 = new Role(2L, "子女");
    Role role3 = new Role(3L, "同学");
    List<Role> roleList = new ArrayList<>();
    roleList.add(role1);
    roleList.add(role2);
    roleList.add(role3);
    return Optional.of(new User(1L, "野原新之助", 5, roleList));
}

打印所有用户的角色名称:

Optional<User> optional = getOptionalUserRole();
optional.map(User::getRoles)
        .ifPresent(roles -> roles.forEach(role -> System.out.println(role.getName()))
        );

运行结果:

学生
子女
同学


本笔记参考于博主 三更草堂 Archie_java 整理
如有侵权,请联系作者 马铃薯头 删除

文章标签: Java
推荐指数:

真诚点赞 诚不我欺~

Java8 函数式编程

点赞 收藏 评论