内容简介:本文是由笔者所原创的本文为作者原创作品,转载请注明出处;一句话,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 可以由两种不同的方式构成,可以是由一个简单的表达式所构成,或者是由一个语句块所构成:
- 当 Body 是由一个简单表达式所构成的时候,该表达式的结果将作为一个 return 语句返回;上述例子中,x + y 等价于 return x + y
- 当 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 所必须满足的条件,
- T 必须是Type
- lambda 表达式的参数必须有和 T 的接口方法中的参数一致,一样的参数个数以及一样的参数类型;
- lambda body 返回值的类型必须与 T 的接口方法的返回类型一致;
- 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 表达式作为方法的参数的时候,所面对的情况会更为复杂,因为需要考虑方法重载的问题;针对方法重载的问题,为了能够准确的找到调用方法,编译器按照如下的两种规则进行推导,
-
Overload resolution
采用重载的方式进行推导; -
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 的:
- 首先, Functional Interface Stream<T> 的方法 map 接收的参数类型为 Function<T, R> ,且只有该唯一一个接口方法,所以,lambda 表达式一定是作为该方法的入参,也就是说,lambda 表达式的 Target Type 一定是 Function<T, R> ,但问题是泛型 T 和 R 又分别表示什么,该如何推导?见下面的步骤,
- 推导 T ,根据调用对象 ps 的类型 List<Person> ,可以推出由 ps.stream() 返回的是Stream<Person>,所以,可以推出 ps.stream().map( Function<T, R> ) 方法中的泛型 T 的类型为 Person ;那么 R 呢?
- 推导 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,那么笔者将带领读者一步一步的去弄懂作者的意图到底是什么,
-
什么是“外部的 target type”?
实际上,上面的这段 Lambda 表达式可以写成这样 Supplier<Runnable> c = () -> inner bodies ,既是将内嵌的 lambda 表达式 () -> { System.out.println(“hi”) } 看作是 inner bodies,这里笔者将其命名为 内部 lambda 表达式 ;所以,我们就有了所谓的内、外之分了,可以看到,外层的 Lambda 表达式的 target type 其实就是Supplier<Runnable>; -
那么又如何根据其外部的 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 既是闭包作用域的意思,这里有两点需要注意,
-
Lambda 的闭包作用域中的
this
指向的是 Enclosing Instance ,既是外部实例;这里需要区别于 Inner Class 的闭包作用域,Inner Class 的闭包中this
指向的是 Inner Class 实例自己,而不是外部实例; - 闭包中被捕获的参数不再强制要求声明为 final,但是该参数必须依然保持 final 的特性;参看 [Variable capture] 章节;
其余更多有关 Lambda 闭包的介绍参看笔者的另外一篇博文 java 闭包系列二:深入 Lambda 闭包 ;
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- JavaScript 基础深入——数据、变量、内存
- 深入剖析Vue源码 - 组件基础
- 深入CSS基础之box model
- java基础(六)-----String性质深入解析
- 框架基础:深入理解Java注解类型(@Annotation)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。