Lambda表达式详解
文章目录
1、Lambda表达式简介
在开发中,利用行为参数化来传递代码有助于应对不断变化的需求。大白话就是将方法作为参数传来传递,这样可以让我们的代码变得更灵活。Lambda表达式就是让方法作为参数传递变得更简洁明了。 下面通过一个简单的示例来了解Lambda表达式。
一个简单的示例:
从一个果篮中去获取指定的水果
1、首先创建一个水果类
/**
* 水果类
*/
public class Fruits {
/**
* 名称 eg: 香蕉、苹果、橙子
*/
String name;
/**
* 重量 单位:克(g)
*/
Integer weight;
public Integer getWeight() {
return weight;
}
public String getName() {
return name;
}
public Fruits(String name, Integer weight) {
this.name = name;
this.weight = weight;
}
@Override
public String toString() {
return "Fruits{" +
"name='" + name + ''' +
", weight=" + weight +
'}';
}
}
2、使用最基础的方式来获取”苹果“
// 通过if语句来判断是否为苹果
private static List<Fruits> getApples(List<Fruits> list) {
List<Fruits> listApple = new ArrayList<>();
for (Fruits fruits : list) {
if ("苹果".equals(fruits.name)) {
listApple.add(fruits);
}
}
return listApple;
}
3、使用上面这种是最简单的,但是却是最不容易扩展的。
在软件工程中,一个众所周知的问题就是,不管你做什么,用户的需求肯定会变的。如果此时让我们来获取”香蕉“、获取”香蕉“和”苹果“,甚至要重量作为筛选条件呢?我们就需要写许多的代码,而且这些代码是高度相似的,这样的代码是打破DRY(Don’t Repeat Yourself,不要重复自己)的软件工程原则的。
// 筛选苹果和香蕉
private static List<Fruits> getApplesAndBanana(List<Fruits> list) {
List<Fruits> listApple = new ArrayList<>();
for (Fruits fruits : list) {
if ("苹果".equals(fruits.name) || "香蕉".equals(fruits.name)) {
listApple.add(fruits);
}
}
return listApple;
}
// 筛选苹果、香蕉和重量超过150g的苹果
private static List<Fruits> getApplesAndBananaAndWeight(List<Fruits> list) {
List<Fruits> listApple = new ArrayList<>();
for (Fruits fruits : list) {
if ("苹果".equals(fruits.name) && fruits.weight > 150) {
listApple.add(fruits);
}
if ("香蕉".equals(fruits.name)) {
listApple.add(fruits);
}
}
return listApple;
}
4、写出上面这样的代码是令人有点失望的。
让我们后退一步来看看更高层次的抽象,我们可以定义一个接口来实现这个获取动作。如果有新的需求就实现这个接口来定义实现方法。
a、定义接口
public interface FruitsPredicate {
// 过滤水果
boolean filter(Fruits fruits);
}
b、选择不同水果的策略
c、具体类实现
public class ApplesPredicate implements FruitsPredicate{
@Override
public boolean filter(Fruits fruits) {
return "苹果".equals(fruits.name) ? true : false;
}
}
public class ApplesAndBananaPredicate implements FruitsPredicate{
@Override
public boolean filter(Fruits fruits) {
return "苹果".equals(fruits.name) || "香蕉".equals(fruits.name) ? true : false;
}
}
public class ApplesAndBananaAndWeightPredicate implements FruitsPredicate{
@Override
public boolean filter(Fruits fruits) {
if ("苹果".equals(fruits.name) && fruits.weight > 150) {
return true;
}
if ("香蕉".equals(fruits.name)) {
return true;
}
return false;
}
}
d、过滤方法
private static List<Fruits> filterFruits(List<Fruits> list, FruitsPredicate fruitsPredicate) {
List<Fruits> listApple = new ArrayList<>();
for (Fruits fruits : list) {
if (fruitsPredicate.filter(fruits)) {
listApple.add(fruits);
}
}
return listApple;
}
5、这里值得小小地庆祝一下。
上面的代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易。filterFruits方法的行为取决于你通过FruitsPredicate对象传递的代码,我们已经实现了filterFruits方法的行为参数化了。
但是上面的方式我们不得不声明好几个实现FruitsPredicate接口的类,然后实例化好几个只会提到一次的FreitsPredicate对象。这是很啰嗦的,很费时间。所以我们想到了可以使用Java的匿名类来进一步改善代码。:
List<Fruits> oranges = filterFruits(list, new FruitsPredicate() {
@Override
public boolean filter(Fruits fruits) {
return "橙子".equals(fruits.name);
}
});
6、使用Lambda表达式。
好的代码应该是一目了然的,即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍不能令人满意,需要书写很多重复的代码。所以Java8的语言设计者通过引入Lambda表达式来解决这个问题。
// 获取所有苹果
List<Fruits> apples = filterFruits(list, fruits -> "苹果".equals(fruits.name));
// 获取所有苹果和香蕉
List<Fruits> applesAndBananas = filterFruits(list, fruits -> "苹果".equals(fruits.name) || "香蕉".equals(fruits.name));
// 获取重量大于150g的苹果和所有香蕉
List<Fruits> applesAndBananasAndWeight = filterFruits(list, fruits -> {
if ("苹果".equals(fruits.name) && fruits.weight > 150) {
return true;
}
if ("香蕉".equals(fruits.name)) {
return true;
}
return false;
});
使用Lambda表达式看起来是不是非常的灵活和简单,这就是Lambda表达式的优点。现在你还不需要学会使用Lambda表达式,只需要认识到Lambda的优点。后面会详细介绍如何使用。
2、如何使用Lambda表达式
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
Lambda主要又三部分组成:参数列表、箭头和Lambda主体。
其实Lambda就是匿名类的简化,我们可以将这个匿名类实现出来进行比较,发现它将匿名类的许多代码都进行了省略,只保存了最重要的部分。
list.sort(new Comparator<Fruits>() {
@Override
public int compare(Fruits f1, Fruits f2) {
return f1.weight - f2.weight;
}
});
下面对每个语法格式的特征进行举例说明:
(1)、语法格式一:无参,无返回值,Lambda主体只有一条语句。
Runnable runnable = () -> System.out.println("Hello World!");
(2)、语法格式二:有一个参数,无返回值,Lambda主体只有一条语句。
Consumer<String> consumer = (x) -> System.out.println(x);
只有一个参数时参数的小括号可以省略
Consumer<String> consumer = x -> System.out.println(x);
(3)、语法格式三:两个参数及以上,有返回值,Lambda主体只有一条语句。
Lambda没有return语句,因为已经隐含了return。
Comparator<Integer> com=(x, y)-> Integer.compare(x,y);
(4)、语法格式四:两个参数及以上,有返回值,Lambda主体有多条语句。
Comparator<Integer> com=(x, y)->{
System.out.println("x的值为:"+x);
System.out.println("y的值为:"+y);
return x*y;
};
(5)、Lambda表达式的参数列表数据类型可以省略不写,因为JVM可以通过上下文推断出数据类型。
Comparator<Integer> com=(Integer x,Integer y)-> Integer.compare(x,y);
Comparator<Integer> com=(x,y)-> Integer.compare(x,y);
3、在哪里使用Lambda表达式
在函数式接口上使用Lambda表达式,当方法的参数是一个函数式接口时就可以使用Lambda表达式。
3.1 函数式接口
函数式接口就是只定义一个抽象方法的接口。
例如我们写的FruitsPredicate接口:
@FunctionalInterface
public interface FruitsPredicate {
/**
* 过滤水果
*
* @param fruits
* @return
*/
boolean filter(Fruits fruits);
}
注意:
1、函数式接口可以使用 @FunctionalInterface 注解来声明。
这个注解只是用来表明这是一个函数式接口,并不是没有这个注解就不是函数式接口了。
有这个注解的话,如果它不是函数式接口的话,编译器将报错。
2、Java8在接口中可以增加默认方法了。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
3.2函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作
函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的
签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。
() -> void // 这个函数描述符代表了参数列表为空,且返回void的函数。Runnable就是这种。
其他常见的函数式接口和对应的函数描述符:
函数式接口 | 函数描述符 |
---|---|
Preicate<T> | T -> boolean |
Consumer<T> | T -> void |
Function<T,R> | T -> R |
Supplier<T> | () -> T |
UnaryOperator<T> | T -> T |
BinaryOperator<T> | (T,T) -> T |
BiPredicate<L,R> | (L,R) -> boolean |
BiConsumer<T,U> | (T,U) -> void |
BiFunction<T,U,R> | (T,U) -> R |
4、四大核心函数式接口
Java 8的库设计师在java.util.function包中引入了几个新的函数式接口,最核心的四个函数式接口分别为Predicate<T>、Consumer<T>、Function<T,R>和Supplier<T>。下面我们来一一介绍它们。
4.1 Predicate
源码:
public interface Predicate<T> {
boolean test(T t);
}
java.util.function.Predicate<T>接口定义了一个test的抽象方法,他接受泛型T对象,并返回一个boolean值。是一个断定型接口。
使用:
// 优化获取所有果篮中苹果的方法,使用Predicate接口
private static List<Fruits> filterApples(List<Fruits> list, Predicate<Fruits> predicate) {
List<Fruits> listApple = new ArrayList<>();
for (Fruits fruits : list) {
if (predicate.test(fruits)) {
listApple.add(fruits);
}
}
return listApple;
}
// 使用filterApples方法来获取结果
filterApples(list,s -> "苹果".equals(s.name) ? true : false);
4.2 Consumer
源码:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T
的对象,没有返回(void)。是一个消费型接口。
使用:
// 去消费果篮中的水果,【可根据不同的消费行为来消费】
private static void printFruits(List<Fruits> list, Consumer<Fruits> consumer) {
for (Fruits fruits : list) {
consumer.accept(fruits);
}
}
// 输出果篮中全部水果名称和重量
printFruits(list,s -> System.out.println("水果名称:" + s.name + ";" + "水果重量:" + s.weight));
// 输出果篮中苹果和重量
printFruits(list,s -> {
if ("苹果".equals(s.name)) {
System.out.println("水果名称:" + s.name + ";" + "水果重量:" + s.weight);
}
});
4.3 Function
源码:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个
泛型T的对象,并返回一个泛型R的对象。是一个函数型接口。
使用:
/**
* 此方法将果篮中的水果一一检查执行对应的操作
*
* 使用了Java泛型,进一步抽象方法。
* 在Java 8中泛型的类型编译器可以自行推断出来
*/
private static <T,R> List<R> getAllFruits(List<T> list, Function<T,R> function) {
List<R> result = new ArrayList<>();
for (T t : list) {
result.add(function.apply(t));
}
return result;
}
// 将果篮中的所有水果贴上标签
List<FruitsLabel> allFruits = getAllFruits(list, t -> {
FruitsLabel fruitsLabel = new FruitsLabel();
fruitsLabel.name = t.name;
fruitsLabel.weight = t.weight;
fruitsLabel.label = "新鲜";
return fruitsLabel;
});
System.out.println(allFruits);
4.4 Supplier
源码:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
java.util.function.Supplier<T>接口定义了一个叫作get的方法,它不需要接受参数,即可返回一个泛型T的对象。是一个供给型接口。
使用:
private static <T> List<T> addFruits(List<T> list, Supplier<T> supplier) {
list.add(supplier.get());
return list;
}
// 向果篮中放一个橙子
addFruits(list,() -> new Fruits("橙子", 100));
其他的函数式接口我们就不在过多的介绍,如果大家后续有用到可以自行查阅。
5、方法引用
当要传递给Lambda体的操作,已经有实现的方法了,就可以使用方法引用!(实现抽象方法的参数列表,必须与方法引用的参数列表一致,方法的返回值也必须一致,即方法的签名一致)。方法引用可以理解为方法引用是Lambda表达式的另一种表现形式。
方法引用的语法:使用操作符"::"将对象或类和方法名分隔开。
我们使用一个实例来进行演示:
// 根据水果的重量来排序
list.sort(Comparator.comparing(s -> s.getWeight()));
我们发现后面的Lambda表达式其实就是调用getWeight()方法来获取Fruits类的重量,这个方法已经存在Fruits类中了,所以我们可以直接使用这个方法,这就是方法引用。
list.sort(Comparator.comparing(Fruits::getWeight));
5.1 方法引用的使用情况
方法引用的使用情况共分为以下三种:
- 对象::实例方法名
- 类::静态方法名
- 类::实例方法名
使用实例:
1、对象::实例方法名
public void test(){
PrintStream out = System.out;
// 使用Lambda实现
Consumer<String> consumer = s -> out.println(s);
// 使用方法引用实现相同效果
Consumer<String> consumer1 = out::println;
// 测试
consumer.accept("hello world");
consumer1.accept("hello world");
}
2、类::静态方法名
public void test(){
// 使用Lambda实现
Comparator<Integer> comparable=(x,y)->Integer.compare(x,y);
//使用方法引用实现相同效果
Comparator<Integer> integerComparable=Integer::compare;
// 测试
System.out.println(integerComparable.compare(4,2));//结果:1
System.out.println(comparable.compare(4,2));//结果:1
}
3、类::实例方法名
public void test(){
// 使用Lambda实现
BiPredicate<String,String> bp=(x,y)->x.equals(y);
//使用方法引用实现相同效果
BiPredicate<String,String> bp2=String::equals;
// 测试
System.out.println(bp.test("1","2"));//结果:false
System.out.println(bp2.test("1","2"));//结果:false
}
注意:
对象::实例化方法和类::实例化方法可能乍看起来有点儿晕。类似于String::equals的方法引
用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。
6、构造器引用
对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个对象。需要注意构造器参数列表要与接口中抽象方法的参数列表一致。
格式:类名::new
演示实例:
public class Employee {
private Integer id;
private String name;
private Integer age;
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + ''' +
", age=" + age +
'}';
}
public Employee(){
}
public Employee(Integer id) {
this.id = id;
}
public Employee(Integer id, Integer age) {
this.id = id;
this.age = age;
}
public Employee(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}
public void test(){
//引用无参构造器
Supplier<Employee> supplier=Employee::new;
System.out.println(supplier.get());
//引用有参构造器
Function<Integer,Employee> function=Employee::new;
System.out.println(function.apply(21));
BiFunction<Integer,Integer,Employee> biFunction=Employee::new;
System.out.println(biFunction.apply(8,24));
}
输出结果:
Employee{id=null, name='null', age=null}
Employee{id=21, name='null', age=null}
Employee{id=8, name='null', age=24}
7、数组引用
数组引用格式:type[]::new
使用示例:
public void test02(){
Function<Integer,String[]> function=String[]::new;
String[] apply = function.apply(10);
System.out.println(apply.length); //结果:10
}
8、复合Lambda表达式的有用方法
Java 8的好几个函数式接口都有为方便而设计的复合方法。例如Comparator、Function和Predicate都提供了允许你进行复合的方法。
这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,
你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结
果成为另一个函数的输入。
你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这违背了函数式接口的定义啊!)窍门在于,Java 8中增加了默认方法。它们不是抽象方法。
比较器复核
之前我们使用了方法引用对果篮中的水果进行排序:
list.sort(Comparator.comparing(Fruits::getWeight));
这个顺序是从小到大排序的,那么如何让它从大到小排序呢?我们需要去创建另一个方法吗?这样岂不是麻烦了?
在Comparator的接口中,提供了一个默认方法reversed可以对比较器逆序,使用如下:
list.sort(Comparator.comparing(Fruits::getWeight).reversed());
如果两个水果重量相同,我们又想按照名称排序怎么办?
在Comparator接口中提供了一个方法thenComParing方法可以接受另一个比较器。使用如下:
list.sort(Comparator.comparing(Fruits::getWeight)
.thenComparing(Fruits::getName));
谓词复合
谓词接口包括三个方法:negate、and和or,已知的Predicate接口就实现了这样的方法,我们测试一下:
1、negate表示取相反的值。例如我们之前从果篮中选取苹果的逻辑,我们如果对Predicate接口加上negate方法则表示取不是苹果的其他水果。
// 得到所有苹果
Predicate<Fruits> getApples = s -> "苹果".equals(s.name) ? true : false;
// 得到所有除苹果外其他水果
Predicate<Fruits> getNotApples = getApples.negate();
2、and方法相当于&&
例如我们之前逻辑选取苹果且重量大于150g的逻辑,我们可以这样写代码
// 得到所有重量大于150g的苹果
getApples.and(s -> s.getWeight() > 150 ? true : false);
3、or方法相当于||
例如我们可以选取果篮中的苹果和香蕉
// 得到香蕉和苹果
getApples.or(s -> "香蕉".equals(s.name) ? true : false);
注意:
and和or方法在表达式中的优先级是从左向右确定优先级的,例如a.or(b).and(c)可以看作(a||b)&&c
函数复核
Function接口有andThen和compose两个默认方法,它们都会返回Function的一个实例。我们可以先定义两个Function,后面将会依赖Funtion来进行演示。
// f = x + 1
Function<Integer, Integer> f = x -> x + 1;
// f = x * 2
Function<Integer, Integer> g = x -> x * 2;
1、andThen方法等价于 g(f(x))
// 相当于 (x+1)*2
Function<Integer, Integer> g = x -> x * 2;
int result = h.apply(1); // 返回4
2、compose方法等价于 f(g(x))
// 相当于 (x*2)+1
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1); // 返回3
9、Lambda表达式的作用域
Lambda表达式可以看作是匿名内部类实例化的对象,Lambda表达式对变量的访问限制和匿名内部类一样,因此Lambda表达式可以访问局部变量、局部引用,静态变量,实例变量。
1、访问局部变量
在Lambda表达式中规定只能引用标记了final的外层局部变量。我们不能在lambda 内部修改定义在域外的局部变量,否则会编译错误。
public class TestFinalVariable {
interface VarTestInterface{
Integer change(String str);
}
public static void main(String[] args) {
//局部变量不使用final修饰
Integer tempInt = 1;
VarTestInterface var = (str -> Integer.valueOf(str+tempInt));
//再次修改,不符合隐式final定义
tempInt =2;
Integer str =var.change("111") ;
System.out.println(str);
}
}
编译会报错:
2、访问局部引用、静态变量、实例变量
Lambda表达式不限制访问局部引用变量,静态变量,实例变量。代码测试都可正常执行,代码:
public class LambdaScopeTest {
/**
* 静态变量
*/
private static String staticVar;
/**
* 实例变量
*/
private static String instanceVar;
@FunctionalInterface
interface VarChangeInterface{
Integer change(String str);
}
/**
* 测试引用变量
*/
private void testReferenceVar(){
ArrayList<String> list = new ArrayList<>();
list.add("111");
//访问外部引用局部引用变量
VarChangeInterface varChangeInterface = ((str) -> Integer.valueOf(list.get(0)));
//修改局部引用变量
list.set(0,"222");
Integer str =varChangeInterface.change("");
System.out.println(str);
}
/**
* 测试静态变量
*/
void testStaticVar(){
staticVar="222";
VarChangeInterface varChangeInterface = (str -> Integer.valueOf(str+staticVar));
staticVar="333";
Integer str =varChangeInterface.change("111") ;
System.out.println(str);
}
/**
* 测试实例变量
*/
void testInstanceVar(){
instanceVar="222";
VarChangeInterface varChangeInterface = (str -> Integer.valueOf(str+instanceVar));
instanceVar="333";
Integer str =varChangeInterface.change("111") ;
System.out.println(str);
}
public static void main(String[] args) {
new LambdaScopeTest().testReferenceVar();
new LambdaScopeTest().testStaticVar();
new LambdaScopeTest().testInstanceVar();
}
}
注意:
Lambda表达式里不允许声明一个与局部变量同名的参数或者局部变量。
//编程报错
Integer tempInt = 1;
VarTestInterface varTest01 = (tempInt -> Integer.valueOf(tempInt));
VarTestInterface varTest02 = (str -> {
Integer tempInt = 1;
Integer.valueOf(str);
});
3、Lambda表达式访问局部变量作限制的原因
Lambda表达式不能访问非final修饰的局部变量的原因是,局部变量是保存在栈帧中的。而在Java的线程模型中,栈帧中的局部变量是线程私有的,如果允许Lambda表达式访问到栈帧中的变量地址(可改变的局部变量),则会可能导致线程私有的数据被并发访问,造成线程不安全问题。
基于上述,对于引用类型的局部变量,因为Java是值传递,又因为引用类型的指向内容是保存在堆中,是线程共享的,因此Lambda表达式中可以修改引用类型的局部变量的内容,而不能修改该变量的引用。
对于基本数据类型的变量,在 Lambda表达式中只是获取到该变量的副本,且局部变量是线程私有的,因此无法知道其他线程对该变量的修改,如果该变量不做final修饰,会造成数据不同步的问题。
但是实例变量,静态变量不作限制,因为实例变量,静态变量是保存在堆中(Java8之后),而堆是线程共享的。在Lambda表达式内部是可以知道实例变量,静态变量的变化。