本文是由笔者所原创的 深入 Java Lambda 系列 之一;本文是笔者在深入分析完官文 lambda state final 以后消化并总结得出的有关 Lambda 的基础特性;化繁为简,不做逐字逐句的翻译;
本文为作者原创作品,转载请注明出处;
一句话,Java Lambda 其实就是初始化匿名实例的另外一种简洁且高效的写法,它的目的就是构造得到一个 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 SE 8,新增了一个新的包,java.util.function,包含了如下新增的常用的 Funcationl Interfaces,
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 可以由两种不同的方式构成,可以是由一个简单的表达式所构成,或者是由一个语句块所构成:
➭ 下面来看看 Lambda 表达式的三种常规写法,
(int x, int y) -> x + y () -> 42 (String s) -> { System.out.println(s); };
➭ 下面,再来看看相关的用例,
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"); } };
这里要注意的是,
继续分析第二个例子,
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 的构造参数类型来进行推导的,更多相关内容参考的相关内容;
Target Type 既目标类型,由 lambda 表达式所构造的匿名实例的类型,亦可称作 Target Type ;
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 所必须满足的条件,
因为 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<>();
类型;这种方式叫做,泛型调用;
第二种方式,通过等式左边的赋值类型推导出表达式 HashMap<>() 的类型为 Map<Integer, String>,这种可被推导的表达式的方式叫做 “diamond” constructor invocations;
该小节笔者将系统介绍可以作为 Lambda Target Typing 的所有上下文环境,如下所述,
下面,笔者将分别对这些 context 场景进行分别描述;
这种场景在前叙文章中,笔者已经多次进行描述,这里再来看一个例子;
Comparator<String> c; c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);
上述 lambda 表达式就是在变量 c 的声明过程中,为 c 进行赋值;
根据 Return 语句的返回类型来推导出 lambda 表达式的类型;
public Runnable toDoLater(){ return () -> { System.out.println("later"); }; }
通过 return 语句返回了一个 lambda 表达式对象,而该 lambda 对象通过上下文推导,知道自己的返回类型是 Runnable,所以,这里通过 return 语句的方式返回的是一个 Runnable 的匿名对象;
根据数组的类型来推导出由 lambda 表达式的类型,且 lambda 表达式充当的是数据中的元素;看一个例子,
filterFiles(new FileFilter[] { f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q") });
方法 filterFiles 接收一个FileFilter类型的数组,里面的三个 lambda 表达式分别构建了三个类型为FileFilter的匿名对象;
当 lambda 表达式作为方法或者构造方法的参数的时候,编译器通过方法或者构造方法的参数类型,推导出 lambda 表达式的类型;因为当 lambda 表达式作为方法的参数的时候,所面对的情况会更为复杂,因为需要考虑方法重载的问题;针对方法重载的问题,为了能够准确的找到调用方法,编译器按照如下的两种规则进行推导,
那么什么时候按照 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 的:
再来看一个例子,
Collections.sort(people, Comparator.comparing(p -> p.getLastName()));
或者写作,
Collections.sort(people, Comparator.comparing(Person::getLastName));
试问,这个时候 comparing 方法的参数类型以及 lambda 表达式的类型是什么?这个例子参考回顾和总结章节的第 4 点内容;
看看官网上的一段话,
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,那么笔者将带领读者一步一步的去弄懂作者的意图到底是什么,
那么又如何根据其外部的 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 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();
当无法通过上下文推导出 lambda 表达式的类型的时候,可以通过如下的方式显示的指明 lambda 表达式的类型;比如,
// Illegal: Object o = () -> { System.out.println("hi"); }; Object o = (Runnable) () -> { System.out.println("hi"); };
当我们无法通过上下文环境推导出 Lambda 表达式的类型的时候,可以通过类型转换的方式显示指定;上述例子中,无法通过赋值语句的参数类型 Object 判断出 lambda 的类型,所以,我们可以通过显示的方式指明该 lambda 表达式的类型;
Lexical scoping 既是闭包作用域的意思,这里有两点需要注意,
this
指向的是 Enclosing Instance
,既是外部实例;这里需要区别于 Inner Class 的闭包作用域,Inner Class 的闭包中 this
指向的是 Inner Class 实例自己,而不是外部实例; 其余更多有关 Lambda 闭包的介绍参看笔者的另外一篇博文 java 闭包系列二:深入 Lambda 闭包 ;