JDK 8新特性-Lambda表达式
JDK 8新特性-Lambda表达式
1.Lambda表达式
1.Lambda表达式介绍
1.目标
了解使用匿名内部类存在的问题体验Lambda
2.使用匿名内部类存在的问题
当需要启动一个线程去完成任务时,通常会通过Runnable 接口来定义任务内容,并使用Thread 类来启动该线程。传统写法,代码如下:
public class Demo01LambdaIntro {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("新线程任务执行!");
}
}).start();
}
}
由于面向对象的语法要求,首先创建一个Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将其交给一个线程来启动。
3.代码分析:
对于Runnable 的匿名内部类用法,可以分析出几点内容:
Thread 类需要Runnable 接口作为参数,其中的抽象run 方法是用来指定线程任务内容的核心为了指定run 的方法体,不得不需要Runnable 接口的实现类
为了省去定义一个Runnable 实现类的麻烦,不得不使用匿名内部类
必须覆盖重写抽象run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错而实际上,似乎只有方法体才是关键所在。
4.Lambda体验
Lambda是一个匿名函数,可以理解为一段可以传递的代码。Lambda表达式写法,代码如下:
借助Java 8的全新语法,上述Runnable 接口的匿名内部类写法可以通过更简单的Lambda表达式达到相同的效果
public class Demo01LambdaIntro {
public static void main(String[] args) {
new Thread(()-> System.out.println("新线程任务执行")).start();
}
}
这段代码和刚才的执行效果是完全一样的,可以在JDK 8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。
5.Lambda的优点
简化匿名内部类的使用,语法更加简单。
6.小结
了解了匿名内部类语法冗余,体验了Lambda表达式的使用,发现Lmabda是简化匿名内部类的简写
2.Lambda的标准格式
1.目标
掌握Lambda的标准格式
练习无参数无返回值的Lambda 练习有参数有返回值的Lambda
2.Lambda的标准格式
Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成:
(参数类型 参数名称) -> {
代码体;
}
3.格式说明:
(参数类型 参数名称):参数列表
{代码体;}:方法体
-> :箭头,分隔参数列表和方法体
Lambda与方法的对比
匿名内部类
public void run() {
System.out.println("aa");
}
Lambda
() -> System.out.println("bb!")
4.练习无参数无返回值的Lambda
掌握了Lambda的语法,我们来通过一个案例熟悉Lambda的使用。
interface Swimmable {
public abstract void swimming();
}
public class Demo02LambdaUse {
public static void main(String[] args) {
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("匿名内部类游泳");
}
});
goSwimming(()-> System.out.println("lambda游泳"));
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
5.练习有参数有返回值的Lambda
下面举例演示java.util.Comparator<T>
接口的使用场景代码,其中的抽象方法定义为:public abstract int compare(T o1, T o2);
当需要对一个对象集合进行排序时, Collections.sort 方法需要一个Comparator
接口实例来指定排序的规则。
传统写法
如果使用传统的代码对ArrayList 集合进行排序,写法如下:
public class Demo03LambdaUse {
public static void main(String[] args) {
List<Person> persons = new ArrayList<>();
persons.add(new Person("刘德华", 58, 174));
persons.add(new Person("张学友", 58, 176));
persons.add(new Person("刘德华", 54, 171));
persons.add(new Person("黎明", 53, 178));
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
/**
* Person(name=黎明, age=53, height=178)
* Person(name=刘德华, age=54, height=171)
* Person(name=刘德华, age=58, height=174)
* Person(name=张学友, age=58, height=176)
*/
for (Person person : persons) {
System.out.println(person);
}
}
}
这种做法在面向对象的思想中,似乎也是"理所当然"的。其中Comparator 接口的实例(使用了匿名内部类)代表了"按照年龄从小到大"的排序规则。
Lambda写法
public class Demo03LambdaUse {
public static void main(String[] args) {
List<Person> persons = new ArrayList<>();
persons.add(new Person("刘德华", 58, 174));
persons.add(new Person("张学友", 58, 176));
persons.add(new Person("刘德华", 54, 171));
persons.add(new Person("黎明", 53, 178));
Collections.sort(persons, (p1,p2)-> p1.getAge()-p2.getAge());
}
}
6.小结
首先学习了Lambda表达式的标准格式
(参数列表) -> {
方法体;
}
以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写
3.了解Lambda的实现原理
1.目标
了解Lambda的实现原理
我们现在已经会使用Lambda表达式了。现在同学们肯定很好奇Lambda是如何实现的,现在我们就来探究Lambda 表达式的底层实现原理。
@FunctionalInterface interface Swimmable {
public abstract void swimming();
}
public class Demo02LambdaUse {
public static void main(String[] args) {
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("匿名内部类游泳");
}
});
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
我们可以看到匿名内部类会在编译后产生一个类:Demo04LambdaImpl$1.class
使用XJad反编译这个类,得到如下代码:
import java.io.PrintStream;
// Referenced classes of package com.llp.demo01lambda: // Swimmable, Demo04LambdaImpl
static class Demo04LambdaImpl$1 implements Swimmable { public void swimming() {
System.out.println("使用匿名内部类实现游泳"); }
Demo04LambdaImpl$1() {
}
}
我们再来看看Lambda的效果,修改代码如下:
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(() -> {
System.out.println("Lambda游泳");
});
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说Lambda并没有在编译的时候产生一 个新的类。使用XJad对这个类进行反编译,发现XJad报错。使用了Lambda后XJad反编译工具无法反编译。我们使用JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。
在DOS命令行输入:
javap -c -p 文件名.class
-c:表示对代码进行反汇编
-p:显示所有类和成员
反汇编后效果如下:
C:\Users\>javap -c -p Demo04LambdaImpl.class Compiled from "Demo04LambdaImpl.java" public class com.llp.demo01lambda.Demo04LambdaImpl {
public com.llp.demo01lambda.Demo04LambdaImpl();
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/llp/demo01lambda/Swimmable;
5: invokestatic #3 // Method goSwimming: (Lcom/llp/demo01lambda/Swimmable;)V
8: return
public static void goSwimming(com.llp.demo01lambda.Swimmable);
Code:
0: aload_0
1: invokeinterface #4, 1 // InterfaceMethod com/llp/demo01lambda/Swimmable.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
}
可以看到在类中多出了一个私有的静态方法lambda$main$0 。这个方法里面放的是什么内容呢?我们通过断点调试来看看:
可以确认lambda$main$0 里面放的就是Lambda中的内容,我们可以这么理解lambda$main$0 方法:
public class Demo04LambdaImpl {
public static void main(String[] args) {
...
}
private static void lambda$main$0() {
System.out.println("Lambda游泳");
}
}
关于这个方法lambda$main$0 的命名:以lambda开头,因为是在main()函数里使用了lambda表达式,所以带有 $main表示,因为是第一个,所以$0。
如何调用这个方法呢?其实Lambda在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加上-Djdk.internal.lambda.dumpProxyClasses
,加上这个参数后,运行时会将生成的内部类class码输出到一个文件中。使用java命令如下:
java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
根据上面的格式,在命令行输入以下命令:
C:\Users\>java -Djdk.internal.lambda.dumpProxyClasses com.llp.demo01lambda.Demo04LambdaImpl Lambda游泳
执行完毕,可以看到生成一个新的类,效果如下:
反编译Demo04LambdaImpl$$Lambda$1.class
这个字节码文件,内容如下:
// Referenced classes of package com.llp.demo01lambda: // Swimmable, Demo04LambdaImpl
final class Demo04LambdaImpl$$Lambda$1 implements Swimmable {
public void swimming() {
Demo04LambdaImpl.lambda$main$0();
}
private Demo04LambdaImpl$$Lambda$1() {}
}
可以看到这个匿名内部类实现了Swimmable 接口,并且重写了swimming 方法, swimming 方法调用
Demo04LambdaImpl.lambda$main$0() ,也就是调用Lambda中的内容。最后可以将Lambda理解为:
2.小结
匿名内部类在编译的时候会一个class文件
Lambda在程序运行的时候形成一个类
- 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
- 还会形成一个匿名内部类,实现接口,重写抽象方法
- 在接口的重写方法中会调用新生成的方法.
4.Lambda省略格式
目标
掌握Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略
- 如果小括号内有且仅有一个参数,则小括号可以省略
- 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
(int a) -> { return new Person(); }
省略后
a -> new Person()
5.Lambda的前提条件
1.目标
掌握Lambda的前提条件
Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
- 方法的参数或局部变量类型必须为接口才能使用Lambda
- 接口中有且仅有一个抽象方法
public interface Flyable {
public abstract void flying();
}
public class Demo05LambdaCondition {
public static void main(String[] args) {
test01(() -> {
});
Flyable s = new Flyable() {
@Override
public void flying() {
}
};
Flyable s2 = () -> {
};
}
public static void test01(Flyable fly) {
fly.flying();
}
}
2.小结
Lambda表达式的前提条件:
- 方法的参数或变量的类型是接口
- 这个接口中只能有一个抽象方法
6.函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以 适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
@FunctionalInterface
注解
与@Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface Operator {
void myMethod();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即 使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
7.Lambda和匿名内部类对比
1.目标
了解Lambda和匿名内部类在使用上的区别
- 所需的类型不一样
匿名内部类,需要的类型可以是类,抽象类,接口
Lambda表达式,需要的类型必须是接口
- 抽象方法的数量不一样
匿名内部类所需的接口中抽象方法的数量随意
Lambda表达式所需的接口只能有一个抽象方法
- 实现原理不同
匿名内部类是在编译后会形成class
Lambda表达式是在程序运行的时候动态生成class
2.小结
当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类
2.JDK 8接口新增的两个方法
目标
了解JDK 8接口新增的两个方法
掌握接口默认方法的使用
掌握接口静态方法的使用
1.JDK 8接口增强介绍
JDK 8以前的接口:
interface 接口名 {
静态常量;
抽象方法;
}
JDK 8对接口的增强,接口还可以有默认方法和静态方法
JDK 8的接口:
interface 接口名 {
静态常量; 抽象方法; 默认方法; 静态方法;
}
2.接口引入默认方法的背景
在JDK 8以前接口中只能有抽象方法。存在以下问题:
如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法。不利于接口的扩展。
class B implements A {
@Override
public void test1() {
System.out.println("BB test1");
}
// 接口新增抽象方法,所有实现类都需要去重写这个方法
@Override
public void test2() {
System.out.println("BB test2");
}
}
class C implements A {
@Override
public void test1() {
System.out.println("CC test1");
}
// 接口新增抽象方法,所有实现类都需要去重写这个方法
@Override
public void test2() {
System.out.println("CC test2");
}
}
例如,JDK 8 时在Map接口中增加了 forEach 方法:
public interface Map<K, V> {
...
abstract void forEach(BiConsumer<? super K, ? super V> action);
}
通过API可以查询到Map接口的实现类如:
如果在Map接口中增加一个抽象方法,所有的实现类都需要去实现这个方法,那么工程量时巨大的。 因此,在JDK 8时为接口新增了默认方法,效果如下:
public interface Map<K, V> {
...
default void forEach(BiConsumer<? super K, ? super V> action) {
...
}
}
接口中的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写。这样就方便接口的扩展。
3.接口默认方法的定义格式
interface 接口名 {
修饰符 default 返回值类型 方法名() {
代码;
}
}
4.接口默认方法的使用
方式一:实现类直接调用接口默认方法
方式二:实现类重写接口默认方法
public class Demo02UserDefaultFunction {
public static void main(String[] args) {
BB b = new BB();
// 方式一:实现类直接调用接口默认方法
b.test02();
CC c = new CC();
// 调用实现类重写接口默认方法
c.test02();
}
}
interface AA {
public abstract void test1();
public default void test02() {
System.out.println("AA test02");
}
}
class BB implements AA {
@Override
public void test1() {
System.out.println("BB test1");
}
}
class CC implements AA {
@Override
public void test1() {
System.out.println("CC test1");
}
// 方式二:实现类重写接口默认方法
@Override
public void test02() {
System.out.println("CC实现类重写接口默认方法");
}
}
5.接口静态方法
为了方便接口扩展,JDK 8为接口新增了静态方法。
1.接口静态方法的定义格式
interface 接口名 {
修饰符 static 返回值类型 方法名() {
代码;
}
}
接口静态方法的使用
直接使用接口名调用即可:接口名.静态方法名();
代码
public class Demo04UseStaticFunction {
public static void main(String[] args) {
// 直接使用接口名调用即可:接口名.静态方法名();
AAA.test01();
}
}
interface AAA {
public static void test01() {
System.out.println("AAA 接口的静态方法");
}
}
class BBB implements AAA {
/* @Override 静态方法不能重写
public static void test01() {
System.out.println("AAA 接口的静态方法");
}
*/
}
2.接口默认方法和静态方法的区别
- 默认方法通过实例调用,静态方法通过接口名调用。
- 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
- 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。
3.小结
接口中新增的两种方法: 默认方法和静态方法
如何选择呢?如果这个方法需要被实现类继承或重写,使用默认方法,如果接口中的方法不需要被继承就使用静态方法
3.常用内置函数式接口
目标
了解内置函数式接口由来了解常用内置函数式接口
1.内置函数式接口来由来
我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽 象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。
public class Demo01UserFunctionalInterface {
public static void main(String[] args) {
// 调用函数式接口中的方法
method((arr) -> {
int sum = 0;
for (int n : arr) {
sum += n;
}
return sum;
});
}
// 使用自定义的函数式接口作为方法参数
public static void method(Operator op) {
int[] arr = {1, 2, 3, 4};
int sum = op.getSum(arr);
System.out.println("sum = " + sum);
}
}
@FunctionalInterface
interface Operator {
public abstract int getSum(int[] arr);
}
2.常用内置函数式接口介绍
它们主要在java.util.function
包中。下面是最常用的几个接口。
-
Supplier接口
@FunctionalInterface public interface Supplier<T> { public abstract T get(); }
-
Consumer接口
@FunctionalInterface public interface Consumer<T> { public abstract void accept(T t); }
-
Function接口
@FunctionalInterface public interface Function<T, R> { public abstract R apply(T t); }
-
Predicate接口
@FunctionalInterface public interface Predicate<T> { public abstract boolean test(T t); } Predicate接口用于做判断,返回boolean类型的值
4.Supplier接口
java.util.function.Supplier<T> 接口,它意味着"供给" , 对应的Lambda表达式需要"对外提供"一个符合泛型类型的对象数据。
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
供给型接口,通过Supplier接口中的get方法可以得到一个值,无参有返回的接口。使用Lambda表达式返回数组元素最大值
使用Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。提示:接口的泛型请使用java.lang.Integer
类。
代码示例:
public class Demo05Supplier {
public static void main(String[] args) {
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);
}
}
5.Consumer接口
java.util.function.Consumer<T> 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
}
使用Lambda表达式将一个字符串转成大写和小写的字符串
Consumer消费型接口,可以拿到accept方法参数传递过来的数据进行处理, 有参无返回的接口。基本使用如:
public class Demo06Consumer {
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");
}
}
默认方法:andThen
如果一个方法的参数和返回值全都是Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer 接口中的default方法andThen 。下面是JDK的源代码:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> {
accept(t);
after.accept(t);
};
}
备注: java.util.Objects 的requireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
要想实现组合,需要两个或多个Lambda表达式即可,而andThen 的语义正是"一步接一步"操作。例如两个步骤组合的情况:
public class Demo07ConsumerAndThen {
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.accept(str);
// 转小写
// c2.accept(str);
// 转大写
c1.andThen(c2).accept(str);
c2.andThen(c1).accept(str);
}
}
运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组 合。
6.Function接口
java.util.function.Function<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件, 后者称为后置条件。有参数有返回值。
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
}
使用Lambda表达式将字符串转成数字
Function转换型接口,对apply方法传入的T类型数据进行处理,返回R类型的结果,有参有返回的接口。使用的场景 例如:将String 类型转换为Integer 类型。
public class Demo08Function {
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));
}
}
默认方法:andThen
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 差不多:
public class Demo09FunctionAndThen {
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
}
}
第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过 andThen 按照前后顺序组合到了一起。
请注意,Function的前置条件泛型和后置条件泛型可以相同。
7.Predicate接口
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用 java.util.function.Predicate<T> 接口。
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
}
Predicate接口用于做判断,返回boolean类型的值
使用Lambda判断一个人名如果超过3个字就认为是很长的名字
对test方法的参数T进行判断,返回boolean类型的结果。用于条件判断的场景:
public class Demo10Predicate {
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);
}
}
条件判断的标准是传入的Lambda表达式逻辑,只要名称长度大于3则认为很长。
1.默认方法:and
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate 条件使用"与"逻辑连接起来实现"并且"的效果时,可以使用default方法and 。其JDK源码为:
使用Lambda表达式判断一个字符串中即包含W,也包含H
使用Lambda表达式判断一个字符串中包含W或者包含H
使用Lambda表达式判断一个字符串中即不包含W
如果要判断一个字符串既要包含大写"H",又要包含大写"W",那么:
public class Demo10Predicate_And_Or_Negate {
public static void main(String[] args) {
// Lambda表达式
test((String s) -> {
return s.contains("H");
}, (String s) -> {
return s.contains("W");
});
}
public static void test(Predicate<String> p1, Predicate<String> p2) {
String str = "HelloWorld";
boolean b1 = p1.test(str);
// 判断包含大写“H” boolean b2 = p2.test(str);
// 判断包含大写“W”
if (b1 && b2) {
System.out.println("即包含W,也包含H");
}
boolean bb = p1.and(p2).test(str);
if (bb) {
System.out.println("即包含W,也包含H");
}
}
}
2.默认方法:or
使用Lambda表达式判断一个字符串中包含W或者包含H
与and 的"与"类似,默认方法or 实现逻辑关系中的"或"。JDK源码为:
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
如果希望实现逻辑"字符串包含大写H或者包含大写W",那么代码只需要将"and"修改为"or"名称即可,其他都不变:
public class Demo10Predicate_And_Or_Negate {
public static void main(String[] args) {
// Lambda表达式
test((String s) -> {
return s.contains("H");
}, (String s) -> {
return s.contains("W");
});
}
public static void test(Predicate<String> p1, Predicate<String> p2) {
String str = "HelloWorld";
boolean b1 = p1.test(str); // 判断包含大写“H”
boolean b2 = p2.test(str); // 判断包含大写“W”
//if (b1 || b2) {
// System.out.println("有H,或者W");
//}
boolean bbb = p1.or(p2).test(str);
if (bbb) {
System.out.println("有H,或者W");
}
}
}
3.默认方法:negate
使用Lambda表达式判断一个字符串中即不包含W
"与"、"或"已经了解了,剩下的"非"(取反)也会简单。默认方法negate 的JDK源代码为:
default Predicate<T> negate() {
return (t) -> !test(t);
}
从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行"!"取反而已。一定要在 test 方法调用之前调用negate 方法,正如and 和or 方法一样:
public class Demo10Predicate_And_Or_Negate {
public static void main(String[] args) {
// Lambda表达式
test((String s) -> {
return s.contains("H");
}, (String s) -> {
return s.contains("W");
});
}
public static void test(Predicate<String> p1, Predicate<String> p2) {
String str = "HelloWorld";
boolean b1 = p1.test(str); // 判断包含大写“H”
boolean b2 = p2.test(str); // 判断包含大写“W”
// 没有H,就打印
if (!b1) {
System.out.println("没有H");
}
boolean test = p1.negate().test(str);
if (test) {
System.out.println("没有H");
}
}
}
8.小结
- Supplier接口
- Consumer接口
- Function接口
- Predicate接口
4.方法引用
目标
了解Lambda的冗余场景掌握方法引用的格式
了解常见的方法引用方式
1.Lambda的冗余场景
使用Lambda表达式求一个数组的和
public class Demo11MethodRefIntro {
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((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);
}
}
体验方法引用简化Lambda
如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接"引 用"过去就好了:
public class DemoPrintRef {
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(Demo11MethodRefIntro::getMax);
}
private static void printMax(Consumer<int[]> consumer) {
int[] arr = {10, 20, 30, 40, 50};
consumer.accept(arr);
}
}
请注意其中的双冒号:: 写法,这被称为"方法引用",是一种新的语法。
2.方法引用的格式
1.符号表示 ::
符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用。
3.常见引用方式
方法引用在JDK 8中使用方式相当灵活,有以下几种形式:
- 对象::方法名
- 类名::静态方法
- 类名::普通方法
- 类名::new 调用的构造器
- String[]::new 调用数组的构造器
4.小结
首先了解Lambda表达式的冗余情况,体验了方法引用,了解常见的方法引用方式
5.对象名::引用成员方法
这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
// 对象::实例方法
@Test
public void test01() {
Date now = new Date();
Supplier<Long> supp = () -> {
return now.getTime();
};
System.out.println(supp.get());
Supplier<Long> supp2 = now::getTime;
System.out.println(supp2.get());
}
方法引用的注意事项
- 被引用的方法,参数要和接口中抽象方法的参数一样
- 当接口抽象方法有返回值时,被引用的方法也必须有返回值
6.类名::引用静态方法
由于在java.lang.System 类中已经存在了静态方法currentTimeMillis ,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用 , 写法是:
// 类名::静态方法
@Test
public void test02() {
Supplier<Long> supp = () -> {
return System.currentTimeMillis();
};
System.out.println(supp.get());
Supplier<Long> supp2 = System::currentTimeMillis;
System.out.println(supp2.get());
}
7.类名::引用实例方法
Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
// 类名::实例方法
@Test
public void test03() {
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"));
BiFunction<String, Integer, String> bif = String::substring;
String hello = bif.apply("hello", 2);
System.out.println("hello = " + hello);
}
8.类名::new引用构造器
由于构造器的名称与类名完全一样。所以构造器引用使用类名称::new 的格式表示。首先是一个简单的Person 类:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 类名::new
@Test
public void test04() {
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> fun2 = Person::new;
System.out.println(fun2.apply("张三", 18));
}
要使用这个函数式接口,可以通过方法引用传递:
9.数组::new 引用数组构造器
数组也是Object 的子类对象,所以同样具有构造器,只是语法稍有不同。
// 类型[]::new
@Test
public void test05() {
Function<Integer, String[]> fun = (len) -> {
return new String[len];
};
String[] arr1 = fun.apply(10);
//[Ljava.lang.String;@78e67e0a, 10
System.out.println(arr1 + ", " + arr1.length);
Function<Integer, String[]> fun2 = String[]::new;
String[] arr2 = fun2.apply(5);
//[Ljava.lang.String;@80503, 5
System.out.println(arr2 + ", " + arr2.length);
}
10.小结
方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式 , 不过要注意的是方法引用只能"引用"已经存在的方法!