一文带你深入了解 Lambda 表达式和方法引用
前言
尽管目前很多公司已经使用 Java8 作为项目开发语言,但是仍然有一部分开发者只是将其设置到 pom 文件中,并未真正开始使用。而项目中如果有8新特性的写法,例如λ表达式。也只是 Idea Alt+Enter 生成的。最近天气非常热,出门晒太阳不如和我一起系统的学习一下 Java8 的新特性。提高开发效率也可、享受同事羡慕的眼神也可,让我们开始吧
声明:本文首发于博客园,作者:后青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 转载请注明,谢谢!
新特性
函数式编程:Lambda表达式、流式编程
其他特性:默认方法、新的Optional类、CompletableFutrue、LocalDate/LocalTime
这篇文章重点讨论 Lambda 及某些情况下更易读、更自然的:方法引用。
Lambda表达式
行为参数化
行为参数化就是一个方法接受多个不同的行为作为参数,并在内部使用他们,完成不同行为的能力。其实说白了就是将一段代码作为另一个方法的形参,使该方法更加的灵活、可以应对多变的需求。
举个关于苹果的例子
例如老师安排张三这么一个任务("法外狂徒"张三改行做程序员了):篮子有很多苹果 List ,需要筛选出这些苹果中的绿色苹果
根据具象筛选苹果
这个需求很简单,张三两下就搞定了:
public static List<Apple> filterGreenApples(List<Apple> appleList){
List<Apple> result = new ArrayList<>();
for (Apple apple : appleList) {
if("green".equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
可是这个时候老师改主意了。说绿色的不好吃想吃红色的苹果,张三只好复制这个方法进行修改,将green
改成red
并修改方法名为 filterRedApples。然而如果老师又让他筛选多种其他颜色的苹果,例如:浅绿色、暗红色、黄色等。这种复制、修改的方法就显得有些难应付。一个良好的原则是尝试抽象其共性。
对于筛选苹果的需求,可以尝试给方法添加一个参数 color。非常简单的就可以应对老师对不同颜色苹果的需求。
public static List<Apple> filterApplesByColor(List<Apple> appleList, String color){
List<Apple> result = new ArrayList<>();
for (Apple apple : appleList) {
if(color.equals(apple.getColor())){
result.add(apple);
}
}
return result;
}
张三满意的提交了代码。但是这时老师又对张三说:我想要一些重一点的苹果,一般大于150g的苹果就是比较重的。作为程序员,张三早就想好老师可能会改重量。因此提前定义一个参数作为苹果的重量:
public static List<Apple> filterApplesByWeight(List<Apple> appleList, int weight){
List<Apple> result = new ArrayList<>();
for (Apple apple : appleList) {
if(apple.getWeight() > weight){
result.add(apple);
}
}
return result;
}
解决方案不错。可是张三复制了大量的方法用于遍历库存。并对每个苹果应用筛选条件。他打破了DRY(Dont repeat youselt 不要重复自己)的软件设计原则。试想一下,如果张三想换一种遍历的方式,那么每个方法都需要再改一次,工作量很大。那有没有一种方法能将颜色和质量组合成一个方法呢?可以尝试加一个 flag,然后根据 flag 的值来确定使用哪个判断条件。但这种方法十分差劲!试想如果以后有了更多的条件:苹果的大小、产地、品种等等。这个代码应该怎么维护?因此张三需要一种更加灵活的方式来实现筛选苹果的方法。
根据抽象条件筛选
不管使用什么条件筛选,他们都有共性:
- 需要一个苹果
- 执行一段代码
- 返回一个 boolean 的值
其中执行一段代码这一步是不确定的,而参数和返回值是确定的,因此我们可以定义一个接口:
public interface ApplePredicate {
boolean test(Apple apple);
}
及不同条件筛选的实现:
public class AppleHeavyWeightPredicate implements ApplePredicate{
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
筛选的方法也可以改成这样:
public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate applePredicate){
List<Apple> result = new ArrayList<>();
for (Apple apple : appleList) {
if(applePredicate.test(apple)){
result.add(apple);
}
}
return result;
}
这时无论应对怎样的需求,张三只需要重新实现 test 方法,然后通过 filterApples 方法传递 test 方法的行为。这表示 filterApples 方法的行为参数化了!
但是张三又觉得这样的实现太麻烦了,每次新来一个需求他都需要创建一个类实现 ApplePredicate 接口。有没有更好的办法呢?答案是肯定的。在 Java8 之前可以通过匿名类来实现:
public static void main(String[] args) {
List<Apple> appleList = new ArrayList<>();
appleList.add(new Apple("red", 150));
List<Apple> result = filterApples(appleList, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor()) && apple.getWeight() > 150;
}
});
}
匿名类虽然可以解决创建新类的问题,但是他太长了。那要如何简化呢? Java8 提供的 Lambda 就是专门用来简化它的。且看代码:
public static void main(String[] args) {
List<Apple> appleList = new ArrayList<>();
appleList.add(new Apple("red", 150));
List<Apple> result = filterApples(appleList, apple -> "green".equals(apple.getColor()) && apple.getWeight() > 150);
}
从苹果的例子可以看到,行为参数化是一种很有用的模式,它能够轻松应对多变的需求,它通过把一个行为(一段代码)封装起来,并通过传递和使用创建的行为将其参数化。这种做法类似于策略设计模式。而JavaAPI中已经在多出实践过这个模式了,例如 Comparator 排序、Runnable执行代码块等等
Lambda管中窥豹
Lambda是一种简洁的传递一个行为的匿名函数,它没有名称,却有参数列表、函数主体、返回值、甚至还可以抛出异常。基本语法像这样:
(parameters) -> {statements;}
或
(parameters) -> expression
在哪里及如何使用Lambda
函数式接口
函数式接口就是只定义一个抽象方法的接口(如果接口中定义了默认方法实现,无论有多少个。只要它只有一个抽象方法,它仍然是函数式接口)
前面我们在 ApplePredicate 接口中只定义了一个抽象方法 test,所以 ApplePredicate 接口就是函数式接口。类似的还有 Comparator 和 Runnable 等。 Lambda 可以代替匿名类来作为函数式接口的实例。
public static void main(String[] args) {
Runnable r1 = () -> System.out.println("Hello World 1");
Runnable r2 = new Runnable() {
@Override
public void run() {
System.out.println("Hello World 2");
}
};
process(r1);
process(r2);
process(() -> System.out.println("Hello World 3"));
}
public static void process(Runnable r){
r.run();
}
@FunctionalInterface
该注解可以用来声明一个接口是函数式接口,如果接口上有声明,但程序员又为接口写了其他抽象方法,编译器会报错
环绕执行模式
资源处理(处理文件、数据库)常见的操作方法就是:打开一个资源、做一些处理、关闭/释放资源。这个打开和关闭阶段总是很相似,并且会围绕执行处理的哪些重要代码。这就是所谓的环绕执行模式。例如:
public static String readLine() throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("a.txt"))){
return br.readLine();
}
}
这个写法是有局限性的,因为你无法灵活的修改处理逻辑的代码。那就跟着我来将他改造成 lambda 可用的形式吧
行为参数化
首先我们要做的行为定义为 processFile。以下是从文件中读取两行的参数化写法
String result = processFile( BufferedReader br -> br.readLine() + br.readLine())
使用函数式接口来传递行为
processFile 这个方法需要匹配的函数描述符长这样: BufferedReader -> String 。那我们可以照着它定义接口
@FunctionalInterface
public interface BufferedReaderProcesser {
String profess(BufferedReader br);
}
执行一个行为
改造 processFile 方法,让 BufferedReaderProcesser 接口作为它所执行行为的载体
public static String processFile(BufferedReaderProcesser brf) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("a.txt"))){
return brf.professFile(br);
}
}
传递Lambda
接下来就可以使用 Lambda 来传递不同的行为来以不同的方式处理文件了:
public static void main(String[] args) throws IOException {
// 读一行
String str1 = processFile(br -> br.readLine());
// 读两行
String str2 = processFile(br -> br.readLine() + " " + br.readLine());
// 找到第一个包含 lambda 的行
String str3 = processFile(br ->
{
String s;
while ((s = br.readLine()).length() > 0) {
if (s.contains("lambda")) {
return s;
}
}
return null;
}
);
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
}
且看控制台的输出:
Java提供的函数式接口
Java8 的设计师们在 java.util.function 包中引入了很多新的函数式接口,以下是几个常用的
Predicate
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
布尔类型接口:在需要将一个任意类型的对象处理成布尔表达式时,可能需要它。例如我们之前处理的苹果,当然 T 也可以是学生对象(筛选出身高大于多少的)、用户对象(筛选具有某特征的用户)等等
Consumer
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
消费类型接口:Consumer 是一个消费型方法,他接收一个泛型 然后处理掉。不返回任何东西。
Function
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
如果你需要定义一个 Lambda 将输入的对象信息映射到输出,那 Function 是再合适不过的了。
function 包下还有需要类似的函数式接口,读者可以自行去关注一下接口中方法的参数和返回值。来决定使用哪个。
类型检查、类型推断及限制
通过上面的介绍,读者已经对 Lambda 表达式的写法有了一定的了解,那么 Java 编译器是如何识别 Lambda 的参数和返回值的呢?
类型检查
Java 通过上下文(比如,接受他传递的方法的参数或是接受他值的局部变量)来推断 Lambda 表达式需要的目标类型而这个目标类型一般是一个函数式接口,之后判断表达式的参数和返回值是否与接口中唯一抽象方法的声明相对应
类型推断
Java 编译器从上下文中推断出表达式的目标类型后,表达式的参数类型也就被编译器所知道。所以书写表达式时可以省略参数类型,例如:
String str1 = processFile(br -> br.readLine());
processFile 方法的参数(Lambda的目标类型)是:BufferedReaderProcesser brf。BufferedReaderProcesser 接口唯一的抽象方法:String profess(BufferedReader br);方法声明的参数类型是 BufferedReader 。Java 编译器可以推断到这里。因此直接写 br 是没问题的。对于两个参数的方法也可以省略参数类型。而一个参数的方法可以省略参数类型和参数两边的括号
方法引用
方法引用让你可以重复使用现有的方法定义,并像 Lambda 一样传递它们。即提前写好的,可复用的 Lambda 表达式。如果一个 Lambda 代表的只是“直接调用这个方法”,那最好还是用名称调用它。方法引用的写法如下:
目标引用::方法名 // 因为这里没有实际调用方法,故方法的 () 不用写
三类方法引用
指向静态方法的方法引用
(args) -> ClassName.staticMethod(args) 写成 ClassName::staticMethod
指向任意类型实例方法的方法引用,例如 T 类的实例 arg0
(arg0, rest) -> arg0.instanceMethod(rest) 写成 T::instanceMethod
指向现有对象的实例方法的方法引用。
(args) -> expr.instanceMethod(args) 写成 expr::instanceMethod
第二类和第三类乍看有些迷糊,仔细分辨可以发现:如果方法的调用者是 Lambda 的参数,则目标引用是调用者的类。如果调用者是已经存在的实例对象,则目标引用是该对象
构造函数方法引用
方法引用还可以被用在构造函数上,写法是这样:ClassName::new
比如获取对于获取类型Supplier的接口,我分别用三种写法写出创建一个苹果对象的方法:
// 方法引用写法
Supplier<Apple> s1 = Apple::new;
// Lambda 写法
Supplier<Apple> s2 = () -> new Apple();
// 普通写法
Supplier<Apple> s3 = new Supplier<Apple>() {
@Override
public Apple get() {
return new Apple();
}
};
复合 Lambda 表达式
上面我们所讨论的 Lambda 表达式都是单独使用的,而 function 包中很多接口中还定义了额外的默认方法,用来复合 Lambda 表达式。
比较器复合
倒序
假如我们有一个给苹果按指定重量排序的方法
List<Apple> appleList = new ArrayList<>();
// 构造一个按质量升序排序的比较器
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
appleList.sort(c);
// 按质量倒叙
appleList.sort(c.reversed());
其中,Comparator.comparing 方法是一个简化版的 compare 方法的实现形式,源码如下:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
该方法接收一个 Function 接口的实现类作为参数,而我们的 Apple::getWeight 方法解析过来就是实现了 Function 接口,重写 apply 方法,apply 方法的声明解析为 int apply(Apple a)
,方法内通过调用 a.getWeight() 方法返回 int 类型的值。后来 return 语句中的 (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
其实就是 Comparator 的 Lambda 表达式实现的匿名类中的方法体。重写的是 int compare(T o1, T o2);
方法
比较器链
我们经常遇到这样的问题,比较苹果质量时,质量相同。那么接下来就需要第二选择条件了。Comparable 接口也提供了便于 Lambda 使用的比较器链方法 thenComparing。比如首先比较质量,当质量相同时按照价格降序
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
Comparator<Apple> compareByWeightThenPrice = c.thenComparing(Apple::getPrice).reversed();
appleList.sort(compareByWeightThenPrice);
谓词复合
Predicate 谓词接口中有三个可用的复合方法: and、or、negate 分别表示与或非。使用方法和比较器复合大同小异,读者可以自行体验
函数复合
Function 函数接口中有 andThen() 和 compose() 方法,参数都是 Function 的实现,区别如下
a.andThen(b) 是先执行 a 再执行 b
a.compose(b) 是先执行 b 再执行 a
总结
- Lambda 和方法引用本身并不难,理解行为参数化是使用 Lambda 和方法引用的前提
- 函数式接口是仅仅声明了一个抽象方法的接口,只有在接受函数式接口的地方才能使用 Lambda 表达式
- 方法引用可以让你复用现有的方法实现
- Comparator、Predicate、Function等函数式接口都提供了几个用来结合 Lambda 表达式的默认方法
一文带你深入了解 Lambda 表达式和方法引用的更多相关文章
- JAVA8之Lambda表达式与方法引用表达式
一.Lambda表达式 基本语法: lambdaParameters->lambdaBody lambdaParameters传递参数,lambdaBody用于编写逻辑,lambda表达式会生成 ...
- java8的新特性之lambda表达式和方法引用
1.1. Lambda表达式 通过具体的实例去体会lambda表达式对于我们代码的简化,其实我们不去深究他的底层原理和背景,仅仅从用法上去理解,关注两方面: lambda表达式是Java8的一个语法糖 ...
- Java函数式编程:一、函数式接口,lambda表达式和方法引用
Java函数式编程 什么是函数式编程 通过整合现有代码来产生新的功能,而不是从零开始编写所有内容,由此我们会得到更加可靠的代码,并获得更高的效率 我们可以这样理解:面向对象编程抽象数据,函数式编程抽象 ...
- java8 探讨与分析匿名内部类、lambda表达式、方法引用的底层实现
问题解决思路:查看编译生成的字节码文件 目录 测试匿名内部类的实现 小结 测试lambda表达式 小结 测试方法引用 小结 三种实现方式的总结 对于lambda表达式,为什么java8要这样做? 理论 ...
- Java提升二:Lambda表达式与方法引用
1.Lambda表达式 1.1.定义 lambda表达式是对于函数式接口(只含有一个抽象方法的接口)的简洁实现方式.它与匿名内部类的作用相似,但是就使用范围而言,匿名内部类更为广泛,而lambda表达 ...
- Java 8 Lambda表达式之方法引用 ::双冒号操作符
双冒号运算符就是java中的方法引用,方法引用的格式是类名::方法名. 这里只是方法名,方法名的后面没有括号“()”.--------> 这样的式子并不代表一定会调用这个方法.这种式子一般是用作 ...
- Lambda表达式和方法引用
1 , 为什么用lambda表达式 将重复固定的代码写法简单化 2 ,lambda表达式的实质 对函数式接口的实现(一个接口中只有一个抽象方法的接口被称为函数式接口) package com.mo ...
- lambda表达式之方法引用
/** * 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器.<br> * 与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码. ...
- Java 8-Lambda表达式、方法引用、标准函数接口与流操作、管道操作之间的关系
1.Lambda表达式与接口之间的关系 只要Lambda表达式的声明形式与接口相一致,在很多情况下都可以替换接口.见如下代码 Thread t1 = new Thread(new Runnable() ...
随机推荐
- 2020-3-3 20175110王礼博 《网络对抗技术》Exp1 PC平台逆向破解
目录 1.实践目标与基础知识 2.直接修改程序机器指令,改变程序执行流程 3.通过构造输入参数,造成BOF攻击,改变程序执行流 4.注入Shellcode并执行 5.实验收获与感想 6.什么是漏洞?漏 ...
- lr使用soap协议,来对webservice接口进行测试
实际项目中基于WSDL来测试WebService的情况并不多,WSDL并不是WebService测试的最佳选择. 最主要的原因还是因为WSDL文档过于复杂. 在案例(天气预报WebService服务) ...
- while和do-while
1. While(条件表达式){ 只要条件表达式结果为true,循环一直执行,当条件表达式结果为false的时候,循环终止 } 2. Do{ 循环体代码:首先执行该循环体代码一次.如果while后边的 ...
- 【Android】EventReminder使用教程(日历事件导出封装库)
碎碎念 为啥要写这个库呢? 尝试自己写一个库调用,学习一下这个流程,为以后做准备 日历库在网上的资料太少了,而这个功能却又很实用 自己做的项目都会涉及到事件导出功能,不想重复写代码 使用方法 引入 在 ...
- AJ学IOS(28)UI之Quartz2D简单介绍
AJ分享,必须精品 iOS开发UI篇—Quartz2D简单介绍 什么是Quartz2D Quartz 2D是⼀个二维绘图引擎,同时支持iOS和Mac系统 Quartz 2D能完成的工作: 绘制图形 : ...
- L15卷积神经网络基础
卷积神经网络基础 本节我们介绍卷积神经网络的基础概念,主要是卷积层和池化层,并解释填充.步幅.输入通道和输出通道的含义. 二维卷积层 本节介绍的是最常见的二维卷积层,常用于处理图像数据. 二维互相关运 ...
- Vue定义全局过滤器filter
这里介绍的是多个过滤器一起添加到全局中 1.创建方法 首先src下新建plugin文件夹,用来存放插件. 在plugin文件夹内新建filters.js,编写方法(如隐藏手机号码等等...) /** ...
- 域名和服务器绑定及https协议更换
服务器是之前已经购买了的 1.腾讯云产品中搜索域名注册(产品太多了懒得找,直接搜索来得快些) 2.进去之后可以选择各种后缀的域名,输入自己喜欢的,看看哪些后缀是没有被注册的.自己挑选一个就可以,按照指 ...
- Spiking-YOLO : 前沿性研究,脉冲神经网络在目标检测的首次尝试 | AAAI 2020
论文提出Spiking-YOLO,是脉冲神经网络在目标检测领域的首次成功尝试,实现了与卷积神经网络相当的性能,而能源消耗极低.论文内容新颖,比较前沿,推荐给大家阅读 来源:晓飞的算法工程笔记 公众 ...
- ambarella H2平台fpga捕捉卡驱动案例
公司最近开发的一款产品用到了ambarella H2平台的一款Soc,众所周知ambarella的H2系列的Soc编码能力很强,最高可达4kp60,捕捉上没有用ambarella开发板推荐的几个捕捉卡 ...