Java 8系列之Lambda表达式
概述
使用Lambda表达式也有一段时间了,有时候用的云里雾里的,是该深入学习Java 8新特性的时候了。作为Java最大改变之一的Lambda表达式,其是Stream的使用基础,那就以它开始吧。
这里,我们先明确需要解决的问题:
- 什么是闭包?
- Lambda表达式如何写?
- 什么是函数接口?
- 类型推断在Lambda中的体现。
Lambda表达式
lambda表达式的语法由参数列表、->和函数体组成。函数体既可以是一个表达式,也可以是一个语句块:
- 表达式:表达式会被执行然后返回执行结果。
- 语句块:语句块中的语句会被依次执行,就像方法中的语句一样——
- return语句会把控制权交给匿名方法的调用者
- break和continue只能在循环中使用
- 如果函数体有返回值,那么函数体内部的每一条路径都必须返回值
表达式函数体适合小型lambda表达式,它消除了return关键字,使得语法更加简洁。
Lambda表达式的变体
不包含参数且主体为表达式
Lambda表达式不包含参数,使用空括号 ()表示没有参数。
OnClickListener mListener = () -> System.out.println("do on Click");
该Lambda表达式实现了OnClickListener接口,该接口也只有一个doOnClick方法,没有参数,且返回类型为void。
public interface OnClickListener {void doOnClick();
}
不包含参数且主体为代码段
该Lambda表达式实现了OnClickListener接口,其主体为一段代码段,在其内用返回或抛出异常来退出。 只有一行代码的Lambda表达式也可使用大括号, 用以明确Lambda表达式从何处开始、到哪里结束。
OnClickListener mListener_ = () -> {System.out.println("插上电源");System.out.println("打开电视");};
包含一个参数且主体为表达式
Lambda表达式可以包含一个参数,将参数写在()内,如果只有一个参数可以将()省略。
OnItemClickListener mItemListener = position -> System.out.println("position = [" + position + "]");
该Lambda表达式实现了OnItemClickListener接口,该接口也只有一个doItemClickListener方法,其参数为int类型,且返回值为void。
public interface OnItemClickListener {void doItemClickListener(int position);
}
包含多个参数且主体为表达式
Lambda表达式可以包含多个参数,将参数写在()内,此时()不可以省略。
IMathListener mPlusListener = (x, y) -> x + y;
int sum = mPlusListener.doMathOperator(10, 5);
该Lambda表达式实现了IMathListener接口,该接口只有一个doMathOperator方法,其参数为(int, int)类型,且返回值为int类型。
public interface IMathListener {int doMathOperator(int start, int plusValue);
}
包含多个参数且主体为代码段
该Lambda表达式实现了IMathListener接口,该接口只有一个doMathOperator方法,在实现其方法时,创建了一个函数,用来处理结果。
IMathListener mMaxListener = (x, y) -> {if (x > y) {return x;} else {return y;}};
包含多个参数,指定参数类型且主体为代码段
该Lambda表达式实现了IMathListener接口,在实现时指定了参数类型,此时,调用时方法时的参数类型是指定的,只能传入相应的类型的参数,若不传入相应参数,编译时会报错。
IMathListener mSubListener = (int x, int y) -> x - y;
尽管与之前相比, Lambda表达式中的参数需要的样板代码很少,但是Java 8仍然是一种静态类型语言。
引用值, 而不是变量
在使用内部类时,我们总是碰到这种情况,需要引用内部类外面的变量,比如其所在方法内的变量,或者该类的全局变量。当使用方法内的变量时,需要将变量声明为final。此时,将变量声明为final, 意味着不能为其重复赋值,同时在匿名内部,实际上是用的使用赋给该变量的一个特定的值。
final String name = getUserName();
button.addActionListener(new ActionListener() {public void actionPerformed(ActionEvent event) {System.out.println("hi " + name);}
});
在Java 8中对放松了这限制,在匿名内部,可以引用其所在方法内的非final变量,但是该变量在既成事实上必须是final,也就是说该变量只能赋值一次。如果再次对其赋值,编译器会报错。
现在,我们暂且将在匿名内部类内使用的其所在方法内的变量命名为A,不管是在匿名类内部还是在匿名类所在的方法内,再次对A进行赋值时,编译器都会报如下错误,其意思是变量A是在内部类中访问的,需要声明为final或有效的final类型。Variable ‘plusFinal’ is accessed from within inner class, needs to be final or effectively final
在Lambda表达式中,也是同样的问题,对于其方法体内引用的外部变量,在Lambda表达式所在方法内对变量再次赋值时,编译器会报同样的错误。也就是意味着,换句话说,Lambda表达式引用的是值,而不是变量。
这种行为也解释了为什么Lambda表达式也被称为闭包。未赋值的变量与周边环境隔离起来,进而被绑定到一个特定的值。在Java 8中引入了闭包这一概念,并将其使用在了Lambda表达式中。众说纷纭的计算机编程语言圈子里,Java是否拥有真正的闭包一直备受争议,因为在 Java 中只能引用既成事实上的final变量。可以肯定的是,Lambda表达式都是静态类型。
闭包在现在的很多流行的语言中都存在,例如 C++、C# 。闭包允许我们创建函数指针,并把它们作为参数传递。
函数接口
函数式接口是什么呢?函数式接口(Functional Interface)是Java 8对一类特殊类型的接口的称呼。这类接口只定义了唯一的抽象方法的接口(除了隐含的Object对象的公共方法),用作Lambda表达式的类型。
从函数接口的定义可以看出,首先要明确的,其是一个接口,而这个接口呢,有且只有一个抽象的方法,那怎么又和函数结合在一起了呢?
public interface IMathListener {int doMathOperator(int start, int plusValue);
}
我们先看一个例子,对于IMathListener接口,这个接口只有一个抽象方法doMathOperator,其接收两个int类型的参数,返回值为int,这个接口可以称为是一个函数接口。当我们声明其对象时,我们可以这样做:
IMathListener mSubListener = (x, y) -> x - y;
mMaxListener.doMathOperator(10, 5));// 其值:5
刚才的声明,就是用Lambda表达式声明了IMathListener的实现,其实现的意义是求两个传入值的差值。这个例子说明了,函数接口可以通过Lambda表达式来实现。下面来看它是如何和函数扯上关系的。
public class Math {public static int doIntPlus(int start, int plusValue) {return start + plusValue;}
}
现有一个Math类,其内声明了一个静态方法doIntPlus,该方法接收两个int类型的参数,返回值为int,也就是说doIntPlus与IMathListener接口中的doMathOperator方法的签名一样。既然签名一样,我们可以搞些什么事情呢。往下看:
IMathListener mPlusListener = Math::doIntPlus;
我们通过函数调用,直接生成了一个IMathListener对象,这里写法不了解的,后续会做介绍,看下Java 8中的引用。我们还是接着说,通过方法引用来支持Lambda表达式。这样现有函数、接口及Lambda表达式完美的结合在一起。
从前面已经知道,Lambda表达式都是静态类型的,也就是说其在编译时就已经被编译,所以刚才被引用的方法必须是静态的,否则编译器会报错。
Non-static method cannot be referenced from a static context
非静态方法不能从静态上下文引用
对于函数接口而言,接口中唯一方法的命名并不重要了,只要方法签名和Lambda表达式的类别相匹配即可。当然了,为了增加代码的易读性,只需在函数接口中为参数起一个代表意义的名字即可。
为了更形象的声明接口,我们可以使用图形来描述不同类型接口。指向函数接口的箭头表示参数, 如果箭头从函数接口射出, 则表示方法的返回类型。若接口没有返回值,没有箭头从函数接口射出。
这里,我们应该对函数接口有了清晰的认识。对于一个函数接口而言,其应该有以下特性:
- 只具有一个方法的接口
- 其可以被隐式转换为lambda表达式
- 现有静态方法可以支持lambda表达式
- 每个用作函数接口的接口都应添加 @FunctionalInterface注解
@FunctionalInterface
public interface IMathListener {int doMathOperator(int start, int plusValue);
}
该注解会强制 javac 检查一个接口是否符合函数接口的标准。 如果该注解添加给一个枚举
类型、 类或另一个注解, 或者接口包含不止一个抽象方法, javac 就会报错。 重构代码时,
使用它能很容易发现问题。
类型推断
关于类型推断,我们在Java 7中,已经不止一次用到了,可能你一直都没有注意到。比如创建一个ArrayList,我们可以这么做:
ArrayList<String> mArrayA = new ArrayList<String>();
ArrayList<String> mArrayB = new ArrayList<>();
在创建mArrayA时,明确指定了ArrayList为String类型,而在创建mArrayB时并未指定ArrayList的类型,编译器是如何知道mArrayB的数据类型呢?在Java 7中,有个神奇的<>操作符,它可使javac推断出泛型参数的类型,这样不用明确声明泛型类型,编译器就可以自己推断出来,这就是它的神奇之处!
对于一个传递的参数,编辑器也可以根据参数的类型来推断具体传入的参数的数据类型。比如有一个方法updateList,其参数为一个String的ArrayList,在调用该方法时,我们传入了一个新建的ArrayList但未指定ArrayList的数据类型,此时编辑器会自行推断传入的ArrayList的数据类型为String,
public void updateList(ArrayList<String> values);updateList(new ArrayList<>());
Lambda表达式中的类型推断,实际上是Java 7就引入的目标类型推断的扩展。javac根据Lambda 表达式上下文信息就能推断出参数的正确类型。 程序依然要经过类型检查来保证运行的安全性, 但不用再显式声明类型罢了,这就是所谓的类型推断。
目标类型是指Lambda表达式所在上下文环境的类型。比如,将 Lambda 表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是 Lambda 表达式的目标类型
以之前提到的IMathListener为例,在下面表达式中,javac会自行将x和y推断为int类型.
IMathListener mSubListener = (x, y) -> x - y;
而在实际开发过程中,为了接口方法的通用性,一般都是使用泛型来指定参数的类型,比如Funtion接口,该接口接收一个F类型的参数并返回一个T类型的值。
Function<String, Integer> string2Integer = Integer::valueOf;
在这个实例中,javac可以推断出接收的数据类型为String,返回类型为Integer。尽管类型推断已经相当智能,但是其也不是无所不能的。在其自行推断前,你需给出其推断的标注。比如下面的例子,javac并不能够推断出Function的具体数据类型:
Function string2Integer = Integer::valueOf;
上述代码,编译都不会通过,编译器给出的报错信息如下:
Operator ‘& #x002B;’ cannot be applied to java.lang.Object, java.lang.Object.
大家都知道泛型的擦除原则,在编译时,编译器会擦除泛型的具体类型。从而,此时编译器认为参数和返回值都是java.lang.Object实例。这已经偏离了我们的思想,就算编译可以通过,也会造成后续逻辑的混乱,从而不知道该行代码,到底在做什么。在使用泛型时,我们一定会指定泛型的具体的数据类型,以作为编译器的类型推断的标准。
方法重载带来的烦恼
在Java中可以重载方法,造成多个方法有相同的方法名,但签名却不一样,尽管这样让多态性展现的淋漓尽致,但是对于类型推断,带来了不少的烦恼,因为javac可能会推断出多种类型。 这时, javac会挑出最具体的类型。比如方法overloadedMethod中,参数类型不同,返回值相同,这是一个典型的方法重载,在使用具体类型调用时,java可以根据具体类型来判断,此时控制台应打印“String”。
overloadedMethod("abc");private void overloadedMethod(Object o) {System.out.print("Object");
}
private void overloadedMethod(String s) {System.out.print("String");
}
如果我们参数传递的是Lambda表达式呢?下面的表达式中,编译器并不知道x和y的数据类型,也并未指定具体的类型,必然造成编译异常。
overloadedMethod((x)->y);
如果在Lambda表达式中指定返回值的数据类型,编译器可以清晰的知道overloadedMethod的参数类型为String类型,根据具体的数据类型,从而调用overloadedMethod(String s) 方法,避免了类型推断不明确的问题。
overloadedMethod((x)->(String)y);
总而言之,Lambda表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循如下规则:
- 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出;
- 如果有多个可能的目标类型,由最具体的类型推导得出;
- 如果有多个可能的目标类型且最具体的类型不明确, 则需人为指定类型。
总结
Lambda是函数式编程的基础,而函数式编程是技术的发展方向。作为一个成熟的Java开发人员,学习新的编程技术那是必须的,也是值得花时间学习的。
大量的使用Lambda表达式,尽管避免了大量的使用匿名内部类,提高了代码的可读性,可是对猿人们要求更高了,应当对相应的接口或者框架有一定的熟悉程度,否则,看代码就活在云里雾里了。这也是自我相逼提升的一种方式吧。
参考文档
- Java中的闭包与回调
- 深入理解Java闭包概念
- 一见钟情!Java闭包
- Java 8 函数式接口
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
---------------------
作者:行云间
来源:CSDN
原文:https://blog.csdn.net/io_field/article/details/54380200
版权声明:本文为博主原创文章,转载请附上博文链接!
Java 8系列之Lambda表达式相关推荐
- Java 函数式编程和 lambda 表达式
为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于 ...
- Java 8 新特性 lambda表达式
/ Created by Manager on 2021/4/1. Java 8 新特性 lambda表达式 StreamAPI 新日期 新注解 */ 视频连接 1https://www.bilibi ...
- JDK8系列之Lambda表达式教程和示例
JDK8系列之Lambda表达式教程和示例 1.Lambada 表达式简介 Lambda 表达式是一种匿名函数,但对Java中的Lambda表达式而已并不完全正确,简单来说,Lambda表达式是一种没 ...
- Java 8中使用Lambda表达式的策略模式
策略模式是" 设计模式:可重用对象的元素"书中的模式之一 . 本书所述的策略模式的意图是: 定义一系列算法,封装每个算法,并使它们可互换. 策略使算法独立于使用该算法的客户端而变化 ...
- 精通lambda表达式:java多核编程_Java8 Lambda表达式和流操作如何让你的代码变慢5倍...
有许许多多关于 Java 8 中流效率的讨论,但根据 Alex Zhitnitsky 的测试结果显示:坚持使用传统的 Java 编程风格--iterator 和 for-each 循环--比 Java ...
- Java基础教程:Lambda表达式
Java基础教程:Lambda表达式 引入Lambda Java 是一流的面向对象语言,除了部分简单数据类型,Java 中的一切都是对象,即使数组也是一种对象,每个类创建的实例也是对象.在 Java ...
- Java 8 新特性Lambda 表达式
Java 8 新特性Lambda 表达式 一.常用循环 二.匿名内部类 三.排序集合 四.循环打印对象 五.根据条件修改 六.排序 七.求和 八.统计方法 九.材料 一.常用循环 public cla ...
- java 函数式接口与lambda表达式的关系
函数式接口与lambda表达式的关系 在java中,lambda表达式与函数式接口是不可分割的,都是结合起来使用的. 对于函数式接口,我们可以理解为只有一个抽象方法的接口,除此之外它和别的接口相比并没 ...
- Java函数式编程和Lambda表达式
文章目录 什么是函数式编程 Lambda表达式 @FunctionalInterface函数式接口 Lambda表达式的格式 方法引用 什么是函数式编程 相信大家都使用过面向对象的编程语言,面向对象编 ...
最新文章
- 中国数学竞赛史上最玩命的“赌徒”,为了国家荣誉,他不惜用生命换来了五次世界第一...
- jQuery 基础教程 (二)之jQuery对象与DOM对象
- 如何在postgresql中模拟oracle的dual表,来测试数据库最基本的连接功能?
- 2020年中国无人经济市场研究报告
- Spring中事务的使用、抽象机制及模拟Spring事务实现
- Activity实现 高亮显示活动节点,和所有已完成过的节点
- Python求1~300之间所有的完数
- 我的本科毕业论文——Messar即时通讯系统
- 【目标检测】目标检测算法-从OverFeat到YOLO
- python shp文件_对python 读取线的shp文件实例详解
- 万能素材库_2016万能高考作文素材大全
- Hive基础知识及底层架构
- 大三上学期实训——基于SpringBoot的电影后台管理系统
- supplier java8_Java 8之 Supplier示例
- Python中计时,看这一篇就够了
- [JVM]成为JavaGC专家(1)—深入浅出Java垃圾回收机制
- pyinstaller系列之七:打包各种问题汇总
- Handler机制——同步屏障
- 富人的底层逻辑,诠释什么是自控力,其实就是对自己狠
- 应用在洗衣机触摸屏中的触摸芯片