title: Java 学习笔记(11)——lambda 表达式
tags: [Java 基础, 学习笔记, 函数式编程, lambda表达式]
date: 2019-07-14 18:51:13
categories: Java
keywords: Java 基础, 学习笔记, 函数式编程, lambda表达式
在写Java代码的时候,如果某个地方需要一个接口的实现类,一般的做法是新定义一个实现类,并重写接口中的方法,在需要使用的时候new一个实现类对象使用,为了一个简单的接口或者说为了一个回调函数就得额外编写一个新类,即使说使用匿名内部类来实现,这样似乎比较麻烦。C中的做法是直接传入一个函数指针,而Java中就需要上述麻烦的操作,能不能简单点呢?为此Java中引入了一个lambda表达式的功能。
lambda 表达式简介
看看之前线程的例子:
public class ThreadDemo{
public static void main(String[] args){
//使用匿名内部类的方式
Runnable thread1 = new Runnable(){
@Override
public void run(){
System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行");
}
}
new Thread(thread1).start();
new Thread(thread1).start();
}
}
上面使用了匿名内部类的方式来简化了书写。使用lambda之后,可以写的更加简单
public class ThreadDemo{
public static void main(String[] args){
//使用匿名内部类的方式
new Thread(()->{
System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行");
});
}
}
相比于之前使用匿名内部类的例子,lambda表达式更加关注的是函数实现的功能,而不再关注使用哪个类来实现。写法上更加的简洁。
lambda 表达式的基本格式为 (参数列表)->{函数体}
; JDK 会根据使用的接口自动创建对应的接口实现类并创建对象。也就是说,这里我们虽然简写了,但是底层仍然是需要通过创建实现类的对象来执行。
上述的代码,JVM在执行时根据 Thread类 构造的情况,自动推导出此时应该需要一个Runnable的实现类,并且将lambda表达式中的函数体作为重写接口方法的函数体。需要注意使用lambda表达式的一些约束条件:
- lambda表达式只能用于重写接口类中的抽象方法。
- 接口中应该只有一个抽象方法。
当然上述的代码可以进一步简写。lambda表达式中凡是可以根据定义推导出来的东西就可以省略不写,例如:
- 括号中参数列表中,参数类型可以不写。这个可以根据接口中方法的定义知道需要传哪些类型的参数
- 括号中参数只有一个,那么类型和括号都可以省略
- 如果函数体中代码只有一行,那么不管它是否有返回值,return和大括号以及语句末尾的分号可以都省略(注意,这里需要都省略)
根据这些简写的规则,上述代码可以进一步简化
public class ThreadDemo{
public static void main(String[] args){
//使用匿名内部类的方式
new Thread(()->System.out.println("当前线程:" + Thread.currentThread().getName() + "正在运行"));
}
}
函数式接口
上面说到,lambda表达式的条件是需要接口中只有一个抽象方法。像这种接口也被叫做是函数式接口。可以使用注解 @FunctionalInterface 来标明定义了一个函数式接口。在 java.util.function
包中提供了一些函数式接口。
- Supplier 生产者接口,它的定义如下
@FunctionalInterface
public interface Supplier<T>{
T get();
}
这个接口的get方法可以产生一个结果供外部程序使用。
- Consumer,消费者接口需要传入一个结果供其处理,它的定义如下:
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
- Predicate:判断的接口,根据给定的值返回True或者False
@FunctionalInterface
public interface Predicate<T>{
default Predicate<T> and(Predicate<? super T> other); //返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑AND。
static <T> Predicate<T> isEqual(Object targetRef); //返回根据 Objects.equals(Object, Object)测试两个参数是否相等的 谓词 。
default Predicate<T> negate(); //返回表示此谓词的逻辑否定的谓词。
default Predicate<T> or(Predicate<? super T> other); //返回一个组合的谓词,表示该谓词与另一个谓词的短路逻辑或。
boolean test(T t); //在给定的参数上评估这个谓词。
}
- Function 接口:接受一个参数并产生结果的函数
@FunctionalInterface
public interface Function<T,R>{
default <V> Function<T,V> andThen(Function<? super R,? extends V> after); //返回一个组合函数,首先将该函数应用于其输入,然后将 after函数应用于结果。
default <V> Function<V,R> compose(Function<? super V,? extends T> before); //返回一个组合函数,首先将 before函数应用于其输入,然后将此函数应用于结果。
static <T> Function<T,T> identity(); //返回一个总是返回其输入参数的函数。
R apply(T t); //接收指定参数处理并返回一个处理结果
}
方法引用
方法引用通过方法的名字来指向一个方法。方法引用可以使语言的构造更紧凑简洁,减少冗余代码。它主要用来针对lambda表达式做进一步的优化
方法引用需要保证被引用的方法已经存在。方法引用使用一对冒号来表示 ::
List names = new ArrayList();
names.add("Google");
names.add("Runoob");
names.add("Taobao");
names.add("Baidu");
names.add("Sina");
names.forEach(t->System.out.println(t));
上述代码是采用lambda表达式的写法,接下来采用方法引用的方式,进一步简化代码
List names = new ArrayList();
names.add("Google");
names.add("Runoob");
names.add("Taobao");
names.add("Baidu");
names.add("Sina");
names.forEach(System.out::println);
方法引用的常见方式有:
- 通过对象名引用对象方法
- 通过类名引用静态方法
- 通过this关键字,引用本类的成员方法
- 通过构造函数引用:
类名::new
class Car {
public static Car create(final Supplier<Car> supplier) {
return supplier.get();
}
public static void collide(final Car car) {
System.out.println("Collided " + car.toString());
}
public void follow(final Car another) {
System.out.println("Following the " + another.toString());
}
public void repair() {
System.out.println("Repaired " + this.toString());
}
}
//使用构造函数的引用
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
//静态方法的引用
cars.forEach( Car::collide );
//
<hr />