本文是博主在学习《java8实战》一书的读书笔记。
java8 lambda表达式语法的两种格式:
(parameters) -> expression
(parameters) -> {statements;}
语法解读:
(parameters),lambda表达式的参数列表,其定义方法为JAVA普通的方法相同,例如(Object a, Object b)。
-> 箭头,是参数列表与lambda表达式主题部分的分隔符号。
expression 单表达式
{statements; } 语句。
测试:如下语句是否是正确的lambda表达式。
正解:
1 new IDemoLambda() {
2 public void test() {
3 }
4}
(2)正确。如果使用匿名类(接口名统一使用IDemoLambda)表示如下:
1 new IDemoLambda() {
2 public String test() {
3return "Raoul"; // 如果直接接一个值,表示返回该值
4 }
5}
(3)正确。如果使用匿名类(接口名统一使用IDemoLambda)表示如下:
1 new IDemoLambda() {
2 public String test() {
3return "Mario";
4 }
5}
(4)错误。因为return是流程控制语句,表示返回,不是一个表达式,故不符合lambda语法,正确的表示方法应该是 (Integer i) ->{ return "Alan" + i;}。如果使用匿名类(接口名统一使用IDemoLambda)表示如下:
1 new IDemoLambda() {
2 public String test(Integer i) {
3return "Alan" + i;
4 }
5}
(5)错误。因为"IronMan"是一个表达式,并不是一个语句,故不能使用{}修饰,应修改为 (String s) -> "IronMan"。如果使用匿名类(接口名统一使用IDemoLambda)表示如下:
1 new IDemoLambda() {
2 public String test(String s) {
3return "IronMan";
4 }
5}
在java8中,一个接口如果只定义了一个抽象方法,那这个接口就可以称为函数式接口,就可以使用lambda表达式来简化程序代码。Lambda表达式可以直接赋值给变量,也可以直接作为参数传递给函数,示例如下:
1public static void startThread(Runnable a) {
2 (new Thread(a)).start();
3}
4
5public static void main(String[] args) {
6 // lambda表达式可以直接赋值给变量,也可以直接以参数的形式传递给方法、
7 Runnable a = () -> {
8 System.out.println("Hello World,Lambda...");
9 };
10 // JDK8之前使用匿名类来实现
11 Runnable b = new Runnable() {
12 @Override
13 public void run() {
14 System.out.println("Hello World,Lambda...");
15 }
16 };
17 startThread(a);
18 startThread(() -> {
19 System.out.println("Hello World,Lambda...");
20 });
21}
那能将(int a) -> {System.out.println("Hello World, Lambda…");}表达式赋值给Runnable a变量吗?答案是不能,因为该表达式不符合函数式编程接口(Runnable)唯一抽象方法的函数签名列表。
温馨提示:如果我们有留意JDK8的Runnable接口的定义,你会发现给接口相对JDK8之前的版本多了一个注解:@FunctionalInterface,该注解是一个标识注解,用来标识这个接口是一个函数式接口。如果我们人为在一个不满足函数式定义的接口上增加@FunctionalInterface,则会在编译时提示错误。
例如有如下代码:
1/**
2 * 处理文件:当前需求是处理文件的第一行数据
3 * @return
4 * @throws IOException
5 */
6public static String processFile() throws IOException {
7 try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
8 return br.readLine();
9 }
10}
当前需求为处理文件的第一行数据,那问题来了,如果需求变化需要返回文件的第一行和第二行数据,那该如何进行改造呢?
Step·1:行为参数化
(BufferedReader bf) -> br.readLine() + br.readLine()
Step2:使用函数式接口来传递行为
1@FunctionalInterface
2public interface BufferedReaderProcessor {
3 public String process(BufferedReader b) throws IoException;
4}
那把processFile方法改造成如下代码:
1/**
2 * 处理文件:当前需求是处理文件的第一行数据
3 * @return
4 * @throws IOException
5 */
6public static String processFile(BufferedReaderProcess brp) throws IOException {
7 try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
8 return brp.process(br);
9 }
10}
Step3:使用lambda表达式作为参数进行传递
1processFile( (BufferedReader br) -> br.readLine() );
2processFile( (BufferedReader bf) -> br.readLine() + br.readLine());
从上面的讲解中我们已然能够得知,要能够将Lambda表达式当成方法参数进行参数行为化的一个前提条件是首先要在方法列表中使用一个函数式接口,例如上例中的BufferReaderProcess,那如果每次使用Labmbda表达式之前都要定义各自的函数式编程接口,那也够麻烦的,那有没有一种方式,或定义一种通用的函数式编程接口呢?答案是肯定的,Java8的设计者,利用泛型,定义了一整套函数式编程接口,下面将介绍java8中常用的函数式编程接口。
该接口通常的应用场景为过滤。例如,要定义一个方法,从集合中进行刷选,具体的刷选逻辑(行为)由参数进行指定,那我们可以定义这样一个刷选的方法:
1public static List filter(List list, Predicate p) {
2List results = new ArrayList<>();
3for(T s: list){
4if(p.test(s)){
5results.add(s);
6}
7}
8return results;
9}
上述函数,我们可以这样进行调用:
1Predicate behaviorFilter = (String s) -> !s.isEmpty(); // lambda表达式赋值给一个变量
2filter(behaviorFilter);
其它add等方法,将在下文介绍(复合lambda表达式)。
另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:IntPredicate、LongPredicate、DoublePredicate。我们选择LongPredicate看一下其函数接口的声明:
1boolean test(long value);
举例如下:
1public static void forEach(List list, Consumer c) {
2 for(T t : list) {
3 c.accept(t);
4 }
5}
其调用示例如下:
1forEach( Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i) );
另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:IntConsumer、LongConsumer、DoubleConsumer。
示例如下:
1public static List map(List list, Function f) {
2 List result = new ArrayList<>();
3 for(T t : list) {
4 result.add( f.apply(t) );
5 }
6 return result;
7}
8List l = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length );
另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:IntFunction< R>、LongFunction< R>、DoubleFunction< R>、IntToDoubleFunction、IntToLongFunction、LongToIntFunction、LongToDoubleFunction、ToIntFunction< T>、ToDoubleFunction< T>、ToLongFunction< T>。
另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:BooleanSupplier、IntSupplier、LongSupplier、DoubleSupplier。
1public static List map(List list, UnaryOperator f) {
2 List result = new ArrayList<>();
3 for(T t : list) {
4 result.add( f.apply(t) );
5 }
6 return result;
7}
8
9map( list, (int i) -> i ++ );
另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:IntUnaryOperator、LongUnaryOperator、DoubleUnaryOperator。
另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:ObjIntConsumer、ObjLongConsumer、ObjDoubleConsumer。
另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:ToIntBiFunction(T,U)、ToLongBiFunction(T,U)、ToDoubleBiFunction(T,U)。
另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:IntBinaryOperator、LongBinaryOperator、DoubleBinaryOperator。
上述就是JDK8定义在java.util.function中的函数式编程接口。重点关注的是其定义的函数式编程接口,其复合操作相关的API将在下文中详细介绍。
java8是如何检查传入的Lambda表示式是否符合约定的类型呢?
1public static List filter(List list, Predicate p) {
2 List results = new ArrayList<>();
3 for(T s: list){
4 if(p.test(s)){
5 results.add(s);
6 }
7 }
8 return results;
9}
10
11List heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
其类型检测的步骤:
函数式接口Predicate中定义的抽象接口为 boolean test(T t),对应的函数描述符( T -> boolean)。
验证Lambda表达式是否符合函数描述符。
注意:如果一个Lambda的主体式一个语句表达式,它就和一个返回void的函数描述符兼容(当然参数列表也必须兼容)。例如,以下两行都是合法的,尽管List的add方法返回一个boolean,而不式Consumer上下文(T -> void)所要求的void:
1// Predicate返回了一个boolean
2Predicate p = s -> list.add(s);
3// Consumer返回了一个void
4Consumer b = s -> list.add(s);
思考题:如下表达式是否正确?
1Object o = () -> {System.out.println("Tricky example"); };
答案是错误的,该语句的含义就是把lambda表达式复制给目标对象(Object o),lambda对应的函数描述符为() -> void,期望目标对象拥有一个唯一的抽象方法,参数列表为空,返回值为void的方法,显然目标对象Object不满足该条件,如果换成如下示例,则能编译通过:
1Runnable r = () {System.out.println("Tricky example"); };
因为Runnable的定义如下:
所谓的类型推断,指的式java编译器能根据目标类型来推断出用什么函数式接口来配合Lambda表达式,这也意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型得到。
例如:
1List greenApples = filter(inventory, (Apple a) -> "green".equals(a.getColor()));
2也可以写成
3List greenApples = filter(inventory, a -> "green".equals(a.getColor()));
4
5Lambda表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator 对象:
6Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
7Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
由于java编译器能根据目标类型来推导出Lambda的函数签名,故lambda的函数签名列表时,可以去掉参数的类型。
Lambda表达式主体部分也能引入外部的变量,例如:
1int portNumber = 1337;
2Runnable r = () -> System.out.println(portNumber);
其中portNumber参数并不是方法签名参数,但这样有一个限制条件,引入的局部变量必须是常量(实际意义上的常量,可以不用final来定义,但不能改变其值。例如如下示例是错误的:
1int portNumber = 1337;
2Runnable r = () -> System.out.println(portNumber);
3portNumber = 1228; // 因为portNumber的值已改变,不符合局部变量的捕获条件,上述代码无法编译通过。
JDK8中有3中方法引用:
1 Apple a = new Apple();
2 process( () -> a.getColor() ); // 则可以写成 process ( a::getColor );
大家可以回想一下,jdk8中定义了一个创建对象的函数式编程接口Supplier,函数描述符:() -> T。适合创建对象的场景,例如 () -> new Object();
1Supplier c1 = Apple:new;
2Apple a1 = c1.get();
如果有1个参数的构造方法呢?
1Function c2 = Apple::new;
2Apple a2 = c2.apply(weight);
Lambda语法的基础知识就介绍到这里,本文详细介绍了Lambda表达式的语法格式、函数式编程接口、lambda与函数式编程接口的关系、方法引用。
·end·
—如果本文有帮助,请分享到朋友圈吧—
我们一起愉快的玩耍!