目录
1. 什么是lambda表达式
lambda表达式本质上是一个匿名方法。匿名类的一个问题是,如果匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能看起来不实用且不清楚。在这些情况下,您通常会尝试将功能作为参数传递给另一个方法,例如当有人单击按钮时应采取的操作。Lambda表达式使您可以执行此操作,将功能视为方法参数,或将代码视为数据。
lambda表达式不能直接使用,一般都是先赋给一个接口,然后对这个接口操作,或者非要直接用必须强转,如:
System.out.println( () -> {} ); //错误! 目标类型不明
System.out.println( (Runnable)() -> {} ); // 正确
2. lambda表达式语法
2.1 lambda表达式的一般语法
(type_1 para_1, type_2 para_2,···,type_n, para_n) -> {
statement1;
statement2;
…………
statementn;
return ……
}
2.2 lambda表达式的单参数语法
如果方法只有一个参数,而且这个参数的类型可以推导出,那么可以省略小括号
para1 -> {
statement1;
statement2;
…………
statementn;
return ……
}
【例】将列表中的字符串转换为全小写
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("Ni");
list.add("Hao");
list.add("Lambda");
List<String> lowerList = list.stream().map(
name -> { return name.toLowerCase();} //单参数lambda表达式
).collect(Collectors.toList());
}
}
2.3 lambda表达式的单语句写法
当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号
param1 -> statment
【例】将列表中的字符串转换为全小写
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("Ni");
list.add("Hao");
list.add("Lambda");
List<String> lowerList = list.stream().map(
name -> name.toLowerCase() //单语句lambda表达式
).collect(Collectors.toList());
}
}
2.4 lambda表达式的无参数写法
即使lambda表达式没有参数,仍然要提供空括号,就像无参方法一样
public class Test {
public static void main(String[] args) {
Thread t = new Thread(
()->{for(int i =10;i>0;i--)System.out.println(i);}
);
t.start();
}
}
2.5 lambda表达式方法引用写法
类似C/C++作用域绑定符(后面会提到)
class :: method
instance :: method
【例】将列表中的字符串转换为全小写
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("Ni");
list.add("Hao");
list.add("Lambda");
List<String> lowerList = list.stream().map(
String::toLowerCase //方法引用写法
).collect(Collectors.toList());
}
}
3. lambda表达式可使用的变量
先举例
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Test {
public static void main(String[] args) {
//将为列表中的字符串添加前缀字符串
String waibu = "lambda :";
List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String>execStrs = proStrs.stream().map(
chuandi -> {
Long zidingyi = System.currentTimeMillis();
return waibu + chuandi + " -----:" + zidingyi;
}
).collect(Collectors.toList());
execStrs.forEach(System.out::println);
}
}
变量waibu —— 外部变量
变量chuandi —— 传递变量,即lambda表达式的参数
变量zidingyi —— 内部自定义变量
lambda表达式可以访问给它传递的变量,访问自己内部定义的变量,同时也能访问它外部的变量。
不过lambda表达式访问外部变量有一个非常重要的限制:变量不可变(只是引用不可变,而不是真正的不可变)。
当在表达式内部修改waibu = waibu + " ";时,IDE就会提示你:
Local variable waibu defined in an enclosing scope must be final or effectively final
编译时会报错。因为变量waibu被lambda表达式引用,所以编译器会隐式的把其当成final来处理。
以前Java的匿名内部类在访问外部变量的时候,外部变量必须用final修饰。现在java8对这个限制做了优化,可以不用显示使用final修饰,但是编译器隐式当成final来处理。
4. lambda表达式中的this概念
this指的是定义该表达式时所在类的对象,而不是lambda表达式所产生的对象
package java_lambda;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class TestThis {
public void whatThis(){
//转全小写
List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
List<String> execStrs = proStrs.stream().map(str -> {
System.out.println(this.getClass().getName());
return str.toLowerCase();
}).collect(Collectors.toList());
execStrs.forEach(System.out::println);
}
public static void main(String[] args) {
TestThis wt = new TestThis();
wt.whatThis();
}
}
5. 函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)
最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口
实际上,在Java中,对lambda表达式所能做的也只是能转换为函数式接口。在其他编程语言像Groovy,是可以用变量保存lambda表达式的,称为闭包,不过Java设计者还是决定保持我们熟悉的接口概念,没有为Java语言增加新的函数类型。
注解是@FunctionalInterface,可加可不加
6.方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器事件就打印这个事件。当然,为此可以调用:
Timer t = new Timer(1000, event -> System.out.println(event));
但是,如果直接把println方法传递到Timer构造器就更好了。具体做法如下:
Timer t = new Timer(1000, System.out::println);
表达式System.out::println是一个方法引用,它等价于lambda表达式:
x->System.out.println(x)
不难看出,要用::操作符分隔方法名与对象或类名,主要有3种情况:
- object :: instanceMethod
- Class :: staticMethod
- Class :: instanceMethod
前2种情况中,方法引用等价于提供方法参数的lambda表达式。如:
System.out::println 等价于 x->System.out.println(x)
Math :: pow 等价于 (x,y) -> Math.pow(x,y)
对于第3种情况,第一个参数会成为方法的目标。例如:
String :: compareToIgnoreCase 等同于 (x,y)->x.compareToIgnoreCase(y)
可以再方法引用中使用this参数。例如:this :; equals 等同于 x -> this.equals(x) ,使用super也是合法的,如 super::instanceMethod
7. 构造器引用
构造器引用和方法引用很类似,只不过方法名为new。例如,Person :: new 是Person构造器的一个引用。哪一个构造器呢?这取决于上下文,假设你有一个字符串列表,可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
如果有多个Person构造器,编译器会选择一个有String参数的构造器,因为它从上下文推导出这是在对一个字符串调用构造器
可以用数组类型建立构造器引用,如: int[]::new 是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式 x -> new int[x]
Java有一个限制,不能构造泛型T的数组。数组构造器对于克服这个很有用。假如:我们需要一个Person对象数组,Stream有一个toArray方法可以返回Object数组:
Object people = stream.toArray();
不过这并不令人满意。用户希望得到一个Person引用数组,而不是Object数组。流库利用构造器引用解决了这个问题。可以把Person[]::new 传入toArray方法:
Person[] people = stream.toArray(Person[]::new);
toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回
简单来说可以理解为:
类名::new
类型名[]::new