深入 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 闭包


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

查看所有标签

猜你喜欢:

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

The Mechanics of Web Handling

The Mechanics of Web Handling

David R. Roisum

This unique book covers many aspects of web handling for manufacturing, converting, and printing. The book is applicable to any web including paper, film, foil, nonwovens, and textiles. The Mech......一起来看看 《The Mechanics of Web Handling》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具