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包。