您当前的位置: 首页 >  Java

蔚1

暂无认证

  • 1浏览

    0关注

    4753博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

8000 字长文让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理

蔚1 发布时间:2020-07-30 23:30:14 ,浏览量:1

尽管 Java 8 发布多年,使用者众多,可神奇的是竟然有很多同学没有用过 Java 8 的新特性,比如 Lambda表达式、比如方法引用,再比如今天要说的 Stream。其实 Stream 就是以 Lambda 和方法引用为基础,封装的简单易用、函数式风格的 API。

本场 Chat 你将收获如下知识点:

  • 什么是 Lambda 表达式
  • 方法引用又是何方神圣
  • 自己动手实现一个方法引用的例子
  • 完全讲解 Stream API 的所有方法的用法和使用场景

就在今年 Java 25 周岁了,可能比在座的各位中的一些少年年龄还大,但令人遗憾的是,竟然没有我大,不禁感叹,Java 还是太小了。(难道我会说是因为我老了?)

而就在上个月,Java 15 的试验版悄悄发布了,但是在 Java 界一直有个神秘现象,那就是「你发你发任你发,我的最爱 Java 8」.

据 Snyk 和 The Java Magazine 联合推出发布的 2020 JVM 生态调查报告显示,在所有的 Java 版本中,仍然有 64% 的开发者使用 Java 8。另外一些开发者可能已经开始用 Java 9、Java 11、Java 13 了,当然还有一些神仙开发者还在坚持使用 JDK 1.6 和 1.7。

尽管 Java 8 发布多年,使用者众多,可神奇的是竟然有很多同学没有用过 Java 8 的新特性,比如 Lambda表达式、比如方法引用,再比如今天要说的 Stream。其实 Stream 就是以 Lambda 和方法引用为基础,封装的简单易用、函数式风格的 API。

Java 8 是在 2014 年发布的,实话说,风筝我也是在 Java 8 发布后很长一段时间才用的 Stream,因为 Java 8 发布的时候我还在 C# 的世界中挣扎,而使用 Lambda 表达式却很早了,因为 Python 中用 Lambda 很方便,没错,我写 Python 的时间要比 Java 的时间还长。

要讲 Stream ,那就不得不先说一下它的左膀右臂 Lambda 和方法引用,你用的 Stream API 其实就是函数式的编程风格,其中的「函数」就是方法引用,「式」就是 Lambda 表达式。

Lambda 表达式

Lambda 表达式是一个匿名函数,Lambda 表达式基于数学中的λ演算得名,直接对应于其中的 lambda 抽象,是一个匿名函数,即没有函数名的函数。Lambda 表达式可以表示闭包。

在 Java 中,Lambda 表达式的格式是像下面这样

// 无参数,无返回值() -> log.info("Lambda") // 有参数,有返回值(int a, int b) -> { a+b }

其等价于

log.info("Lambda");private int plus(int a, int b){      return a+b;}

最常见的一个例子就是新建线程,有时候为了省事,会用下面的方法创建并启动一个线程,这是匿名内部类的写法,new Thread需要一个 implements 自Runnable类型的对象实例作为参数,比较好的方式是创建一个新类,这个类 implements Runnable,然后 new 出这个新类的实例作为参数传给 Thread。而匿名内部类不用找对象接收,直接当做参数。

new Thread(new Runnable() {    @Override    public void run() {        System.out.println("快速新建并启动一个线程");    }}).run();

但是这样写是不是感觉看上去很乱、很土,而这时候,换上 Lambda 表达式就是另外一种感觉了。

new Thread(()->{    System.out.println("快速新建并启动一个线程");}).run();

怎么样,这样一改,瞬间感觉清新脱俗了不少,简洁优雅了不少。

Lambda 表达式简化了匿名内部类的形式,可以达到同样的效果,但是 Lambda 要优雅的多。虽然最终达到的目的是一样的,但其实内部的实现原理却不相同。

匿名内部类在编译之后会创建一个新的匿名内部类出来,而 Lambda 是调用 JVM invokedynamic指令实现的,并不会产生新类。

方法引用

方法引用的出现,使得我们可以将一个方法赋给一个变量或者作为参数传递给另外一个方法。::双冒号作为方法引用的符号,比如下面这两行语句,引用 Integer类的 parseInt方法。

Function s = Integer::parseInt;Integer i = s.apply("10");

或者下面这两行,引用 Integer类的 compare方法。

Comparator comparator = Integer::compare;int result = comparator.compare(100,10);

再比如,下面这两行代码,同样是引用 Integer类的 compare方法,但是返回类型却不一样,但却都能正常执行,并正确返回。

IntBinaryOperator intBinaryOperator = Integer::compare;int result = intBinaryOperator.applyAsInt(10,100);

相信有的同学看到这里恐怕是下面这个状态,完全不可理喻吗,也太随便了吧,返回给谁都能接盘。

007S8ZIlly1gfhrh522urj308c08cjri.jpg

先别激动,来来来,现在咱们就来解惑,解除蒙圈脸。

Q:什么样的方法可以被引用?

A:这么说吧,任何你有办法访问到的方法都可以被引用。

Q:返回值到底是什么类型?

A:这就问到点儿上了,上面又是 Function、又是Comparator、又是 IntBinaryOperator的,看上去好像没有规律,其实不然。

返回的类型是 Java 8 专门定义的函数式接口,这类接口用 @FunctionalInterface 注解。

比如 Function这个函数式接口的定义如下:

@FunctionalInterfacepublic interface Function {    R apply(T t);}

还有很关键的一点,你的引用方法的参数个数、类型,返回值类型要和函数式接口中的方法声明一一对应才行。

比如 Integer.parseInt方法定义如下:

public static int parseInt(String s) throws NumberFormatException {    return parseInt(s,10);}

首先parseInt方法的参数个数是 1 个,而 Function中的 apply方法参数个数也是 1 个,参数个数对应上了,再来,apply方法的参数类型和返回类型是泛型类型,所以肯定能和 parseInt方法对应上。

这样一来,就可以正确的接收Integer::parseInt的方法引用,并可以调用Funcitonapply方法,这时候,调用到的其实就是对应的 Integer.parseInt方法了。

用这套标准套到 Integer::compare方法上,就不难理解为什么即可以用 Comparator接收,又可以用 IntBinaryOperator接收了,而且调用它们各自的方法都能正确的返回结果。

Integer.compare方法定义如下:

public static int compare(int x, int y) {    return (x < y) ? -1 : ((x == y) ? 0 : 1);}

返回值类型 int,两个参数,并且参数类型都是 int

然后来看ComparatorIntBinaryOperator它们两个的函数式接口定义和其中对应的方法:

@FunctionalInterfacepublic interface Comparator {    int compare(T o1, T o2);}@FunctionalInterfacepublic interface IntBinaryOperator {    int applyAsInt(int left, int right);}

对不对,都能正确的匹配上,所以前面示例中用这两个函数式接口都能正常接收。其实不止这两个,只要是在某个函数式接口中声明了这样的方法:两个参数,参数类型是 int或者泛型,并且返回值是 int或者泛型的,都可以完美接收。

JDK 中定义了很多函数式接口,主要在 java.util.function包下,还有 java.util.Comparator 专门用作定制比较器。另外,前面说的 Runnable也是一个函数式接口。

functionInterface 引用

自己动手实现一个例子

1. 定义一个函数式接口,并添加一个方法

定义了名称为 KiteFunction 的函数式接口,使用 @FunctionalInterface注解,然后声明了具有两个参数的方法 run,都是泛型类型,返回结果也是泛型。

还有一点很重要,函数式接口中只能声明一个可被实现的方法,你不能声明了一个 run方法,又声明一个 start方法,到时候编译器就不知道用哪个接收了。而用default 关键字修饰的方法则没有影响。

@FunctionalInterfacepublic interface KiteFunction {    /**     * 定义一个双参数的方法     * @param t     * @param s     * @return     */    R run(T t,S s);}

2. 定义一个与 KiteFunction 中 run 方法对应的方法

在 FunctionTest 类中定义了方法 DateFormat,一个将 LocalDateTime类型格式化为字符串类型的方法。

public class FunctionTest {    public static String DateFormat(LocalDateTime dateTime, String partten) {        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);        return dateTime.format(dateTimeFormatter);    }}

3.用方法引用的方式调用

正常情况下我们直接使用 FunctionTest.DateFormat()就可以了。

而用函数式方式,是这样的。

KiteFunction functionDateFormat = FunctionTest::DateFormat;String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");

而其实我可以不专门在外面定义 DateFormat这个方法,而是像下面这样,使用匿名内部类。

public static void main(String[] args) throws Exception {    String dateString = new KiteFunction() {        @Override        public String run(LocalDateTime localDateTime, String s) {            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s);            return localDateTime.format(dateTimeFormatter);        }    }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");    System.out.println(dateString);}

前面第一个 Runnable的例子也提到了,这样的匿名内部类可以用 Lambda 表达式的形式简写,简写后的代码如下:

public static void main(String[] args) throws Exception {        KiteFunction functionDateFormat = (LocalDateTime dateTime, String partten) -> {            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);            return dateTime.format(dateTimeFormatter);        };        String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");        System.out.println(dateString);}

使用(LocalDateTime dateTime, String partten) -> { } 这样的 Lambda 表达式直接返回方法引用。

Stream API

为了说一下 Stream API 的使用,可以说是大费周章啊,知其然,也要知其所以然吗,追求技术的态度和姿势要正确。

当然 Stream 也不只是 Lambda 表达式就厉害了,真正厉害的还是它的功能,Stream 是 Java 8 中集合数据处理的利器,很多本来复杂、需要写很多代码的方法,比如过滤、分组等操作,往往使用 Stream 就可以在一行代码搞定,当然也因为 Stream 都是链式操作,一行代码可能会调用好几个方法。

Collection接口提供了 stream()方法,让我们可以在一个集合方便的使用 Stream API 来进行各种操作。值得注意的是,我们执行的任何操作都不会对源集合造成影响,你可以同时在一个集合上提取出多个 stream 进行操作。

我们看 Stream 接口的定义,继承自 BaseStream,机会所有的接口声明都是接收方法引用类型的参数,比如 filter方法,接收了一个 Predicate类型的参数,它就是一个函数式接口,常用来作为条件比较、筛选、过滤用,JPA中也使用了这个函数式接口用来做查询条件拼接。

public interface Stream extends BaseStream {  Stream filter(Predicate            
关注
打赏
1560489824
查看更多评论
0.0561s