【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://developer.aliyun.com/article/1631535
出自【进步*于辰的博客】启发博文:《Lambda表达式超详细总结》(转发)。
这是我系统学习Lambda表达式时参考的文章。在下文中,我会引用这篇博文中的一些资源,如:图片、阐述。因为我认为那位前辈总结的很好,我再尽力做也最多如此了。如有侵权,请与我联系!
参考笔记一,P33.8、P43.5;笔记三,P33.1、P59.1。注:掌握Lambda表达式,这两个知识是基础:
- 匿名内部类,帮助在于理解“变式”,因为最初开发Lambda表达式的目的之一是简化匿名内部类;
- 泛型,用于Lambda表达式的“扩展”。
1、Lambda优化匿名内部类
1.1 内部类
Java中内部类分为成员内部类、静态内部类、局部内部类和匿名内部类。
class TestInnerClass {
// 成员内部类
class MemberInner {
}
// 静态内部类
static class StaticInner {
}
public static void main(String[] args) {
// 局部内部类
class LocalInner {
}
// 匿名内部类
Runnable run = new Runnable() {
public void run() {
}
};
new Thread(run).start();
}
}
与Lambda表达式相关的只有匿名内部类。
1.2 优化
示例:
Runnable runa = new Runnable() {
public void run() {
sout "Hello!! CSDN";
}
};
new Thread(runa).start();
等同于:
class MyRunnable implements Runnable {
public void run() {
sout "Hello!! CSDN";
}
}
class TestThread {
public static void main(String[] args) {
Runnable runa = new MyRunnable();
new Thread(runa).start();
}
}
匿名内部类的一大特点:一次性,故这种写法既冗余,又LOW。
使用Lambda进行优化:
Runnable runa = () -> {
sout "Hello!! CSDN";
};
new Thread(runa).start();
有什么变化?是不是省去了run()
的声明部分。
继续优化。
new Thread(() -> {
sout "Hello!! CSDN";
}).start();
这就是“将函数如参数般传递”。大家先初步了解,继续看。
1.3 扩展说明
上面第一个示例中的引用runa
,指向 Runnable 接口的匿名内部类(上转),而下文所述的() -> {}
和三种引用都可以说是对匿名内部类的简化,但其已不是匿名内部类,其类型是Lambda
(一种类型)。
我暂未找到相关资料,就自己验证。看下述示例。
Predicate<Integer> ser1 = new Predicate<Integer>() {
@Override
public boolean test(Integer a) {
return a.equals(0);
}
};
Class z1 = ser1.getClass();
sout z1;// class Test2$1
z1.getSimpleName();// ""
z1.getEnclosingClass();// class Test2
Supplier<Integer> ser2 = () -> {
return 0;
};
z1 = ser2.getClass();
sout z1;// class Test2$$Lambda$1/1791741888
z1.getSimpleName();// Test2$$Lambda$1/1791741888
z1.getEnclosingClass();// null
具体说明简单又啰嗦,就不赘述,大家只要了解getSimpleName()
和getEnclosingClass()
这两个方法就明白了。如果大家感兴趣,可以看看我对Class<T>
类的解析。
2、Lambda优化规范
2.1 铺垫
从:
Runnable runa = new Runnable() {
public void run() {
sout "Hello!! CSDN";
}
};
new Thread(runa).start();
优化为:
Runnable runa = () -> {
sout "Hello!! CSDN";
};
new Thread(runa).start();
难道可以随便写?当然不是。那规范是什么?
在上个例子中,大家有没有注意一个问题?有没有run()
的声明?没有。因此,Lambda只能用于优化只有一个抽象方法的接口的匿名内部类,这类接口称之为“函数式接口”(注解是@FunctionalInterface
)。
举个例:定义java.lang.Number
抽象类的匿名内部类。
这是Number抽象类的源码。
可见,有4个抽象方法。因此,创建 Number 类的匿名内部类,必须重写这4个抽象方法。即:
new Number() {
@Override
public int intValue() {
return 0;
}
@Override
public long longValue() {
return 0;
}
......
};
如这般,就无法使用 Lambda 进行优化。
2.2 规范
Lambda 基础语法:
() -> {
}
->
的左边是圆括号,对应匿名内部类重写的唯一抽象方法的参数列表;右边是花括号,对应方法体。
参数列表:
- 若无参,则必须是
()
,即:() -> {}
。 - 若只有1个参数
xx
,则可以是(xx)
或xx
,即:(xx) -> {}
或xx -> {}
。(注:xx
无类型,其名称任意) - 若有2个参数 a 和 b,则必须是
(a, b)
,即:(a, b) -> {}
。 - 举一反三。
方法体:
由于方法体没有方法的声明,故只需注意一些“省略规范”。
1、若只有一条语句,可省略分号和花括号。示例:
new Thread(() -> sout "Hello!! CSDN").start();
// 等同于:
new Thread(() -> {
// run() 无参
sout "Hello!! CSDN";
}).start();
2、若方法有返回值,且只有一条语句时,可省略分号、花括号和return
。示例:
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
}
}
2.3 补充说明
以上例为例:
两个问题:
- JVM如何知道图中红框部分是
getTrigonometric()
的方法体? - JVM是如何知道参数
xx
的类型是double
?(因为Math.sin()
的形参类型是 double)
下图是Math.sin()
的API截图:
解释: (以下阐述转载自博文《Lambda表达式超详细总结》)
因为JVM可以通过上下文推断出为何接口实现抽象方法,即“接口推断”推断出所实现的相应抽象方法的参数列表(包括形参类型),即“类型推断”
简言之,Lambda表达式依赖于上下文环境。3、Java内置函数式接口
3.1 四大核心函数式接口
(此表格引用自启发博文。)
|函数式接口|参数类型|返回值类型|说明|
|-|-|-|-|
|Consumer<T>
消费型接口|T|void|对类型为T
的对象应用操作,包含方法:void accept(T t)
|
|Supplier<T>
供给型接口|无|T|返回类型为T
的对象,包含方法:T get()
|
|Function|T|R|对类型为T
的对象应用操作,并返回类型为R
的对象,包含方法:R apply(T t)
|
|Predicate<T>
断定型接口|T|boolean
|断定类型为T
的对象是否满足某约束,并返回boolean
结果,包含方法:boolean test(T t)
|
使用示例:
1:消费型接口 Consumer<T>
,方法:void accept(T t)
。
Consumer<String> service1 = str -> sout str;
service1.accept("Hello!! CSDN");// Hello!! CSDN
2:供给型接口 Supplier<T>
,方法:T get()
。
Supplier<Integer> service2 = () -> (int)(Math.random() * 100);// 获取0~100的随机整数
service2.get();// 66
3:函数型接口 Function<T,R>
,方法:R apply(T t)
。
Function<String, Integer> service3 = str -> str.length();
service3.apply("I love China!!");// 14
4:断定型接口 Predicate<T>
,方法:boolean test(T t)
。
Integer i1 = 10;
Predicate<Integer> service4 = xx -> i1.equals(xx);
service4.test(10);// true
service4.test(20);// false
3.2 其他函数式接口
(此表格引用自启发博文。)
函数式接口 | 参数类型 | 返回值类型 | 说明
|:-:|:-:|:-:|:-:|
BiFunction|T, U|R|对类型为T
、U
的对象应用操作,返回类型为R
的对象,包含方法:R apply(T t, U u)
UnaryOperator<T>
(Function<T>
的子接口)|T|T|对类型为T
的参数进行一元运算,并返回类型为T
的结果,包含方法:T aaply(T t)
BinaryOperator<T>
(BiFunction的子接口)|T, T|T|对类型为T
的参数进行二元运算,并返回类型为T
的结果,包含方法:T apply(T t1, T t2)
BiConsumer|T, U|void|对类型为T
、U
的对象应用操作,包含方法:void accept(T t, U u)
ToIntFunction<T>
、ToLongFunction<T>
、ToDoubleFunction<T>
|T|int、long、double|分别计算 int、long、double 的函数IntFunction<R>
、LongFunction<R>
、DoubleFunction<R>
|int、long、double|R|参数分别为 int、long、double 的函数
使用示例:
1:函数型接口 BiFunction<T, U, R>
,方法:R apply(T t, U u)
。
BiFunction<Character[], Character, Integer> service5 = (charArr, c) -> Arrays.binarySearch(charArr, c);
service5.apply(new Character[]{
65, 66, 67}, 'B');// 1
关于binarySearch()
,解析可查阅Arrays类的第2.2项。
2:函数型接口 UnaryOperator<T>
,方法:T apply(T t)
。
UnaryOperator<String> service6 = str -> str.trim().split("!")[1].trim().toUpperCase();
service6.apply(" Hello KiTi! I am 小白 of csdn ");// I AM 小白 OF CSDN
3:函数型接口 BinaryOperator<T>
,方法:T apply(T t1, T t2)
。
BinaryOperator<List<Character>> service7 = (list1, list2) -> {
Collections.copy(list2, list1);
return list2;
};
service7.apply(Arrays.asList('C', 'h'), Arrays.asList('#', '#', 'i', 'n', 'a'));// [C, h, i, n, a]
关于copy()
,解析可查阅Collections类的第2.10项;关于asList()
,解析可查阅 Arrays 类的第2.1项。
4:消费型接口 BiConsumer<T,R>
,方法:void accept(T t, U u)
。
BiConsumer<char[], Character>service8 = (charArr, c) -> {
Arrays.fill(charArr, c);
Arrays.toString(charArr);// [#, #, #, #, #]
};
service8.accept(new char[]{
'进', '步', '*', '于', '辰'}, '#');
关于fill()
,解析可查阅 Arrays 类的第2.8项。
5:未知型接口 To[Int/Long?Double]Function<T>
,方法:int/long/double applyAs[Int/Long/Double]
。
// 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
6:函数型接口 [Int/Long/Double]Function<R>
,方法:R apply(int/long/double i)
。
// 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
3.3 其他函数式接口补充
函数式接口 | 参数类型 | 返回值类型 | 说明
|:-:|:-:|:-:|:-:|
断定型接口 BiPredicate<T, U> | T, U | boolean | 确定类型分别为T/U的对象是否满足某约束,并返回 boolean 值。包含方法:
boolean test(T t, U u)`
使用示例:
1、断定型接口 BiPredicate<T,U>
,方法:boolean test(T t, U u)
。
BiPredicate<String, String > service1 = (t, u) -> t.equals(u);
service1.test("abc", "abc");// true
service1.test("abc", "123");// false
3.4 示例说明
大家在看上面的示例时,肯定想吐槽:“你写的那些方法体,很多都是多此一举。”
以实用性的角度来说的确是,例如:sout str
、str.length
、xx > 0? true: false
,直接调用相应方法不香么?还用Lambda转个弯实现。
那我为何还这样写?
因为我觉得使用Lambda的核心思想在于“灵活、扩展、通用”。因此,我写那些示例的初衷是“任意举例、简单易懂”,目的不在于实现何种功能,而是展示Lambda的使用。
4、Lambda三大引用
先行说明:
以下对于引用的示例,我会尽量用上文中【Java内置函数式接口】及其所举示例进行$“演变”$举例,从而降低大家阅读代码的成本。
4.1 方法引用
4.1.1 介绍
先说结论:
方法引用中所使用的“缺省参数列表”抽象方法的参数列表相同,返回值类型也必须相同。
何为“缺省参数列表”?
“缺省参数列表”指方法引用中已指定参数与所引用方法的参数列表相比较的缺失参数部分。
这是我自定义的概念,看着有点绕口,我会在示例中举例说明。
格式:
1、对象 :: 成员方法名
2. 类 :: 类方法名
3. 类 :: 成员方法名
4.1.2 说明示例
1:boolean equals(String str)
为 String 类的成员方法boolean equals(String str)
定义方法引用,以此例为基础进行举例。
BiPredicate<String, String > service1 = (t, u) -> t.equals(u);
演变办法一:
// 格式:类 :: 成员方法名
BiPredicate<String, String> service = String::equals;
service.test("csdn", "bilibili");// false
service.test("csdn", "csdn");// true
为什么选择 BiPredicate 接口实现方法引用?
equals()
的返回值类型为 boolean,则此抽象方法的返回值类型也必须是 boolean;equals()
是成员方法,一共需要2个变量,方法引用为String::equals
,由于未指定任何变量,故缺省2个变量。而这两个变量必须由抽象方法的参数列表提供,故参数列表必须且仅有两个参数,且类型要与方法引用的类型相同;equals()
所需的两个变量,类型都是 String,则抽象方法的两个参数的类型只能是 String。故<T>
和<U>
的类型实参为 String。
演变办法二:
// 格式:对象 :: 成员方法名
String str = "csdn";
Predicate<String> service = str::equals;
service.test("bilibili");// false
service.test("csdn");// true
为什么选择 Predicate接口实现方法引用?
equals()
的返回值类型为 boolean,则此抽象方法的返回值类型也必须是 boolean;equals()
是成员方法,一共需要2个变量,方法引用为str::equals
,由于已指定一个变量,故缺省1个变量。而这两个变量必须由抽象方法的参数列表提供,故参数列表必须且仅有一个参数,且类型要与方法引用的类型相同;equals()
所需的两个变量,类型都是 String,则抽象方法的参数的类型只能是 String。故<T>
的类型实参为 String。
2:void fill()
为 Arrays 类的静态方法void fill()
定义方法引用,以此例为基础进行举例。
BiConsumer<char[], Character>service8 = (charArr, c) -> {
Arrays.fill(charArr, c);
Arrays.toString(charArr);// [#, #, #, #, #]
};
service8.accept(new char[]{
'进', '步', '*', '于', '辰'}, '#');
演变:
// 格式:类 :: 静态方法名
BiConsumer<char[], Character> service8 = Arrays::fill;
char[] charArr = new char[]{
'进', '步', '*', '于', '辰'};
service8.accept(charArr, '#');
Arrays.toString(charArr);// [#, #, #, #, #]
为什么选择 BiConsumer接口实现方法引用?
fill()
无返回值,accept()
也无返回值,故匹配;fill()
是类方法,需要两个变量,方法引用是Arrays::fill
,由于未指定任何变量,故缺省2个变量。而这两个变量必须由抽象方法的参数列表提供,故参数列表必须且仅有两个参数,且类型要与方法引用的类型相同;fill()
的第1个参数类型为基本数据类型数组,为char[]
;第2个参数类型为基本数据类型,为char
。这2个参数都由accept()
提供,故<T>
的类型实参$为char[]
,<R>
的类型实参为 Character,
补充说明:泛型的类型实参只能是类,为何这里可以是char[]
(数组)?具体原因在博文《[Java]泛型》的第7项中有所说明。
4.2 构造器引用
4.2.1 介绍
格式:
类 :: new
顾名思义,“构造器引用”的作用就是实例化,即返回实例。
例如:为实体类Users
创建构造器引用,则构造器引用固定为Users :: new
,即返回一个 Users 实例。
约束:方法的参数列表决定了匹配哪个构造方法,即==构造器引用等同于构造方法==。
4.2.2 示例
实体类。
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) +
'}';
}
}
测试。
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]}
4.3 数组引用
格式:
类型[] :: new
与构造器引用同理。不过,数组引用返回的是数组。(我暂不知如何使用数组引用创建非空数组)
示例:
Function<Integer, Integer[]> service1 = Integer[]::new;
Integer[] arr = service1.apply(5);
Arrays.toString(arr);// [null, null, null, null]
5、Lambda作用域
以下阐述转载自博文《Lambda表达式超详细总结》。
Lambda表达式可以看作是匿名内部类实例化的对象,Lambda表达式对变量的访问限制和匿名内部类一样。因此Lambda表达式可以访问局部变量、局部引用,静态变量和成员变量。
5.1 引用局部常量
规定在Lambda表达式中只能引用由final
修饰的局部变量,即局部常量,包括局部基本类型常量和局部引用类型常量。
5.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
service.apply(5.9);// 16.0
d1
定义为变量(A),可当引用于 Lambda 中时(C),会隐式转为常量,但当为d1
赋值时(D),这种“隐式转换”功能会失效,d1
仍为变量,则C会编译报错。
若将d1
显式定义为常量(B),则C可编译通过,但由于常量不可修改,D将会编译报错。
5.1.2 引用局部引用类型常量
示例1:
String subStr = "csdn";
Predicate<String> service = str -> str.contains(subStr);
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();
service.get();// 3
list.add(true);
service.get();// 4
执行list.add(true)
是对list
进行了修改,按照上面的结论,这个示例是编译报错的。可实际上编译通过。为什么?难道上面的结论有纰漏??
在后面加上这么一条代码试试:
list = new ArrayList();
这样就编译报错了。大家看出来了吧。
结论:
由 Lambda 引用的局部常量不可修改,指的是不可修改引用指向。
5.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
Arrays.toString(arr1);// [csdn, bilibili, 博客园]
t1.originStr1 = "";-----------------------B
Supplier<String[]> service3 = () -> originStr2.split(",");
String[] arr2 = service3.get();-----------C
Arrays.toString(arr2);// [csdn, bilibili, 博客园]
originStr2 = "";--------------------------D
}
}
B、D处分别修改成员变量originStr1
与类变量originStr2
,都编译通过。可见,==Lambda 不限制对成员变量和类变量的引用==。
PS:至于Lambda有没有如上文中【局部常量】般将成员变量或类变量隐式转为常量,暂未可知。不过,我觉得没有隐式转换,因为B、D处编译通过。
5.3 引用成员常量、类常量
以上述【引用成员变量、类变量】的示例为基础,在成员变量originStr1
和类变量originStr2
的定义前加上final
,即:
final String originStr1 = "csdn,bilibili,博客园";
final static String originStr2 = "csdn,bilibili,博客园";
则A、C处都编译通过,说明,==Lambda 不限制对成员常量和类常量的引用==;而B、D处都编译报错。这是常量本身的性质,与 Lambda 无关。
5.4 限制访问局部变量的原因
具体原因,那位前辈已经总结得很全面,我就不班门弄斧了,详述可查阅博文《Lambda表达式超详细总结》(转发)的第8.3项。
总结:限制Lambda访问局部变量的根本原因是为了规避线程安全问题。
最后
本文中的示例是为了方便大家理解、以及阐述Lambda表达式的运用而简单举出的,不一定有实用性,仅是抛砖引玉。示例很多,不过,我所举的示例都是“以简为宗旨”,重心不在于使用Lambda表达式编写多么强大的功能,而在于尽量扩展对Lambda表达式的使用,让大家能够更透彻地理解它的格式、规范和限制等。
本文完结。