Java8中两个优雅的特性
- Optional
- Stream
主要就是如上两个特性,总结的过程中也学了一些以前不知道的特性。
Optional
先从Optional来吧,他其实是对原有的数据类型进行了一层包装,将nullptr判断的部分操作进行了一定的封装。
可以先来看一些常规的操作
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 getStrOpt() {
return Math.random() > 0.5 ? Optional.of("optional str") : Optional.empty();
}
可以看到可以通过如下三种方式来从Optional中获取值,并为没有值的情况选择一个默认的返回情况,可以是一个default值,也可以是通过调用某个函数返回的值,以及抛出一个异常
Optional.orElse Optional.orElseGet Optional.orElseThrow
无能为力之处
但是其实它并不能化简如下的这种操作,遇到这种情况时其实和判断!=null并没有什么太多的区别。
public static void main(String[] args) {
String s = getStr();
if(s != null){
System.out.println(s);
}
Optional sOpt = getStrOpt();
if(sOpt.isPresent()){
System.out.println(sOpt.get());
}
}
public static String getStr() {
return Math.random() > 0.5 ? "str" : null;
}
public static Optional getStrOpt() {
return Math.random() > 0.5 ? Optional.of("optional str") : Optional.empty();
}
可以看到要进行的操作是差不多的,并不比null判断要方便多少(或许他的isPresent让人看的更有逼格?)
优势
那Optional的好处在于哪呢?他真正方便的地方可能在于一些链式的操作。
假设下面 flatMap
、 getUSB
、 getVersion
处理
和 返回
的都是个Optional\ 对象,就可以用如下链式操作
String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.flatMap(USB::getVersion)
.orElse("UNKNOWN");
否则需要进行多次的!=null的判断比较,才能进行下一步的操作。
对于一个empty的Optional,flatMap的调用之后的结果也是一个空的Optional,从而避免了nullptr的异常。
假如调用函数处理的不是Optional对象,则应该调用Optional.map来进行处理
Optional a = getStrOpt().map(String::toUpperCase);
public Optional map(Function mapper) public Optional flatMap(Function<? super T, Optional> mapper)
总之呢,个人感觉Optional确实解决了一部分空指针的烦恼,但也需要你的项目代码里有较多的Optional代码的支持,否则就会出现之前 isPresent
判断并没有什么优化的尴尬情况,反而让人觉得多套了一层这个Optional的麻烦。
Stream
流式操作, stream
这是一个在 Collection
接口的方法,可以将集合以一种很优雅且性能相当高的方式进行操作。
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上的操作是终结操作还是中间操作
Optional findAny(); Stream filter(Predicate 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() findAny() anyMatch(lamda) allMatch(lamda) noneMatch(lamda) reduce(lamda)
Collector
有时候对流进行操作之后只是想获得过滤之后的集合,就可以用
-
toArray
默认会返回一个Object[], 想指定类型的话可以使用stream.toArray(String[]::new)
但 Collectors
提供了一个更为便捷的方式
stream.collect(Collectors.toList()); stream.collect(Collectors.toSet()); stream.collect(Collectors.toMap(Person::getName, Person::getAge)) stream.collect(Collectors.toCollection(TreeSet::new));
通过如上方式都可以很简单将筛选后的流转换成Set、List、或者指定数据类型等形式。
甚至可以讲所有的结果以字符串的形式连接起来
stream.collect(Collectors.joining());
stream.collect(Collectors.joining(", "));
或者收集一个可以同时获取最大值、最小值、平均值的数据类型
IntSummarzingInt summary = stream.collect(Collectors.summarizingInt(String::length)); // 这里的Int也可以是Double|Long summary.getAverage(); summary.getMax(); summary.getMin();
对toMap有更多的定制化的方式
// 当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的功能
Map<Integer, List> map = people.collect(
Collectors.groupingBy(Person::getAge)
);
如果group by之后的结果希望是一个Set
people.collect(
Collectors.groupingBy(Person::getAge, Collectors.toSet())
);
第二个当然不止可以传入 Collectors.toSet
也可以是一些
Collectorss.counting() Collectors.maxBy(...) Collectors.minBy(...) Collectors.mapping(...)
之类其他下游收集器操作
常用的操作就到这吧,其他的一些需要时再查阅可能会更方便一些。