Java 8 Lambda表达式.docx

上传人:b****8 文档编号:9299839 上传时间:2023-02-04 格式:DOCX 页数:23 大小:32.77KB
下载 相关 举报
Java 8 Lambda表达式.docx_第1页
第1页 / 共23页
Java 8 Lambda表达式.docx_第2页
第2页 / 共23页
Java 8 Lambda表达式.docx_第3页
第3页 / 共23页
Java 8 Lambda表达式.docx_第4页
第4页 / 共23页
Java 8 Lambda表达式.docx_第5页
第5页 / 共23页
点击查看更多>>
下载资源
资源描述

Java 8 Lambda表达式.docx

《Java 8 Lambda表达式.docx》由会员分享,可在线阅读,更多相关《Java 8 Lambda表达式.docx(23页珍藏版)》请在冰豆网上搜索。

Java 8 Lambda表达式.docx

Java8Lambda表达式

深入学习Java8Lambda(语言篇——lambda表达式的深入学习)

关于

1.深入理解Java8Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

2.深入理解Java8Lambda(类库篇——StreamsAPI,Collector和并行)

3.深入理解Java8Lambda(原理篇——Java编译器如何处理lambda)

本文是深入理解Java8Lambda系列的第一篇,主要介绍Java8新增的语言特性(比如lambda和方法引用),语言概念(比如目标类型和变量捕获)以及设计思路。

本文是对 BrianGoetz 的 StateofLambda 一文的翻译,那么问题来了:

为什么要翻译这个系列?

1.工作之后,我开始大量使用Java

2.公司将会在不久的未来使用Java8

3.作为资质平庸的开发者,我需要打一点提前量,以免到时拙计

4.为了学习Java8(主要是其中的lambda及相关库),我先后阅读了Oracle的 官方文档,CayHorstmann(CoreJava的作者)的 Java8fortheReallyImpatient 和RichardWarburton的 Java8Lambdas

5.但我感到并没有多大收获,Oracle的官方文档涉及了lambda表达式的每一个概念,但都是点到辄止;后两本书(尤其是Java8Lambdas)花了大量篇幅介绍Javalambda及其类库,但实质内容不多,读完了还是没有对Javalambda产生一个清晰的认识

6.关键在于这些文章和书都没有解决我对Javalambda的困惑,比如:

∙Java8中的lambda为什么要设计成这样?

(为什么要一个lambda对应一个接口?

而不是StructuralTyping?

∙lambda和匿名类型的关系是什么?

lambda是匿名对象的语法糖吗?

∙Java8是如何对lambda进行类型推导的?

它的类型推导做到了什么程度?

∙Java8为什么要引入默认方法?

∙Java编译器如何处理lambda?

∙等等……

7.之后我在Google搜索这些问题,然后就找到 BrianGoetz 的三篇关于Javalambda的文章(StateofLambda,StateofLambdalibrariesversion 和 Translationoflambda),读完之后上面的问题都得到了解决

8.为了加深理解,我决定翻译这一系列文章

警告(Caveats)

如果你不知道什么是函数式编程,或者不了解 map,filter,reduce 这些常用的高阶函数,那么你不适合阅读本文,请先学习函数式编程基础(比如 这本书)。

StateofLambda by BrianGoetz

Thehigh-levelgoalofProjectLambdaistoenableprogrammingpatternsthatrequiremodelingcodeasdatatobeconvenientandidiomaticinJava.

关于

本文介绍了JavaSE8中新引入的lambda语言特性以及这些特性背后的设计思想。

这些特性包括:

olambda表达式(又被成为“闭包”或“匿名方法”)

o方法引用和构造方法引用

o扩展的目标类型和类型推导

o接口中的默认方法和静态方法

1.背景

Java是一门面向对象编程语言。

面向对象编程语言和函数式编程语言中的基本元素(BasicValues)都可以动态封装程序行为:

面向对象编程语言使用带有方法的对象封装行为,函数式编程语言使用函数封装行为。

但这个相同点并不明显,因为Java对象往往比较“重量级”:

实例化一个类型往往会涉及不同的类,并需要初始化类里的字段和方法。

不过有些Java对象只是对单个函数的封装。

例如下面这个典型用例:

JavaAPI中定义了一个接口(一般被称为回调接口),用户通过提供这个接口的实例来传入指定行为,例如:

1

2

3

publicinterfaceActionListener{

voidactionPerformed(ActionEvente);

}

这里并不需要专门定义一个类来实现 ActionListener,因为它只会在调用处被使用一次。

用户一般会使用匿名类型把行为内联(inline):

1

2

3

4

5

button.addActionListener(newActionListener(){

publicvoidactionPerformed(ActionEvente){

ui.dazzle(e.getModifiers());

}

});

很多库都依赖于上面的模式。

对于并行API更是如此,因为我们需要把待执行的代码提供给并行API,并行编程是一个非常值得研究的领域,因为在这里摩尔定律得到了重生:

尽管我们没有更快的CPU核心(core),但是我们有更多的CPU核心。

而串行API就只能使用有限的计算能力。

随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Modelcodeasdata)的方法。

匿名内部类并不是一个好的 选择,因为:

1.语法过于冗余

2.匿名类中的 this 和变量名容易使人产生误解

3.类型载入和实例创建语义不够灵活

4.无法捕获非 final 的局部变量

5.无法对控制流进行抽象

上面的多数问题均在JavaSE8中得以解决:

o通过提供更简洁的语法和局部作用域规则,JavaSE8彻底解决了问题1和问题2

o通过提供更加灵活而且便于优化的表达式语义,JavaSE8绕开了问题3

o通过允许编译器推断变量的“常量性”(finality),JavaSE8减轻了问题4带来的困扰

不过,JavaSE8的目标并非解决所有上述问题。

因此捕获可变变量(问题4)和非局部控制流(问题5)并不在JavaSE8的范畴之内。

(尽管我们可能会在未来提供对这些特性的支持)

2.函数式接口(Functionalinterfaces)

尽管匿名内部类有着种种限制和问题,但是它有一个良好的特性,它和Java类型系统结合的十分紧密:

每一个函数对象都对应一个接口类型。

之所以说这个特性是良好的,是因为:

o接口是Java类型系统的一部分

o接口天然就拥有其运行时表示(Runtimerepresentation)

o接口可以通过Javadoc注释来表达一些非正式的协定(contract),例如,通过注释说明该操作应可交换(commutative)

上面提到的 ActionListener 接口只有一个方法,大多数回调接口都拥有这个特征:

比如 Runnable 接口和 Comparator 接口。

我们把这些只拥有一个方法的接口称为 函数式接口。

(之前它们被称为 SAM类型,即 单抽象方法类型(SingleAbstractMethod))

我们并不需要额外的工作来声明一个接口是函数式接口:

编译器会根据接口的结构自行判断(判断过程并非简单的对接口方法计数:

一个接口可能冗余的定义了一个 Object 已经提供的方法,比如 toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴)。

不过API作者们可以通过 @FunctionalInterface 注解来显式指定一个接口是函数式接口(以避免无意声明了一个符合函数式标准的接口),加上这个注解之后,编译器就会验证该接口是否满足函数式接口的要求。

实现函数式类型的另一种方式是引入一个全新的 结构化 函数类型,我们也称其为“箭头”类型。

例如,一个接收 String 和 Object并返回 int 的函数类型可以被表示为 (String,Object)->int。

我们仔细考虑了这个方式,但出于下面的原因,最终将其否定:

o它会为Java类型系统引入额外的复杂度,并带来 结构类型(StructuralType) 和 指名类型(NominalType) 的混用。

(Java几乎全部使用指名类型)

o它会导致类库风格的分歧——一些类库会继续使用回调接口,而另一些类库会使用结构化函数类型

o它的语法会变得十分笨拙,尤其在包含受检异常(checkedexception)之后

o每个函数类型很难拥有其运行时表示,这意味着开发者会受到 类型擦除(erasure) 的困扰和局限。

比如说,我们无法对方法 m(T->U) 和 m(X->Y) 进行重载(Overload)

所以我们选择了“使用已知类型”这条路——因为现有的类库大量使用了函数式接口,通过沿用这种模式,我们使得现有类库能够直接使用lambda表达式。

例如下面是JavaSE7中已经存在的函数式接口:

ojava.lang.Runnable

ojava.util.concurrent.Callable

ojava.security.PrivilegedAction

ojava.util.Comparator

ojava.io.FileFilter

ojava.beans.PropertyChangeListener

除此之外,JavaSE8中增加了一个新的包:

java.util.function,它里面包含了常用的函数式接口,例如:

oPredicate——接收 T 并返回 boolean

oConsumer——接收 T,不返回值

oFunction——接收 T,返回 R

oSupplier——提供 T 对象(例如工厂),不接收值

oUnaryOperator——接收 T 对象,返回 T

oBinaryOperator——接收两个 T,返回 T

除了上面的这些基本的函数式接口,我们还提供了一些针对原始类型(Primitivetype)的特化(Specialization)函数式接口,例如 IntSupplier 和 LongBinaryOperator。

(我们只为 int、long 和 double 提供了特化函数式接口,如果需要使用其它原始类型则需要进行类型转换)同样的我们也提供了一些针对多个参数的函数式接口,例如 BiFunction,它接收 T 对象和 U 对象,返回 R 对象。

3.lambda表达式(lambdaexpressions)

匿名类型最大的问题就在于其冗余的语法。

有人戏称匿名类型导致了“高度问题”(heightproblem):

比如前面 ActionListener的例子里的五行代码中仅有一行在做实际工作。

lambda表达式是匿名方法,它提供了轻量级的语法,从而解决了匿名内部类带来的“高度问题”。

下面是一些lambda表达式:

1

2

3

(intx,inty)->x+y

()->42

(Strings)->{System.out.println(s);}

第一个lambda表达式接收 x 和 y 这两个整形参数并返回它们的和;第二个lambda表达式不接收参数,返回整数‘42’;第三个lambda表达式接收一个字符串并把它打印到控制台,不返回值。

lambda表达式的语法由参数列表、箭头符号 -> 和函数体组成。

函数体既可以是一个表达式,也可以是一个语句块:

o表达式:

表达式会被执行然后返回执行结果。

o语句块:

语句块中的语句会被依次执行,就像方法中的语句一样——

∙return 语句会把控制权交给匿名方法的调用者

∙break 和 continue 只能在循环中使用

∙如果函数体有返回值,那么函数体内部的每一条路径都必须返回值

表达式函数体适合小型lambda表达式,它消除了 return 关键字,使得语法更加简洁。

lambda表达式也会经常出现在嵌套环境中,比如说作为方法的参数。

为了使lambda表达式在这些场景下尽可能简洁,我们去除了不必要的分隔符。

不过在某些情况下我们也可以把它分为多行,然后用括号包起来,就像其它普通表达式一样。

下面是一些出现在语句中的lambda表达式:

1

2

3

4

5

6

7

8

FileFilterjava=(Filef)->f.getName().endsWith("*.java");

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

newThread(()->{

connectToService();

sendNotification();

}).start();

4.目标类型(Targettyping)

需要注意的是,函数式接口的名称并不是lambda表达式的一部分。

那么问题来了,对于给定的lambda表达式,它的类型是什么?

答案是:

它的类型是由其上下文推导而来。

例如,下面代码中的lambda表达式类型是 ActionListener:

1

ActionListenerl=(ActionEvente)->ui.dazzle(e.getModifiers());

这就意味着同样的lambda表达式在不同上下文里可以拥有不同的类型:

1

2

3

Callablec=()->"done";

PrivilegedActiona=()->"done";

第一个lambda表达式 ()->"done" 是 Callable 的实例,而第二个lambda表达式则是 PrivilegedAction 的实例。

编译器负责推导lambda表达式类型。

它利用lambda表达式所在上下文 所期待的类型 进行推导,这个 被期待的类型 被称为 目标类型。

lambda表达式只能出现在目标类型为函数式接口的上下文中。

当然,lambda表达式对目标类型也是有要求的。

编译器会检查lambda表达式的类型和目标类型的方法签名(methodsignature)是否一致。

当且仅当下面所有条件均满足时,lambda表达式才可以被赋给目标类型 T:

oT 是一个函数式接口

olambda表达式的参数和 T 的方法参数在数量和类型上一一对应

olambda表达式的返回值和 T 的方法返回值相兼容(Compatible)

olambda表达式内所抛出的异常和 T 的方法 throws 类型相兼容

由于目标类型(函数式接口)已经“知道”lambda表达式的形式参数(Formalparameter)类型,所以我们没有必要把已知类型再重复一遍。

也就是说,lambda表达式的参数类型可以从目标类型中得出:

1

Comparatorc=(s1,s2)->pareToIgnoreCase(s2);

在上面的例子里,编译器可以推导出 s1 和 s2 的类型是 String。

此外,当lambda的参数只有一个而且它的类型可以被推导得知时,该参数列表外面的括号可以被省略:

1

2

3

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

button.addActionListener(e->ui.dazzle(e.getModifiers()));

这些改进进一步展示了我们的设计目标:

“不要把高度问题转化成宽度问题。

”我们希望语法元素能够尽可能的少,以便代码的读者能够直达lambda表达式的核心部分。

lambda表达式并不是第一个拥有上下文相关类型的Java表达式:

泛型方法调用和“菱形”构造器调用也通过目标类型来进行类型推导:

1

2

3

4

5

Listls=Collections.emptyList();

Listli=Collections.emptyList();

Mapm1=newHashMap<>();

Mapm2=newHashMap<>();

5.目标类型的上下文(Contextsfortargettyping)

之前我们提到lambda表达式智能出现在拥有目标类型的上下文中。

下面给出了这些带有目标类型的上下文:

o变量声明

o赋值

o返回语句

o数组初始化器

o方法和构造方法的参数

olambda表达式函数体

o条件表达式(?

:

o转型(Cast)表达式

在前三个上下文(变量声明、赋值和返回语句)里,目标类型即是被赋值或被返回的类型:

1

2

3

4

5

6

7

8

Comparatorc;

c=(Strings1,Strings2)->pareToIgnoreCase(s2);

publicRunnabletoDoLater(){

return()->{

System.out.println("later");

}

}

数组初始化器和赋值类似,只是这里的“变量”变成了数组元素,而类型是从数组类型中推导得知:

1

2

3

4

filterFiles(

newFileFilter[]{

f->f.exists(),f->f.canRead(),f->f.getName().startsWith("q")

});

方法参数的类型推导要相对复杂些:

目标类型的确认会涉及到其它两个语言特性:

重载解析(Overloadresolution)和参数类型推导(Typeargumentinference)。

重载解析会为一个给定的方法调用(methodinvocation)寻找最合适的方法声明(methoddeclaration)。

由于不同的声明具有不同的签名,当lambda表达式作为方法参数时,重载解析就会影响到lambda表达式的目标类型。

编译器会通过它所得之的信息来做出决定。

如果lambda表达式具有 显式类型(参数类型被显式指定),编译器就可以直接使用lambda表达式的返回类型;如果lambda表达式具有 隐式类型(参数类型被推导而知),重载解析则会忽略lambda表达式函数体而只依赖lambda表达式参数的数量。

如果在解析方法声明时存在二义性(ambiguous),我们就需要利用转型(cast)或显式lambda表达式来提供更多的类型信息。

如果lambda表达式的返回类型依赖于其参数的类型,那么lambda表达式函数体有可能可以给编译器提供额外的信息,以便其推导参数类型。

1

2

Listps=...

Streamnames=ps.stream().map(p->p.getName());

在上面的代码中,ps 的类型是 List,所以 ps.stream() 的返回类型是 Stream

map() 方法接收一个类型为 Function 的函数式接口,这里 T 的类型即是 Stream 元素的类型,也就是 Person,而 R 的类型未知。

由于在重载解析之后lambda表达式的目标类型仍然未知,我们就需要推导 R 的类型:

通过对lambda表达式函数体进行类型检查,我们发现函数体返回 String,因此 R 的类型是 String,因而 map() 返回 Stream

绝大多数情况下编译器都能解析出正确的类型,但如果碰到无法解析的情况,我们则需要:

o使用显式lambda表达式(为参数 p 提供显式类型)以提供额外的类型信息

o把lambda表达式转型为 Function

o为泛型参数 R 提供一个实际类型。

(.map(p->p.getName()))

lambda表达式本身也可以为它自己的函数体提供目标类型,也就是说lambda表达式可以通过外部目标类型推导出其内部的返回类型,这意味着我们可以方便的编写一个返回函数的函数:

1

Supplierc=()->()->{System.out.println("hi");};

类似的,条件表达式可以把目标类型“分发”给其子表达式:

1

Callablec=flag?

(()->23):

(()->42);

最后,转型表达式(Castexpression)可以显式提供lambda表达式的类型,这个特性在无法确认目标类型时非常有用:

1

2

//Objecto=()->{System.out.println("hi");};这段代码是非法的

Objecto=(Runnable)()->{System.out.println("hi");};

除此之外,当重载的方法都拥有函数式接口时,转型可以帮助解决重载解析时出现的二义性。

目标类型这个概念不仅仅适用于lambda表达式,泛型方法调用和“菱形”构造方法调用也可以从目标类型中受益,下面的代码在JavaSE7是非法的,但在JavaSE8中是合法的:

1

2

3

Listls=Collections.checkedList(newArrayList<>(),String.class);

Setsi=flag?

Collections.singleton(23):

Collections.emptySet();

6.词法作用域(Lexicalscoping)

在内部类中使用变量名(以及 this)非常容易出错。

内部类中通过继承得到的成员(包括来自 Object 的方法)可能会把外部类的成员掩盖(shadow),此外未限定(unqualified)的 this 引用会指向内部类自己而非外部类。

相对于内部类,lambda表达式的语义就十分简单:

它不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。

lambda表达式基于词法作用域,也就是说lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括lambda表达式的形式参数)。

此外,’this’关键字及其引用在lambda表达式内部和外部也拥有相同的语义。

为了进一步说明词法作用域的优点,请参考下面的代码,它会把 "Hello,world!

" 打印两遍:

1

2

3

4

5

6

7

8

9

10

1

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > PPT模板 > 简洁抽象

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1