一 lambda 表达式初探
关于定义:Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用 Lambda 表达式可以使代码变的更加简洁紧凑。
上面这句话,在不了解代码的时候,读起来有点生涩。在了解完相关特性之后,再回来看这个定义,才能更好的理解。
/**
* @author gaopeng@doctorwork.com
* @description
* @date 2019-06-05 9:00
**/
public interface MathOperation {
int operation(int a, int b);
}l
===================================
/**
* @author gaopeng@doctorwork.com
* @description
* @date 2019-06-05 9:03
**/
public class LambdaTest {
public static void main(String[] args) {
// 匿名函数与 lambda 表达式的对比,将匿名函数的声明完全简化,仅仅保留了入参列表和具体实现的表达式,
// 如果函数内方法的实现仅有一行,那么这个 lambda 表达式看起来就十分的简洁
MathOperation addition = (int a, int b) -> a + b;
MathOperation normalAddition = new MathOperation() {
@Override
public int operation(int a, int b) {
return a + b;
}
};
// 不使用参数的类型声明
MathOperation substract = ((a, b) -> a - b);
// 在大括号中包含返回值
MathOperation multiplication = (((a, b) -> { return a * b;}));
// 最简洁的写法:没有大括号、返回语句
MathOperation division = (a, b) -> a / b;
/**
* lambda 表达式的变量作用域
*/
}
}
面对数学操作 MathOperation 接口,要想使用它,我们有两种大的方式分类:
- 独立的类,实现这个接口并单独使用
- 使用匿名函数
老的方法,使用匿名函数的话,写法就是 demo 中的样式:
MathOperation normalAddition = new MathOperation() {
@Override
public int operation(int a, int b) {
return a + b;
}
};
这个场景,是 JDK8 之后 lambda 表达式的适用场景:
// 匿名函数与 lambda 表达式的对比,将匿名函数的声明完全简化,仅仅保留了入参列表和具体实现的表达式,
// 如果函数内方法的实现仅有一行,那么这个 lambda 表达式看起来就十分的简洁
MathOperation addition = (int a, int b) -> a + b;
二 函数式接口
前面一节的例子中,自定义接口 MathOperation,有且仅有一个方法定义,所有实现了这个接口的类都被强制要求实现该方法。这样的接口范例,在 JDK8 之中有了一个新的官方定义 函数式接口,也叫 SAM 接口-Single Abstract Method Interface,并得到了一个专有的注解
@FunctionalInterface
如果自定义的接口里面,加上了这个注解,在编译阶段就会检查这个接口是否符合 函数式接口的定义规范,如果不规范,那么这个类就无法完成编译。
那么问题来了,为什么要定义这样一个注解呢,只要接口使用了这个注解并通过了编译检查,那么这些类型的接口,就都可以方便地使用 lambda 表达式编写,使用 lambda 表达式完成匿名内部类的简写。
// 自定义比较器
Comparator<Student> studentComparator = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
};
// 使用lambda表达式完成的自定义比较器
Comparator<Student> lambdaComparator = (student1, student2) -> student1.getAge() - student2.getAge();
常用的自定义比较器 Comparator 在 JDK8 的版本上,都被增加了这个 @FunctionalInterface 注解。
函数式接口的别名让我们就可以很清楚地了解到这类接口的要求:只有一个抽象方法的声明。如果使用了注解还声明了多个抽象方法,那么就会报错,无法通过编译检查。
那么除了抽象方法,函数式接口还是允许拥有一些其他的东西:
- 接口默认方法(JDK8 特性)-接口默认方法也不会要求实现接口的类去实现它
- 静态方法-静态方法不能是抽象方法
- Object 基类中的 public 方法,java 中所有的类都继承自基类
这一节我们使用了 JDK 自带的 Comparator 接口来做示例,除了这个,还有 JDK 中的 Runnable 接口也被 JDK 标注上了函数式接口注解,这表示我们在使用 runnable 接口的时候,一样可以顺当地利用 lambda 表达式。
除此之外,我们的自定义接口也可以使用了,在第一节使用到的 MathOperation 接口,虽然没有加上这个注解,但是其接口使用,完全也是符合 SAM 标准,即一个单一的抽象方法声明接口。我可以简单完善一下这个接口:
@FunctionalInterface
public interface MathOperation {
int operation(int a, int b);
}
如果去掉函数式接口注解,并在接口里声明另一个方法,那么这个接口就无法顺利地使用 lambda 表达式。使用则会报错
Multiple non-overriding abstract methods found in xxx
三 lambda 表达式变量作用域
1 lambda 表达式中不允许声明一个与局部变量同名的参数或者局部变量
Student demo = new Student();
MathOperation mathOperation = (demo, demo2) -> demo - demo2;
在这段示例代码中,表达式里面的 demo 变量会报错,因为这个变量名在上一行代码中已经被其他的对象声明给占用了。
2 lambda 表达式中可以直接访问外层的局部变量
int outNumber = 12;
Thread thread1 = new Thread(() -> System.out.println(outNumber));
Thread thread2 = new Thread(() -> System.out.println(params));
上面的示例代码,outNumber 是一个方法内的局部变量,params 则是一个类的静态成员。这些类型的参数均可以在 lambda 表达式中引用。
3 lambda 表达式中被引用的变量值不可以被更改
Thread thread3 = new Thread(() -> System.out.println(outNumber--));
这样一行代码是无法通过编译检查的,在表达式中我尝试修改方法的局部变量 outNumber。然后 idea 就提示我: variable used in lambda expression should be final or effectively final。即 lambda 表达式中使用的变量只能是 final 类型或者隐式的 final 类型。
最明显的好处:final类型的参数传入 lambda 表达式的使用,表示这个操作是 线程安全的,我们可以放心的时候而不必担心并发问题。需要注意的是,如果传入的不是基础数据类型,是一个对象引用,那么修改对象引用内的成员是对的。
Thread thread4 = new Thread(() -> demo.setName("gaop"));
这样的使用方法,需要注意产生并发修改的场景。
网友评论