上一篇介绍了lambda和函数接口,这一篇主要讲解Java8 新增的Optional和Stream的使用。

讲解Optional前,我们先看看Java中的空指针异常。

Java世界两大异常 OOM和NEP。

OOM即OutOfMemoryError,一旦出现OOM,就意味着非常严重的问题,虚拟机直接down掉。JMM 5大区域:程序计数器、Java 栈、方法区、堆、本地方法栈,规定只有程序计数器不会出现OOM。现在随着JVM和硬件越来越强,OOM极其少见,本篇暂不展开。

NEP即NullPointerException,空指针异常是Java世界最臭名昭著的异常,至少60%的错误都是NEP问题造成的,NEP很复杂吗?恰恰相反,但是如果不能深入理解,NEP可能跟随程序员的一生,即使是老手,也常常出错。Java祖师爷高斯林也承认这是一项失误。

1
2
3
4
5
6
//NEP的根源就是null, 我们看看null是什么
Object obj = null;
Runnable runnable = null;
String str = null;
Integer integer = null;
int num = null; //error, 不能通过编译

null 是个引用类型的特殊值,可以赋值给任何引用类型,不能赋值给基本类型。
null是个右值,只能出现在赋值表达式右边。
C语言的Null ((void*)0),代表0x0000逻辑地址[不存在实际物理地址],Java 也是类似。引用类型就相当于一个弱化的指针,无法对其进行指针运算,只能对其指向的内容操作。
Object类型的引用 obj可以指向任何对象,null可以赋值个任意类型的引用。

造成NEP的最根本原因是一个引用类型的指向null,却调用了其实例方法[属性]

1
2
3
4
5
6
7
//造成NEP的最根本原因是一个引用类型的指向null,却调用了其实例方法。
obj.toString(); //NEP
runnable.run(); //NEP
str.equals(str1); //NEP
integer.intValue(); //NEP
//一定要注意是*实例方法*,通过引用调用 静态方法不会NEP
integer.max(10,20); //20, 虽然通过引用调用静态方法很少见

NEP 有很大一部分是自动装箱与拆箱(Autoboxing and unboxing)造成的,自动装箱与拆箱是Java5 引入的语法糖,但是却让NEP更加难以察觉.

1
2
3
4
5
6
// 装箱就是自动将基本数据类型转换为包装器类型
int a = 10;
Integer integer = a; //自动装箱 实际上JVM自动调用 Integer.valueOf(a);
Object obj = 10; //基本类型本来不能直接赋值给引用类型,但是自动装箱后可以了。
int b = integer; //自动拆箱 integer.intValue();
//**装箱调用的是静态方法,所以不会有NEP问题。恶心的是拆箱,一旦出现包装类对象为null,就会出现NEP.**

装箱调用的是静态方法,所以不会有NEP问题。恶心的是拆箱,一旦出现包装类对象为null,就会出现NEP.

Java中并没有提供跟null相关的操作符,所以代码随处可见 if 判断,Java8 吸收了Google Guava框架Optional设计,让null处理更加优雅,更加可读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*我们看一下 C# 是怎么解决 NEP
1.null与任何数数学运算结果为null,null与bool运算也为null。[Java null除了==,不能参与任何运算]
2.提供可空类型(Nullable), null运算符 ??、?.、等*/
int? a = null; // int?相当于Java Integer,不过int?是值类型,本质是个结构体类型
int b = 2 * a ?? 0; //?? 左操作数不为 null,则返回左操作数;否则返回右操作数. 所有 b=0

//Java8 之前几乎只能依赖于if,代码随处可见 if 判断
if (obj == null) { // or obj != null
//do something
}
// ** instanceof 除了判断某个对象是某个类的实例,还可以判断某个对象是否为null**
if (integer instanceof Integer) {
//如果 integer== null,将不会走这个逻辑, instanceof是null安全运算符。
}

以上两种办法其实简化粗鲁,我们看看 Optional<T>,Optional 是 final类,且构造方法私有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Optional是一个值的容器,这个值<T>可以为null
1.Optional.empty(); // 创建 Optional, T = null;
2.Optional.of(T value); // T = value, 此时value不能为空,否则抛出NEP
3.Optional.ofNullable(T value); // T = value, value可以为null [常用]

4.optional.isPresent();//是否存在 T != null; true
5.optional.ifPresent(Consumer<? super T> consumer);//T != null, consumer.accept(value);

6.optional.filter(Predicate<? super T> predicate);
7.optional.map(Function<? super T, ? extends U> mapper);
8.optional.flatMap(Function<? super T, Optional<U>> mapper);

9.optional.get(); //value = null; 抛出NEP [不常用]
10.optional.orElse(T other); // value = null; 值为other [常用]
11.optional.orElseGet(Supplier<? extends T> other);
12.optional.orElseThrow(Supplier<? extends X> exceptionSupplier);
/* 1-3 为创建Optional,api 非常简单易懂,可以自行查看源码
4-5, 判断optional的值 T 是否为null
6-8 中间处理,结果还是Optional,值可能变化
9-12 获取值*/
//到Java11,新增
isEmpty(); //和 ifPresent相对
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction);//存在干什么,不存在干什么
stream(); //实用

我们看一个很重要的概念:map/reduce。本身内涵丰富,Hadoop设计思想之一。在其他语言就是一个函数,看一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//js map
//map相当于对集合每一项都施加映射操作,最终集合元素个数不变
function pow(x) {
return x * x; //求平方函数
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]

//js reduce
//reduce 相当于对集合每一项都施加归并操作,最终集合元素变成一个。
function add(x, y) {
return x + y;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.reduce(add); //45 求和

可以发现,map函数特点,一个参数一个返回值.reduce 两个参数一个返回值
map 相当于 Function 函数接口。
reduce 相当于 BinaryOperator extends BiFunction<T,T,T>。
forEach 需要的是 Consumer。

1
2
3
4
5
6
7
8
9
10
//看一下Optional 实际使用
Integer ints = 1;
Function<Integer, Integer> mapper = e -> {
if (e == null) {
return 0;
}
return 2 * e;
};
//将一个可能为null的数放入Optional,经过map, 判断是否大于3,如果T = null,就取T=3
int a = Optional.ofNullable(ints).map(mapper).filter(e -> e > 3).orElse(3); //3

Optional 一般用于检验方法入参,或者作为函数返回值,一般不用于函数参数。OOP和函数式没有孰优孰劣,当方法链越来越长,再夹杂一些异步,就会丧失可读性。

Stream是对集合和增强,简化了并行[forkJoin],非常的实用,一定要掌握。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 先看看 Stream 的强大作用
// List 去除 null, 去重后求和
List<Integer> nums = Arrays.asList(1, 1, null, 2, 3, 4, null, 5, 6, 7, 8, 9, 10);
HashSet<Integer> integers = new HashSet<>(nums); //传统方法
int mount = 0;
for (Integer integer : integers) {
if (integer == null) {
continue;
}
mount += integer;
}
//Stream
int sum = nums.stream().filter(Objects::nonNull).distinct().mapToInt(Integer::intValue).sum();

Stream 不是集合元素,并不保存数据,Stream 就如同一个高级迭代器(Iterator),单向,不可往复,数据只能遍历一次。Stream 分类:1.适用引用类型 Stream<T> 2.基本类型对应的Stream,IntStream,LongStream,DoubleStream。这些可以相互转化。

Stream 使用,三步:

1.获取Stream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.从集合获取
Collection.stream();
Collection.parallelStream(); // 获取并行Stream
2.Array工具类Arrays可以获取任何类型Stream,除了下面还有一些重载方法
stream(T[] array); //Stream
stream(int[] array); //IntStream
stream(long[] array); //LongStream
stream(double[] array); //DoubleStream
eg:
Integer[] arr = {10, 7, 8, 4, 6};
Stream<Integer> stream = Arrays.stream(arr);
3.Stream 静态方法
Stream<T> empty();
Stream.of(T t);
Stream.of(T... values); //使用Arrays.stream(T[] array)实现
Stream.ofNullable(T t);


Java SE      java

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!