一文带你深入了解 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() ...
随机推荐
- Python pip高级用法
1.pip 高级用法为了便于用户安装和管理第三方库和软件,越来越多的编程语言拥有自己的包管理工 具,如 nodejs 的 npm, ruby 的 gem. Python 也不例外,现在 Python ...
- 外观模式(c++实现)
外观模式 目录 外观模式 模式定义 模式动机 UML类图 源码实现 优点 缺点 模式定义 外观模式(Facade),为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子 ...
- linux之cat 操作
1.查看或创建 cat 1.txt #如果目录有这个文件则会打开查看,没有则会创建 2.压缩空白 cat 1.txt 我是第一行 我是第二 行 cat -bs 1.txt # 变成 cat 1.txt ...
- 美的PDF转换成Word转换器完全免费
下载地址:百度网盘提取码:02ap 安装破解步骤:先安装主程序,末尾是full结尾的,安装完成后不要打开软件,然后接着安装破解补丁,即可破解成功! 需要的老铁们直接拿去用吧,亲测好用!有配套的功能强大 ...
- python工业互联网监控项目实战4—python opcua
前面章节我们采用OPC作为设备到上位的信息交互的协议,本章我们介绍跨平台的OPC UA.OPC作为早期的工业通信规范,是基于COM/DCOM的技术实现的,用于设备和软件之间交换数据,最初,OPC标准仅 ...
- [机器学习实战-Logistic回归]使用Logistic回归预测各种实例
目录 本实验代码已经传到gitee上,请点击查收! 一.实验目的 二.实验内容与设计思想 实验内容 设计思想 三.实验使用环境 四.实验步骤和调试过程 4.1 基于Logistic回归和Sigmoid ...
- IIS WebDAV安全配置
本文为转载,原文地址:http://www.2cto.com/article/201307/228165.html IIS WebDAV安全配置 2013-07-16 12:13:00 作者:瞌睡龙收 ...
- NGINX反向代理,后端服务器获取真实IP
一般使用中间件做一个反向代理后,后端的web服务器是无法获取到真实的IP地址. 但是生产上,这又是不允许的,那么怎么解决? 1.在NGINX反向代理服务器上进行修改 2.修改后端web服务器配置文件 ...
- CentOS 7 + Win 双系统的安装遇到的重要问题
前言:对于刚学linux的朋友们,多多小小因为各种原因需要装双系统,亦或者爱好使然.多数是问题解决,第一次装系统者不推荐看-. 那么现在内德在此就说说在本本上装双系统会遇到的问题及其解决方法. 环境准 ...
- printf 参数检查 __attribute__((format(printf, 1, 2)))
With GCC, I can specify __attribute__((format(printf, 1, 2))) , telling the compiler that this funct ...