【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://blog.csdn.net/m0_69908381/article/details/130522535
出自【进步*于辰的博客】
坦白说,在我学会如何使用Lambda表达式后,不禁感叹:“开发出Lambda表达式的人真是天才”。
“将函数如参数般传递” 作为Lambda表达式的核心思想,新颖、实用、又便捷,可谓是为高质量的代码的编写作出了巨大的贡献。
当然,也如很多前辈所言。对于没学过或暂未弄懂Lambda表达式的博友们而言,阅读带有Lambda表达式的代码,难度会增加很多,更遑论修改和维护。
而我本人的看法。Lambda表达式作为一种“新颖”的编码思想(当然,Lambda表达式已经出生很多年了,不能算是“新概念”),它的理解难度在于。。嗯。。。怎么说呢,其实Lambda表达式的难度不大,关键是能不能转过那个“弯”(有点抽象,不知如何表述)。
我个人觉得,要想学会Lambda表达式,2个知识是基础:1、匿名内部类;2、泛型。学会“匿名内部类”对理解Lambda表达式的帮助在于理解“变式”,而“泛型”则是用于Lambda表达式的“扩展”。
“喝水不忘挖井人”。这是我系统学习Lambda表达式时参考的博文【Lambda表达式超详细总结】(转发)。在下文中,我会借用这篇博文中的一些资源,如:图片、描述。(因为我觉得那位前辈总结的很好,我再尽力做也最多如此了)。
参考笔记一,P33.8、P43.5。
文章目录
- 1、关于内部类
- 1.1 匿名内部类
- 2、关于Lambda优化匿名内部类
- 3、Lambda优化规范
- 3.1 问题
- 3.2 规范
- 3.2.1 参数列表
- 3.2.2 方法体
- 3.2.3 补充说明
- 4、Java内置函数式接口
- 4.1 四大核心函数式接口
- 4.2 其他函数式接口
- 4.3 其他函数式接口补充
- 4.4 示例补充说明
- 5、Lambda表达式的三种引用
- 5.1 方法引用
- 5.1.1 先说结论
- 5.1.2 格式
- 5.1.3 说明示例
- 5.2 构造器引用
- 5.2.1 概述
- 5.2.2 示例
- 5.3 数组引用
- 6、Lambda表达式的作用域
- 6.1 引用局部常量
- 6.1.1 引用局部基本类型常量
- 6.1.2 引用局部引用类型常量
- 6.2 引用成员变量、类变量
- 6.3 引用成员常量、类常量
- 6.4 Lambda表达式限制访问局部变量的原因
- 7、最后
1、关于内部类
java中内部类分为成员内部类、静态内部类、局部内部类和匿名内部类。
示例:
class TestInnerClass {
/**
* 成员内部类
*/
class MemberInner {
}
/**
* 静态内部类
*/
static class StaticInner {
}
public static void main(String[] args) {
/**
* 局部内部类
*/
class LocalInner {
}
/**
* 匿名内部类
*/
Runnable run = new Runnable() {
@Override
public void run() {
}
};
new Thread(run).start();// 启动线程
}
}
与Lambda表达式相关的只有匿名内部类。
之所以将其他内部类也列举出来呢,主要是我曾分不清这4种内部类(因为极少用)。
1.1 匿名内部类
示例1:
Runnable runa = new Runnable() {
@Override
public void run() {
int letter = (int) (Math.random() * 26) + 97;
sout "随机生成的小写字母是:" + (char) letter;
}
};
new Thread(runa).start();
等同于:
示例2:
class MyRunnable implements Runnable {
@Override
public void run() {
int letter = (int) (Math.random() * 26) + 97;
sout "随机生成的小写字母是:" + (char) letter;
}
}
class TestThread {
public static void main(String[] args) {
Runnable runa = new MyRunnable();
new Thread(runa).start();
}
}
这是很简单、单一的创建线程的方法之一,大家耳熟能详。
示例2中,先手动创建一个类MyRunnable
实现Runnable
接口,再将MyRunnable类实例化并上转为Runnable类型变量runa
,最后将runa作为Thread
构造方法实参创建线程。很显然,这种方法冗余、代码质量不高(很Low
)。
示例1是对示例2的优化。先执行new Runnable()
(Runnable是接口,自然不是实例化),因其实现了run()
抽象方法,因此,new Runnable()
创建了一个Runnable子类的实例,与示例2中的new MyRunnable()
相同。
自然的,示例1相较于示例2优化许多,这就是匿名内部类的作用。
2、关于Lambda优化匿名内部类
优化上面中的示例1。
Runnable runa = () -> {
int letter = (int) (Math.random() * 26) + 97;
sout "随机生成的小写字母是:" + (char) letter;
};
new Thread(runa).start();
有什么变化?是不是省去了重写run()
抽象方法的方法定义部分。
继续优化。
new Thread(() -> {
int letter = (int) (Math.random() * 26) + 97;
sout "随机生成的小写字母是:" + (char) letter;
}).start();
这是将“Runnable
接口的匿名内部类的定义部分”作为Thread
构造方法实参来创建线程。
就如序言中所言 “将函数如参数般传递”。这里的“函数”对应“Runnable
接口的匿名内部类的定义部分”。
大家先初步了解,继续看。。。
3、Lambda优化规范
从:
Runnable runa = new Runnable() {
@Override
public void run() {
int letter = (int) (Math.random() * 26) + 97;
sout "随机生成的小写字母是:" + (char) letter;
}
};
new Thread(runa).start();
优化到:
Runnable runa = () -> {
int letter = (int) (Math.random() * 26) + 97;
sout "随机生成的小写字母是:" + (char) letter;
};
new Thread(runa).start();
难道可以随便写?当然不是。那规范是什么?
3.1 问题
大家从我简单举例说明Lambda优化匿名内部类的例子中,肯定看出了一个问题。
上个例子。
Runnable runa = () -> {
int letter = (int) (Math.random() * 26) + 97;
sout "随机生成的小写字母是:" + (char) letter;
};
new Thread(runa).start();
Runnable
接口只有一个抽象方法run()
,可在此例中,有任何有关run方法的定义吗?没有。因此,Lambda只能优化只有一个抽象方法的接口的匿名内部类的定义部分(这类接口称之为“函数式接口”,后续说明)。
举个例:定义java.lang.Number
抽象类的匿名内部类。
这是Number抽象类的源码。
可见,有4个抽象方法。因此,创建Number抽象类的匿名内部类,必须重写这4个抽象方法。即:
new Number() {
@Override
public int intValue() {
return 0;
}
@Override
public long longValue() {
return 0;
}
......
};
如这般,就无法使用Lambda表达式进行优化。
3.2 规范
既已知道Lambda优化的限制,直接步入正题。
Lambda表达式的基础语法:() -> {}
。
说明:->
的左边是圆括号,对应匿名内部类重写的唯一抽象方法的参数列表;右边是花括号,对应相应方法体。
(注:从此处之后,我的阐述中将不会再加上“匿名内部类”或“唯一抽象方法”等字样,上文这般言是遵循循序渐进,为了方便大家理解)
3.2.1 参数列表
- 若无参,则必须是
()
,即:() -> {}
; - 若只有1个参数xx,则可以是
(xx)
或xx
,即:(xx) -> {}
或xx -> {}
; - 若有2个参数a、b,则必须是
(a, b)
,即:(a, b) -> {}
; - 举一反三。
3.2.2 方法体
方法体这边由于不用关注方法定义部分,因此没有什么需要注意的,唯一需要注意的就是一些省略规范和返回值类型。当然,此处仅对省略规范进行阐述。
1、 若只有一条语句,可省略分号和花括号。
示例:
new Thread(() -> sout "只有一条语句").start();
// 等同于:
new Thread(() -> {
sout "只有一条语句";
}).start();
Runnable
接口的run()
无参。
2、 若方法有返回值,且只有一条语句时,可省略分号、花括号和return
。
示例:
@FunctionalInterface
interface SelfInterface {
double getTrigonometric(double angle);
}
class TestSelf {
public static void main(String[] args) {
SelfInterface service = xx -> Math.sin(xx);
// 等同于:
SelfInterface service = xx -> {
return Math.sin(xx);
};
double radian = service.getRadian(10);
sout radian;// 打印:-0.5440211108893698
}
}
SelfInterface 是一个自定义函数式接口。其中,@FunctionalInterface
注解用于标识“函数式接口”,与@Overide
注解属于“同类型”注解。
3.2.3 补充说明
以上例为例:
两个问题:
- JVM是如何知道图中红框部分是
getTrigonometric()
的方法体的? - JVM是如何知道参数
xx
的类型是double?(因为Math.sin()
的形参类型是double)
下图是Math.sin()
的API截图:
解释: (以下阐述转载至博文【Lambda表达式超详细总结】)
因为JVM可以通过上下文推断出为何接口实现抽象方法,即“接口推断”,以及推断出*所实现的相应抽象方法的参数列表(包括形参类型),即“类型推断”。
简言之,Lambda表达式依赖于上下文环境。
4、Java内置函数式接口
提前说明:
- 所有Java内置函数式接口都运用了泛型;
关于泛型,详述请查阅博文【关于对【java泛型】的理解与简述(读后简结)】。也建议大家先大致浏览这篇博文,再看下面的示例,这样会更容易理解。
- 在下述每个示例的后面,我会适当的对代码进行一些阐述(例如:方法出处)。不过,不是解释代码。因为以下示例我是按照“由易到难、循序渐进”的原则进行举例的,相信大家能够在逐步的阅读中慢慢理解。因此,我觉得做代码解释是赘述。
4.1 四大核心函数式接口
(此图出自博文【Lambda表达式超详细总结】)
使用示例:
1、消费型接口 Consumer<T>
:
Consumer<String> service1 = str -> sout str;
service1.accept("消费型接口Consumer");// 打印:消费型接口Consumer
方法:void
accept(T t)
。
2、供给型接口 Supplier<T>
:
Supplier<Integer> service2 = () -> (int)(Math.random() * 100);// 获取0~100的随机整数
sout service2.get();// 打印:66
方法:T get()
。
3、函数型接口 Function<T,R>
:
Function<String, Integer> service3 = str -> str.length();
sout service3.apply("I love China!!");// 打印:14
方法:R apply(T t)
。
4、断定型接口 Predicate<T>
:
Integer i1 = 10;
Predicate<Integer> service4 = xx -> i1.equals(xx);
sout service4.test(10);// 打印:true
sout service4.test(20);// 打印:false
方法:boolean test(T t)
。
4.2 其他函数式接口
(此图出自博文【Lambda表达式超详细总结】)
使用示例:
1、函数型接口 BiFunction<T, U, R>
:
BiFunction<Character[], Character, Integer> service5 = (charArr, c) -> Arrays.binarySearch(charArr, c);
sout service5.apply(new Character[]{65, 66, 67}, 'B');// 打印:1
方法:R apply(T t, U u)
。
这个示例的关键是Arrays.binarySearch()
,其出处:博文【Java-API简读_java.util.Arrays(基于JDK1.8)(浅析源码)】的2.2项。
2、函数型接口 UnaryOperator<T>
:
UnaryOperator<String> service6 = str -> str.trim().split("!")[1].trim().toUpperCase();
sout service6.apply(" Hello KiTi! I am 小白 of csdn ");// 打印:I AM 小白 OF CSDN
方法:T apply(T t)
。
3、函数型接口 BinaryOperator<T>
:
BinaryOperator<List<Character>> service7 = (list1, list2) -> {
Collections.copy(list2, list1);
return list2;
};
sout service7.apply(Arrays.asList('C', 'h'), Arrays.asList('#', '#', 'i', 'n', 'a'));// 打印:[C, h, i, n, a]
方法:T apply(T t1, T t2)
。
示例中使用了2个方法:
Collections.copy()
,出处:博文【Java-API简读_java.util.Collections(基于JDK1.8)(浅析源码)】的2.10项;Arrays.asList()
,出处:博文【Java-API简读_java.util.Arrays(基于JDK1.8)(浅析源码)】的2.1项。
4、消费型接口 BiConsumer<T,R>
:
BiConsumer<char[], Character>service8 = (charArr, c) -> {
Arrays.fill(charArr, c);
sout Arrays.toString(charArr);// 打印:[#, #, #, #, #]
};
service8.accept(new char[]{'进', '步', '*', '于', '辰'}, '#');
方法:void accept(T t, U u)
。
这个示例的关键是Arrays.fill()
,其出处:博文【Java-API简读_java.util.Arrays(基于JDK1.8)(浅析源码)】的2.8项。
5、未知型接口 To[Int/Long?Double]Function<T>
:
// ToIntFunction<T>
ToIntFunction<List> service9= list -> list.size();
int size = service9.applyAsInt(Arrays.asList(true, 3, 2, 1, "yes", 'f', 'i', 'r', 'e'));// 结果:9
// ToLongFunction<T>
ToLongFunction<Integer> service10 = n-> {
long startTime = System.nanoTime();
while ((n--) > 0) {}
long endTime = System.nanoTime();
return endTime - startTime;
};
long time = service10.applyAsLong(100000);// 结果:6486400ms
// ToDoubleFunction<T>
ToDoubleFunction<Double> service11 = radian -> Math.toDegrees(radian);
double angle = service11.applyAsDouble(Math.PI);// 结果:180.0
方法:int/long/double applyAs[Int/Long/Double]
。
6、函数型接口 [Int/Long/Double]Function<R>
:
// IntFunction<R>
IntFunction<String> service12 = length -> {
StringBuffer builder = new StringBuffer(10);
while ((length--) > 0) {
int x = (int) (Math.random() * 10);// 获取0~9的随机数
builder.append(x);
}
return builder.toString();
};
String code = service12.apply(6);// 结果:695821
// LongFunction<R>
LongFunction<Date> service13 = timeStamp -> new Date(timeStamp);
Date current = service13.apply(System.currentTimeMillis());// 结果:Tue May 09 22:17:26 CST 2023
// DoubleFunction<R>
DoubleFunction<Long> service14 = originN -> Math.round(originN);
long round = service14.apply(10.5);// 结果:11
方法:R apply(int/long/double i)
。
4.3 其他函数式接口补充
函数式接口 | 参数类型 | 返回值类型 | 说明 |
---|---|---|---|
断定型接口 `BiPredicate<T, U> | T, U | boolean | 确定类型分别为T/U的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t, U u) |
使用示例:
1、断定型接口 `BiPredicate<T,U>:
BiPredicate<String, String > service1 = (t, u) -> t.equals(u);
sout service1.test("abc", "abc");// 打印:true
sout service1.test("abc", "123");// 打印:false
方法:boolean test(T t, U u)
。
4.4 示例补充说明
大家在看上文中我使用Java内置函数式接口写的示例时,肯定想吐槽:“你写的那些方法体,很多都是多此一举。”
从实用性来说的确是,例如:sout str
、str.length
、xx > 0? true: false
。直接调用相应方法不好么?还用Lambda表达式转个弯实现。
那我为何还这样写?
因为我觉得使用Lambda表达式编写函数式接口的关键在于“灵活、扩展、通用”这六个字。因此,我作那些示例的初衷是“任意举例、简单易懂”,目的不在于实现何种功能,而是阐述Lambda表达式的使用。
5、Lambda表达式的三种引用
提前说明:
以下关于引用的示例,我会尽量用上文中Java内置函数式接口及其所举示例进行“演变”举例,以降低大家阅读代码的成本。
5.1 方法引用
5.1.1 先说结论
方法引用中所使用的“缺省参数列表”必须与抽象方法的参数列表相同,返回值类型也必须相同。
何为“缺省参数列表”?这是我自定义的概念,我会在示例中举例说明。
5.1.2 格式
- 格式1:对象 :: 成员方法名
- 格式2:类 :: 类方法名
- 格式3:类 :: 成员方法名
5.1.3 说明示例
1、String类的成员方法boolean equals(String str)
。
以此例为基础进行举例。
BiPredicate<String, String > service1 = (t, u) -> t.equals(u);
演变:
// 示例1:类 :: 成员方法名
BiPredicate<String, String> service = String::equals;
sout service.test("csdn", "bilibili");// 打印:false
sout service.test("csdn", "csdn");// 打印:true
// 示例2:对象 :: 成员方法名
String str = "csdn";
Predicate<String> service = str::equals;
sout service.test("bilibili");// 打印:false
sout service.test("csdn");// 打印:true
示例说明:
- equals()的返回值类型为 boolean 类型,则此抽象方法的返回值类型也必须是 boolean 类型;
- equals()是实例方法,一共需要2个变量,1、String类型对象;2、String类型实参;
- 在示例1中,方法引用是
String::equals
;使用了第3种格式。由于未给定任何变量(即:缺省2个变量),则这2个变量必须由抽象方法的形参提供。因此,此时抽象方法的参数列表必须也是2个参数,且类型要与方法引用的类型相同。方法引用为String::equals
,则这2个变量的类型只能是String。故在BiPredicate<T, U>
中,泛型<T>、<U>
的类型实参为String; - 在示例2中,方法引用是
str::equals
;使用了第1种格式。由于给定了String对象str
(即:缺省1个变量)。同理,抽象方法的形参必须只有一个,且类型为String。故在Predicate<T>
中,泛型<T>
的类型实参为String。
2、Arrays类的静态方法void fill()
。
以此例为基础进行举例。
BiConsumer<char[], Character>service8 = (charArr, c) -> {
Arrays.fill(charArr, c);
sout Arrays.toString(charArr);// 打印:[#, #, #, #, #]
};
service8.accept(new char[]{'进', '步', '*', '于', '辰'}, '#');
演变:
// 类 :: 静态方法名
BiConsumer<char[], Character> service8 = Arrays::fill;
char[] charArr = new char[]{'进', '步', '*', '于', '辰'};
service8.accept(charArr, '#');
sout Arrays.toString(charArr);// 打印:[#, #, #, #, #]
示例说明:
fill()
无返回值,而抽象方法accept()
也无返回值,故匹配;fill()
出处:博文【Java-API简读_java.util.Arrays(基于JDK1.8)(浅析源码)】的2.8项,参数列表有2个参数,第1个参数类型为基本数据类型数组;第2个参数类型为基本数据类型。这2个参数都由抽象方法(即:accept()
)提供。故在BiConsumer<T, U>
中,泛型<T>
的类型实参为char[]
,而泛型<R>
的类型实参为Character
,
5.2 构造器引用
5.2.1 概述
格式:类 :: new。
说明:
顾名思义,“构造器引用”的作用就是实例化,即 返回实例。
例如:为实体类Users
创建构造器引用,则构造器引用固定为Users::new
,即返回一个Users实例。
约束:
抽象方法的参数列表决定了匹配哪个构造方法,即 构造器引用等同于构造方法。
5.2.2 示例
1、实体类。
class Users {
private Integer id;
private String[] hobby;
public Users() {
}
public Users(Integer id) {
this.id = id;
}
public Users(String[] hobby) {
this.hobby = hobby;
}
public Users(Integer id, String[] hobby) {
this.id = id;
this.hobby = hobby;
}
@Override
public String toString() {
return "Users{" +
"id=" + id +
", hobby=" + Arrays.toString(hobby) +
'}';
}
}
2、测试。
Supplier<Users> service1 =Users::new;
Users user1 = service1.get();
sout user1;// 打印:Users{id=null, hobby=null}
Function<Integer, Users> service2 = Users::new;
Users user2 = service2.apply(1001);
sout user2;// 打印:Users{id=1001, hobby=null}
Function<String[], Users> service3 = Users::new;
Users user3 = service3.apply(new String[]{"编程", "Game"});
sout user3;// 打印:Users{id=null, hobby=[编程, Game]}
BiFunction<Integer, String[], Users> service4 = Users::new;
Users user4 = service4.apply(1002, new String[]{"java", "cf"});
sout user4;// 打印:Users{id=1002, hobby=[java, cf]}
5.3 数组引用
格式: 类型[] :: new。
说明:
与构造器引用同理。不过,数组引用返回的是 数组。(目前我还不知如何使用数组引用创建非空数组)
示例:
Function<Integer, Integer[]> service1 = Integer[]::new;
Integer[] arr = service1.apply(5);
sout Arrays.toString(arr);// 打印:[null, null, null, null]
6、Lambda表达式的作用域
如下阐述转载至博文【Lambda表达式超详细总结】。
Lambda表达式可以看作是匿名内部类实例化的对象,Lambda表达式对变量的访问限制和匿名内部类一样。因此Lambda表达式可以访问局部变量、局部引用,静态变量,实例变量。
6.1 引用局部常量
规定在Lambda表达式中只能引用由final修饰的局部变量(即:
局部常量),包括局部基本类型常量和局部引用类型常量。
6.1.1 引用局部基本类型常量
double d1 = 10.2;-------------------------------------------A
// final double d1 = 10.2;----------------------------------------B
UnaryOperator<Double> service = d -> Math.floor(d + d1);----C
// d1 = 5.1;------------------------------------------------D
sout service.apply(5.9);// 打印:16.0
d1定义为变量(A),可当引用于Lambda表达式中(C)时,会隐式转为常量,但当为d1赋值(D)时,这种“隐式转换”功能会失效,d1仍为变量,则C会编译报错。若将d1显式定义为常量(B),则C可编译通过,但由于常量不可修改,D将会编译报错。
6.1.2 引用局部引用类型常量
示例1:
String subStr = "csdn";
Predicate<String> service = str -> str.contains(subStr);
sout service.test("csdn, bilibili, 博客园");// 打印:true
// subStr = "bili";
此示例与上文中【引用局部基本类型常量】的示例同理。
示例2:
List list = new ArrayList();
list.add(2023);
list.add("年");
list.add(5.12);
Supplier<Integer> service = () -> list.size();
sout service.get();// 打印:3
list.add(true);
sout service.get();// 打印:4
执行list.add(true)
是对list进行了修改,按照上面的结论,这个示例是编译报错的。可实际上编译通过。为什么?难道上面的结论有纰漏??
在后面加上这么一条代码试试:
list = new ArrayList();
这样就编译报错了。大家看出来了吧。。。
结论:
由Lambda表达式引用的局部常量不可修改,指的是不可修改引用指向。
6.2 引用成员变量、类变量
public class TestReference {
String originStr1 = "csdn,bilibili,博客园";
static String originStr2 = "csdn,bilibili,博客园";
public static void main(String[] args) {
Supplier<TestReference> service1 = TestReference::new;
TestReference t1 = service1.get();
Supplier<String[]> service2 = () -> t1.originStr1.split(",");
String[] arr1 = service2.get();-----------A
sout Arrays.toString(arr1);// 打印:[csdn, bilibili, 博客园]
t1.originStr1 = "";-----------------------B
Supplier<String[]> service3 = () -> originStr2.split(",");
String[] arr2 = service3.get();-----------C
sout Arrays.toString(arr2);// 打印:[csdn, bilibili, 博客园]
originStr2 = "";--------------------------D
}
}
B、D处分别修改成员变量originStr1
与类变量originStr2
,编译通过。可见,Lambda表达式不限制对成员变量和类变量的引用。
至于Lambda有没有如上文中【局部常量】般将变量隐式转为“常量”,暂未可知。不过,我觉得没有隐式转换,因为B、D处编译通过。
6.3 引用成员常量、类常量
以上述【引用成员变量、类变量】的示例为基础,在成员变量originStr1
和类变量originStr2
的定义前加上final
,即:
final String originStr1 = "csdn,bilibili,博客园";
final static String originStr2 = "csdn,bilibili,博客园";
则A、C处都编译通过,说明,Lambda表达式不限制对成员常量和类常量的引用;而B、D处都编译报错。这是常量本身的性质,与Lambda表达式无关。
6.4 Lambda表达式限制访问局部变量的原因
关于原因,那位前辈已经总结得很全面,我就不班门弄斧了,详述请查阅博文【Lambda表达式超详细总结】(转发)的8.3项。
7、最后
本文中的示例是为了阐述Lambda表达式、方便大家理解而简单举出的,不一定有实用性。示例很多,不过,我所举的示例都是“以简为宗旨”,重心不在于使用Lambda编写多么强大的功能,而在于尽量扩展对Lambda的使用,让大家能够更透彻地理解Lambda的格式、规范、限制等。
旁白:
这是我迄今为止写过的内容最多的一篇文章,近1.5万字(包括代码),我都有点佩服我自己(真的)。
当然,这个文章内容量与大神们动则几万、十几万的大作相比,不值一提(我不是谦虚,类如几万、十几万的大作,那完全是上了另一个层面的文章了,用“论文”形容更贴切)。不过,我还是挺有成就感的。
最后,望我的写作对大家有帮助!!
本文完结。