Java8新特性之Lambda表达式

Lambda表达式是Java8的重要更新,也是一个被广大开发者期待已久的新特性。Lambda表达式支持将代码块作为方法参数,Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。

Lambda表达式入门

public class CommandTest {
    public static void main(String[] args) {
        ProcessArray pa = new ProcessArray();
        int[] target = {3,-4,6,4};
        //处理数组,具体处理行为取决于匿名内部类
        pa.processA(target, new Command() {
            @Override
            public void processC(int[] target) {
                int sum=0;
                for(int tmp:target) {
                    sum+=tmp;
                }
                System.out.println("数组元素的总和是:"+sum);
            }
        });
    }
}

interface Command{
    void processC(int[] target);
}

class ProcessArray{
    public void processA(int[] target,Command cmd){
        cmd.processC(target);
    }
}

ProcessArray类的processA()方法处理数组时,希望可以动态传入一段代码作为具体的处理行为,因此程序创建了一个匿名内部类实例来封装处理行为。从上面代码可以看出,用于封装处理行为的关键就是实现程序中的processC方法。但是为了向processA()方法传入这段粗体字代码,程序不得不使用匿名内部类的语法来创建对象。
Lambda表达式完全可用于简化创建匿名内部类对象,因此可将上面代码改为如下形式。

ProcessArray pa = new ProcessArray();
int[] array = {3,-4,6,4};
//处理数组,具体处理行为取决于匿名内部类
pa.processA(array,(int[] target)-> {
    int sum = 0;
    for (int tmp:target) {
        sum +=tmp;
    }
    System.out.println("数组元素的总和是:"+sum);
});

上面代码与创建匿名内部类时需要实现的processC(int[] target)方法完全相同,只是不需要new Xxx(){}这种烦琐的代码,不需要指出重写的方法名字,也不需要给出重写的方法的返回值类型——只要给出重写的方法括号以及括号里的形参列表即可。

从上面的介绍可以看出,当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会替代实现抽象方法的方法体,Lambda表达式就相当于一个匿名方法。

从上面的语法格式可以看出,Lambda表达式的主要作用就是代替匿名内部类的烦琐语法。它由三部分组成。

  • 形参列表。形参列表允许省略形参类型。如果形参列表只有一个参数,甚至连形参列表的圆括号也可以省略
  • 箭头(->)。必须通过英文画线号和大于符号组成
  • 代码块。如果代码块只包含一个语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。

下面是Lambda表达式的几种简化写法:

interface Eatable{
    void taste();
}
interface Flyable{
    void fly(String weather);
}
interface Addable{
    int add(int a,int b);
}

public class LambdaQs {
    //调用该方法需要使用Eatable对象
    public void eat(Eatable e){
        System.out.println(e);
        e.taste();
    }
    //调用该方法需要Flyable对象
    public void drive(Flyable f){
        System.out.println("我正在驾驶:"+f);
        f.fly("[晴天]");
    }
    //调用该方法需要Addable对象
    public void test(Addable add){
        System.out.println("5与3的和为:"+add.add(5,3));
    }

    public static void main(String[] args) {
        LambdaQs lq = new LambdaQs();
        //Lambda表达式的代码块只有一条语句,可以省略花括号
        lq.eat(()-> System.out.println("苹果好吃"));
        //Lambda表达式的形参列表只有一个形参,可以省略圆括号
        lq.drive(weather -> {
            System.out.println("今天的天气是:"+weather);
            System.out.println("直升机飞机平稳");
        });
        //Lambda表达式的代码块只有一条语句,可以省略花括号
        //代码块中只有一条语句,即使该表达式需要返回值,也可以省略return关键字
        lq.test((a,b)->a+b);
    }
}

运行结果是:

com.tianhao.luo.lambda.LambdaQs$$Lambda$1/990368553@6d311334
苹果好吃
我正在驾驶:com.tianhao.luo.lambda.LambdaQs$$Lambda$2/1023892928@214c265e
今天的天气是:[晴天]
直升机飞机平稳
5与3的和为:8

上面代码中,第一段Lambda表达式相当于不带形参的匿名方法,由于该Lambda表达式的代码块只有一行代码,因此可以省略代码块的花括号;第二段Lambda表达式相当于只带一个形参的匿名方法,由于该Lambda表达式的形参列表只有一个形参,因此省略了形参列表的圆括号;第三段Lambda表达式的代码块中只有一行语句,这行语句的返回值将作为该代码块的返回值。

上面三个方法,传入的都是Lambda表达式;程序可以正常编译、运行,这说明Lambda表达式实际上会被当成一个"任意类型"的对象,到底要当成何种类型的对象,这取决于运行环境的需要。下面将详细介绍Lambda表达式被当成何种对象。

Lambda表达式与函数式接口

Java8函数式接口与@FunctionlInterface

Lambda表达式的类型,也被称为"目标类型",Lambda表达式的目标类型必须是"函数式接口"。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。

如果采用匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下可采用Lambda表达式来创建对象,该表达式创建出来的对象的目标类型就是这个函数式接口。查询Java8的API文档,可以发现大量的函数式接口,例如:Runnable、ActionListener等接口都是函数式接口。

new Thread(()-> {
    System.out.println("aaaa");
}).start();

这里就是Runnable接口的Lambda表达式创建对象。

由于Lambda表达式的结果就是被当成对象,因此程序中完全可以使用Lambda表达式进行赋值,例如如下代码:

//Runnable接口中只包含一个无参数的方法
//Lambda表达式代表的匿名方法实现了Runnable接口中唯一的、无参数的方法
//因此下面的Lambda表达式创建了一个Runnable对象
Runnable r = () ->{
    for (int i = 0; i < 100; i++) {
        System.out.print(i+",");
    }
};

Lambda表达式实现的是匿名方法——因此它只能实现特定函数式接口中的唯一方法。这意味着Lambda表达式有如下两个限制。

  • Lambda表达式的目标类型必须是明确的函数式接口。
  • Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象。

为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式。

  • 将Lambda表达式赋值给函数式接口类型的变量
  • 将Lambda表达式作为函数式接口类型的参数传给某个方法
  • 使用函数式接口对Lambda表达式进行强制类型转换

Java8在java.util.function包下预定义了大量函数式接口,典型地包含如下4类接口。

  • XxxFunction:这类接口通常包含一个apply()抽象方法,该方法对参数进行处理、转化(apply()方法的处理逻辑由Lambda表达式来实现),然后返回一个新的值。该函数式接口通常用于对指定数据进行转换处理
  • XXXConsumer:这类接口中通常包含一个accept()抽象方法,该方法与XXXFunction接口中的apply()方法基本相似,也负责对参数进行处理,只是该方法不会返回处理结果
  • XXXXPredicate:这类接口中通常包含一个test()抽象方法,该方法通常用来对参数进行某种判断(test()方法的判断逻辑由Lambda表达式来实现),然后返回一个boolean值。该接口通常用于判断参数是否满足特定条件,经常用于进行筛选数据
  • XXXSupplier:这类接口中通常包含一个getAsXXX()抽象方法,这个方法不需要输入参数,该方法会按某种逻辑算法(getAsXXX()方法的逻辑算法由Lambda表达式来实现)返回一个数据。

综上所述,不难发现Lambda表达式的本质很简单,就是使用简洁的语法来创建函数式接口的实例——这种语法避免了匿名内部类的烦琐。

方法引用与构造器引用

上面已经知道,如果Lambda表达式的代码块只有一条代码,程序就可以省略Lambda表达式中代码块的花括号。不仅如此,如果Lambda表达式的代码块只有一条代码,还可以再代码块中使用方法引用和构造器引用。
方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。Lambda表达式支持如下几种引用方式

引用类方法

public class MethodRefer {
    public static void main(String[] args) {
//        Converter converter = from -> Integer.valueOf(from);
        //方法引用代替Lambda表达式:引用类方法
        //函数式接口中被实现方法的全部参数传给该类方法作为参数
        Converter converter = Integer::valueOf;
        Integer val = converter.convert("99");
        System.out.println(val);
    }
}

@FunctionalInterface
interface Converter {
    Integer convert(String from);
}

其中定义了一个函数式接口Converter,使用Lambda表达式来创建接口对象。上面Lambda表达式的代码块只有一行调用类方法的代码,因此可以使用如下方法引用进行替换:

//方法引用代替Lambda表达式:引用类方法
//函数式接口中被实现方法的全部参数传给该类方法作为参数
Converter converter = Integer::valueOf;

对于上面的类方法引用,也就是调用Integer类的valueOf()方法来实现Coverter函数式接口中唯一的抽象方法,当调用Converter接口中的唯一的抽象方法时,调用参数将会传给Integer类的valueOf()类方法。

引用特定对象的实例方法

//        -----------------引用特定对象的实例方法-------------------------------
        //下面代码使用Lambda表达式创建Converter对象
//        Converter converter = from -> "fkit.org".indexOf(from);
        //方法引用替代Lambda表达式:引用特定对象的实例方法
        //函数式接口中被实现方法的全部参数传给该方法作为参数
        Converter converter = "fkit.org"::indexOf;
        Integer value = converter.convert("it");
        System.out.println(value);

对于上面的实例方法引用,也就是调用"fkit.org"对象的indexOf()实例方法来实现Coverter函数式接口中唯一的抽象方法,当调用Converter接口中的唯一抽象方法时,调用参数将会传给"fkit.org"对象的indexOf()实例方法。

引用某类对象的实例方法

//        ----------------------引用某类对象的实例方法-----------------------------------------
        //下面代码使用Lambda表达式创建MyTest对象
//        MyTest mt = (a,b,c) -> a.substring(b,c);
        //方法引用替代Lambda表达式:引用某类对象的实例方法
        //函数式接口被实现方法的第一个参数作为调用者
        //后面的参数全部传给该方法作为参数
        MyTest mt =  String::substring;
        String str = mt.test("Java I Love You", 2, 9);
        System.out.println(str);

对于上面的实例方法引用,也就是调用某个String对象的subString()实例方法来实现MyTest函数式接口中唯一的抽象方法,当调用MyTest接口中的唯一抽象方法时,第一个调用参数将作为substring()方法的调用者,剩下的调用参数会作为substring()实例方法的调用参数。

引用构造器

//        ----------------------引用构造器-------------------------
        //下面代码使用Lambda表达式创建YourTest对象
//        YourTest yt = (String a)->new JFrame(a);
        //构造器引用代替Lambda表达式
        //函数式接口中被实现的方法的全部参数传给该构造器作为参数
        YourTest yt = JFrame::new;
        JFrame jf = yt.win("我的窗口");
        System.out.println(jf);

对于上面的构造器引用,也就是调用某个JFrame类的构造器来实现YourTest函数式接口中唯一的抽象方法,当调用YourTest接口中的唯一的抽象方法时,调用参数将会传给JFrame构造器。从上面的程序中可以看出,调用YourTest对象的win()抽象方法时,实际只传入了一个String类型的参数,这个String类型的参数会被传给JFrame构造器——这就确定了是调用JFrame类的、带一个String参数的构造器。

Lambda表达式与匿名内部类的联系和区别

从上面可以看出,Lambda表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用,Lambda表达式与匿名内部类存在如下相同点。

  • Lambda表达式与匿名内部类一样,都可以直接访问"effectively final"的局部变量,以及外部类的成员变量(包括实例变量和类变量)
  • Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法
public class LambdaAndInner {
    private int age = 12;
    private static String name = "软件中心";
    public void test(){
        String book = "疯狂讲义";
        Displayable dis = ()->{
            //访问"effectively final"的局部变量
            System.out.println("book局部变量:"+book);
            //访问外部类的实例变量和类变量
            System.out.println("外部类的age实例变量为:"+age);
            System.out.println("外部类的name类变量为:"+name);
        };
        dis.display();
        //调用dis对象从接口中继承的add方法
        System.out.println(dis.add(3,5));
    }

    public static void main(String[] args) {
        LambdaAndInner andInner = new LambdaAndInner();
        andInner.test();
    }
}

@FunctionalInterface
interface Displayable{
    //定义一个抽象方法和默认方法
    void display();
    default int add(int a,int b){
        return a+b;
    }
}

运行结果是:

book局部变量:疯狂讲义
外部类的age实例变量为:12
外部类的name类变量为:软件中心
8

上面的Lambda表达式的代码块,分别访问了"effectively final"的局部变量、外部类的实例变量和类变量。从这点来看,Lambda表达式的代码块与匿名内部类的方法体是相同的。

与匿名内部类相似的是,由于Lambda表达式访问了book局部变量,因此该局部变量相当于有一个隐式的final修饰,因此同样不允许对book局部变量重新赋值。

当程序使用Lambda表达式创建了Displayable的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法。

Lambda表达式与匿名内部类主要存在如下区别:

  • 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例
  • 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用接口中定义的默认方法。

虽然Lambda表达式的目标类型:Displayable中包含了add()方法,但Lambda表达式的代码块不允许调用这个方法;如果将上面的Lambda表达式改为匿名内部类的写法,当匿名内部类实现display()抽象方法时,则完全可以调用这个add()方法。

使用Lambda表达式调用Arrays的类方法

前面有介绍Array类的功能,Arrays类的有些方法需要Comparator、XXXOperator、XXXFunction等接口的实例,这些接口都是函数式接口,因此可以使用Lambda表达式来调用Arrays的方法。

public class LambdaArrays {
    public static void main(String[] args) {
        String[] arr1 = new String[]{"java", "fkava", "fkit", "ios", "android"};
        Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
        System.out.println(Arrays.toString(arr1));
        int[] arr2 = new int[]{3, -4, 25, 16, 30, 18};
        Arrays.parallelPrefix(arr2, ((left, right) -> left * right));
        System.out.println(Arrays.toString(arr2));
        long[] arr3 = new long[5];
        //operand代表正在计算的元素索引
        Arrays.parallelSetAll(arr3, operand -> operand * 5);
        System.out.println(Arrays.toString(arr3));
    }
}

运行结果如下:

[ios, java, fkit, fkava, android]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]

Lambda表达式让程序更加简洁

posted @ 2020-04-23 16:34  春刀c  阅读(225)  评论(0编辑  收藏  举报