Java8中两个优雅的特性

Posted by kingkk on 2019-12-16

前言

算是之前写代码的时候学到的两个比较有意思的特性,也是Java8新增的两个功能,其实说到底就是减少了一些nullptr的判断和for循环的遍历,让代码变得更加优雅,可读性更高。

  • Optional
  • Stream

主要就是如上两个特性,总结的过程中也学了一些以前不知道的特性。

Optional

先从Optional来吧,他其实是对原有的数据类型进行了一层包装,将nullptr判断的部分操作进行了一定的封装。

可以先来看一些常规的操作

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
String r1 = getStrOpt().orElse("default string");
String r2 = getStrOpt().orElseGet(() -> String.valueOf(Math.random()));
String r3 = getStrOpt().orElseThrow(IllegalStateException::new);
System.out.println(r1);
System.out.println(r2);
System.out.println(r3);
}

public static Optional<String> getStrOpt() {
return Math.random() > 0.5 ? Optional.of("optional str") : Optional.empty();
}

可以看到可以通过如下三种方式来从Optional中获取值,并为没有值的情况选择一个默认的返回情况,可以是一个default值,也可以是通过调用某个函数返回的值,以及抛出一个异常

  • Optional.orElse
  • Optional.orElseGet
  • Optional.orElseThrow

无能为力之处

但是其实它并不能化简如下的这种操作,遇到这种情况时其实和判断!=null并没有什么太多的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
String s = getStr();
if(s != null){
System.out.println(s);
}

Optional<String> sOpt = getStrOpt();
if(sOpt.isPresent()){
System.out.println(sOpt.get());
}
}

public static String getStr() {
return Math.random() > 0.5 ? "str" : null;
}

public static Optional<String> getStrOpt() {
return Math.random() > 0.5 ? Optional.of("optional str") : Optional.empty();
}

可以看到要进行的操作是差不多的,并不比null判断要方便多少(或许他的isPresent让人看的更有逼格?)

优势

那Optional的好处在于哪呢?他真正方便的地方可能在于一些链式的操作。

假设下面flatMapgetUSBgetVersion处理返回的都是个Optional\<T> 对象,就可以用如下链式操作

1
2
3
4
String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.flatMap(USB::getVersion)
.orElse("UNKNOWN");

否则需要进行多次的!=null的判断比较,才能进行下一步的操作。

对于一个empty的Optional,flatMap的调用之后的结果也是一个空的Optional,从而避免了nullptr的异常。

假如调用函数处理的不是Optional对象,则应该调用Optional.map来进行处理

1
Optional<String> a = getStrOpt().map(String::toUpperCase);
  • public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
  • public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper)

总之呢,个人感觉Optional确实解决了一部分空指针的烦恼,但也需要你的项目代码里有较多的Optional代码的支持,否则就会出现之前isPresent判断并没有什么优化的尴尬情况,反而让人觉得多套了一层这个Optional的麻烦。

Stream

流式操作,stream这是一个在Collection接口的方法,可以将集合以一种很优雅且性能相当高的方式进行操作。

1
2
3
long count = words.stream()
.filter(w -> w.length() > 12)
.count()

可以很容易的看出这是从words中筛选长度大于12的字符串,并计算它的个数。

流的创建

可以通过两种方式创建流

  • Collection.stream() − 为集合创建串行流。

  • Collection.parallelStream() − 为集合创建并行流。

其实当数据量不大时,用stream完全没有问题,当数据量大到需要优化时,可以采用并行流的方式。

但是并行流由于是并行的关系,所以对于遍历顺序之类的操作是无法指定的,否则也失去了并行的优势。

或者通过Stream的一些静态方法

  • Stream.of("a","b","c")
  • Stream.generate(() -> "kingkk") 通过一个函数生成流
  • Stream.iterate(BigInterger.ZERO, n -> n.add(BigInter.ONE)) 通过一个种子和一个函数,反复调用生成流
  • Stream.empty() 生成一个空流

通过这种函数调用之类的方式,可以生成一个无限流,也就类似与Python中的yield生成器。

流式操作还有几个特点

  • 流并不存储元素
  • 流并不会修改数据源
  • 尽可能的惰性处理(也是提升性能的一个很重要的步骤)

终结操作和中间操作

流的操作主要分为中间操作终结操作,他们之间的区别主要在于

  • 中间操作:中间操作在一个流上进行操作,返回结果是一个新的流。这些操作是延迟执行的。
  • 终结操作:终结操作遍历流来产生一个结果或是副作用。在一个流上执行终结操作之后,该流被消费,无法再次被消费

其实直接看代码也很好区分一个stream上的操作是终结操作还是中间操作

1
2
Optional<T> findAny();
Stream<T> filter(Predicate<? super T> predicate);

可以看到中间操作的返回类型都是Stream,而终结操作的返回类型是其他的结果,也就不能在进行流的链式操作了。

有很多,我就列几个常用的吧

中间操作

  • map(lamda) 就是map,别问我是啥
  • flatMap(lamda) 将数据类型铺平之后进行map操作,类似于 [[a,b,c], [d,e] ] -> [func(a), func(b), func(c), func(d), func(e) ]
  • filter(lamda) 传入一个判断函数,过滤只符合条件的数据
  • distinct() 类似Mysql的distinct,去除重复的数据
  • imit(num) 截断流使其最多只包含指定数量的数据
  • skip(num) 返回一个新的流,并跳过原始流中的前 N 个元素。
  • sorted([lamda]) 对流进行排序,也可以传入一个函数,按指定方式排序
  • peek(lamda) 产生另一个流,通常用于调试之类的操作,由于它不是终结操作,流并不会终止
  • concat(stream) 连接流

终结操作

  • forEach(lamda) 循环遍历,但不保证顺序
  • forEachOrdered(lamda) 顺序遍历
  • max() 最大值
  • min() 最小值
  • findFirst() 返回第一个元素(类型为Optional),如果不存在,则返回一个空的Optional
  • findAny() 返回任何一个元素(类型为Optional),和findFirst的区别在于它不要求顺序
  • anyMatch(lamda) 是否存在一个匹配
  • allMatch(lamda) 是否全部匹配
  • noneMatch(lamda) 是否有不匹配的
  • reduce(lamda) 学过map/reduce应该就知道是干啥的

Collector

有时候对流进行操作之后只是想获得过滤之后的集合,就可以用

  • toArray 默认会返回一个Object[], 想指定类型的话可以使用stream.toArray(String[]::new)

Collectors提供了一个更为便捷的方式

1
2
3
4
stream.collect(Collectors.toList());
stream.collect(Collectors.toSet());
stream.collect(Collectors.toMap(Person::getName, Person::getAge))
stream.collect(Collectors.toCollection(TreeSet::new));

通过如上方式都可以很简单将筛选后的流转换成Set、List、或者指定数据类型等形式。

甚至可以讲所有的结果以字符串的形式连接起来

1
2
stream.collect(Collectors.joining());
stream.collect(Collectors.joining(", "));

或者收集一个可以同时获取最大值、最小值、平均值的数据类型

1
2
3
4
IntSummarzingInt summary = stream.collect(Collectors.summarizingInt(String::length)); // 这里的Int也可以是Double|Long
summary.getAverage();
summary.getMax();
summary.getMin();

对toMap有更多的定制化的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 当value的值是元素本身时
people.collect(Collectors.toMap(Person::getId, Function.identity()));

// 当存在key值冲突时
locales.collect(
Collectors.toMap(
Locale::getDisplayLanguage,
l -> l.getDisplayLanguage(l),
(a, b) -> a
)
);

// 并指定特定的数据结构
locales.collect(
Collectors.toMap(
Locale::getDisplayLanguage,
l -> l.getDisplayLanguage(l),
(a, b) -> a,
TreeMap::new
)
);

还提供了一个mysql中group by的功能

1
2
3
Map<Integer, List<Person>> map = people.collect(
Collectors.groupingBy(Person::getAge)
);

如果group by之后的结果希望是一个Set

1
2
3
people.collect(
Collectors.groupingBy(Person::getAge, Collectors.toSet())
);

第二个当然不止可以传入Collectors.toSet也可以是一些

  • Collectorss.counting()
  • Collectors.maxBy(...)
  • Collectors.minBy(...)
  • Collectors.mapping(...)

之类其他下游收集器操作

常用的操作就到这吧,其他的一些需要时再查阅可能会更方便一些。

最后

怎么越来越像一名菜鸡Java开发的了。。。