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(); } }
匿名内部类一般有一个特点:一次性 \color{red}{一次性}一次性,故这种写法既冗余,又L O W LOWLOW。
使用 L a m b d a 进行优化: \color{red}{使用Lambda进行优化:}使用Lambda进行优化:
Runnable runa = () -> { sout "Hello!! CSDN"; }; new Thread(runa).start();
有什么变化?是不是省去了run()
的声明部分。
继续优化。
new Thread(() -> { sout "Hello!! CSDN"; }).start();
这就是“将函数如参数般传递” \color{red}{“将函数如参数般传递”}“将函数如参数般传递”。大家先初步了解,继续看。。。
题外话: 题外话:题外话:
此项的第一个示例中的引用runa
,指向 Runnable 接口的匿名内部类(上转),而下文所述的() -> {}
和三种引用都可以说是对匿名内部类的简化,但其已不是匿名内部类,其类型是Lambda
(一种类型)。
我暂未找到相关资料,就自己验证。看下述示例。
Predicate<Integer> ser1 = new Predicate<Integer>() { @Override public boolean test(Integer a) { return a.equals(0); } }; Class z1 = ser1.getClass(); System.out.println(z1);// class Test2$1 System.out.println(z1.getSimpleName());// "" System.out.println(z1.getEnclosingClass());// class Test2 Supplier<Integer> ser2 = () -> { return 0; }; z1 = ser2.getClass(); System.out.println(z1);// class Test2$$Lambda$1/1791741888 System.out.println(z1.getSimpleName());// Test2$$Lambda$1/1791741888 System.out.println(z1.getEnclosingClass());// null
具体说明简单又啰嗦,就不赘述,大家只要了解getSimpleName()
和getEnclosingClass()
这两个方法就明白了。如果大家感兴趣,可以看看我对Class类的解析。
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();
难道可以随便写?当然不是。那规范是什么?
在上个例子中,大家有没有注意一个问题? \color{grey}{在上个例子中,大家有没有注意一个问题?}在上个例子中,大家有没有注意一个问题?
在示例中,有没有run()
的声明?没有。因此,Lambda只能优化只有一个抽象方法的接口的匿名内部类,这类接口称之为函数式接口 \color{green}{函数式接口}函数式接口(注解是@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 的基础语法:
() -> {}
。
->
的左边是圆括号,对应匿名内部类重写的唯一抽象方法的参数列表;右边是花括号,对应方法体。
参数列表: \color{red}{参数列表:}参数列表:
- 若无参,则必须是
()
,即:() -> {}
; - 若只有1个参数
xx
,则可以是(xx)
或xx
,即:(xx) -> {}
或xx -> {}
;(注:xx
无类型,其名称任意) - 若有2个参数 a 和 b,则必须是
(a, b)
,即:(a, b) -> {}
; - 举一反三。
方法体: \color{red}{方法体:}方法体:
由于方法体不用关注方法的声明,故只注意一些省略规范 省略规范省略规范。
1、 若只有一条语句,可省略分号和花括号。
示例:
new Thread(() -> sout "Hello!! CSDN").start(); // 等同于: new Thread(() -> { sout "Hello!! CSDN"; }).start();
run()
无参。
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 补充说明
以上例为例:
两个问题: \color{grey}{两个问题:}两个问题:
- JVM如何知道图中红框部分是
getTrigonometric()
的方法体? - JVM是如何知道参数
xx
的类型是double
?(因为Math.sin()
的形参类型是 double)
下图是Math.sin()
的API截图:
解释: \color{red}{解释:}解释: (以下阐述转载自博文《Lambda表达式超详细总结》)
因为JVM可以通过上下文推断出为何接口实现抽象方法,即接口推断 \color{purple}{接口推断}接口推断;以及推断出所实现的相应抽象方法的参数列表(包括形参类型),即类型推断 \color{brown}{类型推断}类型推断。
简言之,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 | 对类型为T 的对象应用操作,并返回类型为R 的对象,包含方法:R apply(T t) |
Predicate<T> 断定型接口 |
T | boolean |
断定类型为T 的对象是否满足某约束,并返回boolean 结果,包含方法:boolean test(T t) |
使用示例:
1、消费型接口 Consumer
:
Consumer<String> service1 = str -> sout str; service1.accept("Hello!! CSDN");// 打印:Hello!! CSDN
方法:void
accept(T t)
。
2、供给型接口 Supplier
:
Supplier<Integer> service2 = () -> (int)(Math.random() * 100);// 获取0~100的随机整数 sout service2.get();// 打印:66
方法:T get()
。
3、函数型接口 Function
:
Function<String, Integer> service3 = str -> str.length(); sout service3.apply("I love China!!");// 打印:14
方法:R apply(T t)
。
4、断定型接口 Predicate
:
Integer i1 = 10; Predicate<Integer> service4 = xx -> i1.equals(xx); sout service4.test(10);// 打印:true sout service4.test(20);// 打印:false
方法:boolean test(T t)
。
3.2 其他函数式接口
(此表格引用自启发博文。)
函数式接口 | 参数类型 | 返回值类型 | 说明 |
BiFunction<T, U, R> | 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, U, R>的子接口) |
T, T | T | 对类型为T 的参数进行二元运算,并返回类型为T 的结果,包含方法:T apply(T t1, T t2) |
BiConsumer<T, U> | 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
:
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)
。
关于binarySearch()
,详情可见Arrays类的第2.2项。
2、函数型接口 UnaryOperator
:
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
:
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)
。
关于copy()
,详情可见Collections类的第2.10项;关于asList()
,详情可见 Arrays 类的第2.1项。
4、消费型接口 BiConsumer
:
BiConsumer<char[], Character>service8 = (charArr, c) -> { Arrays.fill(charArr, c); sout Arrays.toString(charArr);// 打印:[#, #, #, #, #] }; service8.accept(new char[]{'进', '步', '*', '于', '辰'}, '#');
方法:void accept(T t, U u)
。
关于fill()
,详情可见 Arrays 类的第2.8项。
5、未知型接口 To[Int/Long?Double]Function
:
// 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
:
// 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)
。
3.3 其他函数式接口补充
函数式接口 | 参数类型 | 返回值类型 | 说明 |
断定型接口 `BiPredicate<T, U> | T, U | boolean | 确定类型分别为T/U的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t, U u) |
使用示例:
1、断定型接口 `BiPredicate:
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)
。
3.4 示例补充说明
大家在看上面的示例时,肯定想吐槽:“你写的那些方法体,很多都是多此一举。”
以实用性的角度来说的确是,例如:sout str
、str.length
、xx > 0? true: false
,直接调用相应方法不香么?还用 Lambda 转个弯实现。
那我为何还这样写? \color{grey}{那我为何还这样写?}那我为何还这样写?
因为我觉得使用 Lambda 的核心思维在于灵活、扩展、通用 \color{red}{灵活、扩展、通用}灵活、扩展、通用。因此,我写那些示例的初衷是“任意举例、简单易懂” \color{green}{“任意举例、简单易懂”}“任意举例、简单易懂”,目的不在于实现何种功能,而是阐述 Lambda 的使用。
4、Lambda表达式的三种引用
先言: \color{red}{先言:}先言:
以下对于引用的示例,我会尽量用上文中Java内置函数式接口及其所举示例进行“演变” “演变”“演变”举例,从而降低大家阅读代码的成本。
4.1 方法引用
4.1.1 概述
先说结论: \color{red}{先说结论:}先说结论:
方法引用中所使用的“缺省参数列表” \color{red}{“缺省参数列表”}“缺省参数列表”必须与抽象方法的参数列表相同,返回值类型也必须相同。
何为“缺省参数列表”? \color{grey}{何为“缺省参数列表”?}何为“缺省参数列表”?
“缺省参数列表”指方法引用中已指定参数与所引用方法的参数列表相比较的缺失参数部分。
这是我自定义的概念,看着有点绕口,我会在示例中举例说明。
方法引用格式: \color{green}{方法引用格式:}方法引用格式:
对象 :: 成员方法名
;类 :: 类方法名
;类 :: 成员方法名
。
4.1.2 说明示例
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; sout service.test("csdn", "bilibili");// 打印:false sout service.test("csdn", "csdn");// 打印:true
为什么选择 B i P r e d i c a t e 接口实现方法引用? \color{red}{为什么选择 BiPredicate 接口实现方法引用?}为什么选择BiPredicate接口实现方法引用?
equals()
的返回值类型为 boolean,则此抽象方法的返回值类型也必须是 boolean;equals()
是成员方法,一共需要2个变量,方法引用为String::equals
,由于未指定任何变量,故缺省2个变量。而这两个变量必须由抽象方法的参数列表提供,故参数列表必须且仅有两个参数,且类型要与方法引用的类型相同;equals()
所需的两个变量,类型都是 String,则抽象方法的两个参数的类型只能是 String。故和的类型实参 \color{green}{类型实参}类型实参为 String。
演变办法二:
// 格式:对象 :: 成员方法名 String str = "csdn"; Predicate<String> service = str::equals; sout service.test("bilibili");// 打印:false sout service.test("csdn");// 打印:true
为什么选择 P r e d i c a t e 接口实现方法引用? \color{red}{为什么选择 Predicate接口实现方法引用?}为什么选择Predicate接口实现方法引用?
equals()
的返回值类型为 boolean,则此抽象方法的返回值类型也必须是 boolean;equals()
是成员方法,一共需要2个变量,方法引用为str::equals
,由于已指定一个变量,故缺省1个变量。而这两个变量必须由抽象方法的参数列表提供,故参数列表必须且仅有一个参数,且类型要与方法引用的类型相同;equals()
所需的两个变量,类型都是 String,则抽象方法的参数的类型只能是 String。故的类型实参 \color{green}{类型实参}类型实参为 String。
4.1.2.2 void fill()
为 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);// 打印:[#, #, #, #, #]
为什么选择 B i C o n s u m e r 接口实现方法引用? \color{red}{为什么选择 BiConsumer接口实现方法引用?}为什么选择BiConsumer接口实现方法引用?
fill()
无返回值,accept()
也无返回值,故匹配;fill()
是类方法,需要两个变量,方法引用是Arrays::fill
,由于未指定任何变量,故缺省2个变量。而这两个变量必须由抽象方法的参数列表提供,故参数列表必须且仅有两个参数,且类型要与方法引用的类型相同;fill()
的第1个参数类型为基本数据类型数组,为char[]
;第2个参数类型为基本数据类型,为char
。这2个参数都由accept()
提供,故的类型实参 \color{green}{类型实参}类型实参为
char[]
,的类型实参 \color{blue}{类型实参}类型实参为 Character,
留言: \color{purple}{留言:}留言:
泛型的类型实参只能是类,为何这里可以是char[]
(数组),欢迎各位博友在评论区讨论!!
4.2 构造器引用
4.2.1 概述
格式: \color{green}{格式:}格式:类 :: new
。
说明: \color{purple}{说明:}说明:
顾名思义,“构造器引用”的作用就是实例化,即返回实例。
例如:为实体类Users
创建构造器引用,则构造器引用固定为Users :: new
,即返回一个 Users 实例。
约束: \color{brown}{约束:}约束:
抽象方法的参数列表决定了匹配哪个构造方法,即构造器引用等同于构造方法。
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 数组引用
格式: \color{orange}{格式:}格式:类型[] :: new
。
说明: \color{dark}{说明:}说明:
与构造器引用同理。不过,数组引用返回的是数组。(我暂不知如何使用数组引用创建非空数组)
示例:
Function<Integer, Integer[]> service1 = Integer[]::new; Integer[] arr = service1.apply(5); sout 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 sout 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); 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();
这样就编译报错了。大家看出来了吧。。。
结论: \color{red}{结论:}结论:
由 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 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 不限制对成员变量和类变量的引用。
留言: \color{brown}{留言:}留言:
至于 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项。
6、最后
本文中的示例是为了方便大家理解、以及阐述 Lambda 表达式的运用而简单举出的,不一定有实用性。示例很多,不过,我所举的示例都是“以简为宗旨” \color{green}{“以简为宗旨”}“以简为宗旨”,重心不在于使用 Lambda 表达式编写多么强大的功能,而在于尽量扩展对 Lambda 表达式的使用,让大家能够更透彻地理解它的格式、规范、限制等。
P S : \color{blue}{PS:}PS:
这是我迄今为止写过的内容最多的一篇文章,超1.5万字,我都有点佩服我自己。
当然,这个内容量与大神们动则几万、十几万的大作相比,不值一提(我不是谦虚,类如几万、十几万的大作,那完全是上了另一个层面的文章了,用“论文”形容更贴切)。不过,我还是挺有成就感的且受益匪浅!
本文完结。