写在前面

lambda表达式是一个匿名函数。在Java 8中,它和函数式接口一起,共同构建了函数式编程的框架。
 
lambda表达式乍看像是匿名内部类的一种语法糖,但实际上,它们是两种本质不同的事物。匿名内部类本质是一个类,只是不需要程序员显示指定类名,编译器会自动为该类取名。而 lambda 表达式本质是一个函数,当然,编译器也会为它取名。在JVM层面,匿名内部类对应的是一个 class 文件,而 lambda 表达式对应的是它所在主类的一个私有方法。
 
lambda 表达式可以在函数体中引用外部的变量,从而实现了闭包。但 Java 对进入闭包的变量有 final 的限制,当然我们可以绕开这个限制。
 
本文的示例代码可从gitee上获取:https://gitee.com/cnmemset/javafp
 

lambda表达式与匿名内部类

lambda表达式可以用来简化某些匿名内部类(Anonymous Inner Classes)的写法,但仅限于对函数式接口的简写。
 

无参的函数式接口

以最常用的Runnable接口为例:
在Java 7中,如果需要新建一个线程,使用匿名内部类的写法是这样:
public static void createThreadWithAnonymousClass() {
// Runnable 是接口名。我们通过匿名内部类的方式,构造了一个 Runnable 的实例。
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running");
}
}); t.start();
}

使用匿名内部类的一个重要目的,就是为了减轻程序员的代码负担,不需要额外再定义一个类,而且这个类是一个一次性的类,没有太多的重用价值。但是,我们会发现,这个对象看起来也是多余的,因为我们实际上并不是要传入一个对象,而只是想传入一个方法。

在Java 8中,因为 Runnable 接口是一个函数式接口(只有一个抽象方法的接口都属于函数式接口),因此我们可以用lambda表达式来简化匿名内部类的写法:
public static void createThreadWithLambda() {
// 在Java 8中,Runnable 是一个函数式接口,因此我们可以使用 lambda 表达式来实现它。
Thread t = new Thread(() -> {
System.out.println("Thread is running");
}); t.start();
}

带参的函数式接口

Runnable是一个无参的函数式接口,我们再来看一个典型的带参数的函数式接口 Comparator:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); ....
}

假设一个场景:给定一个省份的拼音列表,需要对该列表中的省份进行排序,排序规则是字母长度最小的省份排在前面,如果两个省份字母长度一样,则按字母顺序排序。

使用匿名内部类的示例代码如下:
public static void sortProvincesWithAnonymousClass() {
List list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu", "Xizang", "Fujian", "Hunan", "Guangxi"); list.sort(new Comparator<String>() {
@Override
public int compare(String first, String second) {
int lenDiff = first.length() - second.length();
return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}
}); list.forEach(s -> System.out.println(s));
}

上述代码输出为:

Hunan
Fujian
Xizang
Guangxi
Jiangsu
Zhejiang
Guangdong
 
 
使用lambda表达式来简化Comparator的实现,示例代码如下:
public static void sortProvincesWithLambda() {
List list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu", "Xizang", "Fujian", "Hunan", "Guangxi"); // 下面的参数列表 first 和 second ,即方法 Comparator.compare 的参数列表
list.sort((first, second) -> {
int lenDiff = first.length() - second.length();
return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}); list.forEach(s -> System.out.println(s));
}

注意到,带参数的lambda表达式,甚至不需要声明类型,因为编译器可以通过上下文来推断出参数的类型。当然,我们也可以显式指定参数类型,尤其是在参数类型推断失败的时候:

(String first, String second) -> {
int lenDiff = first.length() - second.length();
return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}

this关键字的作用域

前面提到过,匿名内部类和lambda表达式本质是不同的:匿名内部类本质是一个类,而lambda表达式本质是一个函数。在JVM层面,匿名内部类对应的是一个class文件,而lambda表达式对应的是它所在主类的一个私有方法。
 
这就导致了this关键字在匿名内部类和lambda表达式中是不一样的。在匿名内部类中,this关键字指向匿名内部类的实例,而在lambda表达式中,this关键字指向的是主类的实例。
 
我们用代码验证一下:
public class ThisScopeExample {
public static void main(String[] args) {
ThisScopeExample example = new ThisScopeExample(); // 输出 "I am Anonymous Class."
example.runWithAnonymousClass();
// 输出 "I am ThisScopeExample Class."
example.runWithLambda();
} public void runWithAnonymousClass() {
// 以匿名类的方式运行
run(new Runnable() {
@Override
public void run() {
// this 是实现了接口 Runnable 的匿名内部类的实例
System.out.println(this);
} @Override
public String toString() {
return "I am Anonymous Class.";
}
});
} public void runWithLambda() {
// 以lambda表达式的方式运行
run(() -> {
// this 是类 ThisScopeExample 的实例
System.out.println(this);
});
} public void run(Runnable runnable) {
runnable.run();
} @Override
public String toString() {
return "I am ThisScopeExample Class.";
}
}
上述代码输出为:
I am Anonymous Class.
I am ThisScopeExample Class.
 
 

lambda表达式的语法

lambda表达式的语法是:参数,箭头(->) 以及方法体。如果方法体无法用一个表达式来完成,就可以像写普通的方法一样,把代码放在大括号 { } 中。反之,如果方法体只有一个表达式,那么就可以省略大括号 { }。
 
例如:
(String first, String second) -> {
int lenDiff = first.length() - second.length();
return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}

上述是一个典型的而且完整的lambda表达式。

对无参数的lambda表达式,参数部分也不能省略,需要提供空括号,例如:
Supplier supplier = () -> {
return new Random().nextInt(100);
}

对于上面的lambda表达式,可以发现它的方法体只有一个表达式,所以,它可以省略大括号,甚至return关键字也省略了,因为编译器可以根据上下文推断是否需要返回值:如果需要,那么就返回该唯一表达式的返回值,如果不需要,则在该唯一表达式后直接return。例如:

// Supplier 是需要返回值的,所以下面的lambda表达式等同于:
// () -> { return new Random().nextInt(100); }
Supplier supplier = () -> new Random().nextInt(100); // Runnable 是不需要返回值的,所以下面的lambda表达式等同于:
// () -> { new Random().nextInt(100); return; }
Runnable runnable = () -> new Random().nextInt(100);

如果编译器可以推断出lambda表达式的参数类型,则可以忽略其类型:

// 在这里,编译器可以推断出 first 和 second 的类型是 String。
Comparator comp = (first, second) -> {
int lenDiff = first.length() - second.length();
return lenDiff == 0 ? first.compareTo(second) : lenDiff;
};

如果lambda表达式只有一个参数,那么参数列表中的小括号也可以省略掉:

// 这里的 value ,等同于 (value)
Consumer consumer = value -> System.out.println(value);

与普通的函数不一样,lambda表达式不需要指定返回类型,它总是由编译器自行推断出返回类型。如果推断失败,则默认为Object类型。

lambda表达式与闭包

首先要理解lambda表达式和闭包(closure)是两个不同的概念,但两者有着紧密的联系。在不追求概念精确的场合,甚至可以说Java中的lambda表达式就是闭包。
 
闭包又称为函数闭包(function closure),是一种延长变量生命周期的技术,从这个意义上说,闭包和面向对象实现的功能是等价的。
 
闭包的定义是:在创建或定义一个函数的时候,除了记录函数本身以外,同时还记录了在创建函数时所能访问到的自由变量(自由变量 free variable,是指在函数外部定义的变量,它既不是函数的参数,也不是函数内的局部变量)。这样一来,闭包的变量作用域除了包含函数运行时的局部变量域外,还包含了函数定义时的外部变量域。
 
文字表达可能不够直观,我们来看一个代码示例:
public class ClosureExample {
public static void main(String[] args) {
// 平方
IntUnaryOperator square = getPowOperator(2); // 立方
IntUnaryOperator cube = getPowOperator(3); // 四次方
IntUnaryOperator fourthPower = getPowOperator(4); // 5的平方
System.out.println(square.applyAsInt(5));
// 5的立方
System.out.println(cube.applyAsInt(5));
// 5的四次方
System.out.println(fourthPower.applyAsInt(5));
} public static IntUnaryOperator getPowOperator(int exp) {
return base -> {
// 变量 exp 是 getPowOperator 的参数,属于lambda 表达式定义时的自由变量,
// 它的生命周期会延长到和返回的 lambda 表达式一样长。
return (int) Math.pow(base, exp);
};
}
}

上述代码的输出是:

25
125
625
 
可以看到,exp是方法 getPowOperator 的参数,但通过闭包技术,它“逃逸”出 getPowOperator 的作用域了。
 
很显然,变量“逃逸”,在多线程环境下,容易导致线程安全问题,防不胜防。因此,Java规定了,在lambda表达式内部引用外部变量的话,必须是final的,即不可变对象,只能赋值一次,不可修改。(在这说句题外话,并不是所有的语言都这么要求闭包的,譬如Python和JavaScript,闭包中引用的外部变量是可以任意修改的。)
 
为了书写代码方便,Java 8不要求显式将变量声明为final,但如果你尝试修改变量的值,编译器将会报错。例如:
public static IntUnaryOperator getPowOperator(int exp) {
// 尝试修改 exp 的值,但编译器会在lambda表达式中报错
exp++;
return base -> {
// 如果尝试修改 exp 的值,会在此处报错:
// Error: 从lambda 表达式引用的本地变量必须是final变量或实际上的final变量
return (int) Math.pow(base, exp);
};
}

但这种限制也是有限的,因为我们可以通过将变量声明为一个数组或一个类就可以修改其中的值。例如:

public static IntUnaryOperator getPowOperator(int[] exp) {
// exp 是一个int数组:exp = new int[1];
exp[0]++;
return base -> {
// 此时不会报错,可以正常运行
return (int) Math.pow(base, exp[0]);
};
}

结语

lambda表达式的出现,一方面为函数式编程提供了支持,另一方面也提升了Java程序员的生产力。我们要熟悉常见的函数式接口,灵活使用lambda表达式和闭包。
 

为方便大家在移动端浏览,已注册微信公众号【员说】,欢迎关注。第一时间更新技术文章,也会不定时分享圈内热门动态和一线大厂内幕。

感谢您阅读本篇文章,如果觉得本文对您有帮助,欢迎点击推荐和关注,您的支持是我最大的写作动力。

文章欢迎转载,但需在文章页面明显位置,给出作者和原文链接,否则保留追究法律责任的权利!

注意!应各位朋友的邀请,创建了一个技术交流群,(聊技术/看内幕/找内推/读书分享等,拒绝水群,保证品质),可添加微信号【yuanshuo824】,备注:交流,即可入群。

Java中的函数式编程(三)lambda表达式的更多相关文章

  1. Java 中的函数式编程(Functional Programming):Lambda 初识

    Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...

  2. Java 函数式编程(Lambda表达式)与Stream API

    1 函数式编程 函数式编程(Functional Programming)是编程范式的一种.最常见的编程范式是命令式编程(Impera Programming),比如面向过程.面向对象编程都属于命令式 ...

  3. Java 函数式编程和Lambda表达式

    1.Java 8最重要的新特性 Lambda表达式.接口改进(默认方法)和批数据处理. 2.函数式编程 本质上来说,编程关注两个维度:数据和数据上的操作. 面向对象的编程泛型强调让操作围绕数据,这样可 ...

  4. Java中的函数式编程(五)Java集合框架中的高阶函数

    写在前面 随着Java 8引入了函数式接口和lambda表达式,Java 8中的集合框架(Java Collections Framework, JCF)也增加相应的接口以适应函数式编程.   本文的 ...

  5. Java中的函数式编程(六)流Stream基础

    写在前面 如果说函数式接口和lambda表达式是Java中函数式编程的基石,那么stream就是在基石上的最富丽堂皇的大厦. 只有熟悉了stream,你才能说熟悉了Java 的函数式编程. 本文主要介 ...

  6. Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于 ...

  7. 函数式编程--使用lambda表达式

    前面一篇博客我们已经说到了,lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口的实例.现在我们来写一段java的命令者模式来自己研究下lambda表达式的语法. 这里重复下命令者模式: ...

  8. Java8函数式编程和lambda表达式

    文章目录函数式编程JDK8接口新特性函数接口方法引用函数式编程函数式编程更多时候是一种编程的思维方式,是一种方法论.函数式与命令式编程区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉 ...

  9. Java8函数式编程以及Lambda表达式

    第一章 认识Java8以及函数式编程 尽管距离Java8发布已经过去7.8年的时间,但时至今日仍然有许多公司.项目停留在Java7甚至更早的版本.即使已经开始使用Java8的项目,大多数程序员也仍然采 ...

随机推荐

  1. Charles-抓取https请求

    在未经设置之前,Charles是无法抓取https请求的,会出现unknown的标识.我们可以通过以下两步设置,解决该问题. 第一步:安装证书 https是在http的基础上加入ssl层,通过ssl来 ...

  2. Python+mirai开发QQ机器人起步教程(2021.9.9测试有效)

    参考:开发 mirai QQ机器人起步教程_叹之-CSDN博客_mirai python 本篇文章参考了以上博客,并对其中的失效内容和版本匹配问题进行了补充修改,实测能够成功运行.部分步骤的运行截图见 ...

  3. 110_SSM框架

    目录 需求分析->功能设计->数据库设计 环境要求 环境 要求 数据库环境 基本环境搭建 创建maven项目 pom.xml添加依赖,添加资源导出 idea连接数据库 提交项目到Git 创 ...

  4. 密钥交换协议之IKEv2

    1. IKEv2 1.1 IKEv2简介 IKEv2(Internet Key Exchange Version 2,互联网密钥交换协议第 2 版)是第 1 版本的 IKE 协议(本文简称 IKEv1 ...

  5. 转:C#读取PDF、TXT内容

    //读取PDF内容 private void button2_Click(object sender, EventArgs e) { label3.Text = OnCreated("D:\ ...

  6. Devexpress 饼状图

    <dxc:ChartControl Name="chart"                                BorderThickness="0&q ...

  7. nodejs安装 Later version of Node.js is already installed. Setup will now exit 及 node与npm版本不符

    暴力删除nodejs导致无法重新安装  Later version of Node.js is already installed. Setup will now exit 1.电脑全局搜索nodej ...

  8. CentOS8部署nextcloud网盘

    Nextcloud是一款开源的存储软件,功能丰富,支持多人协同工作,目前完全免费. 官网:https://www.nextcloud.com 架构:LAMP或LNMP 本文以LAMP为基础 注意:ph ...

  9. minix3使用轻快入门

    minix3是一款迷你的unix作业系统,但又不在at&t代码的基础上构建.当年开发这款作业系统的作者仅仅是拿来自用,给学生上课使用的. 如果你已经安装了minix3,你还需要安装openss ...

  10. Markdown公式用法大全

    目录 基本语法 两种代码引用方式 插入链接并描述 插入图片 有序列表 无序列表 分割线 表格 如何插入公式 如何输入上下标 如何输入括号和分隔符 如何输入分数 如何输入开方 如何输入省略号 如何输入矢 ...