关于java:Java8中的Lambda表达式

42次阅读

共计 7685 个字符,预计需要花费 20 分钟才能阅读完成。

作者:汤圆

集体博客: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));

后记

最初,感激大家的观看,谢谢

正文完
 0