lambda的理解

420 阅读5分钟

Java引入lambda:

为了简化代码,在面向对象中需要先构造一个对象然后在对象的方法中实现具体的内容,再把构造的对象传递给某个对象或者方法,lambda直接将代码传递给对象或方法

函数式接口

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

@FunctionalInterface
public interface sum{
    int add(int a,int b);
}

@FunctionalInterface对编译器申明这是一个函数式接口,可省略,如果有多个抽象方法则报错

行为参数化

把行为定义成参数,行为就是函数式接口,类似泛型中的类型参数化,类型参数化是把类型定义成参数

  • 用函数式接口做形参
  • 传入接口的各种实现内容(即lambda表达式)作为实参
  • 在lambda内实现各种行为(多态)

实现

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才是最终归宿

@FunctionalInterface
public interface Function<TR> {
// 都是接受一个参数,返回另一个参数
 apply(T t);
}
函数式接口参数类型返回类型抽象方法名描述其他方法
SupplierTget提供一个T类型的值
ConsumerTvoidaccept处理一个T类型的值andThen
BigConsumer<T,U>T,Uvoidaccept处理一个T和U类型的值andThen
Function<T,R>TRapply有一个T类型参数的函数compose、andThen、identity
BigFunction<T,U,R>T,URapply有一个T和U类型参数的函数andThen
PredicateTbooleantest布尔值函数and、or、negate、isEqual
... ...

方法引用

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

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

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

  • Object::instanceMethod(对象的实例方法)
  • Class::staticMethod(类的静态方法)
  • 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;
  }
}

构造引用

// 这里调用 new Cat()
Supplier<Cat> constructRef1 = Cat::new;
// 这里调用 new Cat(Integer)
// 需要调用的构造器的参数列表要与函数式接口中抽象方法的参数列表保持一致。
//Function接口中的抽象方法apply的参数列表要与要调用的Cat的构造器参数列表保持一致
Function<Integer, Cat> constructRef2 = Cat::new;

限制

要求引入lambda表达式中的变量,局部变量必须是最终变量,由final修饰,不然编译器报错

从Java设计者解释:

Java本身是值传递,不管是基本类型还是引用类型。每个方法它的入参与局部变量都是都是存放在栈空间中,而基本类型还是引用类型都是存放在栈帧中。在JVM的优化中,如逃逸分析会让确定生命周期的对象存放在栈空间内(本地内存)。而所谓的传值调用就是栈空间进行了副本拷贝。lambda就是创建一个匿名对象并且调用匿名对象的某一个方法,这个方法对于调用对象是一个局部变量。编译器认为一个方法调用一个对象的一个方法,那此时方法的局部变量也会隐式的生成一个构造方法将其传递进去,通过栈帧copy的方式传递。而方法的调用顺序(调用的方法可能是异步的)是不确定的,而方法结束后栈空间是要回收的,此时存在风险,当lambda表达式是在调用函数执行完后再执行的而刚才传递的局部变量可能已经被回收,而lambda得到的这个变量所指向的内存地址是不可靠的,即闭包。1.8以后提供effective final由编译器自动给变量补充,Java直接限制了闭包行为。当开发者显式的修改变量的值时会提示开发者异常信息。

从程序语言设计角度解释:

应用场景:局部作用域与非局部作用域(如匿名对象及java8开始的Lambda)共享局部变量。最理想的语义实现是:后者能表现得跟局部作用域完全一样。

在真正支持函数嵌套的语言如Pascal&Delphi中是理所当然的(因为都分配于栈中,可直接共享局部变量),但在其它(典型如C系编译型:C,C++,java,C#,go…)语言中由于不支持嵌套函数,而只能通过语法糖用堆来模拟嵌套函数,而具体实现方式主要分两种:支持直接引用局部变量地址的实现如C++20,这种实现简单暴力,存在非法访问的隐患;C#,Go则是将局部变量转移到堆上,但语法糖会让其表现得跟局部变量一样,这种实现牺牲了效率,但胜在简单不会有前述隐患。这两种截然相反的设计在逻辑上存在一个正交点,那就是被标记为不可变的局部变量,也就是说无论将变量维持在栈上,还是转移到堆上,还是栈上原版并而堆上副本,能对此类变量的操作都是一样的。