19.lambda 表达式【Java温故系列】

参考自–《Java核心技术卷1》

lambda 表达式

      • 1 lambda 表达式简介
      • 2 函数式接口
      • 3 方法引用
      • 4 构造器引用
      • 5 变量作用域
      • 6 处理 lambda 表达式

1 lambda 表达式简介

lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。

如下:定制比较器完成数组排序,按长度而不是默认的字典顺序对字符串排序:

class LengthComparator implements Comparator<String>{
    //代码块
	public int compare(String first,String second){
		return first.length()-second.length();
	}
}
Arrays.sort(strings,new LengthComparator());  //strings是数组,LengthComparator为指定的比较器

比较器中的 compare 方法不是立即调用。实际上,在数组完成排序之前,sort 方法会一直调用 compare 方法对数组中两两元素进行比较,只要数组元素的顺序不正确就会重新排列元素。将比较元素所需的代码块放在 sort 方法中,这个代码将与其余的排序逻辑集成(可能你并不打算重新实现其余的这部分逻辑)。

使用 lambda 表达式的情况都有这样的一些特点:将一个代码块传递到某个对象,这个代码块会在将来某个时间调用。

到目前为止,在 Java 中传递一个代码块并不容易,不能直接传递代码块。Java 是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需要的代码块。

lambda 表达式的语法

如上述的排序比较器代码块:它需要比较一个字符串是否比另一个字符串短

first.length()-second.length();

Java 是一种强类型语言,所以必须指定 first 和 second 的类型:

(String first,String second)
  -> first.length()-second.length();

这就是一个 lambda 表达式。lambda 表达式就是一个代码块,以及必须传入代码的变量规范。

上述是 lambda 表达式的一种表达形式:参数,箭头(->)以及一个表达式。

如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,将这些代码放在 {} 中,并包含显式的 return 语句:例如

(String first,String second) -> {
	if(first.length()<second.length())
        return -1;
    else if(first.length()>second.length())
        return 1;
    else 
        return 0;
}

即使 lambda 表达式没有参数,也要提供空括号,就像无参数方法一样:

() -> {
	for(int i=10;i>=0;i--){
        System.out.println(i);
    }
}

如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型:

Comparator<String> comp
    = (first,second)
    	-> first.length()-second.length();	

在这里,编译器可以推导出 first 和 second 必然是字符串,因为这个 lambda 表达式将赋给一个字符串比较器。

如果方法只有一个参数,而且这个参数的类型可以推导出,那么甚至可以省略小括号(参数).

另外,无须指定 lambda 表达式的返回类型。它的返回类型总是会由上下文推导得出。

:如果一个 lambda 表达式只在某些分支返回一个值,而在另外一些分支不返回值,则不合法。如:

(int x) -> {if(x>=0)  return 1;}   //不合法

在程序中加入 lambda 表达式:

public static void main(String[] args){
	String[] planets = new String[]{"Mercury","Venus","Earth","Mars","Jupiter",
                                        "Saturn","Uranus","Neptune"};
    System.out.println(Arrays.toString(planets));
    //默认字典顺序排序
    Arrays.sort(planets);
    System.out.println(Arrays.toString(planets));
    System.out.println("++++++++++++++++++");
    //在比较器中使用lambda表达式,比较字符串长度进行排序
    Arrays.sort(planets,(first,second)->first.length()-second.length());
    System.out.println(Arrays.toString(planets));
}

上述的lambda表达式就是一个 Comparator 接口实例:

Comparator comparator = (first,second)->first.length()-second.length();

2 函数式接口

Java 中已经有很多封装代码块的接口,如 Comparator 等等,lambda 表达式与这些接口是兼容的。

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口。

:在 Java SE 8中,接口可以声明非抽象方法。

下面再看看 Arrays.sort(T[] a,Comparator c) 方法。它的第二个参数需要一个 Comparator 实例,Comparator 就是只有一个方法的接口,所以可以提供一个 lambda 表达式:

Arrays.sort(planets,
            (first,second)->first.length()-second.length());

在底层,Arrays.sort() 方法会接收实现了 Comparator 接口的某个类的对象。在这个对象上调用 compare 方法会执行这个 lambda 表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。最好把 lambda 表达式看作是一个函数,而不是一个对象,另外要接受 lambda 表达式可以传递到函数式接口。

lambda 表达式可以转换为接口,这一点让 lambda 表达式很有吸引力。具体的语法也很简短。

例如:计时器的实现中,第一个参数为delay延迟时间(毫秒),第二个参数为 ActionListener 接口的实现

//每个一段时间打印当前时间
Timer t = new Timer(1000,event -> {
	System.out.println(new Date());
});
t.start();
//弹出组件
JOptionPane.showMessageDialog(null,"Quit?");
System.exit(0);

上述语句中,编译器执行完 Timer 后创建一个定时器,每隔 1 秒打印一次当前时间,然后等待 JOptionPane.showMessageDialog(null,"Quit?"); 语句执行结束(即对弹出窗口进行操作完毕后),执行 exit(0) 退出程序。

上述通过 lambda 表达式实现 ActionListener 接口要比实现ActionListener 接口的的代码可读性好得多。

通过实现ActionListener 接口的完成上述定时器:

//实现ActionListener接口的类
public class TimePrinter implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("new Time: "+new Date());
    }
}
public class TimerTest {
    //接口回调
    public static void main(String[] args){
        ActionListener listener = new TimePrinter();
        Timer t = new Timer(1000,listener);
        t.start();
        JOptionPane.showMessageDialog(null,"Quit?");
        System.exit(0);
    }
}

实际上,在 Java 中,对 lambda 表达式所能做的也只是能转换为函数式接口。在其他支持函数字面量的程序设计语言中,可以声明函数类型(如(String,String)->int)、声明这些类型的变量,还可以使用变量保存函数表达式。不过,Java 还是保持着接口概念,没有增加函数类型。

:甚至不能把 lambda 表达式赋给类型为 Object 的变量,Object 不是一个函数式接口。

了解: Java API 在 java.util.function 包中定义了很多非常通用的函数式接口。比如其中一个接口 BiFunction 描述了参数类型为 T,U而且返回类型为 R 的函数。如:

BiFunction<String,String,Integer> comp
    = (first,second) -> first.length()-second.length();

java.util.function 有一个尤其有用的接口 Predicate:

public interface Predicate<T>{
	boolean test(T t);
}

在 ArrayList 类有一个 removeIf 方法,它的参数就是一个 Predicate ,这个接口专门用来传递 lambda 表达式:

list.removeIf(e -> e == null);

3 方法引用

有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设每隔一段时间打印定时器事件对象,可以调用:

Timer t = new Timer(1000,event -> {
	System.out.println(event);
});

但是,如果直接把 println 方法传递到 Timer 构造器就更好了:

Timer t = new Timer(1000,System.out::println);

上述表达式 System.out::println 是一个方法引用,它等价于 lambda 表达式 x -> System.out.println(x)

System.out::println 的使用:

Consumer<String> con = System.out::println;
con.accept("123456");   //打印出 123456

再看,若对字符串排序(按字典顺序),而不考虑字母的大小写,可以传递如下的方法表达式:

Arrays.sort(strings,String::compareToIgnoreCase);

:: 分隔方法名与对象或类名,主要分3种情况:

  • object::instanceMethod 对象的实例方法(没有static的就是实例方法)
  • Class::staticMethod 类的静态方法
  • Class::instanceMethod 类的实例方法

对于前两种情况,方法引用等价于提供方法参数的 lambda 表达式。如 System.out::println 等价于 x -> System.out.println(x) ,类似的,Math::pow 等价于 (x,y) -> Math.pow(x,y) .

对于第三种情况,第一个参数会成为方法的目标参数。例如:String::compareToIgnoreCase 等价于 (x,y) -> x.compareToIgnoreCase(y) .

:如果有多个同名的重载方法,编译器就会尝试从上下文中找出需要的方法。例如,Math.max 方法就有两个版本,一个用于整数,一个用于 double 值。选择哪个版本的方法取决于 Math::max 转换为哪个函数式接口的方法参数。类似于 lambda 表达式,方法引用不能独立存在,总会转换为函数式接口的实例。

方法引用中也可以使用 this 参数,例如:this::equals 等价于 x->this.equals(x)

同时,方法引用也可以使用 super 关键字,其表达式如:super::instanceMethod 调用超类的实例方法

//超类
class Greeter{
	public void greet(){
        System.out.println("hello world");
    }
}
//子类
class TimeGreeter extends Greeter{
    public void greet(){
        Timer t = new Timer(1000,super::greet);
        t.start();
    }
}

TimeGreeter.greet 方法开始执行时,首先会创建一个 Timer,它会在每次定时器过 1 秒后执行 super::greet 方法,而这个方法会调用超类的 greet 方法


4 构造器引用

构造器引用与方法引用类似,只不过方法名为 new。例如,Person::new 是 Person 构造器的一个引用。而此过程中引用哪一个构造器取决为上下文。

构造器引用示例:

//Employee类
public class Employee{
	private String name;
    private double salary;
    
    public Employee() {
    }

    public Employee(String name) {
        this.name = name;
    }
    
    public Employee(String name,double salary) {
        this.name = name;
        this.salary = salary;
    } 
}
//通过lambda创建Employee对象
public class test{
    public static void main(String[] args) {
        //无参构造
    	Supplier<Employee> lam = ()->new Employee();
        //带一个参数的构造方法
        Function<String,Employee> lam1 = (x)->new Employee(x);
        //带两个参数的构造方法
        BiFunction<String, Double,Employee> lam2 = (x, y)->new Employee(x,y);
        //使用lambda表示的构造器方法创建Employee对象
        Employee e0 = lam.get();
        Employee e1 = lam1.apply("zs");
        Employee e2 = lam2.apply("lisi",100.0);
    }
}

public class test{
    public static void main(String[] args) {
        //通过构造引用创建Employee对象
        //无参构造
		Supplier<Employee> fun0 = Employee::new;
        //带一个参数的构造方法
		Function<String,Employee> fun1 = Employee::new;
        //带两个参数的构造方法
		BiFunction<String, Double,Employee> fun2 = Employee::new;
		Employee e0 = fun0.get();
		Employee e1 = fun1.apply("zs");
		Employee e2 = fun2.apply("lisi",100.0);
        
        //对象的实例方法引用
        Supplier<String> fun = e1::getName;
        //通过方法引用的形式获取
        String name = fun.get();   //zs
    }
}

数组的构造器引用:

public static void main(String[] args){
	//lambda表达式
    Function<Integer,String[]> lam = (x) -> new String[x];
    //创建长度为10的数组
    String[] arr1 = lam.apply(10);
        
    //构造器调用
    Function<Integer,String[]> fun = String[]::new;
    String[] arr2 = fun.apply(10);
}

5 变量作用域

lambda 表达式是如何获取外围方法或类中的变量呢?lambda 表达式共有3部分:

1)1段代码块

2)参数

3)自由变量的值,指的是非参数而且不在代码块中定义的变量

看看下面这个例子:

public static void repeatMessage(String text,int delay){
	ActionListener listener = event -> {
        System.out.println(text);
    };
    new Timer(delay,listener).start();
}

调用上述定义的方法:

repeatMessage("hello",1000);   //每隔1秒打印一次"hello"

上述例子中,lambda 有一个自由变量 text(值为"hello")。我们可以把一个lambda表达式看作包含一个方法的对象,自由变量就是这个对象的实例变量。

在 lambda 表达式中获取外围作用域中的变量有一个重要的限制:在 lambda 表达式中,只能引用值不会改变的变量(即 lambda 表达式引用的变量无论在 lambda 中或者 lambda 表达式外部都不能发生改变)。

规则:lambda 表达式捕获引用的变量必须实际上是最终变量(即此变量初始化之后就不会再为它赋新值)。

其次,lambda 表达式的体与嵌套块(语句块)有相同的作用域,那么就需要注意命名冲突的问题:

String first = "abc";
Comparator<String> comparator = (first,second)->first.length()-second.length();
//error:lambda表达式中声明了一个与外部局部变量同名的参数

在 lambda 表达式中使用 this 关键字时,是指创建这个 lambda 表达式的方法的 this 参数:

public class Application(){
	public void init(){
        ActionListener listener = event -> {
            System.out.println(this.toString());
            ...
        }
    }
}

this.toString() 会调用 Application 对象的 toString 方法。


6 处理 lambda 表达式

使用 lambda 表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而不需要包装在一个 lambda 表达式中。延迟执行代码块的原因有很多:

  • 在一个单独的线程中运行代码
  • 多次运行代码
  • 在算法的适当位置运行代码(如排序中的比较操作)
  • 发生某种情况时才需要执行代码
  • 只在必要时才运行代码

例如:想要重复执行某段代码(重复打印 “hello”)

要接受 lambda 表达式,需要选择(或提供)一个函数式接口,这里使用 Runnable 接口:

public static void repeat(int n,Runnable action){
	for(int i=0;i<n;i++){
		action.run();
	}
}

//这是重复运行10次的 lambda 表达式
repeat(10,()->System.out.println("hello"));

只调用一次:

Runnable fun = ()->System.out.println("nm");
fun.run();  //打印一次 "nm"

调用 run 方法时,会执行 lambda 表达式的主体。

常见函数式接口

19.lambda 表达式【Java温故系列】_第1张图片

基本类型的函数式接口:最好使用这些带参数规范的函数式接口,这样可以减少自动装箱

19.lambda 表达式【Java温故系列】_第2张图片

:如果需要设计自己的函数式接口,其中只有一个抽象方法,可以使用 @FunctionalInterface 注解来标记这个接口,这样有两个优点:1.如果无意中增加了另一个非抽象方法,编译器会报错;2.Javadoc 页会指出你的接口是一个函数式接口。当然,不是必须使用注解,任何有一个抽象方法的接口都是函数式接口。不过使用注解是比较好的做法。

你可能感兴趣的:(【Java温故系列】)