📚 Lambda 出现之前
📚 Anonymous 匿名类
线程去完成任务时,会通过 Runnable 接口来定义任务内容,并使用 Thread 类来启动该线程。 创建 Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将其交给一个线程来启动!传统写法,代码如下:
public class Demo01LambdaIntro {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("新线程任务执行!");
}
}).start();
}
}
复制代码
📚 分析
对于 Runnable 的匿名内部类用法,可以分析出几点内容:
Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心;
为了指定 run 的方法体,不得不需要 Runnable 接口的实现类;
为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类;
必须覆盖重写抽象 run 方法,方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
而实际上,似乎只有方法体才是关键所在;
📚 Lambda 表达式
借助 Java 8 的全新语法,上述 Runnable 接口的匿名内部类写法可以通过更简单的 Lambda 表达式达到相同的效果 Lambda 表达式写法,代码如下:
public class Demo01LambdaIntro {
public static void main(String[] args) {
new Thread(() -> System.out.println("新线程任务执行!")).start(); // 启动线程
}
}
复制代码
📚 优点
简化匿名内部类的使用,语法更加简单;
📚 Lambda 标准格式
Lambda 的标准格式格式由 3 个部分组成:
(参数类型 参数名称) -> {
代码体;
}
📚 格式说明
(参数类型 参数名称):参数列表;
{代码体;}:方法体;
-> :箭头,分隔参数列表和方法体;
📚 无参数无返回值 Lambda
public class Test {
public static void main(String[] args) throws Exception {
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("匿名内部类游泳");
}
});
goSwimming(()->{ System.out.println("Lambda表达式游泳"); });
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
interface Swimmable {
public abstract void swimming();
}
//控制台打印
匿名内部类游泳
Lambda表达式游泳
复制代码
📚 有参数有返回值 Lambda
public class Test {
public static void main(String[] args) throws Exception {
goSwimming(new Swimmable() {
@Override
public String swimming(String str) {
return str.toUpperCase();
}
});
goSwimming((str)->{ return str.toLowerCase(); });
}
public static void goSwimming(Swimmable swimmable) {
String str = "Hello World";
str = swimmable.swimming(str);
System.out.println(str);
}
}
interface Swimmable {
public abstract String swimming(String str);
}
//控制台打印
HELLO WORLD
hello world
复制代码
以后我们调用方法时,看到参数是接口并且该接口只有一个抽象方法就可以考虑使用 Lambda 表达式,Lambda 表达式相当于是对接口中抽象方法的重写;
📚 Lambda 实现原理
还是看前面 Test 类的例子,我们可以看到匿名内部类会在编译后产生一个类:Test$1.class
反编译 Test$1.class 代码为
final class Test$1 implements Swimmable{
public String swimming(String str){
return str.toUpperCase();
}
}
复制代码
我们再来看看 Lambda 的效果,修改 Test.java 去除匿名内部类的调用:
public class Test {
public static void main(String[] args) throws Exception {
goSwimming((str)->{ return str.toLowerCase(); });
}
public static void goSwimming(Swimmable swimmable) {
String str = "Hello World";
str = swimmable.swimming(str);
System.out.println(str);
}
}
interface Swimmable {
public abstract String swimming(String str);
}
复制代码
运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说 Lambda 并没有在编译的时候产生一个新的类,进行反编译报错。
我们使用 JDK 自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令:
javap -c -p 文件名.class
-c:表示对代码进行反汇编
-p:显示所有类和成员
复制代码
PS F:\CloneTest\out\production\CloneTest> javap -c -p Test.class
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:()LSwimmable;
5: invokestatic #3 // Method goSwimming:(LSwimmable;)V
8: return
public static void goSwimming(Swimmable);
Code:
0: ldc #4 // String Hello World
2: astore_1
3: aload_0
4: aload_1
5: invokeinterface #5, 2 // InterfaceMethod Swimmable.swimming:(Ljava/lang/String;)Ljava/lang/String;
10: astore_1
11: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
14: aload_1
15: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: return
private static java.lang.String lambda$main$0(java.lang.String);
Code:
0: aload_0
1: invokevirtual #8 // Method java/lang/String.toLowerCase:()Ljava/lang/String;
4: areturn
}
PS F:\CloneTest\out\production\CloneTest>
复制代码
看到在类中多出了一个私有的静态方法 lambda$main$0 。这个方法里面放的是什么内容呢?通过断点调试来看看:
可以确认 lambda$main$0 里面放的就是 Lambda 中的内容;
lambda$main0 的命名 : 以 lambda 开头 , 因为是在 main()函数里使用了 lambda 表达式 , 所以带有 0 的命名:以 lambda 开头,因为是在 main()函数里使用了 lambda 表达式,所以带有 0 的命名:以 lambda 开头,因为是在 main()函数里使用了 lambda 表达式,所以带有 main 表示,因为是第一个,所以 $0。
添加 JVM 参数:-Djdk.internal.lambda.dumpProxyClasses 进行输出相关的 Lambda 表达式。
加上这个参数后,运行时会将生成的内部类 class 码输出到一个文
import java.lang.invoke.LambdaForm.Hidden;
// 形成一个匿名内部类,实现接口,重写抽象方法
// $FF: synthetic class
final class Test$$Lambda$1 implements Swimmable {
private Test$$Lambda$1() {}
@Hidden
public String swimming(String var1) {
//接口的重写方法中会调用新生成的方法
return Test.lambda$main$0(var1);
}
}
复制代码
可以看到这个匿名内部类实现了 Swimmable 接口,并且重写了 swimming 方法, swimming 方法调用 return Test.lambda$main$0(var1),也就是调用 Lambda 中的内容。最后可以将 Lambda 理解为:
public class Test {
public static void main(String[] args) throws Exception {
goSwimming(new Swimmable() {
@Override
public String swimming(String str) {
return lambda$main$0(str);
//对应前面debug到这,是执行lambda$main$0方法
}
});
}
//新增一个方法,这个方法的方法体就是Lambda表达式中的代码
private static String lambda$main$0(String str) {
return str.toLowerCase();
}
public static void goSwimming(Swimmable swimmable) {
String str = "Hello World";
str = swimmable.swimming(str);
System.out.println(str);
}
}
interface Swimmable {
public abstract String swimming(String str);
}
复制代码
📚 匿名类和 Lambda 的区别
匿名内部类在编译的时候会一个 class 文件,Lambda 在程序运行的时候形成一个类:
📚 Lambda 省略格式
在 Lambda 标准格式的基础上,使用省略写法的规则为:
(int a) -> {return new Person();}
复制代码
📚 Lambda 的前提条件
Lambda 的语法非常简洁,但是 Lambda 表达式不是随便使用的,使用时有几个条件要特别注意:
public class Test {
public static void main(String[] args) throws Exception {
//方法的参数是接口:Lambda表达式,省略参数类型以及{}、return、;
goSwimming(str -> str.toUpperCase());
//局部变量类型为接口:匿名内部类形式
Swimmable swimmable = new Swimmable() {
@Override
public String swimming(String str) {
return null;
}
};
//局部变量类型为接口:Lambda表达式
Swimmable swimmable2 = str -> str.toUpperCase();
}
public static void goSwimming(Swimmable swimmable) {
String str = "Hello World";
str = swimmable.swimming(str);
System.out.println(str);
}
}
//定义一个接口,有且仅有一个抽象方法
interface Swimmable {
public abstract String swimming(String str);
}
复制代码
📚 函数式接口
函数式接口在 Java 中是指:有且仅有一个抽象方法的接口;
函数式接口,即适用于函数式编程场景的接口。Java 中的函数式编程体现就是 Lambda,所以函数式接口就是可以适
用于 Lambda 使用的接口。只有确保接口中有且仅有一个抽象方法,Java 中的 Lambda 才能顺利地进行推导;
📚 FunctionalInterface 注解
与 @Override 注解的作用类似,Java 8 中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface Operator {
void myMethod();
}
复制代码
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样;
📚 常用内置函数式接口
我们知道使用 Lambda 表达式的前提是需要有函数式接口。而 Lambda 使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用 Lambda 方便,JDK 提供了大量常用的函数式接口;
它们主要在 java.util.function 包中,下面是最常用的几个接口:
📚 Supplier 接口
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
复制代码
java.util.function.Supplier 接口,意味着“供给” , 对应的 Lambda 表达式需要“对外提供”符合泛型类型的对象数据;
供给型接口,通过 Supplier 接口中的 get 方法可以得到一个值,无参有返回的接口;
使用场景演示:使用 Lambda 表达式返回数组元素最大值!
import java.util.Arrays;
import java.util.function.Supplier;
public class Test {
public static void main(String[] args) throws Exception {
printMax(() -> {
int[] arr = {10, 20, 100, 30, 40, 50};
// 先排序,最后就是最大的
Arrays.sort(arr);
return arr[arr.length - 1]; // 最后就是最大的
});
}
private static void printMax(Supplier<Integer> supplier) {
int max = supplier.get();
System.out.println("max = " + max);
}
}
//控制台打印:max = 100
复制代码
📚 Consumer 接口:
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
}
复制代码
java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定;使用场景演示:使用 Lambda 表达式将一个字符串转成大写和小写的字符串!
import java.util.function.Consumer;
public class Test {
public static void main(String[] args) {
// Lambda表达式
test((String s) -> {
System.out.println(s.toLowerCase());
});
}
public static void test(Consumer<String> consumer) {
consumer.accept("HelloWorld");
}
}
//控制台打印:helloworld
复制代码
如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的 default 方法 andThen;
要想实现组合,需要两个或多个 Lambda 表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> {
accept(t);
after.accept(t);
};
}
复制代码
例如两个步骤组合的情况:
import java.util.function.Consumer;
public class Test {
public static void main(String[] args) {
// Lambda表达式
test((String s) -> {
System.out.println(s.toLowerCase());
}, (String s) -> {
System.out.println(s.toUpperCase());
});
// Lambda表达式简写
/*test(s -> System.out.println(s.toLowerCase()), s ->
System.out.println(s.toUpperCase()));*/
}
public static void test(Consumer<String> c1, Consumer<String > c2) {
String str = "Hello World";
//先执行c1(转为小写)再执行c2(转为大写)
c1.andThen(c2).accept(str);
//先执行c2(转为大写)再执行c1(转为小写)
c2.andThen(c1).accept(str);
}
}
//控制台输出:
hello world
HELLO WORLD
HELLO WORLD
hello world
复制代码
📚 Function 接口:
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
}
复制代码
java.util.function.Function<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值
import java.util.function.Function;
public class Test {
//
public static void main(String[] args) {
// Lambda表达式
test((String s) -> {
return Integer.parseInt(s); // 10
});
}
public static void test(Function<String, Integer> function) {
Integer in = function.apply("10");
System.out.println("in: " + (in + 5));
}
}
//控制台输出:
in: 15
复制代码
Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK 源代码如:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
复制代码
该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:
import java.util.function.Function;
public class Test {
public static void main(String[] args) {
// Lambda表达式
test((String s) -> {
return Integer.parseInt(s);
}, (Integer i) -> {
return i * 10;
});
}
public static void test(Function<String, Integer> f1, Function<Integer, Integer> f2) {
// Integer in = f1.apply("66"); // 将字符串解析成为int数字
// Integer in2 = f2.apply(in);// 将上一步的int数字乘以10
Integer in3 = f1.andThen(f2).apply("66");
System.out.println("in3: " + in3); // 660
}
}
//控制台输出:in3: 660
复制代码
第一个操作是将字符串解析成为 int 数字,第二个操作是乘以 10。两个操作通过 andThen 按照前后顺序组合到了一起;
📚 Predicate 接口
Predicate 接口用于做判断,返回 boolean 类型的值
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
}
复制代码
有时候需要对某种类型的数据进行判断,从而得到一个 boolean 值结果。这时可以使用 java.util.function.Predicate 接口;
例如:使用 Lambda 判断一个人名如果超过 3 个字就认为是很长的名字
import java.util.function.Predicate;
public class Test {
public static void main(String[] args) {
test(s -> s.length() > 3, "迪丽热巴");
}
private static void test(Predicate<String> predicate, String str) {
boolean veryLong = predicate.test(str);
System.out.println("名字很长吗:" + veryLong);
}
}
//控制台输出:名字很长吗:true
复制代码
Predicate 拥有三个默认方法:
默认方法:and,将两个 Predicate 条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用 default 方法 and 。
默认方法:or,与 and 的“与”类似,默认方法 or 实现逻辑关系中的“或”。
默认方法:negate,“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法 negate 的 JDK 源代码为:
📚 方法引用
import java.util.function.Consumer;
public class Test {
//传统定义一个方法,直接调用即可
public static void getMax(int[] arr) {
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println(sum);
}
public static void main(String[] args) {
//Lambda表达式实现,需要定义一个方法,然后lambda里面还要再实现一次。对比一下,lambda有点冗余了
printMax((int[] arr) -> {
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println(sum);
});
}
private static void printMax(Consumer<int[]> consumer) {
int[] arr = {10, 20, 30, 40, 50};
consumer.accept(arr);
}
}
//控制台输出:150
复制代码
如果我们在 Lambda 中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接“引用”过去就好了:
import java.util.function.Consumer;
public class Test {
public static void getMax(int[] arr) {
int sum = 0;
for (int n : arr) {
sum += n;
}
System.out.println(sum);
}
public static void main(String[] args) {
printMax(Test::getMax);
}
private static void printMax(Consumer<int[]> consumer) {
int[] arr = {10, 20, 30, 40, 50};
consumer.accept(arr);
}
}
//控制台输出:150
复制代码
请注意其中的双冒号 :: 写法,这被称为“方法引用”,是一种新的语法。
方法引用的格式:
常见引用方式:
方法引用在 JDK 8 中使用方式相当灵活,有以下几种形式:
instanceName::methodName 对象::方法名;
ClassName::staticMethodName 类名::静态方法;
ClassName::methodName 类名::普通方法;
ClassName::new 类名::new 调用的构造器;
TypeName[]::new String[]::new 调用数组的构造器;
对象名::引用成员方法:
如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
import java.util.Date;
import java.util.function.Supplier;
public class Test {
public static void main(String[] args) {
Date now = new Date();
//局部变量类型为函数式接口时可以使用lambda
Supplier<Long> supp = () -> {
return now.getTime();
};
System.out.println(supp.get());
Supplier<Long> supp2 = now::getTime;
System.out.println(supp2.get());
}
}
//控制台输出:
1587694986055
1587694986055
复制代码
方法引用的注意事项:
被引用的方法,参数要和接口中抽象方法的参数一样;
当接口抽象方法有返回值时,被引用的方法也必须有返回值;
比如:
类名::引用静态方法
import java.util.function.Supplier;
public class Test {
public static void main(String[] args) {
Supplier<Long> supp = () -> {
return System.currentTimeMillis();
};
System.out.println(supp.get());
Supplier<Long> supp2 = System::currentTimeMillis;
System.out.println(supp2.get());
}
}
//控制台输出:
1587695223790
1587695223809
//public static native long currentTimeMillis();
复制代码
由于在 java.lang.System 类中已经存在了静态方法 currentTimeMillis ,所以当我们需要通过 Lambda 来调用该方法时,可以使用方法引用 ;
类名::引用实例方法
import java.util.function.Function;
public class Test {
public static void main(String[] args) {
//Lambda表达式
Function<String, Integer> f1 = (s) -> {
return s.length();
};
System.out.println(f1.apply("abc"));
//方法引用
Function<String, Integer> f2 = String::length;
System.out.println(f2.apply("abc"));
}
}
//控制台打印:
3
3
//public int length() { return value.length;}
复制代码
类名::new 引用构造器
public class Person {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public Person() {
}
}
复制代码
import java.util.function.BiFunction;
import java.util.function.Supplier;
public class Test {
public static void main(String[] args) {
//Lambda表达式
Supplier<Person> sup = () -> {
return new Person();
};
System.out.println(sup.get());
//方法引用
Supplier<Person> sup2 = Person::new;
System.out.println(sup2.get());
//带入参的方法引用,BiFunction(String, Integer为方法入参,Person为出参)
BiFunction<String, Integer, Person> fun2 = Person::new;
System.out.println(fun2.apply("张三", 18));
}
}
//控制台打印:
Person@3d075dc0
Person@7cca494b
Person@58372a00
复制代码
数组::new 引用数组构造器
import java.util.function.Function;
public class Test {
public static void main(String[] args) {
//Lambda表达式
Function<Integer, String[]> fun = (len) -> {
return new String[len];
};
String[] arr1 = fun.apply(10);
System.out.println(arr1 + ", " + arr1.length);
//方法引用
Function<Integer, String[]> fun2 = String[]::new;
String[] arr2 = fun2.apply(5);
System.out.println(arr2 + ", " + arr2.length);
}
}
//控制台打印:
[Ljava.lang.String;@3d075dc0, 10
[Ljava.lang.String;@7cca494b, 5
复制代码
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同;
📚 Lambda 和匿名内部类对比
所需的类型不一样:匿名内部类,需要的类型可以是类,抽象类,接口,Lambda 表达式,需要的类型必须是接口;
抽象方法数量不一样:匿名内部类所需接口中抽象方法的数量随意,Lambda 表达式所需的接口只能有一个抽象方法;
实现原理不同:匿名内部类是在编译后会形成 class,Lambda 表达式是在程序运行的时候动态生成 class;
评论