📚 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 WORLDhello 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.classCompiled 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 classfinal 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 。该注解可用于一个接口的定义上:
@FunctionalInterfacepublic interface Operator { void myMethod();}
复制代码
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样;
📚 常用内置函数式接口
我们知道使用 Lambda 表达式的前提是需要有函数式接口。而 Lambda 使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用 Lambda 方便,JDK 提供了大量常用的函数式接口;
它们主要在 java.util.function 包中,下面是最常用的几个接口:
📚 Supplier 接口
@FunctionalInterfacepublic 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 接口:
@FunctionalInterfacepublic 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 worldHELLO WORLDHELLO WORLDhello world
复制代码
📚 Function 接口:
@FunctionalInterfacepublic 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 类型的值
@FunctionalInterfacepublic 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()); }}//控制台输出:15876949860551587694986055
复制代码
方法引用的注意事项:
被引用的方法,参数要和接口中抽象方法的参数一样;
当接口抽象方法有返回值时,被引用的方法也必须有返回值;
比如:
类名::引用静态方法
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()); }}//控制台输出:15876952237901587695223809//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")); }}//控制台打印:33//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@3d075dc0Person@7cca494bPerson@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;
评论