深入 Java Lambda 二:Lambda 基础篇

栏目: Java · 发布时间: 5年前

内容简介:本文是由笔者所原创的本文为作者原创作品,转载请注明出处;一句话,Java Lambda 其实就是初始化匿名实例的另外一种简洁且高效的写法,它的目的就是构造得到一个 Functional Interface 的匿名实例对象;

本文是由笔者所原创的 深入 Java Lambda 系列 之一;本文是笔者在深入分析完官文 lambda state final 以后消化并总结得出的有关 Lambda 的基础特性;化繁为简,不做逐字逐句的翻译;

本文为作者原创作品,转载请注明出处;

概述

一句话,Java Lambda 其实就是初始化匿名实例的另外一种简洁且高效的写法,它的目的就是构造得到一个 Functional Interface 的匿名实例对象;

Functional Interface

什么是 Functional Interface,一句话,其实就是只包含一个方法的接口;不过要注意的是,因为 Java 8 为接口新增了 static 和 default 的实现方法,如果这些方法出现在了接口中,统统不算数;来看一个例子,在前文 深入 Java Lambda 一:为什么需要 Lambda 中所使用到的 MyTest 接口就是一个 Functional Interface,因为它只包含一个接口方法;

public interface MyTest<T> {
  public boolean test(T t);
}

可以使用 @FunctionalInterface 注解来强制约束该 Interface 定义为 Functional Interafce,这样做的好处是,在编译时刻,编译器会验证当前的接口是不是合法的 Functional Interface,如果不是,则报错;那么我们可以将 @FunctionalInterface 加载上述的例子中,强制编译器在编译时刻校验;

@FunctionalInterface
public interface MyTest<T> {
  public boolean test(T t);
}

所以根据上述的定义,Java SE 7 已经有如下接口适合于作为 Functional Interface;

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.beans.PropertyChangeListener

Java SE 8,新增了一个新的包,java.util.function,包含了如下新增的常用的 Funcationl Interfaces,

  • Predicate – a boolean-valued property of an object
  • Consumer – an action to be performed on an object
  • Function<T,R> – a function transforming a T to a R
  • Supplier – provide an instance of a T (such as a factory)
  • UnaryOperator – a function from T to T
  • BinaryOperator – a function from (T, T) to T

Lambda 表达式的定义

Lambda 表达式将定义一个匿名类实例所需要的 5 行代码精简为了一行代码,简而言之,Lambda 既是将Vertical Problem 通过横向的方式解决了;相关例子参考 使用 Lambda 解决 Vertical Problem 小节中所描述的例子;

➭ Lambda 表达式由如下三个部分构成,

Argument List Arrow Token Body
(int x, int y) -> x + y
  • Argument List

    表示 Functional Interface 接口方法的入参;包含参数的类型和变量;

  • Arrow Token

    约定俗成,没有特别的含义,主要是用来分隔定义 Argument List 与 Body;

  • Body

    Body 可以由两种不同的方式构成,可以是由一个简单的表达式所构成,或者是由一个语句块所构成:

    1. 当 Body 是由一个简单表达式所构成的时候,该表达式的结果将作为一个 return 语句返回;上述例子中,x + y 等价于 return x + y
    2. 当 Body 是由一个语句块所构成的时候,就等同于一个普通方法的内部实现,可以根据实际情况决定是否 return;

➭ 下面来看看 Lambda 表达式的三种常规写法,

(int x, int y) -> x + y

() -> 42

(String s) -> { System.out.println(s); };
  • 第一种方式,最常规的方式,要注意的是,这里的 x + y 等价于执行 return x + y;
  • 第二种方式,如果 Functional Interface 的接口方法不包含参数,可以不写 Argument List,直接用 () 表示;
  • 第三种方式,既是 Body 由语句块所构成的例子;

➭ 下面,再来看看相关的用例,

FileFilter java = (File f) -> f.getName().endsWith(".java");

String user = doPrivileged(() -> System.getProperty("user.name"));

new Thread(() -> {
  connectToService();
  sendNotification();
}).start();
  • 分析一下第一个例子,

    FileFilter java = (File f) -> f.getName().endsWith(".java");
    

    首先看看 FileFilter 接口的实现,可见,它就是一个 Functional Interface;

    @FunctionalInterface
    public interface FileFilter {
    
        /**
         - Tests whether or not the specified abstract pathname should be
         - included in a pathname list.
         *
         - @param  pathname  The abstract pathname to be tested
         - @return  <code>true</code> if and only if <code>pathname</code>
         -          should be included
         */
        boolean accept(File pathname);
    }
    

    它等价于由下面 Java SE 7 的匿名类的方式实现了一个实例 java,

    FileFilter java = new FileFilter(){
    
       @Override
       public boolean accept(File f) {
    
          return f.getName().endsWith(".java");
       }
       
    };
    

    这里要注意的是,

    • Lambda 表达式中的 (File f) 对应的就是 FileFilter 的接口方法 accept(File f) 的参数部分;
    • Lambda 表达式中的 f.getName().endsWith(“.java”); 就等价于匿名类中的 accpet 方法的方法体,只是在填充的时候,这种情况下默认会加上关键字 return
  • 继续分析第二个例子,

    String user = doPrivileged(() -> System.getProperty("user.name"));
    

    方法 doPrivileged,顾名思义,是用来进行权限控制的,从 lambda body: () -> System.getProperty(“user.name”) 可以知道,该 lambda 匿名实例方法将返回一个 String 对象,既 username,那么可以断言的是,方法 doPrivileged 的参数必定是一个方法返回值为 String 的 Functional Interface;

  • 继续分析第三个例子,

    new Thread(() -> {
      connectToService();
      sendNotification();
    }).start();
    

    它通过下面的 lambda 表达式

    () -> {
       connectToService();
       sendNotification();
    }
    

    构造出了一个 Runnable 接口的匿名实例,并作为构造参数初始化了一个 Thread 实例;读者可能会问了,我怎么知道上述的 lambda 表达式是用来构造一个 Runnable 接口实例的?其实,Java 是通过 Lambda 的上下文语境来推导出该 Lambda 表达式所要表示的类型的,这里就是通过 Thread 的构造参数类型来进行推导的,更多相关内容参考的相关内容;

推导 Lambda Target Type

Target Type

Target Type 既目标类型,由 lambda 表达式所构造的匿名实例的类型,亦可称作 Target Type

Target Typing

Typing 这里表示动作,表示为 lambda 赋予类型;

  • 先来看官网教程中的一个例子,

    A lambda expression can only appear in a context whose target type is a functional interface .

    这里限定了 lambda 表达式可以出现的位置,它只能出现在 target type 为的上下文环境中;其实也就是描述了,lambda 表达式的 target type 必须是 Functional Interface;

  • 那么,Java 编译器是如何为 lambda 表达式赋予类型的呢?也就是如何对它进行 typing 的呢?笔者将通过一个例子来逐步对其进行解剖,

    ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());
    

    首先,我们知道,lambda 表达式 (ActionEvent e) -> ui.dazzle(e.getModifiers()) 所要做的事情就是构造出一个匿名实例;但是我们知道,lambda 表达式只包含 Argument List 和 Function Body ,它本身并不包含任何类型( Type )的信息,那么,Java 编译器是如何知道它所构造出来的匿名实例应该是什么类型的呢?

    Ok,这就是 Java 编译器所要做的工作了,在编译的时候,编译器根据上下文环境,来推导出( inferring ) lambda 的;这里的上下文环境对应的就是其等式左边的类型声明既 ActionListener ;所以,编译器在编译过程中所推导出来的 target type 为 ActionListener ;这样,我们再来理解官网上的这段有关推导过程的话,印象就更为深刻了,

    It uses the type expected in the context in which the expression appears; this type is called the target type .

    lambda 表达式使用的既是与其相关的 context 的类型,这个类型,也就称作 target type ;其实,换而言之,context 的 target type 既是 lambda 表达式的 target type ;对应到上述的例子,可知 Action Listener 既是这里所说的 target type

  • 编译时刻如何判断一个 Target Type 能否赋予当前的 lambda 表达式?来看看 target type T 所必须满足的条件,

    1. T 必须是Type
    2. lambda 表达式的参数必须有和 T 的接口方法中的参数一致,一样的参数个数以及一样的参数类型;
    3. lambda body 返回值的类型必须与 T 的接口方法的返回类型一致;
    4. lambda body 中抛出的异常必须在 T 的几口方法上进行申明;
  • 因为 Target Type 的接口方法的参数类型是显而易见的,因此,lambda 表达式中的 参数类型 是可以被 省略掉 的,比如

    Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
    

    lambda 表达式的参数类型可以很容易的根据 Comparator 接口的接口方法推导得出,

    public interface Comparator<T> {
        int compare(T o1, T o2);
    }
    

    由泛型 T 的类型为 String 可知,lambda 表达式的参数列表(s1, s2)中的 s1 和 s2 都是 String 类型;再比如,如果我们只有一个参数的情形,连 方括号 () 都可以被省略掉 ,诸如,

    FileFilter java = f -> f.getName().endsWith(".java");
    
    button.addActionListener(e -> ui.dazzle(e.getModifiers()));
    

    省略掉参数类型的初衷是,”Don’t turn a vertical problem into a horizontal problem.” 别让一个垂直的问题变成了水平的问题;

  • Lambda 表达式并非第一个通过上下文环境来推导出表达式类型( Target Type )的做法,看看下面的例子,

    List<String> ls = Collections.emptyList();
    List<Integer> li = Collections.emptyList();
    
    Map<String,Integer> m1 = new HashMap<>();
    Map<Integer,String> m2 = new HashMap<>();
    
    第一种方式,通过等式左边的赋值类型 List 推导出 Collections.emptyList() 表达式的类型是 List

    类型;这种方式叫做,泛型调用;

    第二种方式,通过等式左边的赋值类型推导出表达式 HashMap<>() 的类型为 Map<Integer, String>,这种可被推导的表达式的方式叫做 “diamond” constructor invocations;

通过不同的上下文环境推导出 Target Type

该小节笔者将系统介绍可以作为 Lambda Target Typing 的所有上下文环境,如下所述,

  • Variable declarations
    变量声明;
  • Assignments
    赋值;
  • Return statements
    返回语句;
  • Array initializers
    数据初始化;
  • Method or constructor arguments
    方法或者构造函数的参数;
  • Lambda expression bodies
    Lambda 表达式的 bodies;
  • Conditional expression (?:)
    条件表达式;
  • Cast expression
    映射表达式;

下面,笔者将分别对这些 context 场景进行分别描述;

变量声明和赋值

这种场景在前叙文章中,笔者已经多次进行描述,这里再来看一个例子;

Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);

上述 lambda 表达式就是在变量 c 的声明过程中,为 c 进行赋值;

Return statemens

根据 Return 语句的返回类型来推导出 lambda 表达式的类型;

public Runnable toDoLater(){
   return () -> {
      System.out.println("later");
   };
}

通过 return 语句返回了一个 lambda 表达式对象,而该 lambda 对象通过上下文推导,知道自己的返回类型是 Runnable,所以,这里通过 return 语句的方式返回的是一个 Runnable 的匿名对象;

Array initializers

根据数组的类型来推导出由 lambda 表达式的类型,且 lambda 表达式充当的是数据中的元素;看一个例子,

filterFiles(new FileFilter[] { 
   f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q") 
   });

方法 filterFiles 接收一个FileFilter类型的数组,里面的三个 lambda 表达式分别构建了三个类型为FileFilter的匿名对象;

Method or constructor arguments

当 lambda 表达式作为方法或者构造方法的参数的时候,编译器通过方法或者构造方法的参数类型,推导出 lambda 表达式的类型;因为当 lambda 表达式作为方法的参数的时候,所面对的情况会更为复杂,因为需要考虑方法重载的问题;针对方法重载的问题,为了能够准确的找到调用方法,编译器按照如下的两种规则进行推导,

  1. Overload resolution
    采用重载的方式进行推导;
  2. Type argument inference
    采用参数类型的方式进行推导;

那么什么时候按照 Overload resolution 来进行推导,什么时候又按照 Type argument inference 的方式来进行推导的呢?编译器将按照如下原则进行,

  • 如果 lambda 表达式的类型是显示指定的( Explicity typed ),那么编译器将会使用 Overload resolution 的方式推导出 lambda 表达式的类型;看下面的这个例子,

    List<Person> ps = Arrays.asList( new Person("Shawn Micheal") );
    
    // 显示的指定 lambda 的 Target Type 为 Function<Person, String>
    Function<Person, String> mapper = p -> p.getName();
    
    names = ps.stream().map( mapper );
    
    names.forEach( name -> { System.out.println(name); } );
    

    可以看到,lambda 表达式的 Target Type 已经被显示的指定为 Function<Person, String>;那么假设,Stream<T> 接口类有多个同名的 map 方法,那么在调用的时候,通过方法参数重载,调用到的将会是 Stream<R> map(Function<? super T, ? extends R> mapper);

  • 如果 lambda 表达式的类型是隐式指定的( Implicitly typed ),那么编译器将会使用 Type argument inference 的方式推导出 lambda 表达式的类型;这就是比较好玩的地方了,看下面的这个例子,

    List<Person> ps = Arrays.asList( new Person("Shawn Micheal") );
    
    // 根据 lambda body 推导出 R
    Stream<String> names = ps.stream().map(p -> p.getName());
    
    names.forEach( name -> { System.out.println(name); } );
    

    可以看到,我们并不知道 lambda 表达式 p -> p.getName() 的类型( Target Type )是什么;那么,编译器又是如何推导出该 lambda 表达式的类型的呢?下面,我们来看看编译器是如何推导出 lambda 表达式的 target type 的:

    1. 首先, Functional Interface Stream<T> 的方法 map 接收的参数类型为 Function<T, R> ,且只有该唯一一个接口方法,所以,lambda 表达式一定是作为该方法的入参,也就是说,lambda 表达式的 Target Type 一定是 Function<T, R> ,但问题是泛型 TR 又分别表示什么,该如何推导?见下面的步骤,
    2. 推导 T ,根据调用对象 ps 的类型 List<Person> ,可以推出由 ps.stream() 返回的是Stream<Person>,所以,可以推出 ps.stream().map( Function<T, R> ) 方法中的泛型 T 的类型为 Person ;那么 R 呢?
    3. 推导 R ,编译器的原则是根据 lambda body 进行推导,可是,如何根据它来进行推导呢?我们知道,lambda body 实际上对应的就是 Functional Interface 接口方法所实现的内容,而 R 正好是 Functional Interface Function<T, R > 对象的接口方法 R apply(T t) 的返回值类型;那么也就是说,根据 apply 方法的返回结果,既可以判断得出 R 的类型;那么什么又是 apply 方法的返回结果呢?这不就是由 lambda body 所决定的吗,既是由 p.getName() 来决定的,而默认情况下 p.getName() 等价于 return p.getName(),返回值的类型为String,所以,得出 R 的类型就是 String

再来看一个例子,

Collections.sort(people, Comparator.comparing(p -> p.getLastName()));

或者写作,

Collections.sort(people, Comparator.comparing(Person::getLastName));

试问,这个时候 comparing 方法的参数类型以及 lambda 表达式的类型是什么?这个例子参考回顾和总结章节的第 4 点内容;

Lambda expression bodies

看看官网上的一段话,

Lambda expressions themselves provide target types for their bodies , in this case by deriving that type from the outer target type . This makes it convenient to write functions that return other functions:

然后官网上给出了下面这个例子,

Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };

Lambda 表达式自身为它们的 bodies 提供 target types,这种情况下,bodies 的 type 是由其外部的 target type 推导出来的;通过这种方式使得由一个 function 返回另外一个 function 的代码在写法(指代码结构上)变得简单;

如果只是逐字逐句的弄懂了英文原文,你大致应该还是不懂作者的意思到底是什么;Ok,那么笔者将带领读者一步一步的去弄懂作者的意图到底是什么,

  1. 什么是“外部的 target type”?
    实际上,上面的这段 Lambda 表达式可以写成这样 Supplier<Runnable> c = () -> inner bodies ,既是将内嵌的 lambda 表达式 () -> { System.out.println(“hi”) } 看作是 inner bodies,这里笔者将其命名为 内部 lambda 表达式 ;所以,我们就有了所谓的内、外之分了,可以看到,外层的 Lambda 表达式的 target type 其实就是Supplier<Runnable>;
  2. 那么又如何根据其外部的 target type 既Supplier<Runnable>推导出内部 Lambda 表达式的类型呢?

    答案是根据 Functional Interface Supplier<Runnable>的接口方法的返回值类型来推导;下面,来看看其接口方法,

    @FunctionalInterface
    public interface Supplier<T>{
       /**
        - Gets a result.
        *
        - @return a result
       **/
       T get();
    }
    

    首先 内部 lambda 表达式() -> { System.out.println(“hi”) } 实际上充当的就是上述 get() 方法的方法体,而从外部 target type Supplier<Runnable>可知,get() 方法的返回类型 T 为Runnable,而又因为 内部 lambda 表达式 就是该方法的方法体,所以它的类型一定是Runnable;也就是说, 内部 lambda 表达式 () -> { System.out.println(“hi”) } 的类型为 Runnable

其实归纳起来,从语义上可以用一句话来总结,那就是 内部 lambda 表达式 充当的就是 外部 lambda 表达式 的 Target Type 的接口方法的方法体;(这里的 Target Type 就等价于上面所介绍的Supplier<Runnable>);从功能上来将,以上面的例子为例,使得通过 Functional Interface Supplier<Runnable>封装另外一个 T 既“接口对象/Function/Lambda 表达式”变得容易;

不过,为了得到上述的结论,笔者这里做了一个假设,那就是表达式 () -> () -> { System.out.println(“hi”); }; 是将内部 Lambda 表达式作为 return 对象返回的;为了验证这种假设,笔者写了一个例证,从反面的例子来推导,既是,内部的 Lambda 表达式还可以表示为外部 Target Type 的接口方法的参数类型,发现这种方式编译不通过;而内部的 Lambda 表达式所表示的类型只能有这两种情况,因此可以推论出,内部 Lambda 表达式必然是作为 return 对象返回的;所以,综上,这个假设就是得到本小节结论的前提;

Conditional expression ( pass down rule )

Conditional expressions can “pass down” a target type from the surrounding context:
Callable<Integer> c = flag ? (() -> 23) : (() -> 42);

这里唯一要弄懂的是“pass down”,什么是 pass down? 其实很简单,就是说,当一个表达式( 不限于条件表达式,可以是其它的表达式 )中有多个表达式( 不限于 Lambda 表达式,可以是其它的表达式 )所处的上下文环境对应的是同一个 Target Type ,那么该 Target Type 将会作用到所有这些表达式上,而这个行为,就叫做“传递”既是 pass down;比如上述的例子中,表达式 () -> 23 和表达式 () -> 42 拥有同一个 Target Type,这种行为,就叫做 pass down

而这种新的 pass down 的特性,除了推导出 Lambda 表达式的类型以外,也用在了 Java 8 的其它地方,比如泛型方法调用和 “diamond” constructor invocations 利用到了这些新的特性,

List<String> ls = Collections.checkedList(new ArrayList<>(), String.class);

可见, Target Type List<String> 传递给了 ArrayList<>;再来看看一个例子,

Set<Integer> si = flag ? Collections.singleton(23) : Collections.emptySet();

这里的 Target Type Set<Integer> 将会分别传递给 Collections.singleton(23) 和 Collections.emptySet();

Cast expressions

当无法通过上下文推导出 lambda 表达式的类型的时候,可以通过如下的方式显示的指明 lambda 表达式的类型;比如,

// Illegal: Object o = () -> { System.out.println("hi"); };
Object o = (Runnable) () -> { System.out.println("hi"); };

当我们无法通过上下文环境推导出 Lambda 表达式的类型的时候,可以通过类型转换的方式显示指定;上述例子中,无法通过赋值语句的参数类型 Object 判断出 lambda 的类型,所以,我们可以通过显示的方式指明该 lambda 表达式的类型;

作用域

Lexical scoping

Lexical scoping 既是闭包作用域的意思,这里有两点需要注意,

  1. Lambda 的闭包作用域中的 this 指向的是 Enclosing Instance ,既是外部实例;这里需要区别于 Inner Class 的闭包作用域,Inner Class 的闭包中 this 指向的是 Inner Class 实例自己,而不是外部实例;
  2. 闭包中被捕获的参数不再强制要求声明为 final,但是该参数必须依然保持 final 的特性;参看 [Variable capture] 章节;

其余更多有关 Lambda 闭包的介绍参看笔者的另外一篇博文 java 闭包系列二:深入 Lambda 闭包


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

图解深度学习

图解深度学习

[日] 山下隆义 / 张弥 / 人民邮电出版社 / 2018-5 / 59.00元

本书从深度学习的发展历程讲起,以丰富的图例从理论和实践两个层面介绍了深度学习的各种方法,以及深度学习在图像识别等领域的应用案例。内容涉及神经网络、卷积神经网络、受限玻尔兹曼机、自编码器、泛化能力的提高等。此外,还介绍了包括Theano、Pylearn2、Caffe、DIGITS、Chainer 和TensorFlow 在内的深度学习工具的安装和使用方法。 本书图例丰富,清晰直观,适合所有对深......一起来看看 《图解深度学习》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

URL 编码/解码
URL 编码/解码

URL 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具