开始试水JDK1.8?先来看看lambda表达式和Streams
前言
jdk1.8已经出了比较长的一段时间了,我们公司也开始逐步接入,各种新特性或API也渐渐用起来了(前路坑漫漫~~)
以下就个人理解,简单介绍下JDK1.8的两大特性:lambda表达式和StreamsAPI。
quick start
需求:将一群用户(User)按年龄从小至大排序。
public class User {
private long id;
private int age;
...
get/set...
}
在java1.8之前:
List<User> userList = new ArrayList<>();
...填充数据
Collections.sort(userList, new Comparator<User>() {
@Override
public int compare(User user1, User user2) {
return user1.getAge() - user2.getAge();
}
});
在java1.8:
List<User> userList = new ArrayList<>();
...填充数据
Collections.sort(userList, (user1, user2) -> user1.getAge() - user2.getAge());
看不懂的童鞋可以先看以下介绍。
语法
其实lambda表达式,通俗一点理解就是将一个方法(函数)写成某种特殊的形式,可以更加方便快捷地开发。
java通过lambda表达式替代了原来的匿名内部类的繁琐的写法,使其比较方便地实现原来的功能,同时使java这门语言往函数式编程的方向发展。
lambda语法:
参数列表 箭头(->) 语句块或表达式
我们以上面的Collections中的sort方法为例:
// Collections类中的sort方法声明为:
public static <T> void sort(List<T> list, Comparator<? super T> c)
// 我们来看下第二个参数,在jdk1.8中Comparator这个接口新增了一个@FunctionalInterface的注解
// 这个注解表示为该接口为函数式接口,可以使用lambda表达式表示其实现。
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
...
}
利用lambda表达式语法,我们可以将实现写成(以UserList为例):
Collections.sort(userList,
// 参数列表 箭头(->)
(User user1, User user2) -> {
return user1.getAge() - user2.getAge(); //语句块
}
);
由于编译器可以自动识别入参的类型,入参类型的声明可以省略:
Collections.sort(userList,
// 参数列表 箭头(->)
(user1, user2) -> {
return user1.getAge() - user2.getAge(); //语句块
}
);
上面的方法体中的是语句块,可以写成表达式:
Collections.sort(userList,
// 参数列表 箭头(->)
(user1, user2) ->user1.getAge() - user2.getAge());// 表达式
注:当该方法有返回值时,表达式的值则会被当成返回值
另外在jdk1.8,List已经提供了sort方法,可以直接userList.sort(...)
当接口中的入参为1个参数时,方法入参的括号可以省略。如List中的forEach方法:
// 将所有user的年龄增加1
userList.forEach(user -> user.setAge(user.getAge() + 1));
当接口的入参为非1个参数时(0个或多个),方法入参的括号不可省略:
// 0个
new Thread(()->{
...
});
// 多个
Collections.sort(userList, **(user1, user2) -> user1.getAge() - user2.getAge());
jdk自带的函数接口
为避免用户定义过多重复的函数接口以及提供给自身API(如Steam)的使用,jdk提供了许多通用的函数接口,基本上满足需求了。如下:
// 无返回值
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
// 返回true或false
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
// 返回某个对象
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
另外Comparator、Runnable等接口也被打上@FunctionalInterface注解,表示为函数式接口。
具体的使用可以参考这篇文章
Stream(流)
Java 8 中的Stream是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作。由于Stream中的集合的操作时可以进行并行操作的,这也就可以充分利用多核处理器的优势。另外在日常的集合操作中也非常方便。
简单使用
需求:将大于18周岁的人的用户(User)按照年龄从小至大排序后,获取其对应的id集合。
// 获取流(Stream<User>)
Stream<User> userStream = userList.stream();
// 过滤操作,过滤后只剩符合条件的流(Stream<User>)
Stream<User> filterStream = userStream.filter(user -> user.getAge() > 18);
// 根据年龄排序,得到排序后的流(Stream<User>)
Stream<User> sortedStream = filterStream.sorted((user1, user2) -> user1.getAge() - user2.getAge());
// 映射,sortedStream为Stream<User>对象,调用map后,映射成了关于id类型的流对象(Stream<Long>)
Stream<Long> idStream = sortedStream.map(user -> user.getId());
// 收集(终结方法),不再返回Stream对象了,而是转成集合对象List(可以理解前面的方法是水流啊流,现在用个盆子装起来了)
List<Long> idList = idStream.collect(Collectors.toList());
--------------------------------------------------------------------------------------------------------------
// 上面写法仅为大家方便理解,其实是可以写出链式写法的(也是日常写法,建议每个方法调用各占一行)
List<Long> idList = userList.stream()
.filter(user -> user.getAge() > 18)
.sorted((user1, user2) -> user1.getAge() - user2.getAge())
.map(user -> user.getId())
.collect(Collectors.toList());
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。即每个Stream对象只能操作/调用相关方法一次,如果再次操作,则会抛出“java.lang.IllegalStateException: stream has already been operated upon or closed”(所以建议写成链式写法)
另外,这里提到的Stream流和IO流不是一个概念。
我们知道lambda表达式,其实就是代表了某个接口(interface)的一个实现,下面来看看filter、sort等方法传入的lambda代表的是什么接口:
// Stream接口
public interface Stream<T> extends BaseStream<T, Stream<T>> {
...
// 上述提到的Predicate接口(有个test()方法,返回boolean)
Stream<T> filter(Predicate<? super T> predicate);
// 常见的比较接口Comparator
Stream<T> sorted(Comparator<? super T> comparator);
// 上述提到的Function接口(里面有个R apply(T)方法,入参为一个类型为T的对象,返回一个R类型的新对象,其实这就是一个映射过程)
Stream<R> map(Function<? super T, ? extends R> mapper);
// 这个方法为终结(Terminal)方法,返回的是某个类型R(通常为集合类型List、Set、Map等),
// 入参为Collector接口,Collectors类中给我们提供了许多生成对应接口的静态方法,如toList()、toSet()等
R collect(Collector<? super T, A, R> collector);
...
}
若对Streams API有兴趣或者不理解,推荐看下Java 8 中的 Streams API 详解,里面提到了Stream 非常详细的API和相关概念。本文的关于Stream的一些概念也是来自于此。
性能相关
网上很多关于Stream的性能评测,许多都太过于片面,或刻意错误使用(频繁装拆箱),或测试数据量过小;以下一篇个人认为是比较全面的测试:Stream Performance.
简单来说,性能方面Stream利用的是现代多核处理器的优势,可以将原本的遍历处理利用多线程来处理,数据量越大、计算机核数越多、操作越复杂,执行效率就越高。
Streams使用的建议:
- 对于简单操作推荐使用外部迭代手动实现(即常规forEach或iterator)
- 对于复杂操作,推荐使用Stream API
- 在多核情况下,推荐使用并行Stream API来发挥多核优势
- 单核情况下不建议使用并行Stream API
- 一般的Stream中装、拆箱会很耗时,建议使用IntStream、LongStream、DoubleStream
啰嗦几句
目前许多框架(如spring)的低版本,可能和JDK1.8兼容性不太好(尤其是lambda和代理类的相关问题),排查问题的时候,可以留意下新的API和lambda的问题,升级踩坑难以避免,量力而行。