目录
一、lambda表达式的简介
1、什么是lambda表达式?
lambda表达式是一个可以传递的代码块,可以在将来执行一次或多次。使用lambda表达式的重点就是延迟执行。
2、什么时候可以用到lambda表达式?
当你需要将一个代码块作为参数传递,并希望这个代码块在将来某个时间调用时就可以用到lambda表达式。
3、为什么要引入lambda表达式?
在java SE 8(jdk1.8)之前,在java中不能直接传递代码块。因为java是一种面向对象语言,如果想传递代码块,就必须构造一个对象,并且这个对象的类需要有一个方法能够包含所需要的代码。在其他语言中可以直接处理代码块,为了让java也能够处理代码块,java设计者们在java SE 8中提出用lambda表达式来处理代码块。
二、lambda表达式的语法
1、基本形式小括号、参数、箭头(->)以及一个表达式,如下。
(String first,String second)->first.length()-second.length()
2、如果箭头(->)后面的代码要完成的计算无法放在一个表达式中,就要像写方法一样,把这些代码放在{}中,并包含显示的return语句。如下:
(String first,String second)->
{
if(first.length()<second.length())return -1;
else if(first.length()>second.length())return 1;
else return 0;
}
3、即使lambda表达式没有参数,仍然要提供空括号,就像无参方法一样。
()->System.out.println("hello world")
4、如果可以推导出lambda表达式的参数类型,则可以忽略参数的类型(通常都可以忽略)。如下
//Compartor是一个函数式接口,lambda表达式可以赋值给函数式接口变量,后面会具体介绍
Compartor<String> comp = (first,second)->first.length()-second.length();
5、如果方法只有一个参数,而且这个参数的类型可以推导得出,那么可以省略小括号。如下
//ActionListener 也是一个函数式接口
ActionListener listener = event->System.out.println("hello world");
注意:最好将lambda表达式看成一个函数来理解。从上面的例子可以看出箭头(->)前面的参数就如同方法的参数,箭头后面的语句就如同方法的方法体。
综上lambda表单式的常见形式如下:
(参数)->单条语句
(参数)->{多条语句}
三、函数式接口
函数式接口:只有一个抽象方法的接口,被称为函数式接口。需要这种接口的对象时,就可以提供一个lambda表达式。
注:在java中,lambda表达式不能独立存在,总是会将其转换成函数式接口的实例来运用。
Java API 的java.util.function包中定义了很多非常通用的函数式接口。例如包中有一个特别有用的函数式接口Predicate:
public interface Predicate<T>{
boolean test(T t);
}
ArrayList类有一个removeIf方法,它的参数就是一个Predicate接口。这个接口专门用来传递lambda表达式。例如,下面语句将一个数组列表删除所有的null值:
list.removeOf(e->e==null);
注:如果设计自己的函数式接口,其中只有一个抽象方法,可以用@FunctionalInterface注解来标记这个接口。这样当你无意中增加了另外的抽象方法时,编译器会报错。
以下是Java API 的java.util.function包中一些常用的函数式接口:
函数式接口 | 参数类型 | 返回 类型 | 抽象方法名 | 描述 | 其他方法 |
---|---|---|---|---|---|
Runnable | 无 | void | run | 作无参数或返回值的动作运行 | |
Supplier<T> | 无 | T | get | 提供一个T类型的值 | |
Consumer<T> | T | void | accept | 处理一个T类型的值 | andThen |
BiConsumer<T,U> | T,U | void | accept | 处理T和U类型的值 | andThen |
Function<T,R> | T | R | apply | 有一个T类型参数的函数 | compose,andThen,identity |
BiFunction<T,U,R> | T,U | R | apply | 有T和U类型参数的函数 | andThen |
Predicate<T> | T | boolean | test | 布尔值函数 | and,or,negate,isEqual |
BiPredicate<T,U> | T,U | boolean | test | 有两个参数的布尔值函数 | and,or,negate |
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 |
---|---|---|---|
BooleanSupplier | none | boolean | getAsBoolean |
PSupplier | none | p | getAsP |
PConsumer | p | void | accept |
ObjPConsumer<T> | T,p | void | accept |
PFunction<T> | p | T | apply |
PtoQFunction | p | q | applyAsQ |
ToPFunction<T> | T | p | applyAsP |
ToPBiFunction<T,U> | T,U | p | applyAsP |
PPredicate | p | boolean | test |
四、lambda表达式作用及运用
最直观的作用就是使代码变得更加简洁。
我们可以对比一下Lambda表达式和传统的java对同一个接口的实现:
这两种方式本质上完全相同,但java8中的写法更简洁。由于lambda表达式可以赋值给一个函数式接口变量,所以我们可以将lambda表达式作为参数传递给函数,而传统的java必须要有明确的接口的定义初始化才行:
下面我们举个例子来说明:
假设Student类的定义和List<Student>的的值都给定。
现在需要你打印出stuList中年龄大于17岁的所有学生的信息。
Lambda写法:定义两个函数式接口,定义一个静态方法,调用静态函数式并给参数赋值。
在第三部分我们已经提到在Java API的java.util.function包中定义了很多非常通用的函数式接口,所以很多时候我们不必自己定义函数接口。这个案例中我们可以不定义Checker和Executor这两个接口,直接用java.util.function包中的Predicate<T>和Consumer<T>接口就可以了。你可以看出这两个接口和Checker、Executor其实是一样的。
我们将静态方法checkAndExcute改用函数式接口包中的接口,代码-4中的内容就只需要以下几步就可完成:
提问:假如现在需要你打印出所有姓张的同学该如何做?需要重新定义方法吗?
回答当然是否,我们只需在调用checkAndExcute方法时第二个参数传另一个lambda表达式即可,如下:
小结:在设计方法时配合函数式接口、lambda表达式可以让我们的方法变得更简洁、更通用。又如我们要编写一个排序方法,可以将排序的比较条件设计成函数式接口中的方法,这样我们在调用这个排序方法时传入不同的lambda表达式就可以得到不同的排序结果。
五、方法引用
有时,可能已经有现成的方法可以完成你想传递到其他代码的某个动作。列如第四部分我们的案例中(如代码-6所示)我们调用checkAndExcute方法时第三个参数传的一个lambda表达式stu -> System.out.println(stu.toString()) ,此处我们可以改为传一个方法引用的表达式System.out::println。表达式System.out::println等价于stu -> System.out.println(stu)。(注:输出语句输出一个对象时,系统会自动调用该对象的toString方法)。
方法引用的基本法则:用::操作符分割方法名与对象或类名。
1、方法引用主要有三种情况:
- object :: instanceMethod 对象名::实例方法名
- Class :: staticMethod 类名::静态方法名
- Class :: instanceMenthod 类名::实例方法名
前两种情况,方法引用等价于提供方法参数的lambda表达式。如System.out::println等价于s -> System.out.println(s)。类似的,Math::pow等价于Math.pow(x,y)。
第三种情况,第1个方法参数会成为方法目标。例如:String::compareToIgnoreCase等价于(x,y) -> x.compareToIgnoreCase(y)。
2、方法引用的其他情况:
- 在方法引用中可以使用this参数。例如,this::equals等同于x -> this.equals(x)。
- 在方法引用中使用super也是合法的。super :: instanceMethod ,使用 super做为目标会调用给的方法的超类版本。
六、构造器引用
1、构造器引用和方法引用很相似,只是把方法名改成了new,即Class::new。例如Person::new是Person构造器的一个引用。系统会根据上下文选择具体调用哪一个构造器。
2、可以用数组类型建立构造器引用。例如int[] ::new是一个构造器引用,它有一个参数:即数组长度,等价于x->new int[x]。
七、变量的作用域
在lambda表达式中可以访问外围方法或类中的变量。如下面这个例子:
调用方法 :repeatMessage("Hello",1000);//每1000毫秒打印一次Hello
可以看出text变量不是lambda表达式中定义的变量,而是repeatMessage方法的一个参数变量。这里有一个问题,lambda表达式的代码可能会在repeatMessage方法调用返回很久后才会运行,而那个时候参数变量已经不存在了。哪究竟是如何保留text变量呢?
首先看lambda表达式的3个部分:
- 一个代码块;
- 参数;
- 自由变量的值(这个是指非参数而且不在代码中定义的变量)
在我们的例子中,这个lambda表达式有1一个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里是“Hello”。我们说它被lambda表达式捕获(当把一个lambda表达式转换成其对应的函数式接口对象时,自由变量的值会复制到这个对象的实例变量中)。
(注:关于代码块以及自由变量值有一个术语:闭包。在java中lambda表达式就是闭包。)
lambda表达式定义和使用变量的注意事项:
- lambda表达式中捕获的变量必须实际上是最终变量(即这个变量初始化之后就不会在为它赋新值)。
- 在lambda表达式中声明一个与局部变量同名的参数或局部变量是不合法的。
- 在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。(在lambda表达式中使用this,和在其他地方使用this没有任何差别)