🔥个人专栏:java学习
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
1. 背景 🚀
🔥 Lambda 表达式 是JDK8新增的特性,Lambda 表达式 可以取代大部分匿名内部类,写出更优雅的Java代码,尤其在集合的遍历和其他集合操作中,可以将函数作为一个方法的参数,也就是函数作为参数传递到方法中,极大地优化代码结构JDK也提供了大量的内置函数式接口供开发者使用,使得 Lambda 表达式 的运用更加方便、高效。
- Lambda 表达式(Lambda expression)可以看作是一个匿名函数 ,基于数学中的λ演算得名,也可称为闭包(Closure)
- Lambda 表达式 的使用场景:用以简化接口实现
2. 基本使用 🖊
🥑 1. 语法格式
🎈 Lambda表达式的基本语法:由 参数列表(parameters)、箭头符号(一 >) 和方法体(expression 或者 statements)组成
-
paramaters: 类似方法中的形参列表,这里的参数是函数式接口里的参数。这里的参数类型可以明确的声明也可不声明而由JVM隐含的推断。另外当只有一个推断类型时可以省略掉圆括号。
- >:可理解为“被用于” 的意思
方法体: 可以是表达式也可以代码块,是函数式接口里方法的实现。-
代码块可返回一个值或者什么都不返回,这里的代码块等同于方法的方法体。
-
如果是表达式,也可以返回一个值或者什么都不返回。
-
其中,表达式会被执行,然后返回执行结果;语句块中的语句会被依次执行,就像方法中的语句一样。
-
Lambda表达式常用的语法格式如下所示:
语法格式 | 描述 |
---|---|
() -> System.out.println("Hello IsLand1314"); | 无参数,无返回值 |
(x) -> System.out.println(x); | 有一个参数,无返回值 |
x -> System.out.println(x); | 若只有一个参数,小括号可以省略不写 |
Comparator |
有两个以上的参数,有返回值,并且Lambda方法体中有多条语句 |
Comparator |
若Lambda方法体中只有一条语句,return和大括号都可以省略不写 |
(Integer x, Integer y) -> Integer.compare(x, y): | Lambda表达式的参数列表的数据类型可以省略不写,因为 Java 虚拟机的编译器可以通过上下文推断出数据类型,即 "类型推断" |
🥑 2. 函数式接口
🥑 虽然说,Lambda 表达式 可以在⼀定程度上简化接口的实现。但是,并不是所有的接口都可以使用lambda表达式来简洁实现的。
🧃 Lambda 表达式 毕竟只是⼀个匿名方法。当实现的接口中的方法过多或者多少的时候,lambda表达式都是不适用的。
- lambda表达式,只能实现函数式接口,函数式接口 定义:一个接口有且只有一个抽象方法
如下:
```java
//有且只有一个实现类必须要实现的抽象方法,所以是函数式接口
interface Test{
public void test();
}
```
- ⼀个接口中,要求实现类必须实现的抽象方法,有且只有⼀个!这样的接口,就是函数式接口
补充:
🔖 @FunctionalInterface
- 是⼀个注解,用在接口之前,判断这个接口是否是⼀个函数式接口。
- 如果是函数式接口,没有任何问题。如果不是函数式接口,则会报错
-
功能类似于 @Override
```java
@FunctionalInterface
interface NoParameterNoReturn {
//注意:只能有一个方法
void test();
}```
但是这种方式也是可以的:
```java
@FunctionalInterface
interface NoParameterNoReturn {
void test();
default void test2() {
System.out.println("JDK1.8新特性,default默认方法可以有具体的实现");
}
}
```
注意:
- 如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口
- 如果我们在某个接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口,这样如果有两个抽象方法,程序编译就会报错的
- 所以,从某种意义上来说,只要你保证你的接口中只有一个抽象方法,你可以不加这个注解。加上就会自动进行检测的
- Lambda 表达式 只能简化函数式接口的匿名内部类的写法
🥑 3. 具体使用
🌈 我们在上面提到过,Lambda表达式本质是一个匿名函数,函数的方法是:返回值 方法名 参数列表 方法体
- 在 Lambda 表达式 中我们只需要关心:参数列表 方法体
首先,我们实现准备好几个接口:
```java
//无返回值无参数
@FunctionalInterface
interface NoParameterNoReturn {
void test();
}
//无返回值一个参数
@FunctionalInterface
interface OneParameterNoReturn {
void test(int a);
}
//无返回值两个参数
@FunctionalInterface
interface MoreParameterNoReturn {
void test(int a,int b);
}
//有返回值无参数
@FunctionalInterface
interface NoParameterReturn {
int test();
}
//有返回值一个参数
@FunctionalInterface
interface OneParameterReturn {
int test(int a);
}
//有返回值多个参数
@FunctionalInterface
interface MoreParameterReturn {
int test(int a,int b);
}
```
我们在上面提到过,Lambda可以理解为:Lambda就是匿名内部类的简化,实际上是创建了一个类,实现了接口,重写了接口的方法 。
-
没有使用 Lambda 表达式 的时候的调用方式
java
NoParameterNoReturn noParameterNoReturn = new NoParameterNoReturn(){
@Override
public void test() {
System.out.println("hello");
}
};
noParameterNoReturn.test(); -
使用 Lambda 表达式 的时候的调用方式
```java
public class Test {
public static void main(String[] args) {
NoParameterNoReturn n = ()->{
System.out.println("无参数无返回值");
};
n.test();OneParameterNoReturn o = (a)-> { System.out.println("无返回值一个参数 "+a); }; o.test(666); MoreParameterNoReturn m = (int a,int b)->{ System.out.println("无返回值两个参数 "+a+" "+b); }; m.test(666,999); System.out.println("================"); NoParameterReturn n1 = ()->{ return 666; }; int ret1 = n1.test(); System.out.println(ret1); System.out.println("================"); OneParameterReturn o1 = (int a)->{ return a; }; int ret2 = o1.test(999); System.out.println(ret2); System.out.println("================"); MoreParameterReturn m1 = (int a,int b)-> { return a+b; }; int ret3 = m1.test(10,90); System.out.println(ret3); }
}
// 打印结果:
无参数无返回值
无返回值一个参数 666
无返回值两个参数 666 999
================
666
================
999
================
100```
🔥 Lambda 表达式 的语法还可以精简,显得非常有逼格,但是可读性就非常差,之前我们上面说过 Lambda 表达式 的语法格式表格。
- 参数类型可以省略,如果需要省略,每个参数的类型都要省略。
- 参数的小括号里面只有一个参数,那么小括号可以省略
- 如果方法体当中只有一句代码,那么大括号可以省略
-
如果方法体中只有一条语句,其是return语句,那么大括号可以省略,且去掉return关键字
```java
public class Test {
public static void main(String[] args) {
MoreParameterNoReturn moreParameterNoReturn = (a, b)->{
System.out.println("无返回值多个参数,省略参数类型:"+a+" "+b);
};
moreParameterNoReturn.test(20,30);
OneParameterNoReturn oneParameterNoReturn = a ->{
System.out.println("无参数一个返回值,小括号可以省略:"+ a);
};
oneParameterNoReturn.test(10);
NoParameterNoReturn noParameterNoReturn = ()->System.out.println("无参数无返回值,方法体中只有 一行代码");
noParameterNoReturn.test();
//方法体中只有一条语句,且是return语句
NoParameterReturn noParameterReturn = ()-> 40;
int ret = noParameterReturn.test();
System.out.println(ret);
}
}// 结果如下:
无返回值多个参数,省略参数类型:20 30
无参数一个返回值,小括号可以省略:10
无参数无返回值,方法体中只有 一行代码
40```
可能还不直观,一般来说,大家只要向下面这样使用就行
```cpp
interface T{
public int test(String name,int age);
}
public class Test {
public static void main(String[] args) {
T t = (name, age) -> {
System.out.println(name + " "+ age + " 岁了");
return age + 1;
};
int age = t.test("qian", 18);
System.out.println(age);
}
}
// 输出如下:
qian 18 岁了
19
```
4. 变量捕获
🔥 Lambda 表达式中存在变量捕获 ,了解了变量捕获之后,我们才能更好的理解 Lambda 表达式 的作用域 。Java当中的匿名类中,会存在变量捕获。
🥝 1. 匿名内部类变量捕获
匿名内部类就是没有名字的内部类 。在前面的博客——>【Java 学习】:内部类详解 中提到了匿名内部类中变量的捕获。
- 而我们这里只是为了说明变量捕获,所以,匿名内部类只要会使用就好
- 匿名内部类中:一定是程序在运行的过程当中没有发生改变的量
那么下面我们来,下面就来看看匿名内部类的使用及其变量捕获
```java
//有参 有返回值
class T{
public void func(){
System.out.println("func()");
}
}
public class Test {
public static void main(String[] args) {
int a = 100;
new T(){
// a = 666; // 如果把 a 在匿名内部类修改,就会报错
@Override
public void func() {
System.out.println("a= " + a);
}}.func();
}
}
// 输出
a= 100
```
注意: 上面在匿名内部类注释的 a = 666,代码中会报错
原因如下:
-
*匿名内部类中可以访问外部方法的局部变量(如 a),但这个变量必须是 final 或 隐式 final,这里的 a 由于没有被修改,因此是隐式 final,所以可以安全访问。*
-
*你注释掉的代码 a = 666; 会导致编译错误,因为局部变量 a 在 Lambda 或匿名内部类中不可修改,必须保持其初始值*
🥝 2. Lambda的变量捕获
在 Lambda 表达式当中也可以进行变量的捕获
- Lambda 表达式的变量捕获,同样也是不能捕获放生改变的,如果发生改变就会报错
具体我们看一下代码
```java
public class Test {
@FunctionalInterface
interface NoParameterNoReturn {
void test();
}
public static void main(String[] args) {
int a = 10;
NoParameterNoReturn noParameterNoReturn = ()->{
// a = 99; error
System.out.println("捕获变量:"+a);
};
noParameterNoReturn.test();
}
}
```
注意事项:
- 这里类似于局部内部类、匿名内部类,依然存在闭包的问题。
- 如果在 Lambda 表达式中,使用到了局部变量,那么这个局部变量会被隐式的声明为 final。是⼀个常量,不能修改值。
5. 函数引用
💦 Lambda 表达式是为了简化接口的实现的。在Lambda 表达式中,不应该出现比较复杂的逻辑。
- 如果在 Lambda 表达式中出现了过于复杂的逻辑,会对程序的可读性造成非常大的影响。
- 如果在 Lambda 表达式中需要处理的逻辑比较复杂,⼀般情况会单独的写⼀个方法。
- 在 Lambda 表达式中直接引用这个方法即可。
函数引用:引用⼀个已经存在的方法,使其替代lambda表达式完成接口的实现
🦄 1. 静态方法引用
语法
```java
类::静态方法
```
案例如下:
```java
interface T{
int test(int a,int b);
}
class Cal{
public static int add(int a,int b ){
return a + b;
}
}
public class Test {
public static void main(String[] args) {
//实现多个参数,一个返回值的接口
//对一个静态方法的引用,语法:类::静态方法
T t = Cal::add;
System.out.println(t.test(4,5));
}
}
// 输出结果:9
```
注意事项
- 在引用的方法后面,不要添加小括号。
- 引用的这个方法,参数(数量、类型)和返回值,必须要跟接口中定义的⼀致
🦄 2. 非静态方法引用
语法
```java
对象::非静态方法
```
案例如下:
```java
interface T{
int test(int a,int b);
}
public class Test {
private static class Cal{
public int add(int a, int b) {
return a+b;
}
}
public static void main(String[] args) {
// //对非静态方法的引用,需要使用对象来完成
// 两种方法
// 方法一
Cal cal = new Cal();
T t = cal::add;
// 方法二:
//T t = new Cal()::add;
System.out.println(t.test(4,5));
}
}
```
注意事项
- 在引用的方法后面,不要添加小括号。
- 引用的这个方法, 参数(数量、类型) 和 返回值, 必须要跟接口中定义的⼀致
🦄 3. 构造方法引用
使用场景
- 如果某⼀个函数式接口中定义的方法,仅仅是为了得到⼀个类的对象。此时我们就可以使用构造方法的引用,简化这个方法的实现。
语法 :
```java
类名::new
```
案例如下:
```java
public class Test {
private static class Student {
String name;
int age;
//无参构造
public Student() {
System.out.println("学生对象的无参构造");
}
//有参构造
public Student(String name, int age) {
System.out.println("学生对象的有参构造");
this.name = name;
this.age = age;
}
}
//定义一个函数式接口,用以获取无参的对象
@FunctionalInterface
private interface GetStudent {
//若此方法仅仅是为了获得一个Student对象,而且通过无参构造去获取一个Student对象作为返回值
Student s();
}
//定义一个函数式接口,用以获取有参的对象
@FunctionalInterface
private interface GetStudentWithParameter {
//若此方法仅仅是为了获得一个Student对象,而且通过有参构造去获取一个Student对象作为返回值
Student a(String name, int age);
}
// 测试
public static void main(String[] args) {
//lambda表达式实现接口
GetStudent lm = Student::new; //引用到学生类中的无参构造方法,获取到一个学生对象
Student s1 = lm.s();
System.out.println("学生的名字:" + s1.name + " 学生的年龄:" + s1.age); //学生的名字:null 学生的年龄:0
System.out.println("--------------------------------");
GetStudentWithParameter lm2 = Student::new;//引用到Student类中的有参构造,来获取一个学生对象
Student s2 = lm2.s("IsLand", 18);
System.out.println("学生的名字:" + s2.name + " 学生的年龄:" + s2.age);//学生的名字:IsLand 学生的年龄:18
}
}
```
这里 Student::new 是对 Student 类中无参构造方法的引用,使用它来实现 GetStudent 接口的 test() 方法。因此,lm.test() 会调用 Student 类的无参构造器,返回一个 Student 对象。在这种情况下,Student 对象的 name 和 age 字段会保持默认值:null 和 0。
注意事项 :可以通过接口中的方法的参数, 区分引用不同的构造方法。
6. Lambda 在集合中的使用
🔥 为了能够让Lambda和Java的集合类集更好的一起使用,集合当中,也新增了部分接口,以便与Lambda表达式对接。
- 多提一句:要用Lambda遍历集合就一定要看懂源码
注意:Collection的forEach()方法是从接口 java.lang.Iterable 拿过来的
🐇 1. Collection 接口
forEach() 方法演示
🥥 forEach()方法遍历集合。如果要打印元素,它需要的实现 Consumer接口,同时要实现重写 accept() 方法,它会把数组里的每一个元素都交给 accept()方法。
该方法在接口 Iterable 当中,原型如下:
```java
default void forEach(Consumer< ? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
```
-
*该方法表示:对容器中的每个元素执行action指定的动作*
```java
public class Test {
public static void main(String[] args) {
ArrayListlist = new ArrayList<>();
list.add("Hello");list.add("IsLand");
list.add("Hello");list.add("lambda");// 两种遍历遍历 list.forEach(new Consumer
() { @Override public void accept(String str) { //简单遍历集合中的元素。 System.out.print(str + " "); } }); System.out.println(); System.out.println("----------------------------"); //表示调用一个,不带有参数的方法,其执行花括号内的语句,为原来的函数体内容。 list.forEach(s -> {System.out.print(s + " ");}); } }
// 输出:
Hello IsLand Hello lambda
Hello IsLand Hello lambda
```
🐇 2. List 接口
sort()方法的演示
sort方法源码:该方法根据c指定的比较规则对容器元素进行排序
```java
public void sort(Comparator< ? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
```
使用示例:
```java
public class Test {
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
list.add("Hello");list.add("IsLand");
list.add("Hello");list.add("lambda");
// 两种排序方式
list.sort(new Comparator() {
@Override
public int compare(String s1, String s2) {
//注意这里比较长度
return s1.length() - s2.length();
}
});
System.out.println(list);
System.out.println("----------------------------");
// 修改为 Lambda 表达式
//调用带有2个参数的方法,且返回长度的差值
list.sort((s1, s2)->s1.length() - s2.length());
System.out.println(list);
}
}
// 输出如下:
[Hello, Hello, IsLand, lambda]
----------------------------
[Hello, Hello, IsLand, lambda]
```
🐇 3. Arrays 接口
```java
public class Test {
public static void main(String[] args) {
String[] a = {"program", "creek", "is", "a", "java", "site"};
Arrays.sort(a, (m,n)->Integer.compare(m.length(), n.length())) ;
System.out.println("Lambda 语句体只有一条语句,参数类型可推断: " + Arrays.toString(a));
Arrays.sort(a, (String m, String n)->{
if( m.length() > n.length()) return -1;
else return 0;
});
System.out.println("Lambda 语句体有多条语句,参数类型可推断: " + Arrays.toString(a));
}
}
// 输出结果如下:
Lambda 语句体只有一条语句,参数类型可推断: [a, is, java, site, creek, program]
Lambda 语句体有多条语句,参数类型可推断: [program, creek, java, site, is, a]
```
🐇 4. Map 接口
HashMap 的 forEach()
该方法原型如下:
```java
default void forEach(BiConsumer< ? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
}
catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
```
- *作用是对Map中的每个映射执行action指定的操作*
代码示例
```java
public class Test {
public static void main(String[] args) {
HashMap map = new HashMap<>();
map.put(1, "Hello");map.put(2, "IsLand");
map.put(3, "Hello");map.put(4, "lambda");
map.forEach(new BiConsumer(){
@Override
public void accept(Integer k, String v){
System.out.println(k + "=" + v);
}
});
System.out.println("--------------------------------------");
// 改为 Lambda 表达式
map.forEach((k, v) -> System.out.println(k + "=" + v));
}
}
// 运行结果如下:
1=Hello
2=IsLand
3=Hello
4=lambda
--------------------------------------
1=Hello
2=IsLand
3=Hello
4=lambda
```
7. 小结
Lambda表达式的优点很明显,在代码层次上来说,使代码变得非常的简洁。缺点也很明显,代码不易读
优点:
- 简化代码 :使得代码更加简洁,尤其是在处理集合、并行处理和事件监听时。
- 增强可读性 :通过将代码行为和逻辑传递给方法,减少了冗长的匿名类实现。
- 支持函数式编程 :Lambda 使得 Java 更加符合函数式编程范式,增强了代码的表达能力。
- 能够与 Stream API 结合 :Lambda 表达式与 Java 8 引入的 Stream API 配合使用,可以更方便地进行集合的操作(如过滤、映射、聚合等)。
缺点:
- 调试困难 :Lambda 表达式是匿名的,难以在调试时逐步跟踪。尤其是复杂的 Lambda 表达式可能使得代码的可调试性下降。
- 过度使用 Lambda :如果 Lambda 表达式过于复杂或滥用,可能会导致代码的可读性下降,尤其是当 Lambda 的行为变得不直观时。
- 性能问题 :虽然 Lambda 表达式的引入让代码更加简洁,但在某些情况下,Lambda 的性能可能不如传统的匿名类或普通方法调用,因为每次调用 Lambda 都涉及到对象的创建(比如生成函数式接口的代理)。
Lambda表达式有什么使用前提: 必须是接口的匿名内部类,接口中只能有一个抽象方法
- 隐式 final :如果局部变量没有明确声明为
final
,但在 Lambda 表达式中没有改变它的值,则编译器会隐式将其视为final