JDK8的Lambda表达式和方法引用
Lambda表达式介绍
使用匿名内部类存在的问题
当需要启动一个线程去完成任务时,通常会通过 Runnable 接口来定义任务内容,并使用 Thread 类来启动该线程。
传统写法,代码如下:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程任务执行");
}
}).start();
}
由于面向对象的语法要求,首先创建一个 Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将其交给一个线程来启动。
代码分析:
对于 Runnable 的匿名内部类用法,可以分析出几点内容:
- Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心
- 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类
- 为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类
- 必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错
- 而实际上,似乎只有方法体才是关键所在。
Lambda体验
Lambda 是一个匿名函数,可以理解为一段可以传递的代码。
Lambda 表达式写法,代码如下:
public static void main(String[] args) {
new Thread(()->{
System.out.println("线程任务执行");
}).start();
}
这段代码和刚才的执行效果是完全一样的,可以在 JDK8 或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。
Lambda的优点
简化匿名内部类的使用,语法更加简单。
Lambda的标准格式
Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成:
(参数类型 参数名称) -> {
代码体;
}
格式说明:
- (参数类型 参数名称):参数列表
- {代码体;}:方法体
- 箭头:分隔参数列表和方法体
练习无参数无返回值的Lambda
public class Test {
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 s) {
s.swimming();
}
}
interface Swimmable {
void swimming();
}
练习无参数无返回值的Lambda
下面举例演示 java.util.Comparator 接口的使用场景代码,其中的抽象方法定义为:public abstract int compare(T o1, T o2);
当需要对一个对象集合进行排序时,Collections.sort 方法需要一个 Comparator 接口实例来指定排序的规则。
public class Person {
private String name;
private int age;
private int height;
...
}
public class Test {
public static void main(String[] args) {
ArrayList<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(); //升序
}
});
Collections.sort(persons, (Person o1, Person o2) -> {
return o2.getAge() - o1.getAge(); //降序
});
for (Person person : persons) {
System.out.println(person);
}
}
}
了解Lambda的实现原理
匿名内部类在编译的时候会一个class文件;
Lambda在程序运行的时候形成一个类:
- 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
- 还会形成一个匿名内部类,实现接口,重写抽象方法
- 在接口的重写方法中会调用新生成的方法
Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略
- 如果小括号内有且仅有一个参数,则小括号可以省略
- 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
Lambda的前提条件
Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
- 方法的参数或局部变量类型必须为接口才能使用Lambda
- 接口中有且仅有一个抽象方法
函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
@FunctionalInterface注解与 @Override注解的作用类似,Java8 中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
Lambda和匿名内部类对比
- 所需的类型不一样
- 匿名内部类,需要的类型可以是类,抽象类,接口
- Lambda表达式,需要的类型必须是接口
- 抽象方法的数量不一样
- 匿名内部类所需的接口中抽象方法的数量随意
- Lambda表达式所需的接口只能有一个抽象方法
- 实现原理不同
- 匿名内部类是在编译后会形成class
- Lambda表达式是在程序运行的时候动态生成class
建议:当接口中只有一个抽象方法时,建议使用Lambda表达式,其他情况还是需要使用匿名内部类
JDK8 接口新增的两个方法
interface 接口名{
静态常量;
抽象方法;
默认方法;
静态方法;
}
接口默认方法
interface 接口名 {
修饰符 default 返回值类型 方法名() {
代码;
}
}
接口默认方法的使用
- 实现类直接调用接口默认方法
- 实现类重写接口默认方法
public class Demo {
public static void main(String[] args) {
BB b = new BB();
// 方式一:实现类直接调用接口默认方法
b.test02();
CC c = new CC();
// 调用实现类重写接口默认方法
c.test02();
}
}
interface AA {
void test1();
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实现类重写接口默认方法");
}
}
接口静态方法
interface 接口名 {
修饰符 static 返回值类型 方法名() {
代码;
}
}
直接使用接口名调用即可:接口名.静态方法名();
接口默认方法和静态方法的区别
- 默认方法通过实例调用,静态方法通过接口名调用。
- 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
- 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用
常用内置函数式接口
我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。
常用内置函数式接口介绍
它们主要在 java.util.function 包中。下面是最常用的几个接口。
@FunctionalInterface
public interface Supplier<T> {
T get();
}
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Supplier接口
java.util.function.Supplier 接口,它意味着"供给",对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
public class Demo {
// 使用Lambda表达式返回数组元素最大值
public static void main(String[] args) {
printMax(() -> {
int[] arr = {11, 99, 88, 77, 22};
Arrays.sort(arr); // 升序排序
return arr[arr.length - 1];
});
}
public static void printMax(Supplier<Integer> supplier) {
int max = supplier.get();
System.out.println("max = " + max);
}
}
Consumer接口
java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
public class Demo {
// 使用Lambda表达式将一个字符串转成大写的字符串
public static void main(String[] args) {
printUpperString((String str) -> {
System.out.println(str.toUpperCase());
});
}
public static void printUpperString(Consumer<String> consumer) {
consumer.accept("Hello World");
}
}
如果一个方法的参数和返回值全都是 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语句和抛出空指针异常的麻烦。
public class Demo {
// 使用Lambda表达式先将一个字符串转成小写的字符串,再转成大写
public static void main(String[] args) {
printString((String str) -> {
System.out.println(str.toLowerCase());
}, (String str) -> {
System.out.println(str.toUpperCase());
});
}
public static void printString(Consumer<String> c1, Consumer<String> c2) {
String str = "Hello World";
// c1.accept(str);
// c2.accept(str);
c1.andThen(c2).accept(str);
}
}
Function接口
java.util.function.Function<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。
public class Demo {
// 使用Lambda表达式将字符串转成数字
public static void main(String[] args) {
getNumber((String str) -> {
return Integer.parseInt(str);
});
}
public static void getNumber(Function<String, Integer> function) {
Integer num = function.apply("10");
System.out.println("num = " + num);
}
}
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));
}
public class Demo {
// 使用Lambda表达式将字符串转成数字, 第二个操作将这个数字乘以5
public static void main(String[] args) {
getNumber((String str) -> {
return Integer.parseInt(str);
}, (Integer i) -> {
return i * 5;
});
}
public static void getNumber(Function<String, Integer> f1, Function<Integer, Integer> f2) {
// Integer num = f1.apply("6");
// Integer num2 = f2.apply(num);
Integer num2 = f1.andThen(f2).apply("6");
System.out.println("num2 = " + num2);
}
}
Predicate接口
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate 接口。
public class Demo {
// 使用Lambda判断一个人名如果超过3个字就认为是很长的名字
public static void main(String[] args) {
isLongName((String name) -> {
return name.length() > 3;
});
}
public static void isLongName(Predicate<String> predicate) {
boolean isLong = predicate.test("tom");
System.out.println("是否是长名字: " + isLong);
}
}
默认方法:and。既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法 and 。其JDK源码为:
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
默认方法: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);
}
默认方法:negate。使用Lambda表达式判断一个字符串中即不包含W“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法 negate 的JDK源代码为:
default Predicate<T> negate() {
return (t) -> !test(t);
}
案例:
public class Demo {
public static void main(String[] args) {
test((String str) -> {
// 判断是否包含W
return str.contains("W");
}, (String str) -> {
// 判断是否包含H
return str.contains("H");
});
}
public static void test(Predicate<String> p1, Predicate<String> p2) {
// 使用Lambda表达式判断一个字符串中即包含W,也包含H
String str = "Hello World";
boolean b = p1.and(p2).test(str);
if (b) {
System.out.println("即包含W,也包含H");
}
// 使用Lambda表达式判断一个字符串中包含W或者包含H
boolean b1 = p1.or(p2).test(str);
if (b1) {
System.out.println("包含W或者包含H");
}
// 使用Lambda表达式判断一个字符串中不包含W
boolean b2 = p1.negate().test("Hello W");
// negate相当于取反 !boolean
if (b2) {
System.out.println("不包含W");
}
}
}
方法引用
符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用。
方法引用在JDK 8中使用方式相当灵活,有以下几种形式:
- instanceName::methodName 对象::方法名
- ClassName::staticMethodName 类名::静态方法
- ClassName::methodName 类名::普通方法
- ClassName::new 类名::new 调用的构造器
- TypeName[]::new String[]::new 调用数组的构造器
对象名::引用成员方法
这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
// 对象::实例方法
public class Demo {
public static void main(String[] args) {
Date now = new Date();
/*Supplier<Long> su1 = () -> {
return now.getTime();
};*/
// 使用方法引用
Supplier<Long> su1 = now::getTime;
Long aLong = su1.get();
System.out.println("aLong = " + aLong);
// 注意:方法引用有两个注意事项
// 1.被引用的方法,参数要和接口中抽象方法的参数一样
// 2.当接口抽象方法有返回值时,被引用的方法也必须有返回值
}
}
类名::引用静态方法
由于在 java.lang.System 类中已经存在了静态方法 currentTimeMillis ,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用 , 写法是:
// 类名::静态方法
public class Demo {
public static void main(String[] args) {
/*Supplier<Long> su = () -> {
return System.currentTimeMillis();
};*/
Supplier<Long> su = System::currentTimeMillis;
Long time = su.get();
System.out.println("time = " + time);
}
}
类名::引用实例方法
java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
// 类名::引用实例方法
public class Demo {
public static void main(String[] args) {
/*Function<String, Integer> f1 = (String str) -> {
return str.length();
};*/
// 类名::实例方法(注意:类名::实例方法实际上会将第一个参数作为方法的调用者)
Function<String, Integer> f1 = String::length;
int length = f1.apply("hello");
System.out.println("length = " + length);
// BiFunction<String, Integer, String> f2 = String::substring;
// 相当于这样的Lambda
BiFunction<String, Integer, String> f2 = (String str, Integer index) -> {
return str.substring(index);
};
String str2 = f2.apply("helloworld", 3);
System.out.println("str2 = " + str2); // loworld
}
}
类名::new引用构造器
由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。
// 类名::new引用类的构造器
public class Demo {
public static void main(String[] args) {
/*Supplier<Person> su1 = () -> {
return new Person();
};*/
Supplier<Person> su1 = Person::new;
Person person = su1.get();
System.out.println("person = " + person);
/*BiFunction<String, Integer, Person> bif = (String name, Integer age) -> {
return new Person(name, age);
};*/
BiFunction<String, Integer, Person> bif = Person::new;
Person p2 = bif.apply("凤姐", 18);
System.out.println("p2 = " + p2);
}
}
数组::new 引用数组构造器
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。
// 类型[]::new
public class Demo {
public static void main(String[] args) {
/*Function<Integer, int[]> f1 = (Integer length) -> {
return new int[length];
};*/
Function<Integer, int[]> f1 = int[]::new;
int[] arr1 = f1.apply(10);
System.out.println(Arrays.toString(arr1));
}
}