目录
一,什么是lambda表达式
在jdk1.8之前,方法和类都是作为java中的“二等公民”的,为什么这么称呼呢?因为你无法将一个类和方法的定义在运行时传递给一个方法,但是如其他的基本类型,String等就可以,而这些能被直接传递值的类型被称为“一等公民”。那么为什么要动态地传递方法呢?请看下面的例子:
@Data
@AllArgsConstructor
public class Employee {
private String name;
private Integer age;
private Double salary;
public static List<Employee> getEmployees(){
List<Employee> lists = new ArrayList<>();
lists.addAll(
Arrays.asList(
new Employee("张三", 20, 3000d),
new Employee("李四", 30, 1500d),
new Employee("王五", 35, 2000d),
new Employee("老王", 40, 4500d),
new Employee("张六", 27, 6000d),
new Employee("王七", 34, 2700d)
));
return lists;
}
}
上面是公司的员工信息,老板有一天告诉你,帮我查一下公司有多少姓“王”的员工,你一听心想这多简单啊,然后一下就写出了下面的代码:
List<Employee> getWangEmployee(){
List<Employee> lists = Employee.getEmployees();
List<Employee> resultList = new ArrayList<>();
for(Employee employee : lists){
if(employee.getName().startsWith("王"))
resultList.add(employee);
}
return resultList;
}
你心想老板真是小看你,一天都给你这是什么活呢。因为很快完成了任务,老板对你很是满意,然后又告诉你,这样吧,你再帮我查一下公司有多少35岁以上的员工,你心里很是不满,“万恶的资本主义”,但是还是得迫于压力,然后很快完成了下面的代码:
List<Employee> getAgeBigThan35(){
List<Employee> lists = Employee.getEmployees();
List<Employee> resultList = new ArrayList<>();
for(Employee employee : lists){
if(employee.getAge() > 35)
resultList.add(employee);
}
return resultList;
}
由于你办事很有效率,这下老板的七大姑,八大姨都统统找上了你,他们有各种各样的需求,小张,帮我看看公司“有哪些20岁的,身高1.75以上的人”; 小张,帮我看看“公司哪些人工资在2000块钱以上”......然后你要接着定义各种getXXX的方法吗?这是你不喜欢的,你感觉很无助,却依旧得干着自己不想做的事。
查看上面的代码,我们发现很多问题,上面有好多的模板方法,比如获取公司的员工,for循环遍历员工等等,但是我们写代码最关心的往往是那几行判断的逻辑,所以你会想如果可以把这些判断的逻辑抽象出来,然后在运行的时候动态的传递给方法,这样做多省事,你不用为各种相同类型的需求每个都写一个方法,很不幸,在1.8之前是做不到的,退而求其次,你可能会想到这么做。
如下:定义自己的Query接口,同时想到以后可能会查询别的类型,你将其定义为泛型的:
public interface MyQuery<T> {
boolean test(T e);
}
该接口只有一个抽象方法,你需要传递一个T类型的e,然后该方法会返回true或false,以判断该参数是否满足query的条件。
之后重新定义一个方法,该方法可以接受一个MyQuery的一个变量,然后动态地把tquery传递过去,这样做就可以省去许多模板代码,如下:
List<Employee> getEmployee(MyQuery<Employee> query){
List<Employee> lists = Employee.getEmployees();
List<Employee> resultList = new ArrayList<>();
for(Employee employee : lists){
if(query.test(employee)){
resultList.add(employee);
}
}
return resultList;
}
然后在使用时可以通过匿名类的方式将MyQuery对象传过去,如下:
List<Employee> employeeList = getEmployee(new MyQuery<Employee>() {
@Override
public boolean test(Employee e) {
if (e.getAge() > 35)
return true;
return false;
}
});
通过匿名内部类传递方法的实现,这算是传递方法的一种折中形式,但是整个定义还是十分的繁琐,我只是想查询满足情况的员工为什么还要绕这么一大圈。终于,到了jdk1.8,你完全不用再这样做,上面的实现直接一行代码即可实现:
List<Employee> employeeList = getEmployee(x -> x.getAge() > 35);
这就是lambda表达式,通过传递方法实现给另一个方法,让方法成为java里的“一等公民”,程序员可以借此写出更清爽干净的代码,同时代码的语义也比之前清晰不少。
二,lambda表达式的语法
在讲lambda表达式的语法前,首先需要说明的是只有函数式接口可以使用lambda表达式。所谓的函数式接口是指接口中只有一个抽象方法需要实现,为了更好的辨别这些接口,jdk1.8有了
@FunctionalInterface 注解,该注解用在一个接口上表明该接口为函数式接口,在java中比较常见的函数式接口有Comparator, Runnable等,细心的同学如果去看相应接口的定义便都能看到该注解。
接下来以Comparator接口为例定义一个lambda表达式:
Comparator<String> comparator = (x, y) -> x.compareTo(y)
如上所示,其中箭头左边的为lambda表达式的方法参数,而箭头右边的为方法体。对于没有接触过lambda的同学来说,上面的例子看的是云里雾里的,为什么这样就可以了呢?首先我们看一下Comparator接口的原型:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
//省去其他默认方法
}
首先映入眼帘的便是@FunctionalInterface注解,之所以只有函数式接口能够使用lambda表达式是因为,在获取方法参数类型,返回值类型时使用了类型推断,因为接口中只有一个方法需要实现,所以lambda表达式能够精确的匹配上方法的声明。
所谓的类型推断常见的如泛型,请看下面的例子:
Map<String, Object> maps = new HashMap<>();
在定义maps的时候,我们指定了泛型参数为String和Object, 因此在new HashMap时我们并没有使用显式的参数指定,而是借助了编译器的类型推断,通过maps的定义便可以获得HashMap的类型参数信息为String, Object。同理到了lambda表达式里,compare方法接受两个参数类型为T的参数,并返回一个int类型的量; 我们在定义comparator变量时,指定了泛型类型为String, 因此编译器可以推断出x, y两个参数的类型为String, 又因为String类实现了Comparable接口,因此在比较时我们可以直接调用x的compareTo方法,而且返回值就是int。
2.1 lambda表达式的语法规定
(1)参数类型是否需要加上
上面的例子我们使用了隐式的类型推断,而在实际使用时我们是可以把参数类型加上的,这样做也不会有错,如下:
Comparator<String> comparator = (String x, String y) -> x.compareTo(y);
在编译器无法推断出参数类型时,还是需要显式的指明参数的类型。
(2)当方法只有一个参数时,可以省略小括号
Predicate<Integer> p = x -> x > 0;
(3)当方法没有参数时,需要使用一个小括号来指明参数为空。
以Runnable接口为例:
Runnable r = () -> System.out.println("我是线程");
(4)当lambda方法体只有一行时可以省略return关键字和方法的大括号
之所以可以取消return关键字是因为通过接口的定义可以获得返回值信息。
Function<String, Integer> function = x -> x.length();
若是使用了return关键字,则需要加上大括号,而不论方法是否为一行,如下:
Function<String, Integer> function = x -> {
return x.length();
};
(5)对于复杂的逻辑需要多行,则需要使用方法的大括号和显式地return。
lambda表达式的方法体和普通方法内部一样,没有什么不同。
Function<String, Integer> function = x -> {
if(x.startsWith("A"))
return 100;
if(x.startsWith("B"))
return 80;
return 60;
};
2.2 jdk1.8自带的函数式接口
jdk在java.util.function包中提供了好多默认的函数式接口。如下:
2.2.1 Consumer接口
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
如上所示,该接口有个accept方法,接受一个T类型的参数,然后不返回值。consumer正如其名字:消费者,传递一个参数然后就被消费了,这样就比较好理解该接口。使用的例子:
Consumer<String> consumer = x -> System.out.println(x);
consumer.accept("hello");
因为println方法正好接受一个参数,然后没有返回值,所以与其接口定义一致于是可以使用。
2.2.2 Supplier接口
@FunctionalInterface
public interface Supplier<T> {
T get();
}
supplier, 提供者接口,没有参数,返回给用户一个T类型的值,使用如下:
Supplier<Double> supplier = () -> new Random().nextDouble();
Double aDouble = supplier.get();
该接口的作用是产生一个随机的double类型的值。
2.2.3 Function接口
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
该接口接受一个T类型的参数,然后返回一个R类型的值。比如传递一个String的变量然后返回其大写就可以这样写:
Function<String, String> function = str -> str.toUpperCase();
String apply = function.apply("Hello world");
System.out.println(apply);
2.2.4 Predicate接口
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
该接口是一个断言的接口,接受一个T类型的参数,然后返回true/false, 使用场景:返回所有员工名字是以“李”开头且大于3000的员工:
List<Employee> employeeList = getEmployees(x -> x.getName().startsWith("李") && x.getSalary() > 3000);
employeeList.forEach(e -> System.out.println(e));
上面的forEach方法接受一个Consumer接口,它的作用就是循环遍历list,然后打印其中的元素。
除过上述4个接口之外,java还提供了其他的函数式接口,如下:
其实可以看出就是上面几种接口的变形,要么参数多了,要么针对的类型不同。
需要注意的是IntConsumer接口,虽然Consumer接口是泛型的,但是它只能放引用类型的,所以才会有这种针对基本类型的函数式接口。包装类是对基本类型封装了一层,其占用的内存就比原来的多,所以此类接口可以一定程度提高程序性能。
三,lambda表达式的使用
3.1 方法引用
所谓的方法引用是指已存在的方法与函数式接口的定义是相符的,即方法参数类型一致,返回结果一致。此时我们就可以把已有的方法赋值给函数式接口对象。方法引用又分为静态方法引用,即类名::方法名的格式,此时调的是类的静态方法;还有就是对象方法引用,此时调的就是类对象的成员方法,这两种引用使用是一致的,如下:
Function<Employee, String> function = Employee::getName;
你要问为什么。。。方法引用的格式便是这样,学过c++的同学可能对::比较了解,其实就是作用域访问符,上面的意思就是function接口的实现就是Employee的getName()方法。function的定义为接受一个Employee参数然后返回String类型的变量,因此getName方法正好是Employee对象调用的,所以方法定义一致可以被引用。如果上面还是不好接受,可以换种形式,如下:
Function<Employee, String> function1 = e -> e.getName();
这种写法与上面是一致的,只是方法引用更为简洁。
此外再举一个例子,以comparator接口为例:
Comparator<Integer> comparator = Integer::compareTo;
//其他形式
// Comparator<Integer> c1 = (x, y) -> Integer.compare(x, y);
// Comparator<Integer> c2 = Integer::compare;
此处,有的同学可能会问Integer的compareTo方法的定义与compare的定义并不一致,为什么可以成立呢?先看Integer方法的实现:
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
看方法定义是接受一个参数没有错误。按我的理解的话就是comparator接口本身接受两个参数,但是在compareTo中,一个参数是作为Integer类型的对象来调用该方法的,所以其实还是针对的是两个Integer。
3.2 构造器引用
构造器引用是指函数式接口引用的方法是类的构造方法,格式为类名::new如下:
Supplier<Employee> supplier = Employee::new;
需要注意的是,构造器引用需要构造方法与接口的参数一致。经测试发现构造器引用是根据函数式接口中方法的参数来决定调用哪个构造方法的。如下:
@FunctionalInterface
public interface MyFunction<A, B, C, R> {
R get(A a, B b, C c);
}
@Test
public void test12(){
MyFunction<String, Integer, Double, Employee> myFunction = Employee::new;
Employee employee = myFunction.get("李明", 20, 1000d);
System.out.println(employee);
}
为了与Employee的有参构造器参数匹配,因此创建了MyFunction接口。@FunctionalInterface注解加不加一样,就和@Override注解一下,只是加上之后编译器可以判断定义是否正确。
3.3 数组引用
数组引用即调用数组的构造方法,如下:
//String[] strings= new String[10];
Function<Integer, String[]> function = String[]::new;
String[] apply = function.apply(10);
System.out.println(apply.length);
需要注意的是new一个数组时需要传递数组的大小,因此采用了Function接口, 接受一个Integer, 返回一个String[]。另外Integer[]的类型为Integer[].class,这点得知道。
3.4 谓词复合
谓词复合是指可以将多个Predicate进行复合使用,例如针对下面的需求:判断某个员工是否是年龄大于35且工资大于3500且名字里含有王:
Employee employee = new Employee("李四", 31, 3500d);
Predicate<Employee> p = e -> e.getAge() > 35;
p.and(e -> e.getSalary() > 3500).and(e -> e.getName().contains("王"));
注意,不止有and关键字还有or及negate, 后者是用于对前面描述条件的否定,即取反操作。
3.5 函数复合
函数复合是指将多个Function组合起来,用数学来举例如下:
F(x) = x + 2;
A(z) = F(x * 2)
所以: A(2) = F(2*2) = 4 + 2 = 6, 用函数复合来表示则为:
Function<Integer, Integer> fx = x -> x + 2;
Function<Integer, Integer> fz = x -> x * 2;
Function<Integer, Integer> function = fz.andThen(fx);
System.out.println(function.apply(2));
注意,除过andThen,函数复合还能使用compose,该方法的作用就是将两个函数的执行顺序调换,例如fz.compose(fx), 则会先执行fx函数然后再执行fz函数。
四,lambda表达式使用注意事项
4.1 lambda表达式内使用方法体外的变量
此时该变量需要定义为final的,或者是实际不可变的。如下面的例子:
String str = "aaa";
Consumer<String> consumer = x -> {
str = “123”;
System.out.println(str + x);
};
str = "bbb";
上面的代码将无法通过编译,错误是str should be final, 为什么是这样呢?其实这和匿名内部的道理是一样的,当在匿名内部类中操作外部定义的参数时,在1.8之前需要将该参数定义为final的, 但是在1.8及之后,变量会自动被声明为final,之所以不支持参数的改变,是因为匿名内部类获得的外部的参数是通过匿名内部类的构造器传进去的,传递的是参数的引用,试想如果该参数支持修改,则在匿名内部类中修改了该参数的指向,但外部的参数是不会有变化的,这会带来语义的矛盾,因为从用户的角度看内部类操作的对象和外部应该是同一个的,所以为了避免二义性参数需要为final的。
五,总结
什么是lambda表达式?lambda表达式可以说是函数式接口的实例,它和匿名内部类挺像,但是却更加的简洁,可以说是种语法糖,不用它也能写代码,而它更多的是用在Stream流里面。