JavaJava 杂谈

开始试水JDK1.8?先来看看lambda表达式和Streams

2017-12-23  本文已影响148人  路过的猪

前言

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使用的建议:

  1. 对于简单操作推荐使用外部迭代手动实现(即常规forEach或iterator)
  2. 对于复杂操作,推荐使用Stream API
  3. 在多核情况下,推荐使用并行Stream API来发挥多核优势
  4. 单核情况下不建议使用并行Stream API
  5. 一般的Stream中装、拆箱会很耗时,建议使用IntStream、LongStream、DoubleStream

啰嗦几句

目前许多框架(如spring)的低版本,可能和JDK1.8兼容性不太好(尤其是lambda和代理类的相关问题),排查问题的时候,可以留意下新的API和lambda的问题,升级踩坑难以避免,量力而行。

上一篇下一篇

猜你喜欢

热点阅读