JDK8 特性(一)
# 1 Lambda表达式
# 1.1 Lambda标准格式
- Lambda省去面向对象的条条框框, Lambda的标准格式格式由3个部分组成
- 格式说明:
(...args)
: 参数列表->
: 分隔或连接参数与方法体的标识符{...;}
: 方法体, 主要的代码逻辑
- eg:
class Demo{
public static void main(String[] args) {
new Thread(() -> {
System.out.println("线程启动");
}).start();
}
}
2
3
4
5
6
7
# 1.2 Lambda初体验
# 无参无返回值
public class Demo1 {
public static void main(String[] args) {
fishDo(() -> System.out.println("小鱼在欢乐的游着..."));
}
private static void fishDo(Fish fish){
fish.swim();
}
}
/**
* 定义一个接口, 并且该接口只有一个需要实现的方法
**/
interface Fish{
void swim();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 有参有返回值
public class Demo1 {
public static void main(String[] args) {
catEat(foodName -> {
System.out.println("小猫在吃" + foodName);
return 3;
});
}
private static void catEat(Cat cat){
System.out.println("小猫吃了" + cat.eat("🐟") + "分钟");
}
}
/**
* 定义一个接口, 并且该接口只有一个需要实现的方法
**/
interface Cat{
int eat(String foodName);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 小结
以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写
# 1.3 Lambda实现原理
现有以下类
接口类
public interface Swimable { void swimming(); }
1
2
3main入口
public class SwimImplDemo { public static void main(String[] args) { goSwimming(new Swimable() { @Override public void swimming() { System.out.println("去匿名内部类游泳了"); } }); } private static void goSwimming(Swimable swim){ swim.swimming(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14将以上的main方法进行执行后, 会编译生成以下字节码文件
将内部类的字节码文件通过 [XJad] 进行反编译
- 匿名内部类在编译后会形成一个新的类.$
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://kpdus.tripod.com/jad.html // Decompiler options: packimports(3) fieldsfirst ansi space // Source File Name: SwimImplDemo.java package com.zhuhjay.lambda; import java.io.PrintStream; // Referenced classes of package com.zhuhjay.lambda: // Swimable, SwimImplDemo static class SwimImplDemo$1 implements Swimable{ public void swimming(){ System.out.println("去匿名内部类游泳了"); } SwimImplDemo$1(){} }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20main入口改为使用Lambda表达式
public class SwimImplDemo { public static void main(String[] args) { goSwimming(() -> { System.out.println("去Lambda游泳了"); }); } private static void goSwimming(Swimable swim){ swim.swimming(); } }
1
2
3
4
5
6
7
8
9
10
11将以上的main方法进行执行后, 不会生成多余的字节码文件
- 使用 [XJad] 反编译工具会失败
- 使用JDK工具来对Lambda表达式的字节码进行反汇编
javap -c -p 文件名.class -c:表示对代码进行反汇编 -p:显示所有类和成员
1
2对Lambda表达式的字节码文件进行反汇编
public class com.zhuhjay.lambda.SwimImplDemo { public com.zhuhjay.lambda.SwimImplDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:()Lcom/zhuhjay/lambda/Swimable; 5: invokestatic #3 // Method goSwimming:(Lcom/zhuhjay/lambda/Swimable;)V 8: return private static void goSwimming(com.zhuhjay.lambda.Swimable); Code: 0: aload_0 1: invokeinterface #4, 1 // InterfaceMethod com/zhuhjay/lambda/Swimable.swimming:()V 6: return private static void lambda$main$0(); Code: 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String 去Lambda游泳了 5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26通过与源代码的比较后, 发现多了静态方法
lambda$main$0
得出: Lambda表达式会在类中新生成一个私有的静态方法, 命名为
lambda$方法名$序列
通过断点调试Lambda也可以从调用栈中发现该静态方法的生成(当然不会显式的在源码中出现)
验证在Lambda表达式运行中会生成一个内部类
命令格式:
java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
执行(需要退出包运行该命令):
java -Djdk.internal.lambda.dumpProxyClasses com.zhuhjay.lambda.SwimImplDemo
- 查看反编译结果
final class SwimImplDemo$$Lambda$1 implements Swimable { public void swimming() { SwimImplDemo.lambda$main$0(); } private SwimImplDemo$$Lambda$1() {} }
1
2
3
4
5
6
7
8
9
对以上结论来推断Lambda生成的字节码文件
public class SwimImplDemo {
public static void main(String[] args) {
// 也是相当于Lambda生成了一个匿名内部类, 来调用Lambda生成的静态方法
goSwimming(new Swimable() {
public void swimming(){
SwimImplDemo.lambda$main$0();
}
});
}
/** Lambda生成的私有静态方法 **/
private static void lambda$main$0(){
System.out.println("去Lambda游泳了");
}
private static void goSwimming(Swimable swim){
swim.swimming();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 小结
- 匿名内部类在编译的时候会一个class文件
- Lambda在程序运行的时候形成一个类
- 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
- 还会形成一个匿名内部类, 实现接口, 重写抽象方法
- 在接口的重写方法中会调用新生成的方法.
# 1.4 Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略
- 如果小括号内有且仅有一个参数,则小括号可以省略
- 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
# 1.5 Lambda前提条件
Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
- 方法的参数或局部变量类型必须为接口才能使用Lambda
- 接口中有且仅有一个抽象方法(@FunctionalInterface用来检测接口是否为函数式接口)
# 1.6 Lambda和匿名内部类对比
- 所需的类型不一样
- 匿名内部类,需要的类型可以是类,抽象类,接口
- Lambda表达式,需要的类型必须是接口
- 抽象方法的数量不一样
- 匿名内部类所需的接口中抽象方法的数量随意
- Lambda表达式所需的接口只能有一个抽象方法
- 实现原理不同
- 匿名内部类是在编译后会形成class
- Lambda表达式是在程序运行的时候动态生成class
# 2 JDK8接口增强
# 2.1 增强介绍
JDK8以前的接口:
interface 接口名 { 静态常量; 抽象方法; }
1
2
3
4JDK8的接口:
interface 接口名 { 静态常量; 抽象方法; 默认方法; 静态方法; }
1
2
3
4
5
6
# 2.2 接口默认方法和静态方法的区别
- 默认方法通过实例调用,静态方法通过接口名调用。
- 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
- 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。
# 3 常用内置函数式接口
# 3.1 内置函数式接口的由来
我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。
# 3.2 常用函数式接口的介绍
它们主要在java.util.function
包中。下面是最常用的几个接口。
# Supplier:生产者
java.util.function.Supplier<T>
接口,它意味着"供给", 对应的Lambda表达式需要"对外提供"一个符合泛型类型的对象数据。
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
2
3
4
# Consumer:消费者
java.util.function.Consumer<T>
接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
/** 该方法使得两个Consumer先后调用 c1.andThen(c2).accept(t) **/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
2
3
4
5
6
7
8
9
- 默认方法: andThen
- 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen
- 要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是"一步接一步"操作
# Function:类型转换
java.util.function.Function<T,R>
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
}
2
3
4
5
6
7
8
9
- 默认方法: andThen
- Function 接口中有一个默认的 andThen 方法,用来进行组合操作
- 该方法同样用于"先做什么,再做什么"的场景,和 Consumer 中的 andThen 差不多
# Predicate:判断
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T>
接口。
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
/** && **/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/** ! **/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/** || **/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
默认方法: and
- 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用"与"逻辑连接起来实现"并且"的效果时,可以使用default方法 and
默认方法: or
- 与 and 的"与"类似,默认方法 or 实现逻辑关系中的"或"
默认方法: negate
- "与"、"或"已经了解了,剩下的"非"(取反)也会简单。它是执行了test方法之后,对结果boolean值进行"!"取反而已。一定要在 test 方法调用之前调用 negate 方法
# 4 方法引用
方法引用的注意事项
- 被引用的方法,参数要和接口中抽象方法的参数一样
- 当接口抽象方法有返回值时,被引用的方法也必须有返回值
# 4.1 方法引用简化Lambda
使用Lambda表达式求一个数组的和
public class Demo2 {
public static void main(String[] args) {
// 将数组进行求和
printSum((arr) -> {
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("sum = " + sum);
});
// 使用已有的方法进行方法引用(让已实现的方法复用)
// 类名::静态方法
printSum(Demo2::getSum);
}
/** 已有的求和方法 **/
private static void getSum(int[] arr){
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("sum = " + sum);
}
/** 使用消费者函数式接口 **/
private static void printSum(Consumer<int[]> consumer){
int[] arr = new int[]{11, 22, 33, 44};
consumer.accept(arr);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 4.2 方法引用格式
- 符号表示:
::
- 符号说明: 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
- 应用场景: 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用
# 4.3 常见引用方式
方法引用在JDK 8中使用方式相当灵活,有以下几种形式:
instanceName::methodName
对象::方法名ClassName::staticMethodName
类名::静态方法ClassName::methodName
类名::普通方法ClassName::new
类名::new 调用的构造器TypeName[]::new
String[]::new 调用数组的构造器
# 对象::成员方法
这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
public class MethodRefDemo {
public static void main(String[] args) {
Date now = new Date();
// 使用Lambda表达式获取当前时间
Supplier<Long> su1 = () -> now.getTime();
System.out.println(su1.get());
// 使用方法引用获取当前时间
Supplier<Long> su2 = now::getTime;
System.out.println(su2.get());
}
}
2
3
4
5
6
7
8
9
10
11
# 类名::静态方法
由于在java.lang.System
类中已经存在了静态方法 currentTimeMillis,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用, 写法是:
public class MethodRefDemo {
public static void main(String[] args) {
Supplier<Long> su3 = () -> System.currentTimeMillis();
// 等同于, 调用该类的静态方法
Supplier<Long> su4 = System::currentTimeMillis;
}
}
2
3
4
5
6
7
# 类名::引用实例方法
Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
public class MethodRefDemo {
public static void main(String[] args) {
Function<String, Integer> f1 = (str) -> str.length();
// 等同于, 将参数作为调用者去调用方法, 然后接收对应数据类型的返回值
Function<String, Integer> f2 = String::length;
BiFunction<String, Integer, String> f3 = String::substring;
// 等同于, 将第一个参数作为调用者, 第二个参数作为参数, 然后接收对应数据类型的返回值
BiFunction<String, Integer, String> f4 = (str, index) -> str.substring(index);
}
}
2
3
4
5
6
7
8
9
10
11
# 类名::new
由于构造器的名称与类名完全一样。所以构造器引用使用类名称::new
的格式表示
public class MethodRefDemo {
public static void main(String[] args) {
// 使用无参构造器实例一个String类
Supplier<String> s1 = String::new;
// 等同于
Supplier<String> s2 = () -> new String();
s1.get();
// 把具体地调用体现在了接口上
// 使用一个参数的构造器实例一个String类
Function<String, String> s3 = String::new;
// 等同于
Function<String, String> s4 = (str) -> new String(str);
s3.apply("张三");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 数组::new
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同
public class MethodRefDemo {
public static void main(String[] args) {
Function<Integer, int[]> f = length -> new int[length];
// 等同于
Function<Integer, int[]> ff = int[]::new;
}
}
2
3
4
5
6
7
# 小结
方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式, 不过要注意的是方法引用只能"引用"已经存在的方法!