Lambda 表达式

简介

  相传,在远古时代,有一位逻辑学家某某,想要形式化的表示能有效计算的数学函数,由于别的书中使用重音符^来表示自由变量,某某受此启发,使用大写的lambda(∧)表示参数,后来又改成了小写的lambda(λ),从此以后,带参数变量的表达式就被称为lambda表达式,读音:lan b(m)da (兰亩达)。

  到了2015年,lambda表达式加入了JDK8,它显著的增强了Java,在最近几年中,lambda表达式已经成为了计算机语言设计的重点关注对象,几年前的泛型重塑了Java,如今lambda表达式也正在重塑Java的编程风格。

  简而言之:lambda表达式,无论如何,就算天崩地裂,得了绝症,也得学!

  言归正传

  lambda表达式本质上就是一个匿名方法,但是这个方法不是独立执行的,而是用于实现由函数式接口定义的另一个方法,因此lambda表达式会导致产生一个匿名类,也可以称之为闭包。

语法结构

  lambda表达式在Java语言中引入了一个新的语法元素和操作符,这个操作符是->,有时候被称为lambda操作符或者箭头操作符,它将lambda表达式分成两个部分,左侧指定了lambda表达式需要的所有参数(不需要参数则使用空括号),右侧是表达式的主体。

  下面看一个最简单的lambda表达式:

  ()-> 12;

这个lambda表达式没有参数,但是它有返回值,返回的是Int类型。

  如果代码要完成的功能无法放在一个表达式中,就可以像写方法一样,把代码放在代码块中,用大括号包起来。

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

这个lambda没有返回值,也可以说它的返回值是void,如果有返回值的话,需要在表达式主体的最后使用return关键字返回指定类型的数据。

  下面来看一个有参数的lambda表达式

  (int n,int m)->n+m;

参数为int,返回值也是int类型。

  不过一般来说我们这么写:

  (n,m)->n+m;

  如果只有一个入参,你甚至连括号都可以省:

  n -> n+1;

省略了参数类型,因为参数类型是可以被自动推断出来的。

函数式接口

  函数式接口是仅包含一个抽象方法的接口,可以反过来这么说,凡是只包含一个抽象方法的接口,都可以叫做函数式接口。

  为什么要说函数式接口呢?因为lambda表达式的运行需要依赖函数式接口。

  从JDK8开始,可以为接口的声明的方法指定默认行为,就是所谓的默认方法(在接口中用default关键字声明的方法,并且可以在接口中直接实现该方法,使得实现该接口的类不需要实现该方法,就如继承一样直接调用),因为该默认方法没有指定默认实现,所以它就是隐式的是抽象方法,没有必要使用abstract修饰符,当然,如果愿意的话,也可以加上abstract修饰符。

示例:

  public interface Admin {
           String getName();
    }

随便写的一个接口,只要这个接口中只有一个抽象方法,那么这就是一个函数式接口。(注意措辞:只有一个抽象方法,并不是只有一个方法,因为还可以存在默认方法)

  lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,也就是说,lambda表达式构成了一个函数式接口定义的抽象方法的实现,该函数式接口定义了它的目标类型。

 

下面示例lambda表达式的使用方法(使用到上面的Admin接口):

        Admin admin = ()->"张三";
        System.out.println(admin.getName());

打印结果为:张三

  当目标类型上下文出现lambda表达式时,会自动创建实现了该函数式接口的一个类的实例,函数式接口声明的抽象方法的行为由lambda表达式定义,当通过目标调用该方法时,就会执行lambda表达式,因此,lambda表达式提供了一种将代码片段转换为对象的方法。

  也就是说,也就是说,也就是说,重要的事情说三遍:如果我写一个方法,参数是Admin类型,那么我可以调用这个方法直接传入lambda表达式即可,为什么连说三遍呢?因为这是最流行的用法,也是lambda的风骚之处:传递行为。

比如:

    private static void myName(Admin admin){
        String name = admin.getName();
        System.out.println(name);
    }
然后我可以这么调用:myName(()->"张三");

  但是本篇博客剩下的例子几乎不会这样子写,是因为代码多了不易让人理解,因此尽量写的直接一些。

 

  当然,老土的办法是用内部类来实现:

        Admin admin = new Admin() {
            @Override
            public String getName() {
                return "张三";
            }
        };
        System.out.println(admin.getName());

 

类型检查与类型推断

  为什么下面这个代码不能编译呢?

  Object o = () -> System.out.println("张三");

  因为lambda表达式上下文的目标类型必须是一个函数式接口,而Object并不是函数式接口

  我下面这个接口是函数式接口

public interface User {
    String userInfo(String name,int age);
}

  那么我这样子可以吗?

  User user = () -> System.out.println("张三");

也是不行的,因为User接口的抽象方法是有入参也有返回值的,但是() -> System.out.println("张三")却是一个没有入参也没有返回值的表达式。

  所以说lambda表达式的入参和返回值必须要和函数式接口兼容。

 

  User函数式接口的入参是String和int,返回值是String,正确的用法应该是这样的:

        String name = "小红";
        int age = 19;
        User user = (String username,int userage) -> {
            return username+"今年"+userage+"岁了";
        };
        System.out.println(user.userInfo(name,age));  

 

可以更简洁一点吗? 可以的。

  Java编译器会从上下文来推断出用什么函数式接口来配合lambda表达式,它也可以推断出适合lambda表达式的签名。

  就像我们经常使用的菱形运算符一样:

        HashMap<String,Integer> map1 = new HashMap<String,Integer>();
        HashMap<String,Integer> map2 = new HashMap<>();

  因此省略参数类型也是可以的:

        User user = (username,userage) -> {
            return username+"今年"+userage+"岁了";
        };

  有时候写明参数类型更易读,有时候省略参数类型更易读,这个就是仁者见仁智者见智了。

 

还可以再简洁一点吗?当然可以。

  注意这个lambda表达式的主体,并不是什么复杂的计算流程,它仅仅只是一个普通的表达式。

  所以可以不用块表达式:

  User<String> user2 = (username,userage) -> username+"今年"+userage+"岁了";

  就这样一行代码就完事,自带隐式的return。

泛型函数式接口

  lambda表达式的类型推断是相当的智能,不过,如果我写这么个函数式接口,泛型,试一试它还能不能智能的起来?

public interface MyUser<T> {
    String userInfo(String name,T age);
}

  显然不可能,编译都过不了,因为它已经懵逼了,不知道你的参数到底是个什么类型。

  这时候,就需要在lambda表达式的目标类型上指定参数类型:

        String name = "小芳";
        int age1 = 17;
        double age2 = 17.5;

        MyUser<Integer> myUser = (n,a) -> n+a;
        String str1 = myUser.userInfo(name,age1);
        System.out.println(str1);

        MyUser<Double> myUser1 = (n,a) -> n+a;
        String str2 = myUser1.userInfo(name,age2);
        System.out.println(str2);

引用值,而不是变量

  我们目前为止在lambda表达式主体中使用的变量都是传进来的参数,在lambda表达式中,可以访问外层作用域定义的变量,将其引用为当前表达式内的局部变量,这叫做变量捕获。

  在这种情况下,lambda表达式只能使用final的局部变量,也就是说,被lambda表达式捕获的外层变量,都会自动变成实质上的final类型,final变量是指在第一次赋值以后,值不能再发生变化的变量。

示例:

        String name = "小红";
        int age = 22;
        int status = 1;
        User user = (n,a) -> {
            //status ++; //不允许
            return n+a+"状态是"+status;
        };
        //status ++; //不允许
        String str = user.userInfo(name,age);
        System.out.println(str);

示例中的status变量被lambda表达式捕获之后,在lambda表达式中不能修改,在外层也不能被修改,

  实际上lambda在访问外层变量时,访问的是变量的副本,并不是原始变量。

  换句话说,lambda表达式引用的是值,而不是变量。

 

方法引用

深入剖析方法引用

  方法引用的基本思想是:如果一个lambda表达式代表的只是直接调用这个方法,那么最好还是用名称来调用它,而不是去描述如何调用它。

  方法引用提供了一种引用而不执行的方式,这种特性与lambda表达式相关,因为它也需要由兼容的函数式接口构成的目标类型上下文,运行的时候,方法引用也会创建函数式接口的一个实例。

  当你需要使用方法引用时,目标引用放在分隔符::前,方法名称放在后面,例如User::getName就是引用了User类中的getName()方法,请记住,不需要括号,因为你没有实际调用这个方法,它其实就是(User a)->a.getName()的快捷写法。

下面走一个例子:

  函数式接口

public interface User {
    String userInfo(String name,int age);
}

  处理用户信息的类

public class MyInfo {
    static String name(String name,int age){
        if(age < 18){
            name = "少年人"+name;
        }else if(age > 18 && age< 28){
            name = "青年人"+name;
        }else{
            name = "老年人"+name;
        }
        return name+"年龄是"+age;
    }
}

  接口调用方法

    public static String userThink(User user,String name,int age){
        return user.userInfo(name,age);
    }

  主函数

    public static void main(String[] args) {
        String name = "小明";
        int age = 15;
        String outStr = userThink(MyInfo::name,name,age);
        System.out.println(outStr);
    }

  观察可以得知 MyInfo::name进入userThink方法之后变成了User接口的一个实例,MyInfo类的name方法需要两个参数,而引用的时候并没有传参,因此更加证实了方法引用并非方法调用。

 

  如果我这么改一下 你可能会恍然大悟:

        User user = MyInfo::name;
        String info = user.userInfo(name,age);
        System.out.println(info);

  原来引用的方法就是lambda表达式的主体,牢记这一点非常重要。

  因此我们这么下结论:如果lambda表达式的主体内容是调用一个方法,那么就可以使用方法引用,当然,引用的方法必须与上下文所使用的函数式接口相兼容。

下面介绍几种方法引用的例子:

lambda表达式 等效的方法引用
() -> Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str,i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println

 

 

 

 

 

  本节示例MyInfo中的name方法是一个静态方法,如果不是静态方法呢?

  不是静态方法的话,那么就需要将整个类new出来,再进行该对象的方法引用。

 

  还有一种情况,就如上面表格中的(str,i) -> str.substring(i)为什么可以用方法引用String::substring来替代呢? 

  substring是静态方法吗? 不是的。

  String类有new出来吗? 也没有。

  原因只有一个:String实例是传入lambda表达式的参数,因此str本身就是String的实例,拥有String的所有方法。

  如果你要引用一个对象的方法,而这个对象本身是lambda的一个参数,那么Java允许你直接引用。

构造函数引用

  构造器引用与方法引用是一样的,只不过方法名固定为new,例如:Admin::new是Admin构造器的一个引用,它就相当于 () -> new Admin(),注意,也就是说当前使用的函数式接口的返回类型必须兼容Admin。

下面走一个示例(写示例是最头疼的事情了 汗):

  一个学生类

public class Student {
    String name;
    int grade = 0;

    public Student(String name) {
        this.name = name;
    }
    
    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }
    public String getName() {
        return name;
    }
    public int getGrade() {
        return grade;
    }
}

 

  函数式接口 返回类型是Student

public interface StudentInterface {
    Student getStudent(String name,int grade);
}

 

  代码这么走:

        StudentInterface studentInterface = Student::new;
        Student student = studentInterface.getStudent("小明",2);
        System.out.println(student.getName()+student.getGrade());

  思考一下,Student类中有两个构造函数,为什么代码就逮着第二个构造函数走呢?

  这就是lambda的类型推断特性了,走哪个构造函数是根据上下文的函数式接口来决定的,StudentInterface接口的入参是两个参数,一个String一个int,返回类型是Student,出入条件都符合第二个构造函数,所以它就会走第二个构造函数。

 

  那如果我要走第一个构造函数该怎么做呢?

很简单,稍微改一下

public interface StudentInterface2 {
    Student getStudent(String name);
}

  用这个函数式接口去接收方法引用即可。

 

泛型中的方法引用

  在泛型类或泛型方法中也可以使用方法引用,再来一个例子:

  这个是要引用的泛型方法

public class MyInfo2 {
    static <T> String name(String name,T age){
        return name+"年龄是"+age;
    }
}

  函数式接口

public interface MyUser<T> {
    String userInfo(String name,T age);
}

  调用

        String name = "小明";
        int age = 16;
        double a = 16.6;
        MyUser user = MyInfo2::name;
        String info = user.userInfo(name,age);
        System.out.println(info);

  这里传int或double都是可以的。

  如果要限制只能传int呢?

  那就这样子 

MyUser<Integer> user = MyInfo2::name; //这时传double就不行了

 

  请注意,它的原型实际上是这样的:

MyUser<Integer> user = MyInfo2::<Integer>name;

  但是由于存在类型推断,所以::后面的类型指定是可以省略的。

 

内置的函数式接口

  当我们设计自己的函数式接口时,可以用注解@FunctionalInterface来标记这个接口,这样子这个接口就只能成为函数式接口,不允许再增加别的抽象方法,另外javadoc里也会指出这是一个函数式接口。

  当然,最好还是使用Java给我们内置的函数式接口,个人认为已经可以满足大部分编程需要了,并且很多接口都有非抽象的方法可以使用。

以下列出常用的函数式接口:

函数式接口 返回类型 参数类型 抽象方法名
Runnable void run
Supplier<T> T get
Consumer<T> void T accept
BiConsumer<T,U> void T,U accept
Function<T,R> R T apply
BiFunction<T,U,R> R T,U apply
UnaryOperator<T> T T apply
BinaryOperator<T> T T,T apply
Predicate<T> boolean T test
BiPredicate<T,U> boolean T,U test

 

 

 

 

 

 

 

 

 

 

 

 

基本类型函数式接口 

  如 ToLongFunction IntToLongFunction IntConsumer 等等等等  接口名称已经代表了他们的功能

  有三四十个这样的函数式接口,我这里就不方便出来了,详细请查阅java.util.function包。

 

posted @ 2018-12-02 11:18  不该相遇在秋天  阅读(966)  评论(1编辑  收藏  举报