zoukankan      html  css  js  c++  java
  • 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包。

  • 相关阅读:
    遥感图像增强方法应用
    如何从优秀的程序员成为伟大的程序员(2)
    略论提问的智慧
    从我的一篇技术文章说开去
    《虎胆龙威4》观后感
    Microsoft Platform SDK 选择
    ‘OpenThread”: undeclared identifier 问题的解决之道
    Java Web整合开发读书笔记
    Python:监控键盘输入、鼠标操作,并将捕获到的信息记录到文件中
    Getting Started with HTTPClient
  • 原文地址:https://www.cnblogs.com/fengyumeng/p/10052561.html
Copyright © 2011-2022 走看看