您的位置:首页 > 编程语言 > Java开发

Java 8 Lambda表达式详解

2016-01-28 16:55 591 查看

Lambda 代替匿名内部类

首先我们来看一个例子:

[code]package com.lyong.test;

public class Person {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void go(Person p, Moveable m) {
        m.move(p);
    }

    interface Moveable {
        public void move(Person p);
    }

    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setAge(20);
        p1.setName("Lyong");
        // 匿名内部类实现方式
        p1.go(p1, new Moveable() {

            @Override
            public void move(Person p) {
                System.out.println(p.getName() + "正在行动!");
            }

        });

        // Lambda实现方式
        p1.go(p1, (Person p)->{
            System.out.println(p.getName() + "正在行动!");
        });

    }
}


上述代码分别以匿名内部类和Lambda两种方式调用了go方法。对比可见Lambda表达式更简洁、方便。

下面我们来分析一下Lambda表达式的语法。

Lambda表达式组成

(1)形参列表:形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。

(2)箭头(->):必须通过英文中划线和大于符号组成。

(3)代码块:如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要花括号表示语句结束。Lambda代码块只有一跳return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。

看完上面的解释之后大家是否会有一个疑问呢?假如Moveable接口中有多个接口方法该如何呢?

接下来我们要针对这一问题继续讲解。

Lambda表达式与函数式接口

Lambda表达式的类型,也被成为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。

使用Lambda表达式限制

(1)Lambda表达式的目标类型必须是明确的函数式接口。

(2)Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象。

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

(1)将Lambda表达式赋值给函数式接口类型的变量。

(2)将Lambda表达式作为函数式接口类型的参数传给某个方法。

(3)使用函数式接口对Lambda表达式进行强制类型转换。

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

// 我们更换Runnable为Object,下面的代码编译时会报错:“不兼容的类型:Object不是函数接口”
Object obj = () -> {
    for(int i = 0;i < 100;i++){
        System.out.println(i);
    }
};

// 下面的对上述代码进行了强制类型转换,这样可以确定该表达式的目标类型为Runnable函数式接口,这样下面的代码编译和运行就完全正确了。
Object obj = (Runnable)() -> {
    for(int i = 0;i < 100;i++){
        System.out.println(i);
    }
};


需要说明的,同样的Lambda表达式的目标类型完全可能是变化的——唯一的要求是,Lambda表达式实现的匿名方法与目标类型(函数式接口)中唯一的抽象方法有相同的形参列表。

例如定义了如下接口:

[code]@FunctionalInterface
interface FkTest {
    void run();
}


上面的函数式接口中仅定义了一个不带参数的方法,因此前面强制转型为Runnable的Lambda表达式也可强转为FkTest类型。

[code]// 同样的Lambda表达式可以被当做成不同的目标类型,唯一的要求是Lambda表达式
// 的形参列表与函数式接口中唯一的抽象方法的形参列表相同。
Object obj = (FkTest)() -> {
    for(int i = 0;i < 100;i++){
        System.out.println(i);
    }
};


Java 8 预定义函数接口

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

(1)XxxFunction:这类接口中通常包含了一个apply()抽象方法,该方法对参数进行处理、转换(apply()方法的处理逻辑由Lambda表达式来实现),然后返回一个新的值。该函数接口通常用于对指定数据进行转换处理。

(2)XxxConsumer:这类接口中通常包含accept()抽象方法,该方法与XxxFunction接口中的apply()方法基本相似,也负责对参数进行处理,只是该方法不会返回处理结果。

(3)XxxPredicate:这类接口中通常包含一个test()抽象方法,该方法通常用来对参数进行某种判断(test()方法的判断逻辑由Lambda表达式来实现),然后返回一个boolean值。该接口通常用于判断参数是否满足特定条件,经常用于进行筛选数据。

(4)XxxSupplier:这类接口中通常包含一个getAsXxx()抽象方法,该方法不需要输入参数,该方法会按照逻辑算法(getAsXxx()方法的逻辑算法有Lambda表达式来实现)返回一个数据。

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

方法引用域构造器引用

Lambda表达式代码块只有一跳代码,还可以在代码中使用方法引用和构造引用。方法引用和构造器引用都需要使用两个英文冒号。Lambda表达式支持如下几种应用方式。

种类示例说明对应的Lambda表达式
引用类方法类名::类方法函数式接口中被实现方法的全部参数传给该类方法作为参数(a,b,…)->类名.类方法(a,b,…)
引用特定对象实例方法特定对象::实例方法函数式接口中被实现方法的全部参数传给该类方法作为参数(a,b,…)->特定对象.实例方法(a,b,…)
引用某类对象实例方法类名::实例方法函数式接口中实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数(a,b,…)->实例方法(b,…)
引用构造器类名::new函数式接口中被实现方法的全部参数传给该构造器作为参数(a,b,…)->类名(a,b,…)
定义如下函数式接口:

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


引用类方法

[code]        // 方式1:使用Lambda表达式创建Converter对象
        // 因为代码块只有一条语句,因此程序省略了该代码块的花括号;
        // 而且由于表达式所实现的convert()方法需要返回值,因此Lambda表达式将会吧这条代码的值作为返回值。
        Converter converter1 = from -> Integer.valueOf(from);
        System.out.println(converter1.convert("99"));

        // 方式2:方法引用代替Lambda表达式:引用类方法
        // 函数式接口中被实现方法的全部参数传给该类方法作为参数
        Converter converter2 = Integer::valueOf;
        System.out.println(converter2.convert("99"));


引用特定对象的实例方法

[code]// 方式1:使用Lambda表达式创建Converter对象
        // 因为代码块只有一条语句,因此程序省略了该代码块的花括号;
        // 而且由于表达式所实现的convert()方法需要返回值,因此Lambda表达式将会吧这条代码的值作为返回值。
        Converter converter3 = from -> "fkit.org".indexOf(from);
        System.out.println(converter3.convert("it"));

        // 方式2:方法引用代替Lambda表达式:引用类方法
        // 函数式接口中被实现方法的全部参数传给该类方法作为参数
        Converter converter4 = "fkit.org"::indexOf;
        System.out.println(converter4.convert("it"));


下面我们看第三种方法引用:引用某类对象的实例方法。例如,定义了如下函数式接口:

[code]    @FunctionalInterface
    interface MyTest {
        String test(String a,int b,int c);
    }


引用某类对象的实例方法

[code]// 方式1:下面代码使用Lambda表达式创建MyTest对象
// 表达式的代码块只有一条语句,因此程序省略了该代码块的花括号;
// 而且由于表达式所实现的test()方法需要返回值,因此Lambda表达式将会把这条代码的值作为返回值。
MyTest mt = (a, b, c) -> a.substring(b, c);
System.out.println(mt.test("Java I Love you", 2, 9));

// 方式2:方法引用代替Lambda表达式:引用某类对象的实例方法
// 函数式接口中被实现方法的第一个参数作为调用者
// 后面的参数全部传给该方法作为参数
MyTest mt2 = String::substring;
System.out.println(mt2.test("Java I Love you", 2, 9));


引用构造器

[code]    @FunctionalInterface
    interface YourTest {
        JFrame win(String title);
    }


[code]// 方式1:使用Lambda表达式创建YourTest对象
// 表达式的代码块只有一条语句,因此程序省略了该代码块的花括号;
// 而且由于表达式所实现的win()方法需要返回值,因此Lambda表达式将会把这条代码的值作为返回值。
YourTest yt = (String s) -> new JFrame(s);
JFrame jf = yt.win("我的窗口");
jf.setSize(500, 500);
jf.setVisible(true);

// 方式2:构造器引用代替Lambda表达式
// 函数式接口中被实现方法的全部参数传给构造器作为参数
// 对于构造器引用,也就是调用某个JFrame类的构造器来实现YourTest函数式接口中唯一的抽象方法,
// 当调用YourTtest接口中的唯一方法时,调用参数将会传给JFrame构造器。
YourTest yt1 = JFrame::new;
JFrame jf1 = yt.win("我的窗口");
jf.setSize(500, 500);
jf.setVisible(true);


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

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

(1)Lambda表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)。

(2)Lambda表达式创建于匿名内部类生成的对象一样,都可以直接调用接口中集成的默认方法。

下面程序示范了Lambda表达式与匿名内部类的相似之处。

[code]public class Test2 {

    @FunctionalInterface
    interface Displayable {
        // 定义一个抽象方法和默认方法
        void display();

        default int add(int a, int b) {
            return a + b;
        }
    }

    private int age = 24;
    private static String name = "Lyong";

    public void test() {
        String book = "Java 大讲堂";
        Displayable dis = () -> {
            // 访问“effectively final” 的局部变量
            System.out.println("book局部变量为:" + book);
            // 访问外部类的实例变量和类变量
            System.out.println("外部类的age实例变量为:" + age);
            System.out.println("外部类的namge类变量为:" + name);
        };
        dis.display();
        // 调用dis对象从接口中继承的add()方法
        System.out.println(dis.add(3, 5));
    }

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

}


上面程序使用Lambda表达式创建了一个Displayable的对象,Lambda表达式的代码中示范了访问”effectiviely final“的局部变量、外部类的实例变量和类变量。从这点来看,Lambda表达式的代码块与匿名内部类的方法体是相同的。

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

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

[code]System.out.println(dis.add(3, 5));


Lambda表达式与匿名内部类区别

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

(1)匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现了所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例。

(2)匿名内部类可以为抽象类甚至普通类创建实例;

(3)匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用接口中的默认方法。

关于Lambda表达式在JDK 1.8 api 中已经开始逐渐出现,例如Arrays类中的一些函数式接口。有兴趣的同学可以去研究一下具体的用法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: