专栏首页汤圆学JavaJava8中的Lambda表达式
原创

Java8中的Lambda表达式

作者:汤圆

个人博客:javalover.cc

前言

大家好啊,我是汤圆,今天给大家带来的是《Java8中的Lambda表达式》,希望对大家有帮助,谢谢

文章纯属原创,个人总结难免有差错,如果有,麻烦在评论区回复或后台私信,谢啦

简介

Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次;

下面贴个对比代码:

// Java8之前:旧的写法
Runnable runnable = new Runnable() {
  @Override
  public void run() {
    System.out.println("old run");
  }
};
Thread t = new Thread(runnable);
​
// Java8之后:新的写法
Runnable runnable1 = ()->{
  System.out.println("lambda run");
};
Thread t1 = new Thread(runnable1);

可以看到,有了lambda,代码变得简洁多了

你可以把lambda当作一个语法糖

下面让我们一起来探索lambda的美好世界吧

目录

下面列出本文的目录

  • lambda的语法
  • 为啥引入lambda
  • 什么是函数式接口
  • 什么是行为参数化
  • 手写一个函数式接口
  • 常用的函数式接口
  • 什么是方法引用
  • 什么是构造引用
  • lambda的组合操作

正文

1. lambda的语法

下面分别说下语法中的三个组成部分

  • 参数: ( Dog dog )
    • 参数类型可省略(当编译器可以自动推导时),比如Comparator<String> comparatorTest = (a, b)->a.length()-b.length();,可以推导出a,b都为String
    • 当参数类型可省略,且只有一个参数时,括弧也可以省略(但是个人习惯保留)
  • 符号: ->
  • 主体:{ System.out.println("javalover"); }
    • 如果是一条语句,则需要加大括号和分号{;}(比如上图所示)
    • 如果是一个表达式,则直接写,啥也不加(比如a.length()- b.length()

2. 为啥引入lambda

为了简化代码

因为Java是面向对象语言,所以在lambda出现之前,我们需要先构造一个对象,然后在对象的方法中实现具体的内容,再把构造的对象传递给某个对象或方法

但是有了lambda以后,我们可以直接将代码块传递给对象或方法

现在再回头看下开头的例子

可以看到,用了lambda表达式后,少了很多模板代码,只剩下一个代码块(最核心的部分)

3. 什么是函数式接口

就是只定义了一个抽象方法的接口

  • 正例:有多个默认方法,但是如果只有一个抽象方法,那它就是函数式接口,示例代码如下
@FunctionalInterface
public interface FunctionInterfaceDemo {
    void abstractFun();
    default void fun1(){
        System.out.println("fun1");    
    }
    default void fun2(){
        System.out.println("fun2");
    }   
}

这里的注解@FunctionalInterface可以省略,但是建议加上,就是为了告诉编译器,这是一个函数式接口,此时如果该接口有多个抽象方法,那么编译器就会报错

  • 反例:比如A extends B,A和B各有一个抽象方法,那么A就不是函数式接口,示例代码如下
// 编译器会报错,Multiple non-overriding abstract methods found in XXX
@FunctionalInterface
public interface NoFunctionInterfaceDemo extends FunctionInterfaceDemo{
  void abstractFun2();
}

上面的父接口FunctionInterfaceDemo中已经有了一个抽象方法,此时NoFunctionInterfaceDemo又定义了一个抽象方法,结果编译器就提示了:存在多个抽象方法

在Java8之前,其实我们已经接触过函数式接口

比如Runnable 和 Comparable

只是没有注解@FunctionalInterface。

那这个函数式接口要怎么用呢?

配合lambda食用,效果最佳(就是把lambda传递给函数式接口),示例代码如下:

new Thread(() -> System.out.println("run")).start();

其中用到的函数式接口是Runnable

4. 什么是行为参数化

就是把行为定义成参数,行为就是函数式接口

类似泛型中的类型参数化<T>,类型参数化是把类型定义成参数

行为参数化,通俗点来说:

  • 就是用函数式接口形参
  • 然后传入接口的各种实现内容(即lambda表达式)作为实参
  • 最后在lambda内实现各种行为(好像又回到多态的那一节了?这也是为啥多态是Java的三大特性的原因之一,应用太广泛了)

这样来看的话,行为参数化和设计模式中的策略模式有点像了(后面章节会分别讲常用的几种设计模式)

下面我们手写一个函数式接口来加深理解吧

5. 手写一个函数式接口

下面我们循序渐进,先从简单的需求开始

  • 第一步:比如我们想要读取某个文件,那可以有如下方法:
public static String processFile() throws IOException {
    // Java7新增的语法,try(){},可自动关闭资源,减少了代码的臃肿
    try( BufferedReader bufferedReader = 
        new BufferedReader(new  FileReader("D:\\JavaProject\\JavaBasicDemo\\test.txt"))){
        return bufferedReader.readLine();
    }
}

可以看到,核心的行为动作就是 return bufferedReader.readLine();,表示读取第一行的数据并返回

那如果我们想要读取两行呢?三行?

  • 第二步:这时就需要用到上面的函数式接口了,下面就是我们自己编写的函数式接口
@FunctionalInterface
interface FileReadInterface{
    // 这里接受一个BufferedReader对象,返回一个String对象
    String process(BufferedReader reader) throws IOException;
}

可以看到,只有一个抽象方法process(),它就是用来处理第一步中的核心动作(读取文件内容)

至于想读取多少内容,那就需要我们在lambda表达式中定义了

  • 第三步:接下来我们定义多个lambda表达式,用来传递函数式接口,其中每个lambda表达式就代表了一种不同的行为,代码如下:
// 读取一行
FileReadInterface fileReadInterface = reader -> reader.readLine();
// 读取两行
FileReadInterface fileReadInterface2 = reader -> reader.readLine() + reader.readLine();
​
  • 第四步:我们需要修改第一步的processFile(),让其接受一个函数式接口,并调用其中的抽象方法,代码如下:
// 参数为第二步我们自己手写的函数式接口
public static String processFile(FileReadInterface fileReadInterface) throws IOException {
        try( BufferedReader bufferedReader =
                 new BufferedReader(new FileReader("./test.txt"))){
                    // 这里我们不再自己定义行为,而是交给函数式接口的抽象方法来处理,然后通过lambda表达式的传入来实现多个行为
          return fileReadInterface.process(bufferedReader);
        }
    }
  • 第五步:拼接后,完整代码如下:
public class FileReaderDemo {
    public static void main(String[] args) throws IOException {
				// 第三步: 
      	// lambda表达式1 传给 函数式接口:只读取一行
      	FileReadInterface fileReadInterface = reader -> reader.readLine();
				// lambda表达式2 传给 函数式接口:只读取两行
      	FileReadInterface fileReadInterface2 = reader -> reader.readLine() + reader.readLine();
      	// 最后一步: 不同的函数式接口的实现,表现出不同的行为
        String str1 = processFile(fileReadInterface);
        String str2 = processFile(fileReadInterface2);
        System.out.println(str1);
        System.out.println(str2);
    }
  	// 第四步: 读取文件方法,接受函数式接口作为参数
    public static String processFile(FileReadInterface fileReadInterface) throws IOException {
        try( BufferedReader bufferedReader =
                 new BufferedReader(new FileReader("./test.txt"))){
					// 调用函数式接口中的抽象方法来处理数据					
          return fileReadInterface.process(bufferedReader);
        }
    }
	// 第一步:
  public static String processFile() throws IOException {
        try( BufferedReader bufferedReader =
                 new BufferedReader(new FileReader("./test.txt"))){
          return bufferReader.readLine();
        }
    }


}

// 第二步: 我们手写的函数式接口
@FunctionalInterface
interface FileReadInterface{
    String process(BufferedReader reader) throws IOException;
}

其实你会发现,我们手写的这个函数式接口,其实就是Function<T>去除泛型化后的接口,如下所示:

@FunctionalInterface
public interface Function<T, R> {
	// 都是接受一个参数,返回另一个参数
  R apply(T t);
}

下面我们列出Java中常用的一些函数式接口,你会发现自带的已经够用了,基本不会需要我们自己去写

这里的手写只是为了自己实现一遍,可以加深理解程度

6. 常用的函数式接口

7. 什么是方法引用

我们先看一个例子

前面我们写的lambda表达式,其实还可以简化,比如

// 简化前
Function<Cat, Integer> function = c->c.getAge();
// 简化后
Function<Cat, Integer> function2 = Cat::getAge;

其中简化后的Cat::getAge,我们就叫做方法引用

方法引用就是引用类或对象的方法

下面我们列出方法引用的三种情况:

  1. Object::instanceMethod(对象的实例方法)
  2. Class::staticMethod(类的静态方法)
  3. Class::instanceMethod(类的实例方法)

像我们上面举的例子就是第三种:类的实例方法

下面我们用代码演示上面的三种方法:

public class ReferenceDemo {
    public static void main(String[] args) {
        // 第一种:引用对象的实例方法
        Cat cat = new Cat(1);
        Function<Cat, Integer> methodRef1 = cat::getSum; 
        // 第二种:引用类的静态方法
        Supplier<Integer> methodRef2 = Cat::getAverageAge;
        // 第三种:引用类的实例方法
        Function<Cat, Integer> methodRef3 = Cat::getAge;
    }
}
class Cat {
    int age;

    public Cat(int age) {
        this.age = age;
    }

    // 获取猫的平均年龄
    public static int getAverageAge(){
        return 15;
    }
    // 获取两只猫的年龄总和
    public int getSum(Cat cat){
        return cat.getAge() + this.getAge();
    }

    public int getAge() {
        return age;
    }    public void setAge(int age) {
        this.age = age;
    }
}

为啥要用这个方法引用呢?

方法引用好比lambda表达式的语法糖,语法更加简洁,清晰

一看就知道是调用哪个类或对象的哪个方法

8. 什么是构造引用

上面介绍了方法引用,就是直接引用某个方法

这里的构造引用同理可得,就是引用某个类的构造方法

构造引用的表达式为:Class::new,仅此一种

如果你有多个构造函数,那编译器会自己进行推断参数(你看看,多好,多简洁)

比如下面的代码:

// 这里调用 new Cat()
Supplier<Cat> constructRef1 = Cat::new;
// 这里调用 new Cat(Integer)
Function<Integer, Cat> constructRef2 = Cat::new;

9. lambda表达式中引入外部变量的限制

要求引入lambda表达式中的变量,必须是最终变量,即该变量不会再被修改

比如下面的代码:

public static void main(String[] args) {
  String str = "javalover.cc";
  Runnable runnable = ()->{
    str = "1";// 这里会报错,因为修改了str引用的指向
    System.out.println(str);
  }
}

可以看到,lambda表达式引用了外面的str引用,但是又在表达式内部做了修改,结果就报错了

为啥要有这个限制呢?

为了线程安全,因为lambda表达式有一个好处就是只在需要的时候才会执行,而不是调用后立马执行

这样就会存在多个线程同时执行的并发问题

所以Java就从根源上解决:不让变量被修改,都是只读的

那你可能好奇,我不把str的修改代码放到表达式内部可以吗?

也不行,道理是一样的,只要lambda有用到这个变量,那这个变量不管是在哪里被修改,都是不允许的

不然的话,我这边先执行了一次lambda表达式,结果你就改了变量值,那我第二次执行lambda,不就乱了吗

10. lambda的组合操作

最后是lambda的必杀技:组合操作

在这里叫组合或者复合都可以

概述:组合操作就是先用一个lambda表达式,然后再在后面组合另一个lambda表达式,然后再在后面组合另另一个lambda表达式,然后。。。有点像是链式操作

学过JS的都知道Promise,里面的链式操作就和这里的组合操作很像

用过Lombok的朋友,应该很熟悉@Builder注解,其实就是构造者模式

下面我们用代码演示下组合操作:

// 重点代码
public class ComposeDemo {
    public static void main(String[] args) {
        List<Dog> list = Arrays.asList(new Dog(1,2), new Dog(1, 1));
        // 1. 先按年龄排序(默认递增)
      	// Dog::getAge, 上面介绍的方法引用
      	// comparingInt, 是Comparator的一个静态方法,返回Comparator<T>
      	Comparator<Dog> comparableAge = Comparator.comparingInt(Dog::getAge);
        // 2. 如果有相同的年龄,则年龄相同的再按体重排序(如果年龄已经比较出大小,则下面的体重就不会再去比较)
        Comparator<Dog> comparableWeight = Comparator.comparingInt(Dog::getWeight);;
        // 3. 调用list对象的sort方法排序,参数是Comparator<? super Dog>
        list.sort(comparableAge.thenComparing(comparableWeight));
        System.out.println(list);
    }
}
// 非重点代码
class Dog{
    private int age;
    private int weight;

    public Dog(int age, int weight) {
        this.age = age;
        this.weight = weight;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "age=" + age +
                ", weight=" + weight +
                '}';
    }
}

输出:[Dog{age=1, weight=1}, Dog{age=1, weight=2}]

比较的流程如下所示:

总结

  1. lambda的语法: 参数+符合+表达式或语句,比如(a,b)->{System.out.println("javalover.cc");}
  2. 函数式接口:只有一个抽象方法,最好加@FunctionalInterface,这样编译器可及时发现错误,javadoc也说明这是一个函数式接口(可读性)
  3. 行为参数化:就是函数式接口作为参数,然后再将lambda表达式传给函数式接口,通过不同的lambda内容实现不同的行为
  4. 方法引用:lambda的语法糖,总共有三种:
    • Object::instanceMethod(对象的实例方法)
    • Class::staticMethod(类的静态方法)
    • Class::instanceMethod(类的实例方法)
  5. 构造引用:就一种,编译器自己可判断是哪个构造函数,语法为Class::new
  6. 在lambda中引入外部变量,必须保证这个变量是最终变量,即不再被修改
  7. lambda的组合操作,就是链式操作,组合是通过函数式接口的静态方法来组合(静态方法会返回另一个函数式接口的对象)

比如list.sort(comparableAge.thenComparing(comparableWeight));

后记

最后,感谢大家的观看,谢谢

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java8中的Lambda表达式

    Lambda 表达式是 Java 8 最受欢迎的功能。人们将函数式编程的概念引入了 Java 这门完全面向对象的命令式编程语言。关于函数式编程是如何运作的,这个...

    java干货
  • Java8中的Lambda表达式 - 崔笑颜的博客

    因为Java是面向对象语言,所以在lambda出现之前,我们需要先构造一个对象,然后在对象的方法中实现具体的内容,再把构造的对象传递给某个对象或方法

    崔笑颜
  • Java8 lambda表达式

    前些天在写代码时,突然发现某一位大佬的代码中充斥着stream来操作List,自己的for循环相比之下黯然失色,遂决定要尽快学习一下。接下来突然的一周加班阻塞了...

    呼延十
  • Java8 Lambda表达式

    在Python中是有的。但是Python中万物皆对象,直接将函数赋值给一个变量即可,那么在Java中该如何使用lambda表达式呢?

    烟草的香味
  • JAVA8 Lambda表达式(下)

    体会到Lambda表达式的优雅与简洁,进一步记录下一步操作,

    疯狂的KK
  • Java8 lambda表达式小记

    按照Java8函数式接口的定义,其只能有一个抽象方法,否则就不是函数式接口,就无法使用Lambda表达式。

    休辞醉倒
  • Java8 Lambda表达式入门

    Lambda表达式的实质就是一个匿名函数。C#3.0引入了Lambda表达式,Java8也不甘示弱。Java8发布很久了,今天安装了JDK体验了Java8中的L...

    卡尔曼和玻尔兹曼谁曼
  • Java8 Lambda表达式教程

    public int add(int x, int y) {         return x + y;     }

    用户7886150
  • java8 lambda 表达式详解

    函数式接口是仅含一个抽象方法的接口,但可以指定 Object 定义的任何公有方法。

    猿芯
  • Java8学习(3)- Lambda 表达式

    猪脚:以下内容参考《Java 8 in Action》 本次学习内容: Lambda 基本模式 环绕执行模式 函数式接口,类型推断 方法引用 Lambda 复...

    Ryan-Miao
  • JAVA8之lambda表达式详解

    Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)

    哲洛不闹
  • Java8之熟透Lambda表达式

    ​ Lambda 表达式可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

    用户1195962
  • 一文看懂 Java8 的 Lambda表达式!

    IT领域的技术日新月异,Java14问世了,但是对于国内的许多程序员来说,连Java8都还没有真正掌握。

    程序员小跃
  • Java8-Lambda表达式-函数式接口

    Erwin
  • Java8新特性——Lambda表达式(一)

    1. 预备知识 在了解Lambda表达式之前首先需要了解以下预备知识。 1.1 如何成为一名高级码农? 如果老板让你写一个用于毁灭地球的函数,而你写了一个毁灭...

    大闲人柴毛毛
  • Java8新特性(1):Lambda表达式

    Lambda表达式可以理解为一种匿名函数:没有名称,但有参数列表、函数主体、返回类型。它是行为参数化的一种实现,行为参数化是指将不同的行为作为参数传递给方法,方...

    布禾
  • java8新特性(一):Lambda表达式

    Lambda 是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的...

    周三不加班
  • 深入理解Java8 Lambda表达式

    匿名函数的应用场景是: 通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用Lambda表达式。lambda表达式所表示的匿名函数的内容应该是很简单...

    ZhangXianSheng
  • java8新特性之Lambda表达式

    java8在2014年就推出了,整天喊着8版本稳定,企业都用jdk8,结果8的特性现在才系统的学,罪过罪过啊!此系列博客可能3-4篇,带你全面地了解java8新...

    用户8902830

扫码关注云+社区

领取腾讯云代金券