Java8中的Lambda表达式

汤圆学Java 2021-04-16 19:22:18
设计模式 readline


作者:汤圆

个人博客:javalover.cc

前言

大家好啊,我是汤圆,今天给大家带来的是《Java8中的Lambda表达式》,希望对大家有帮助,谢谢

文章纯属原创,个人总结难免有差错,如果有,麻烦在评论区回复或后台私信,谢啦

简介

Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次;

下面贴个对比代码:

// Java8之前:旧的写法
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("old run");
}
};
Thread t = new Thread(runnable);
// Java8之后:新的写法
Runnable runnable1 = ()->{
System.out.println("lambda run");
};
Thread t1 = new Thread(runnable1);

可以看到,有了lambda,代码变得简洁多了

你可以把lambda当作一个语法糖

下面让我们一起来探索lambda的美好世界吧

目录

下面列出本文的目录

  • lambda的语法
  • 为啥引入lambda
  • 什么是函数式接口
  • 什么是行为参数化
  • 手写一个函数式接口
  • 常用的函数式接口
  • 什么是方法引用
  • 什么是构造引用
  • lambda的组合操作

正文

1. lambda的语法

lambda语法

下面分别说下语法中的三个组成部分

  • 参数: ( Dog dog )
    • 参数类型可省略(当编译器可以自动推导时),比如Comparator<String> comparatorTest = (a, b)->a.length()-b.length();,可以推导出a,b都为String
    • 当参数类型可省略,且只有一个参数时,括弧也可以省略(但是个人习惯保留)
  • 符号: ->
  • 主体:{ System.out.println("javalover"); }
    • 如果是一条语句,则需要加大括号和分号{;}(比如上图所示)
    • 如果是一个表达式,则直接写,啥也不加(比如a.length()- b.length()

2. 为啥引入lambda

为了简化代码

因为Java是面向对象语言,所以在lambda出现之前,我们需要先构造一个对象,然后在对象的方法中实现具体的内容,再把构造的对象传递给某个对象或方法

但是有了lambda以后,我们可以直接将代码块传递给对象或方法

现在再回头看下开头的例子

lambda减少了模板代码

可以看到,用了lambda表达式后,少了很多模板代码,只剩下一个代码块(最核心的部分)

3. 什么是函数式接口

就是只定义了一个抽象方法的接口

  • 正例:有多个默认方法,但是如果只有一个抽象方法,那它就是函数式接口,示例代码如下
@FunctionalInterface
public interface FunctionInterfaceDemo {
void abstractFun();
default void fun1(){
System.out.println("fun1");
}
default void fun2(){
System.out.println("fun2");
}
}

这里的注解@FunctionalInterface可以省略,但是建议加上,就是为了告诉编译器,这是一个函数式接口,此时如果该接口有多个抽象方法,那么编译器就会报错

  • 反例:比如A extends B,A和B各有一个抽象方法,那么A就不是函数式接口,示例代码如下
// 编译器会报错,Multiple non-overriding abstract methods found in XXX
@FunctionalInterface
public interface NoFunctionInterfaceDemo extends FunctionInterfaceDemo{
void abstractFun2();
}

上面的父接口FunctionInterfaceDemo中已经有了一个抽象方法,此时NoFunctionInterfaceDemo又定义了一个抽象方法,结果编译器就提示了:存在多个抽象方法

在Java8之前,其实我们已经接触过函数式接口

比如Runnable 和 Comparable

只是没有注解@FunctionalInterface。

那这个函数式接口要怎么用呢?

配合lambda食用,效果最佳(就是把lambda传递给函数式接口),示例代码如下:

new Thread(() -> System.out.println("run")).start();

其中用到的函数式接口是Runnable

4. 什么是行为参数化

就是把行为定义成参数,行为就是函数式接口

类似泛型中的类型参数化<T>,类型参数化是把类型定义成参数

行为参数化,通俗点来说:

  • 就是用函数式接口形参
  • 然后传入接口的各种实现内容(即lambda表达式)作为实参
  • 最后在lambda内实现各种行为(好像又回到多态的那一节了?这也是为啥多态是Java的三大特性的原因之一,应用太广泛了)

这样来看的话,行为参数化和设计模式中的策略模式有点像了(后面章节会分别讲常用的几种设计模式)

下面我们手写一个函数式接口来加深理解吧

5. 手写一个函数式接口

下面我们循序渐进,先从简单的需求开始

  • 第一步:比如我们想要读取某个文件,那可以有如下方法:
public static String processFile() throws IOException {
// Java7新增的语法,try(){},可自动关闭资源,减少了代码的臃肿
try( BufferedReader bufferedReader =
new BufferedReader(new FileReader("D:\\JavaProject\\JavaBasicDemo\\test.txt"))){
return bufferedReader.readLine();
}
}

可以看到,核心的行为动作就是 return bufferedReader.readLine();,表示读取第一行的数据并返回

那如果我们想要读取两行呢?三行?

  • 第二步:这时就需要用到上面的函数式接口了,下面就是我们自己编写的函数式接口
@FunctionalInterface
interface FileReadInterface{
// 这里接受一个BufferedReader对象,返回一个String对象
String process(BufferedReader reader) throws IOException;
}

可以看到,只有一个抽象方法process() ,它就是用来处理第一步中的核心动作(读取文件内容)

至于想读取多少内容,那就需要我们在lambda表达式中定义了

  • 第三步:接下来我们定义多个lambda表达式,用来传递函数式接口,其中每个lambda表达式就代表了一种不同的行为,代码如下:
// 读取一行
FileReadInterface fileReadInterface = reader -> reader.readLine();
// 读取两行
FileReadInterface fileReadInterface2 = reader -> reader.readLine() + reader.readLine();
  • 第四步:我们需要修改第一步的processFile(),让其接受一个函数式接口,并调用其中的抽象方法,代码如下:
// 参数为第二步我们自己手写的函数式接口
public static String processFile(FileReadInterface fileReadInterface) throws IOException {
try( BufferedReader bufferedReader =
new BufferedReader(new FileReader("./test.txt"))){
// 这里我们不再自己定义行为,而是交给函数式接口的抽象方法来处理,然后通过lambda表达式的传入来实现多个行为
return fileReadInterface.process(bufferedReader);
}
}
  • 第五步:拼接后,完整代码如下:
public class FileReaderDemo {
public static void main(String[] args) throws IOException {
// 第三步: 
// lambda表达式1 传给 函数式接口:只读取一行
FileReadInterface fileReadInterface = reader -> reader.readLine();
// lambda表达式2 传给 函数式接口:只读取两行
FileReadInterface fileReadInterface2 = reader -> reader.readLine() + reader.readLine();
// 最后一步: 不同的函数式接口的实现,表现出不同的行为
String str1 = processFile(fileReadInterface);
String str2 = processFile(fileReadInterface2);
System.out.println(str1);
System.out.println(str2);
}
// 第四步: 读取文件方法,接受函数式接口作为参数
public static String processFile(FileReadInterface fileReadInterface) throws IOException {
try( BufferedReader bufferedReader =
new BufferedReader(new FileReader("./test.txt"))){
// 调用函数式接口中的抽象方法来处理数据 
return fileReadInterface.process(bufferedReader);
}
}
// 第一步:
public static String processFile() throws IOException {
try( BufferedReader bufferedReader =
new BufferedReader(new FileReader("./test.txt"))){
return bufferReader.readLine();
}
}
}
// 第二步: 我们手写的函数式接口
@FunctionalInterface
interface FileReadInterface{
String process(BufferedReader reader) throws IOException;
}

其实你会发现,我们手写的这个函数式接口,其实就是Function<T>去除泛型化后的接口,如下所示:

@FunctionalInterface
public interface Function<T, R> {
// 都是接受一个参数,返回另一个参数
R apply(T t);
}

下面我们列出Java中常用的一些函数式接口,你会发现自带的已经够用了,基本不会需要我们自己去写

这里的手写只是为了自己实现一遍,可以加深理解程度

6. 常用的函数式接口

常用的函数式接口

7. 什么是方法引用

我们先看一个例子

前面我们写的lambda表达式,其实还可以简化,比如

// 简化前
Function<Cat, Integer> function = c->c.getAge();
// 简化后
Function<Cat, Integer> function2 = Cat::getAge;

其中简化后的Cat::getAge,我们就叫做方法引用

方法引用就是引用类或对象的方法

下面我们列出方法引用的三种情况:

  1. Object::instanceMethod(对象的实例方法)
  2. Class::staticMethod(类的静态方法)
  3. Class::instanceMethod(类的实例方法)

像我们上面举的例子就是第三种:类的实例方法

下面我们用代码演示上面的三种方法:

public class ReferenceDemo {
public static void main(String[] args) {
// 第一种:引用对象的实例方法
Cat cat = new Cat(1);
Function<Cat, Integer> methodRef1 = cat::getSum;
// 第二种:引用类的静态方法
Supplier<Integer> methodRef2 = Cat::getAverageAge;
// 第三种:引用类的实例方法
Function<Cat, Integer> methodRef3 = Cat::getAge;
}
}
class Cat {
int age;
public Cat(int age) {
this.age = age;
}
// 获取猫的平均年龄
public static int getAverageAge(){
return 15;
}
// 获取两只猫的年龄总和
public int getSum(Cat cat){
return cat.getAge() + this.getAge();
}
public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
}
}

为啥要用这个方法引用呢?

方法引用好比lambda表达式的语法糖,语法更加简洁,清晰

一看就知道是调用哪个类或对象的哪个方法

8. 什么是构造引用

上面介绍了方法引用,就是直接引用某个方法

这里的构造引用同理可得,就是引用某个类的构造方法

构造引用的表达式为:Class::new,仅此一种

如果你有多个构造函数,那编译器会自己进行推断参数(你看看,多好,多简洁)

比如下面的代码:

// 这里调用 new Cat()
Supplier<Cat> constructRef1 = Cat::new;
// 这里调用 new Cat(Integer)
Function<Integer, Cat> constructRef2 = Cat::new;

9. lambda表达式中引入外部变量的限制

要求引入lambda表达式中的变量,必须是最终变量,即该变量不会再被修改

比如下面的代码:

public static void main(String[] args) {
String str = "javalover.cc";
Runnable runnable = ()->{
str = "1";// 这里会报错,因为修改了str引用的指向
System.out.println(str);
}
}

可以看到,lambda表达式引用了外面的str引用,但是又在表达式内部做了修改,结果就报错了

为啥要有这个限制呢?

为了线程安全,因为lambda表达式有一个好处就是只在需要的时候才会执行,而不是调用后立马执行

这样就会存在多个线程同时执行的并发问题

所以Java就从根源上解决:不让变量被修改,都是只读的

那你可能好奇,我不把str的修改代码放到表达式内部可以吗?

也不行,道理是一样的,只要lambda有用到这个变量,那这个变量不管是在哪里被修改,都是不允许的

不然的话,我这边先执行了一次lambda表达式,结果你就改了变量值,那我第二次执行lambda,不就乱了吗

10. lambda的组合操作

最后是lambda的必杀技:组合操作

在这里叫组合或者复合都可以

概述:组合操作就是先用一个lambda表达式,然后再在后面组合另一个lambda表达式,然后再在后面组合另另一个lambda表达式,然后。。。有点像是链式操作

学过JS的都知道Promise,里面的链式操作就和这里的组合操作很像

用过Lombok的朋友,应该很熟悉@Builder注解,其实就是构造者模式

下面我们用代码演示下组合操作:

// 重点代码
public class ComposeDemo {
public static void main(String[] args) {
List<Dog> list = Arrays.asList(new Dog(1,2), new Dog(1, 1));
// 1. 先按年龄排序(默认递增)
// Dog::getAge, 上面介绍的方法引用
// comparingInt, 是Comparator的一个静态方法,返回Comparator<T>
Comparator<Dog> comparableAge = Comparator.comparingInt(Dog::getAge);
// 2. 如果有相同的年龄,则年龄相同的再按体重排序(如果年龄已经比较出大小,则下面的体重就不会再去比较)
Comparator<Dog> comparableWeight = Comparator.comparingInt(Dog::getWeight);;
// 3. 调用list对象的sort方法排序,参数是Comparator<? super Dog>
list.sort(comparableAge.thenComparing(comparableWeight));
System.out.println(list);
}
}
// 非重点代码
class Dog{
private int age;
private int weight;
public Dog(int age, int weight) {
this.age = age;
this.weight = weight;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
@Override
public String toString() {
return "Dog{" +
"age=" + age +
", weight=" + weight +
'}';
}
}

输出:[Dog{age=1, weight=1}, Dog{age=1, weight=2}]

比较的流程如下所示:

组合操作

总结

  1. lambda的语法: 参数+符合+表达式或语句,比如(a,b)->{System.out.println("javalover.cc");}

  2. 函数式接口:只有一个抽象方法,最好加@FunctionalInterface,这样编译器可及时发现错误,javadoc也说明这是一个函数式接口(可读性)

  3. 行为参数化:就是函数式接口作为参数,然后再将lambda表达式传给函数式接口,通过不同的lambda内容实现不同的行为

  4. 方法引用:lambda的语法糖,总共有三种:

    • Object::instanceMethod(对象的实例方法)

    • Class::staticMethod(类的静态方法)

    • Class::instanceMethod(类的实例方法)

  5. 构造引用:就一种,编译器自己可判断是哪个构造函数,语法为Class::new

  6. 在lambda中引入外部变量,必须保证这个变量是最终变量,即不再被修改

  7. lambda的组合操作,就是链式操作,组合是通过函数式接口的静态方法来组合(静态方法会返回另一个函数式接口的对象)

比如list.sort(comparableAge.thenComparing(comparableWeight));

后记

最后,感谢大家的观看,谢谢

版权声明
本文为[汤圆学Java]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/3117223/blog/5022277

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云