关于lamdba表达式,网上文章很多,我也看了很多篇文章,不过在实际编程中,却怎么也运用不到实际中,因为没有系统地整理过这些知识块。所以,就利用一个下午,去专门整理一下,把自己的理解写出来。若有错误,或者补充,欢迎评论。
本篇先以运用为开头,原理在后文。先用4个例子来运用一下lamdba表达式。文章顺序如下:
- 4个例子 :用上jdk1.8 提供的java.util.function的函数式接口,以及自己创建的函数式接口。从4个例子中,知道如何运用lamdba表达式。
- 总结4个例子的特点
- lambda的类型检查,推断过程。
- lamdba的局部变量。
- Lambda表达式 vs 匿名类
- lamdba的方法引用
4个例子:
Predicate接口:
- 关注的点:
- 是java.util.function 里面的函数式接口。
- testPredicate()方法的参数其中一个是Predicate接口,该接口是函数式接口,因此此类型的参数,是可以用lamdba表达式的。
- 接口里面有个boolean test() 方法,可以看到testPredicate()方法里面直接调用了这个test()方法,返回值是boolean
/**
* 找到列表里面的是给我丶鼓励字符串就输出
* @param list
* @param pre Predicate接口是函数式接口,test(T t)方法返回值是boolean
* @param
*/
public static void testPredicate(List list, Predicate pre){
for(T s:list){
if (pre.test(s)){
System.out.println("这就是你要找的"+s);
}}}
public static void main(String[] ages){
List list=new ArrayList<>();
list.add("无敌小脾气");
list.add("无敌小任性");
list.add("开心小程序");
list.add("给我丶鼓励");
//用lamdba重写test()方法, s.equals(xxxx) 就是重写test()方法的内容
testPredicate(list,(s)->s.equals("给我丶鼓励"));
//使用Predicate的默认方法 从jdk1.8 api 可以看到接口里具有默认方法,.isEqual()就是一个默认方法
testPredicate(list,Predicate.isEqual("开心小程序"));
}
输出
这就是你要找的给我丶鼓励
这就是你要找的开心小程序
Consumer接口
- 关注的点
- 也是java.util.function 里面的函数式接口。
- 该接口里面的accept(T t)方法返回值是void。
/**
* 将list列表的数据输出
* @param list
* @param con Consumer是一个函数式接口,accept方法返回值是void
* @param
*/
public static void testConsumer(List list, Consumer con){
for(T s:list){
con.accept(s);
}
}
public static void main(String[] ages){
System.out.println("ok");
List list=new ArrayList<>();
list.add("无敌小脾气");
list.add("无敌小任性");
list.add("开心小程序");
list.add("给我丶鼓励");
//用lamdba重写accept()方法
testConsumer(list,s-> System.out.println(s));
//用方法引用
testConsumer(list,System.out::println);
}
Function接口
- 注意的点:
- Function
Function接口具有两个泛型,一般接受一个泛型T对象,并返回一个R对象。 - 该接口的applys(T t)方法,返回的是R类型结果。
- Function
/**
* 每个列表元素最后一个字凑成一个字符串输出
*
*/
public static void testFunction(List list, Function fun){
StringBuffer stringBuffer=new StringBuffer();
for(T s:list){
//将每个list对象的最后一个字符提取出来放到stringBuffer变量里。
stringBuffer.append(fun.apply(s));
}
System.out.println(stringBuffer);
}
public static void main(String[] ages){
System.out.println("ok");
List list=new ArrayList<>();
list.add("无敌小脾气");
list.add("无敌小任性");
list.add("开心小程序");
list.add("给我丶鼓励");
//用lamdba重写apply()方法
testFunction(list,s->s.charAt(s.length()-1));
}
自定义接口
- 注意的点:
- 建议规范,自定义的函数式接口用@FunctionalInterface 注解,其作用就像@Override 重写注解一样,告诉编译期,该接口是函数式接口。
- 函数式接口只能有一个抽象方法,可以有多个默认方法。
//自定义函数式接口
@FunctionalInterface
interface mylamdba{
//抽象方法
abstract void mylamdba(T s);
//默认方法
default String getName(Object obj){
return obj.toString();
}}
public static void testMy(List list,mylamdba my){
for (T s:list) {
//调用自定义接口的抽象方法。将用lambda方法重写
my.mylamdba(s);
}
}
public static void main(String[] ages){
System.out.println("ok");
List list=new ArrayList<>();
list.add("无敌小脾气");
list.add("无敌小任性");
list.add("开心小程序");
list.add("给我丶鼓励");
//自己写的一个函数式接口
testMy(list,System.out::println);
}
总结
- 函数式接口之间的区别:
- 抽象方法的返回值不一样,参数个数也不一样。
- 默认方法啊不一样。
lambda的类型检查,推断过程。
- Lambda的类型是从使用Lamdba的上下文推断出来的。
- 因此,在写Lambda表达式的时候,我们可以不写参数类型,这样可以大大简洁我们的Lamdba表达式。
上下文:比如接受它传递的方法的参数,或接受他的值的局部变量。
lamdba的局部变量
- 使用局部变量的时候,自动添加(final)
- 它不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。lambda 表达式基于词法作用域,也就是说 lambda 表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括 lambda 表达式的形式参数)。
public class Hello {
Runnable r1 = () -> { System.out.println(this); }
Runnable r2 = () -> { System.out.println(toString()); }
public String toString() { return "Hello, world"; }
public static void main(String... args) {
new Hello().r1.run();
new Hello().r2.run();
}
}
输出:
Hello$1@5b89a773 和 Hello$2@537a7706
因为toString()可以当做是方法内存块的变量,该变量指向一个堆地址。所以在lamdba里面,输出的是该变量指向的地址。
- lambda 表达式对 值 封闭,对 变量 开放.
int sum = 0;
list.forEach(e -> { sum += e.size(); }); // 对值封闭,编译出错
List aList = new List<>();
list.forEach(e -> { aList.add(e); }); // 对变量开放,编译通过
Lambda表达式 vs 匿名类
既然lambda表达式即将正式取代Java代码中的匿名内部类,那么有必要对二者做一个比较分析。
- 一个关键的不同点就是关键字 this。匿名类的 this 关键字指向匿名类,而lambda表达式的 this 关键字指向包围lambda表达式的类。
- 另一个不同点是二者的编译方式。Java编译器将lambda表达式编译成类的私有方法。使用了Java 7的 invokedynamic 字节码指令来动态绑定这个方法。
方法引用的种类
方法引用有很多种,它们的语法如下:
- 静态方法引用:ClassName::methodName
- 实例上的实例方法引用:instanceReference::methodName
- 超类上的实例方法引用:super::methodName
- 类型上的实例方法引用:ClassName::methodName
- 构造方法引用:Class::new
- 数组构造方法引用:TypeName[]::new
- 对于静态方法引用,我们需要在类名和方法名之间加入 :: 分隔符,例如 Integer::sum
对于具体对象上的实例方法引用,我们则需要在对象名和方法名之间加入分隔符:
Set knownNames = ...
Predicate isKnown = knownNames::contains;
这里的隐式 lambda 表达式(也就是实例方法引用)会从 knownNames 中捕获 String 对象,而它的方法体则会通过Set.contains 使用该 String 对象。
有了实例方法引用,在不同函数式接口之间进行类型转换就变的很方便:
Callable c = ...
Privileged a = c::call;
引用任意对象的实例方法则需要在实例方法名称和其所属类型名称间加上分隔符:
Function upperfier=String::toUpperCase;
这里的隐式 lambda 表达式(即 String::toUpperCase 实例方法引用)有一个 String 参数,这个参数会被 toUpperCase 方法使用。
数组的构造方法引用的语法则比较特殊,为了便于理解,你可以假想存在一个接收 int 参数的数组构造方法。参考下面的代码:
IntFunction arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 创建数组 int[10]
例子
我们通过一个实际例子(按照姓对名字列表进行排序)来演示这一点:
List people = ...
Collections.sort(people, new Comparator() {
public int compare(Person x, Person y) {
return x.getLastName().compareTo(y.getLastName());
}
})
冗余代码实在太多了!有了lambda表达式,我们可以去掉冗余的匿名类:
Collections.sort(
people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));
在类型推导和静态导入的帮助下,我们可以进一步简化上面的代码:
Collections.sort(people, comparing(p -> p.getLastName()));
我们注意到这里的 lambda 表达式实际上是 getLastName的代理(forwarder),于是我们可以用方法引用代替它:
Collections.sort(people, comparing(Person::getLastName));
最后,使用 Collections.sort 这样的辅助方法并不是一个好主意:它不但使代码变的冗余,也无法为实现 List
接口的数据结构提供特定(specialized)的高效实现,而且由于 Collections.sort方法不属于 List接口,用户在阅读 List接口的文档时不会察觉在另外的 Collections 类中还有一个针对 List 接口的排序(sort())方法。
默认方法可以有效的解决这个问题,我们为 List增加默认方法 sort(),然后就可以这样调用:
people.sort(comparing(Person::getLastName));
此外,如果我们为 Comparator接口增加一个默认方法 reversed()(产生一个逆序比较器),我们就可以非常容易的在前面代码的基础上实现降序排序。
people.sort(comparing(Person::getLastName).reversed());