java8实战读书笔记:Lambda表达式语法与函数式编程接口

简介: java8实战读书笔记:Lambda表达式语法与函数式编程接口

本文是博主在学习《java8实战》一书的读书笔记。

image.png

java8 lambda表达式语法的两种格式:


  • (parameters)  ->  expression
  • (parameters) -> {statements;}


语法解读:


  1. (parameters),lambda表达式的参数列表,其定义方法为JAVA普通的方法相同,例如(Object a, Object b)。
  2. -> 箭头,是参数列表与lambda表达式主题部分的分隔符号。
  3. expression 单表达式
  4. {statements; } 语句。


测试:如下语句是否是正确的lambda表达式。


(1)  () -> {}

(2)  () -> "Raoul"

(3)  () -> {return "Mario";}

(4)  (Integer i) -> return "Alan" + i;

(5)  (String s) -> {"IronMan";}


正解:


(1) 正确。如果使用匿名类(接口名统一使用IDemoLambda)表示如下:

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}

image.png

在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)唯一抽象方法的函数签名列表。


Runnable的函数式签名列表为public abstract void run();


温馨提示:如果我们有留意JDK8的Runnable接口的定义,你会发现给接口相对JDK8之前的版本多了一个注解:@FunctionalInterface,该注解是一个标识注解,用来标识这个接口是一个函数式接口。如果我们人为在一个不满足函数式定义的接口上增加@FunctionalInterface,则会在编译时提示错误。


image.png

例如有如下代码:

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}

当前需求为处理文件的第一行数据,那问题来了,如果需求变化需要返回文件的第一行和第二行数据,那该如何进行改造呢?


在理想的情况下,需要重用执行设置和关闭流的代码,并告诉processFile()方法对文件执行不同的操作,换句话说就是要实现对processFile的行为进行参数化。


Step·1:行为参数化


要读取文件的头两行,用Lambda语法如何实现呢?思考一下,下面这条语句是否可以实现?


(BufferedReader bf) -> br.readLine() + br.readLine()


答案是当然可以,接下来就要思考,定义一个什么样的方法,能接收上面这个参数。


Step2:使用函数式接口来传递行为


要使用(bufferedReader bf) -> br.readLine() + br.readLine(),则需要定义一个接受参数为BufferedReader,并返回String类型的函数式接口。


定义如下:

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表达式作为参数进行传递


将行为参数化后,并对方法进行改造,使方法接受一个函数式编程接口后,就可以将Lambda表达式直接传递给方法,例如:


1processFile(  (BufferedReader br)  -> br.readLine()  );
2processFile( (BufferedReader bf) -> br.readLine() + br.readLine());


image.png

从上面的讲解中我们已然能够得知,要能够将Lambda表达式当成方法参数进行参数行为化的一个前提条件是首先要在方法列表中使用一个函数式接口,例如上例中的BufferReaderProcess,那如果每次使用Labmbda表达式之前都要定义各自的函数式编程接口,那也够麻烦的,那有没有一种方式,或定义一种通用的函数式编程接口呢?答案是肯定的,Java8的设计者,利用泛型,定义了一整套函数式编程接口,下面将介绍java8中常用的函数式编程接口。


Predicate


22f7bf5bd2ba2a7c13ebce2cbf4d6d2f.jpg


所谓函数式编程接口就是只能定义一个抽象方法,Predicate函数接口中定义的抽象方法为boolean test(T t),对应的函数式行为为接收一类对象t,返回boolean类型,其可用的lambda表达式为(T t) -> boolean类型的表达式,例如(Sample a) -> a.isEmpty()。


该接口通常的应用场景为过滤。例如,要定义一个方法,从集合中进行刷选,具体的刷选逻辑(行为)由参数进行指定,那我们可以定义这样一个刷选的方法:


1public static <T> List<T> filter(List<T> list, Predicate<T> p) {
2List<T> results = new ArrayList<>();
3for(T s: list){
4if(p.test(s)){
5results.add(s);
6}
7}
8return results;
9}

上述函数,我们可以这样进行调用:

1Predicate<String> behaviorFilter = (String s) -> !s.isEmpty();  // lambda表达式赋值给一个变量
2filter(behaviorFilter);

其它add等方法,将在下文介绍(复合lambda表达式)。


另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:IntPredicate、LongPredicate、DoublePredicate。我们选择LongPredicate看一下其函数接口的声明:


1boolean test(long value);


Consumer


e827ed9b3e82787f2a024761a894c0ef.png

该函数式编程接口适合对对象进行处理,但没有返回值,对应的函数描述符:T -> void


举例如下:


1public static <T> void forEach(List<T> list, Consumer<T> 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。


Function


c0c49dd85e949fdbd7f0dadb46f685aa.png

其适合的场景是,接收一个泛型T的对象,返回一个泛型为R的对象,其对应的函数描述符:  T -> R。


示例如下:

1public static <T,R> List<R> map(List<T> list, Function<T,R> f) {
2          List<R> result = new ArrayList<>();
3          for(T t : list) {
4                result.add(  f.apply(t) );
5          }
6          return result;
7}
8List<Integer> 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>。


Supplier< T>


f5c9fc771fac62234718cd2000103eba.png

函数描述符:() -> T。适合创建对象的场景,例如  () -> new Object();


另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:BooleanSupplier、IntSupplier、LongSupplier、DoubleSupplier。


UnaryOperator< T >


bc28e3f2cc81e7db44d826e26257666a.png

一元运算符函数式接口,接收一个泛型T的对象,同样返回一个泛型T的对象。

示例如下:


1public static <T> List<T> map(List<T> list, UnaryOperator<T> f) {
2          List<R> 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。


BiPredicate


9d7f3bae4cc15483ec7accba23de9875.png

接收两个参数,返回boolean类型。其对应的函数描述符:(T,U) -> boolean。


BiConsumer


d0067e8281edf9a8d9d00831310b4e78.png

与Consume函数式接口类似,只是该接口接收两个参数,对应的函数描述符(T,U)  -> void。


另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:ObjIntConsumer、ObjLongConsumer、ObjDoubleConsumer。


BiFunction


7fc22a0ae982f52b213819373b504f23.png

与Function函数式接口类似,其对应的函数描述符:(T,U) -> R。


另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:ToIntBiFunction(T,U)、ToLongBiFunction(T,U)、ToDoubleBiFunction(T,U)。


BinaryOperator< T >



7fc22a0ae982f52b213819373b504f23.png


二维运算符,接收两个T类型的对象,返回一个T类型的对象。


另外,为了避免java基本类型与包装类型的装箱与拆箱带来的性能损耗,JDK8的设计者们提供了如下函数式编程接口:IntBinaryOperator、LongBinaryOperator、DoubleBinaryOperator。


上述就是JDK8定义在java.util.function中的函数式编程接口。重点关注的是其定义的函数式编程接口,其复合操作相关的API将在下文中详细介绍。


image.png


类型检查


java8是如何检查传入的Lambda表示式是否符合约定的类型呢?


例如


1public static <T> List<T> filter(List<T> list, Predicate<T> p) {
 2    List<T> 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<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
  • 其类型检测的步骤:
    首先查看filter函数的参数列表,得出Lambda对应的参数类型为Predicate。
  • 函数式接口Predicate中定义的抽象接口为  boolean test(T t),对应的函数描述符(  T  ->  boolean)。
  • 验证Lambda表达式是否符合函数描述符。


注意:如果一个Lambda的主体式一个语句表达式,它就和一个返回void的函数描述符兼容(当然参数列表也必须兼容)。例如,以下两行都是合法的,尽管List的add方法返回一个boolean,而不式Consumer上下文(T -> void)所要求的void:


1// Predicate返回了一个boolean
2Predicate<String> p = s -> list.add(s);
3// Consumer返回了一个void
4Consumer<String> 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的定义如下:

f82ec8a2e5efb344023c67328fd7063d.jpg


类型推断


所谓的类型推断,指的式java编译器能根据目标类型来推断出用什么函数式接口来配合Lambda表达式,这也意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型得到。


例如:

1List<Apple> greenApples =  filter(inventory, (Apple a) -> "green".equals(a.getColor()));
2也可以写成
3List<Apple> greenApples =  filter(inventory, a  -> "green".equals(a.getColor()));
4
5Lambda表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator 对象:
6Comparator<Apple> c =  (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
7Comparator<Apple> 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的值已改变,不符合局部变量的捕获条件,上述代码无法编译通过。


image.png


方法引用常用的构造方法


JDK8中有3中方法引用:


(1)指向静态方法的方法引用

  Integer.parseInt  对应的方法引用可以写成: Integer::parseInt。

(2)指向任意类型的实例方法的引用

(Strng str ) -> str.length  对应的方法引用:String::length。(注意这里的属性为方法列表)

(3)lambda捕获外部的实例对象


  例如如下代码:

1    Apple a = new Apple();
2    process(  () -> a.getColor()  );  // 则可以写成  process ( a::getColor );


构造函数引用


大家可以回想一下,jdk8中定义了一个创建对象的函数式编程接口Supplier,函数描述符:() -> T。适合创建对象的场景,例如  () -> new Object();


对于没有构造函数的,我们可以这样来创建对象:


1Supplier<Apple> c1 = Apple:new;
2Apple a1 = c1.get();

如果有1个参数的构造方法呢?

1Function<Integer, Apple> c2 = Apple::new;
2Apple a2 = c2.apply(weight);

Lambda语法的基础知识就介绍到这里,本文详细介绍了Lambda表达式的语法格式、函数式编程接口、lambda与函数式编程接口的关系、方法引用。


下一节主要介绍复合Lambda表达式使用。


相关文章
|
7天前
|
Java
Java基础却常被忽略:全面讲解this的实战技巧!
本次分享来自于一道Java基础的面试试题,对this的各种妙用进行了深度讲解,并分析了一些关于this的常见面试陷阱,主要包括以下几方面内容: 1.什么是this 2.this的场景化使用案例 3.关于this的误区 4.总结与练习
|
24天前
|
Java 程序员
Java基础却常被忽略:全面讲解this的实战技巧!
小米,29岁程序员,分享Java中`this`关键字的用法。`this`代表当前对象引用,用于区分成员变量与局部变量、构造方法间调用、支持链式调用及作为参数传递。文章还探讨了`this`在静态方法和匿名内部类中的使用误区,并提供了练习题。
26 1
|
26天前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
28天前
|
Java API 开发者
Java中的Lambda表达式:简洁代码的利器####
本文探讨了Java中Lambda表达式的概念、用途及其在简化代码和提高开发效率方面的显著作用。通过具体实例,展示了Lambda表达式如何在Java 8及更高版本中替代传统的匿名内部类,使代码更加简洁易读。文章还简要介绍了Lambda表达式的语法和常见用法,帮助开发者更好地理解和应用这一强大的工具。 ####
|
1月前
|
并行计算 Java 编译器
深入理解Java中的Lambda表达式
在Java 8中引入的Lambda表达式,不仅简化了代码编写,还提升了代码可读性。本文将带你探索Lambda表达式背后的逻辑与原理,通过实例展示如何高效利用这一特性优化你的程序。
|
1月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
58 6
|
1月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
1月前
|
搜索推荐 Java API
探索Java中的Lambda表达式
本文将深入探讨Java 8引入的Lambda表达式,这一特性极大地简化了代码编写,提高了程序的可读性。通过实例分析,我们将了解Lambda表达式的基本概念、使用场景以及如何优雅地重构传统代码。文章不仅适合初学者,也能帮助有经验的开发者加深对Lambda表达式的理解。
|
1月前
|
Java
探索Java中的Lambda表达式
【10月更文挑战第37天】本文将带你深入理解Java的Lambda表达式,从基础语法到高级特性,通过实例讲解其在函数式编程中的应用。我们还将探讨Lambda表达式如何简化代码、提高开发效率,并讨论其在实际项目中的应用。
|
1月前
|
Java API
Java中的Lambda表达式与函数式编程####
【10月更文挑战第29天】 本文将深入探讨Java中Lambda表达式的实现及其在函数式编程中的应用。通过对比传统方法,我们将揭示Lambda如何简化代码、提高可读性和维护性。文章还将展示一些实际案例,帮助读者更好地理解和应用Lambda表达式。 ####