Lambda表达式

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

引子

首先,请大家去在线代码编辑器执行一段JS代码:

// Math.abs()是JavaScript的内置函数
let result = Math.abs(-10)
console.log('result=' + result)

// JavaScript允许将Function赋值给变量。注意,不能带(),否则就是方法调用
let func = Math.abs

// 更神奇的是,func变量拼上()就可以当方法调用了
console.log('result=' + func(-20))

// 其实,在JavaScript中不仅允许方法赋值,还允许直接传递Function
function sum(x, y, func) {
	return func(x) + func(y)
}

result = sum(-10, -20, Math.abs)
console.log('result=' + result)

你会得到以下结果:

result=10

result=20

result=30

sandbox> exited with status 0

这种写法对于Java程序员来说第一感觉是“神奇”,紧接着能感受到JS的“轻巧”。

其实不仅是JS,Python也是如此:

# encoding: utf-8

# abs是Python的内置函数
result = abs(-1)
print(result)

# 定义add()方法,f显然是一个函数
def add(x, y, f):
    return f(x) + f(y)

print(add(-1, -2, abs))

运行结果是:

1

3

sandbox> exited with status 0

Python把上面的表达式称为高阶函数:

  • 函数的参数可以是一个/多个函数
  • 函数的返回值也可以是一个函数

PHP也支持,就不演示了。

在JS、Python等脚本语言的世界里,这种现象似乎很普遍,因为Function是那个世界的一等公民,而对于Java来说对象才是一等公民。比如,有时为了在A对象的方法中调用B对象的某个方法,我们不得不new一个B对象并把它作为参数传入:

class A {
    public void methodA(B b) {
        System.out.println("methodA is called");
        b.methodB();
    }
}

class B {
    public void methodB() {
        System.out.println("methodB is called");
    }
}

在设计模式中,上面这种写法美其名曰:策略模式。

当然,广义上策略模式关注的是可替换的多种实现,狭义上可以理解为传递不同的方法。

幸运的是Oracle紧跟潮流,终于在2014年发布Java8并宣布支持Lambda表达式,弥补了Java函数式编程的遗憾。

Java8有两个非常重要的新特性:Lambda和Stream API,而一旦你了解了Lambda的来龙去脉,Stream API几乎是信手拈来。本系列文章会把绝大部分笔墨花在Lambda上,意在让初学者在不知不觉中将Lambda内化为自己的知识体系的一部分。


出行案例

快过年了,很多人会选择年底旅游,比如日本、泰国,或者国内的海南省。

现在交通这么发达,出行的方式可以有很多种:坐火车、坐飞机、骑自行车等等。

接下来,我们就以出行作为主题展开。

假设我有女朋友,因为我和我女朋友算是两个独立个体,是分工进行的。在程序里,我们相当于两个线程。

MyThread(模仿JDK的Thread,最大的区别是最终调用的还是主线程而不是异步线程)

public class MyThread {

    public void run() {
        System.out.println("去12306买了一张票");
        System.out.println("坐火车...");
    }

    public void start() {
        run(); 
    }
}

Demo

public class Demo {
    public static void main(String[] args) {
        // new一个MyThread对象,调用start()
        new MyThread().start();
    }
}

输出:

去12306买了一张票

坐火车...

策略模式优化

上面的代码有个问题:要运行的代码在MyThread的run里面写死了,我只能坐火车,不能坐飞机。 分析:

我们知道,Java是面向对象的,一个事物一般可以最终抽象成 属性+方法。

属性代表你有什么,方法代表你能干什么。

比如 我有一张嘴,我能吃饭:

class Person {
    private String mouth; 
    public void eat(){} 
}

在这个案例中,“坐火车”、“坐飞机”都是一个动作。按上面分析,应该是一个方法。我们要动态地传入一个动作,也就是要动态地传入一个方法。但是我们知道,在Java中只能传递数据(参数),不能传递方法。JS之所以可以传递function,是因为function本身是JS的一种数据类型。

抽取一下上面的观点:

Java只能传递规定的数据类型的数据,比如int型的age, String型的name, 引用类型的person。

等等!上面说对象包含属性和方法,你又说Java可以传递person这样的引用类型。

那么,我完全可以传递一个对象(包含方法),然后在内部调用对象的方法,这和直接传递方法是异曲同工的。

所以,我打算把“坐火车”、“坐飞机”这些交通策略单独抽取成一个个类,在new MyThread()的时候通过构造函数传递这些类的实例进去。

但同时可以预见,“坐火车”、“坐飞机”等等对象会非常多,MyThread的有参构造的形参类型需要形成多态。于是我写了一个MyRunnable接口,让每个策略类都去实现它(接口多态)。

MyRunnable

public interface MyRunnable {
    void run();
}

ByTrain

public class ByTrain implements MyRunnable {
    @Override
    public void run() {
        System.out.println("去12306买了一张票");
        System.out.println("坐火车...");
    }
}

ByAir

public class ByAir implements MyRunnable {
    @Override
    public void run() {
        System.out.println("在某App上订了飞机票");
        System.out.println("坐飞机...");
    }
}

改写MyThread,接收不同的出行策略并调用方法

public class MyThread {

    // 成员变量
    private MyRunnable target;

    public MyThread() {}
    
    // 构造方法,接收外部传递的出行策略
    public MyThread(MyRunnable target) {
        this.target = target;
    }

    // MyThread自己的run(),现在基本不用了
    public void run() {
        System.out.println("去12306买了一张票");
        System.out.println("坐火车...");
    }

    // 如果外部传递了出行策略,就会调用该策略里的run()
    public void start() {
        if (target != null) {
            target.run();
        } else {
            this.run();
        }
    }
}

Demo

public class Demo {
    public static void main(String[] args) {
        ByTrain byTrain = new ByTrain();
        new MyThread(byTrain).start();

        ByAir byAir = new ByAir();
        new MyThread(byAir).start();
    }
}

输出:

去12306买了一张票

坐火车...

在某App上订了飞机票

坐飞机...

匿名类优化

尽管使用了策略模式,上面的代码还是有问题。如果后面还有“坐电瓶车”、“骑自行车”等出行方式,那么出行策略类就太多了,会发生所谓的“类爆炸”。我们可以使用匿名类的方式继续改进Demo:

public class Demo {
    public static void main(String[] args) {
        new MyThread(new MyRunnable() {
            @Override
            public void run() {
                System.out.println("不用买票");
                System.out.println("骑电瓶车...");
            }
        }).start();
    }
}

初见Lambda表达式

嗯,没错,到此为止这大概是JDK8之前我们能想到的最好的解决办法,不仅运用了策略模式,还使用匿名类的方式成功阻止了类的暴增。

但是,程序永远存在可优化的地方。于是,JDK8又帮我们向前迈进了一大步。

如果大家边看文章边复制代码运行的话,会发现上面new MyRunable变灰了。鼠标移动上去会提示你:匿名类对象可以用Lambda表达式代替。

来,一起看看JDK8怎么玩:

public class Demo {
    public static void main(String[] args) {
        new MyThread(() -> {
            System.out.println("不用买票");
            System.out.println("骑电瓶车...");
        }).start();
    }
}

非常优雅,非常简洁。

Why Lambda?

为什么Java要引入Lambda表达式呢?两个原因。

  • 首先,就是我们刚才的思路,当然是为了让代码更简洁,更优雅,优化程序。
  • 其次,为了跟上潮流。

Java从诞生那天起,就是纯正的面向对象语言。而从1995年诞生至今,已经过去24年。期间程序届又诞生了很多优秀的语言。别看现在Java是霸主,就觉得Java语言是世界最好的语言。

长江后浪推前浪,一般来说后续的新语言在创建之初一定会吸取之前语言的教训,尽量规避之前的设计缺陷。所以,单从语言层面上讲,老的语言往往是没有优势的,甚至存在代码冗余,不够优雅等弊病。

Lambda表达式可以看做函数式编程的子集,但函数式编程绝对不是Java首创的,它只是看到别的语言用的挺好,自己也引入了而已。

Lambda表达式的本质

这里我们先帮大家引入一个概念,可能未必准确,但绝对好理解:

Lambda表达式,其实是一段可传递的代码。

更精确的描述是:

Lambda表达式,其实是一段可传递的代码。它的本质是以类的身份,干方法的活。

什么意思呢?看代码:

public class Demo {
    public static void main(String[] args) {
        new MyThread(() -> System.out.println("哈哈哈")).start();
    }
}

分析上面的代码,你会发现new MyThread()这个构造方法原本需要传递一个MyRunnable接口的子类对象(匿名类对象)。 但我们反手就扔了一个Lambda表达式进去,它还真吃下去了。说明什么?

说明Lambda表达式在身份上与匿名类对象等价。

但是Lambda表达式这么一串代码扔进去后,实际干活的也就->后的System.out.println("哈哈哈")这段代码(可以看做一个方法,因为方法是对代码块的封装)。这又说明了什么?

说明Lambda表达式在作用上与方法等价。

再次回味一下上面那句话:

Lambda表达式,其实是一段可传递的代码。Lambda本质是以类的身份,干方法的活。

如果我们把方法看做行为,那么Lambda表达式其实就是把行为参数化(方法作为参数传递)。

Lambda表达式格式解析

我写了一个compareString()方法,形参有三个,str1 str2 comparator,本质和文章开头的JS案例一样:

public class Demo {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abcd";

        int i = compareString(str1, str2, new Comparator() {
            @Override
            public int compare(String s1, String s2) {
                return s1.length() - s2.length();
            }
        });
    }

    public static int compareString(String str1, String str2, Comparator comparator) {
        return comparator.compare(str1, str2);
    }
}

经过上面文章的调教,相信大家已经非常敏感:

嗯?传递方法?可以用Lambda表达式!

满足你:

public class Demo {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abcd";
        
        // 上面推导得出Lambda表达式与匿名类对象等价,所以我们可以把Lambda表达式赋值给Comparator接口
        Comparator comparator = (String s1, String s2) -> {
            return s1.length() - s2.length();
        };
        // 调用
        int k = compareString(str1, str2, comparator);
    }

    public static int compareString(String str1, String str2, Comparator comparator) {
        return comparator.compare(str1, str2);
    }
}

如果你是在IDEA下编程,上面代码会出现灰色,提示代码有优化空间:

public class Demo {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abcd";

        // 上面推导得出Lambda表达式与匿名类对象等价,所以我们可以把Lambda表达式赋值给Comparator接口
        Comparator comparator = (String s1, String s2) -> {
            return s1.length() - s2.length();
        };
        // 调用
        int k = compareString(str1, str2, comparator);

        // 改进一下,跳过赋值这一步,直接把整个Lambda传给compareString()方法:
        compareString(str1, str2, (String s1, String s2) -> {
            return s1.length() - s2.length();
        });

        // 上面的代码虽然能运行,但是idea一直显示灰色,说有更优雅的写法。好吧,我改改。
        int x = compareString(str1, str2, (s1, s2) -> s1.length() - s2.length());

        // 不对,还是不够精简,再改改(方法引用):
        x = compareString(str1, str2, Comparator.comparingInt(String::length));

        // 完美。
    }

    public static int compareString(String str1, String str2, Comparator comparator) {
        return comparator.compare(str1, str2);
    }
}

什么是方法引用,我们后面再介绍,可以单纯地理解为可复用的Lambda(把其他类已经定义的方法作为Lambda),实际开发时依靠IDEA提示改进即可,并不需要我们记忆。

如果你足够细心,就会发现:

Lambda表达式_第1张图片

类名、方法名、形参类型、返回值类型、return关键字及方法体的{}都被省略了。

从语法格式来讲,Lambda表达式其实就是对一个方法掐头去尾,只保留形参列表和方法体。可以粗略认为:

Lambda表达式 = 形参列表 + 方法体

对于匿名内部类而言,类名、方法名真的没什么用。只有当一个方法需要被使用多次时,我们才需要为它命名,以便其他程序通过方法名重复调用。而匿名内部类的性质就是仅调用一次,所以名字对它来说是可有可无的。至于返回值与形参类型,Lambda都可以通过上下文推断,所以也可以省略:

// 基于上下文,很容易推算出返回值类型就是String。形参同理。
public someType get() {
    return "Hello World!";   
}

至于方法体的{}能不能省略,本质和if是否需要{}一样:

if(true) 
    System.out.println("可以省略{}"); 

if(true) {
    int i = 1;
    System.out.println("不可以省略{}"); 
}

至于是否需要return,看方法本身是否需要返回值。回顾上面的推演过程,能省略的都省略后就成了现在的Lambda表达式。

那实际使用Lambda表达式需要注意什么呢?

  • 关注如何编写Lambda,需要实现什么样的逻辑
  • 不要关注当前Lambda将会被如何调用,出现在代码的哪一块

比如:

public static void main(String[] args) {
	// 原始数据
    List list = new ArrayList<>(Arrays.asList(1,2,3));
	// filter()方法需要传入一个过滤逻辑
    List result = list.stream().filter(value -> value > 2).collect(Collectors.toList());
	
    System.out.println(result);
}

结果是:

[3]

Process finished with exit code 0

如果你只是调用者,尽量不要去想filter()方法内部代码是怎么写的,只需考虑如何实现过滤规则:大于2,所以你只要写一个能判断value是否大于2的Lambda并传入即可(value->value>2 就是 入参->方法体)。初期不熟练时可以先写匿名内部类,IDEA会提示你优化的。有了这么智能的工具,就让大脑稍微休息下吧。

函数式接口

介绍完Lambda表达式,最后提一下函数式接口。大家肯定会有疑问:难道所有接口都可以接收Lambda表达式吗?

显然不是的,接口要想接收Lambda表达式,必须是一个函数式接口。所谓函数式接口,最核心的特征是:有且只有一个抽象方法。

这句话有两个重点:抽象方法、唯一。

你可能觉得:啥玩意,Java的接口不就抽象方法吗?难道还有别的方法?

是的,Java8的接口可以添加静态方法和默认方法,越来越像一个类。关于Java8为什么需要静态方法和默认方法,后面介绍Stream流操作时我们再来介绍。

Lambda表达式_第2张图片

除了抽象方法需要注意,我们再看看函数式接口的另一个要求:唯一。也就是说,接口里只能有一个抽象方法。现在我们给上面的MyRunnable接口加一个抽象方法:

Lambda表达式_第3张图片

Lambda表达式不能用了:

Lambda表达式_第4张图片

只能用匿名类对象,把两个方法都实现:

Lambda表达式_第5张图片

因为Lambda的本质是传递一个方法体,而MyRunnable此时有两个方法需要实现,那么你这个Lambda表达式到底是给哪个方法的呢?另一个又该怎么办呢?此时只能用匿名类对象,把两个方法都实现。

当然,我们还可以从另一个角度理解:Java8的Lambda都是基于上下文推导的,当一个接口只有一个方法时,推导结果是唯一确定的,但是方法不唯一时,无法推导得到唯一结果。

为了提醒程序员编写Lambda接口时不要写多个抽象方法,Java8提供了一个新注解:

Lambda表达式_第6张图片

当你在接口上加上这个注解,编译器会检测当前接口是否仅有唯一的抽象方法:

Lambda表达式_第7张图片

去掉eat():

Lambda表达式_第8张图片

也就是说,如果你希望一个接口能接收Lambda表达式充当匿名类对象,那么接口必须仅有一个抽象方法,这是函数式接口的定义。通常我们可以在接口上加一个@FunctionalInterface检测,作用于@Override一样。但函数式接口和@FunctionalInterface注解没有必然联系,只要这个接口符合函数式接口的定义,它就是函数式接口。

拓展:Lambda与匿名内部类

上面的案例一直在对比匿名内部类和Lambda,很多人可能在心里已经自动把Lambda等同于匿名内部类,认为Lambda是匿名内部类的语法糖。

然而并不是。

复制下方代码运行看看:

@SpringBootTest
public class LambdaTest {

    @Test
    public void testClosure() throws InterruptedException {
        // 在匿名内部类的外面定义一个String变量
        final String str = "hello";
        // 构造一个匿名内部类对象
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(str);
            }
        };

        new Thread(r).start();

        TimeUnit.SECONDS.sleep(1);
    }

}

接下来,我们给上面的代码加上一句:

Lambda表达式_第9张图片

打印结果:

Lambda表达式_第10张图片

注意,带$表示匿名内部类,比如LambdaTest$1表示LambdaTest中的匿名内部类,编译后会产生两个文件。

但如果把Runnbale换成Lambda表达式实现:

Lambda表达式_第11张图片

你会发现Lambda表达式方法体内部的this指向了LambdaTest,而且编译后也只有一个class文件。

Lambda表达式_第12张图片

其实也难怪啦,Lambda方法体外部并没有匿名内部类,当然只能指向LambdaTest。更准确地说,this是指向方法的调用者,是隐式传递的。从这个角度看,Lambda和匿名内部类本质上还是不同的。

Lambda表达式_第13张图片

以后编码时,再遇到这种编译错误就不会迷惑了:

Lambda表达式_第14张图片

改成Lambda表达式即可,因为Lambda表达式外层就是当前类的实例:

Lambda表达式_第15张图片

根据IDEA提示,做一下简化:

Lambda表达式_第16张图片

Lambda表达式_第17张图片

关于方法引用,后面会介绍。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

Lambda表达式_第18张图片进群,大家一起学习,一起进步,一起对抗互联网寒冬

你可能感兴趣的:(java基础进阶,java基础)