Java 8新特性(What's New in Java 8 中文翻译版)
更新日期:
本文翻译自1
前言
和很多Java开发者一样,第一次见到lambda表达式的时候,我就对它有了浓厚的兴趣;也和很多人一样,当得知它被推迟的时候,我感到很失望。不过,推迟总比没有好。
Java 8是Java语言的一大步改进,写这本书的过程强迫我学习了很多。在Lambda项目中,Java有了闭包语法、方法引用和接口的默认方法,项目规划加入了很多函数式语言的特性,并且如Java开发者期待的那样,并没有损失清晰性和简洁性。
除去Lambda项目,Java 8也有很多其他改动,包括新的Date和Time的API(JSR 310)、Nashorn JavaScript引擎、在HotSpot虚拟机中移除了永久代等等。
感谢以下作者提供了很有价值的资源:
- Brian Goetz – Lambda综述
- Aleksey Shipilev – jdk8 lambda示例
- Richard Warburton – Java 8 Lambdas
- Julien Ponge – Oracle Nashorn, 2014年1~2月的Java Magazine 上的一篇文章
- Venkat Subramaniam – agiledeveloper.com
- Java 8的所有开发人员
- Guava、joda-time、Groovy和Scala的开发人员
1 概览
本书是Java 8的简短介绍,读完后,你会对这些新特性有一个基本的了解,并可以开始使用。
本书假定读者已经很了解Java语言和JVM虚拟机,如果不熟悉包含Java 7在内的语言特性,书中的一些例子可能会比较困难。
Java 8包含以下特性:
- lambda表达式
- 方法引用
- 默认方法(Defender方法)
- 新的Stream API
- Optional2
- 新的Date/Time API
- 新的JavaScript引擎Nashorn
- 移除永久代
- 其他
阅读本书的最好办法是打开一个支持Java 8的IDE来试试这些新特性。
代码示例在这里
2 lambda表达式
Java 8最大的新特性就是语言级的支持了lambda表达式(Lambda项目)。lambda表达式很像包含一个自动推断类型方法的匿名类的语法糖3,然而对于简化开发有重大意义。
2.1 语法
lambda表达式的主要语法是:参数->方法体。编译器通常可以根据lambda表达式的上下文,来确定使用的函数接口4和参数类型。这个语法中有4个重要规则:
- 声明参数的类型是非强制的;
- 如果只有一个参数,那么参数外的括号()是非强制的;
- 使用大括号{}是非强制的(除非需要使用多个语句);
- 如果只有一个语句返回一个结果,那么
return
关键字是非强制的。
这里是一些语法的示例:
最后一个表达式可以用来做list的排序,如下:
在此例中,lambda表达式实现了Comparator
接口来按长度排序字符串。
2.2 范围
这是一个使用lambda及Runnable接口的短例:
值得关注的是r1
和r2
两个lambda调用Hello
类的toStrin()
方法,这展示了lambda可用的范围。
也可以引用常量或实际上的常量(effectively final variables),变量如果只被赋值一次,就是实际上的常量。
例如,使用Spring的Hibernate模板:
以上代码中,你可以引用变量sql
因为它只被赋值了一次,如果它再被赋值一次的话,就会导致编译错误。
2.3 方法引用
lambda表达式类似一个非对象的方法,如果我们可以引用已有方法来替代lambda表达式岂非更好?这正是方法引用所能做的。
例如,如果你需要经常根据文件的类型来过滤一系列文件,假定你已有以下一些用于确定文件类型的方法:
在需要过滤文件的时候,你可以用方法引用,如下例所示(假设已经定义了方法getFiles
返回Stream
):
方法引用可以指向:
- 静态方法
- 实例方法
- 特定实例上的方法
- 构造器(如,
TreeSet::new
)
例如,使用新的java.nio.file.Files.lines
方法:
以上代码读入文件"Nio.java",对每一行调用trim()
,并打印每一行。
注意,System.out::println
表示PrintStream
实例的println
方法。
2.4 函数接口
Java 8中定义的函数接口是仅包含一个抽象方法的接口,这对之前版本的Java中添加的接口也有效。
Java 8在java.util.function
包中引入了一些新的函数接口。
- Function
- 输入T类型的对象返回R类型的对象。 - Supplier
- 仅返回T类型对象。 - Predicate
- 根据T类型的输入返回布尔值。 - Consumer
- 根据T类型的输入执行操作。 - BiFunction - 和Function类似,但有2个参数。
- BiConsumer - 和Consumer类似,但有2个参数。
它也为基础类型引入了一些派生的接口,例如:
- IntConsumer
- IntFunction
- IntPredicate
- IntSupplier
更多信息请参考java.util.function Javadocs
函数接口最屌的是可以用任何能完成其职责的对象来赋值给它,正如以下代码示例,
这些代码在Java 8中是完全合法的,第一行定义了在String前插入'@'的函数;后两行是相同的,定义了获取String长度的函数。
Java编译器已经足够聪明到可以将String的length()
的方法引用转换为Function
(函数接口),其apply
方法输入String并返回Integer。例如:
会打印所有输入字符串的长度。
任意接口都可以是函数接口,而不仅仅是哪些由Java引入的方法。可以使用注解@FunctionalInterface
来表示你认为一个接口是函数接口。尽管不必要,如果你的接口不满足要求(如,只有一个抽象方法),将会导致编译错误。
Github
更多例子请参考jdk8 lambda示例
2.5 与Java 7的比较
为了更好的阐述lambda表达式的优势,这里的一些例子展示了如何在Java 8中简化Java 7的代码。
创建ActionListener
打印一列字符串
排序一列字符串
排序
对排序的例子,假定已有如下的Person
类:
以下展示了你如何在Java 7中按姓和名来排序:
在Java 8中,代码可以减短为如下这样:
本例使用了接口(
comparing
)的静态方法和下一章讨论的默认方法(thenComparing
)。
3 默认方法
为了在核心的Collection API中加入stream
方法,Java需要另一个新特性——默认方法(也称作Defender方法,或虚拟扩展方法Virtual Extension methods)。这样,就可以为List
接口增加新的方法而不破坏所有已有的实现(向后兼容性)。
默认方法可以加入到任何接口中,如默认方法这个名称表达的意思,任何实现了接口单没有重写方法的类会获得默认实现。
例如,Collection
接口中的stream
方法就类似如下的定义:
更多分隔符(spliterator)的信息可以参考the Java docs
如果你需要其他行为的话,你可以重载默认方法。
3.1 默认的和函数的(接口)
接口可以有一个或多个默认方法,并且仍然是函数接口。
例如,来看看Iterable
接口:
它包含了iterator()
方法和forEach
方法。
3.2 多个默认方法
在一些罕见的例子里,你的类实现了2个或多个接口,这些接口中可能定义了相同的默认方法,此时Java会跑出编译错误。你必须重载这些方法或选择其中一个接口的实现。例如:
以上代码中,talk
被重载了,它调用了Foo
的talk
方法。这与你引用一个Java 8之前的超类的语法相类似。
3.3 接口中的静态方法
尽管与默认方法不是强相关,可以给接口加入静态方法对Java语言来说也是一个类似的改动。
例如,Stream接口中有很多静态方法。这让"帮助"方法很容易就能找到,因为他们能很容易在接口中直接定位,而不是在另一个类中,如StreamUtil或Streams。
这是一个新的Stream接口的示例:
以上方法根据给定值创建一个新的流。
4 Stream(流)
Stream
接口在Java 8中是如此基础的一部分,因此值得为其单独花一章来说。
4.1 什么是Stream?
Stream
接口在java.util.stream
包中,它表示一列对象,有些类似Iterator接口。然而,与Iterator不同的是,它支持并行执行。
Stream接口支持map/filter/reduce模式,且惰性执行,这构成了Java 8函数式编程的基石(和lambda一起)。
出于性能考虑,也有一些派生自原始流的IntStream、DoubleStream和LongStream。
4.2 生成Stream
Java 8中有很多种方法来创建流,很多现有的Java核心库的类都有返回Stream的方法。
Stream化的Collection(集合)
最常见的创建Stream的方法是从Collection
。
Colletion接口有两个默认方法来创建Stream:
stream()
:返回源是Collection的一个顺序流;parallelStream()
:返回源是Collection的一个可能并行的流。
Stream的顺序性依赖于源头的Collection,正如Iterator一样。
Stream化的文件
BufferedReader
现在有了lines()
方法可以返回Stream,例如5:
也可以使用Files.lines(Path filePath)
来把文件当做Stream来读取,例如:
注意,这会惰性求值,它不会读取整个文件,仅在你调用时读取。
!!!
Files.lines(Path)
:任何在处理文件时(在文件打开之后)抛出的IOException
会被包装在UncheckedIOException
中并抛出。
Stream化文件树
Files
类中有几个静态方法可以用Stream来浏览文件树。
list(Path dir)
– 给定目录中的文件Stream。walk(Path dir)
6 – 从给定目录开始以深度有些方式便利文件树的Stream。walk(Path dir, int maxDepth)
– 和walk(dir)
相同,但是有最大深度限制。
Stream化文本模式
Pattern类现在有了方法splitAsStream(CharSequence)
可以创建Stream,例如:
以上代码使用了一个简易的模式,逗号',',将文本拆分成Stream并打印。这会产生以下输出:
无限Stream
使用Stream的generate
和iterate
静态方法,你可以创建Stream包含无穷的对象。例如,可以调用generate
来创建提供无穷对象的Stream,如下所示:
例如,你可以使用这样的技术来产生CPU负载或内存使用信息的Stream。但是,你必须小心使用,它和无穷循环很类似。
你也可以使用generate
来创建无穷随机数源的Stream,例如:
然而,java.util.Random
类已经在新方法中提供了这些功能:ints()
、longs()
和doubles()
。这些方法的每一个都类似如下定义:
ints()
:随机整数的无穷Stream。ints(int n, int m)
:n(含)和m(不含)之间的随机整数的无穷Stream。ints(long size)
:给定长度的随机整数Stream。ints(long size, int n, int m)
:给定长度,给定范围的随机整数Stream。
iterate
方法和generate
方法类似,但是它提供了初始值,和改变值的Function
。例如,你可以用以下代码来便利整数:
这会持续打印出"1234......"直到你停止程序。
我们之后会讨论一些停止无穷Stream的方法(
filter
和limit
)。
Range
还有一些方法是用于创建一段有限的整数Stream。
例如,IntStream
接口的静态方法range
:
以上代码会打印数字1到10。
每个基本Stream(IntStream、DoubleStream和LongStream)都有一个相应的range
方法。
Stream化任何对象
使用以下两个方法,就可以从任意个元素或者数组创建Stream:
Stream.of
可以输入任意类型的任意个参数。
4.3 For Each
对Stream可以进行最基础操作就是循环,可以使用forEach
方法来完成。
例如,打印当前目录下的所有文件,可以如下操作:
对大多数情况而言,可以替代"for循环",而且更加简洁,并且更加面向对象,因为代理了实际循环的实现。
4.4 Map/Filter/Reduce
lambda表达式和默认方法让我们在Java 8中可以实现map/filter/reduce,实际上,标准库中已经实现了这些。
例如,设想你从一列运动员姓名中获取他们的当前分数,并找出其中最高的分数。一个简单的PlayerPoints
类和getPoints
方法可以如下定义:
找出最高分运动员可以使用Java 8非常简单的实现,如下:
在Java 7中也可以使用dollar
库(或其他类似Guava和Functional-Java的库)来实现,但是可能会非常的冗长,如下所示:
用这种方法编程的最大益处(除了代码行数减少)是可以隐藏map/reduce的内在实现的能力。例如,map和reduce可能是并发实现的,允许你容易的发挥多处理器的优势。我们将在下面的章节介绍一种这么做的方法(ParallelArray)。
4.5 Parallel Array(并行数组)
ParallelArray
是JSR-166的一部分,但最终被排除在标准的Java库。它确实存在,并被发布到公共领域(可以通过JSR网站下载)。
虽然它早就在那,但是实在不易使用,直到闭包出现在Java语言中才改变了这一点。在Java 7中可以如下使用ParallelArray:
在Java 8中,可以这么做:
然而,Java 8提供了stream()
和parallelStream()
使这项工作更加容易:
这使从顺序执行的实现转为并行实现变得格外简单。
Groovy GPars
如果使用Groovy和GPars库,现在可以类似的使用,如下所示:
静态方法
GParsPool.withPool
输入一个闭包并使用多个方法增强任意Collection(使用Groovy的类别方法)。parallel
方法实际上从给定的Collection创建了ParallelArray
,并通过一个薄包装来使用它。
4.6 Peek(偷看)
你可以"偷看"Stream来做一些操作但却不中断Stream。
例如,可以打印元素来调试代码:
可以使用任何想要的操作,但是不能修改元素,如果想修改的话,可以使用map
来替代。
4.7 Limit(限制)
limit(int n)
方法可以用来限制Stream中元素为给定个数,例如:
以上代码打印10个随机整数。
4.8 Sort(排序)
Stream也有sort()
方法来给流排序。像所有Stream的中间方法(例如map、filter和peek),sort()
方法是惰性执行的,在中止操作调用(如reduce和forEach)之前,什么也不做。但是,你必须在对无限流调用sort()
之前调用限制操作如limit
。
例如,以下代码会抛出运行时异常(使用构建版本1.8.0-b132):
然而,以下代码就工作正常:
也可以在调用filter()
之后调用sorted()
。例如,以下代码打印当前目录下的前5个Java文件的文件名:
以上代码做了这些事情:
- 列出当前目录下的所有文件。
- 将这些文件映射到文件名(译者注:即获取文件名)。
- 获取那些以".java"结尾的文件名。
- 只取前5个文件名(按字母排序)。
- 打印这些文件名。
4.9 Collector(收集器)和统计量
正因Stream是惰性求值,并支持并行执行,因此需要特别的方法来汇总结果,这就是Collector(收集器)。
Collector表示汇总Stream的元素成一个结果的方法,它包含3个部分:
- 初始值。
- 将值加到初始值上的累加器。
- 将两个结果合并成一个的归并器。
有两个方法来完成:collect(supplier,accumulator,combiner)
和collect(Collector)
(省略类型)。
可喜的是,Java 8提供的多个内建的Collector。可以通过如下方法Import这些类:
简单的Collector
最简单的collector是像toList()
和toCollection()
那样的:
Join(合并)
如果你熟悉Apache Common的StringUtil.join
,joining
collector与其很相似。它可以使用给定的分隔符合并Stream,如下:
以上代码合并所有的名字为一个字符串,并使用逗号分割。
统计量
更加复杂的collector合并成单一值,例如,可以使用"averaging"Collector来获取平均值,如下:
以上代码计算文件"Nio.java"中的所有非空行长度的平均值。
有些情况下需要获取集合的多个统计量,但是因为Stream会因为调用collect
而被消费,所以,必须一次性计算所有的统计量。这正是SummaryStatistics的功能,如果要使用的话,需要先import:
然后就可以使用summarizingInt
collector,如下:
以上代码得到了和之前一样的平均值,并且同时也计算出了最大值、最小值和元素个数。
也提供了
summarizingLong
和summarizingDouble
。
另一个等价的方法是,把Stream map到基础类型,然后调用summaryStatistics()
,如下:
4.10 分组和分块
groupingBy
collector根据提供的方法把元素分组,例如:
类似的,partitioningBy
方法创建一个布尔类型为键的映射,例如:
并行分组
为了并行的执行分组(如果不关心顺序的话),可以使用
groupingByConcurrent
方法。被操作的Stream应该是无序的,这样分组才能并行执行,例如:
dragons.parallelStream().unordered().collect(groupingByConcurrent(Dragon::getColor));.
4.11 与Java 7的比较
为了更好的展示Java 8的Stream的优势,以下是一些Java 7里的示例代码和新版代码的比较:
求最大值
计算平均值
打印数字1到10
合并多个字符串
5 Optional类
Java 8在java.util
包中提供了Optional
类来防止返回null值(会导致NullPointerException
)。它和Google Guava的Optional很相似,也类似Nat Pryce的Maybe类和Scala的Option类。
百万美元错误
Tony Hoare,null的发明者,已经因为它的"百万美元错误"而被记录在案。除了你对nul的看法,已经有人在编译期null检查部分和自动代码检查过程中做出了很大努力,例如,JSR-305中的
@Nonnull
注解。Optional
让API设计者可以更简单的来避免null。
可以用Optional.of(x)
来包装一个非null值,Optional.empty()
来表示值缺失,Optional.ofNullable(x)
来从可能为空的引用创建Optional
。
在创建Optional的实例之后,然后使用isPresent()
确认是否有值,并用get()
来获取值。Optional提供了一些其他有用的方法来处理值缺失:
orElse(T)
– 如果Optional是空,则返回给定的值。orElseGet(Supplier<T>)
– 如果Optional是空,则调用给定的提供者来产生一个值。orElseThrow(Supplier<X extends Throwable>)
– 如果Optional是空,则调用给定的提供者来抛出一个异常。
也提供了一些函数式(对lambda友好)的方法,如下:
filter(Predicate<? super T> predicate)
– 过滤值并返回新的Optional。flatMap(Function<? super T,Optional<U>> mapper)
– 进行Map操作并返回Optional。ifPresent(Consumer<? super T> consumer)
– 仅当有值(无返回值)的时候,执行给定的消费者map(Function<? super T,? extends U> mapper)
– 用给定的Map方法并返回新的Optional。
Stream Optional(流的Optional)
新的
Stream
接口有一些返回Optional的方法(当Stream中没有值的时候):
reduce(BinaryOperator<T> accumulator)
– 把Stream reduce成单个值。、max(Comparator<? super T> comparator)
– 返回最大值。min(Comparator<? super T> comparator)
– 返回最小值。
6 Nashorn
Nashorn替换了Rhino
成为了Oracle JVM中默认的JavaScript引擎。由于使用的JVM的invokedynamic
特性,Nashorn更加快,它也包含了命令行工具(jjs
)。
6.1 jjs
JDK 8包含了命令行工具jjs
来运行JavaScript。
你可以通过命令行运行JavaScript文件(假定你已经把Java 8的bin目录放在了$PATH
里面):
这对运行脚本很有用,例如,假如你想很快求出几个数的和,如下:
运行上述代码会打印27
。
6.2 脚本
使用-scripting
参数运行jjs进入交互的shell,然后就可以键入并执行JavaScript。
可以在字符串中嵌入变量并对它们求值,例如:
以上代码会打印出当前的日期和时间。
6.3 脚本引擎
也可以在Java中动态的运行JavaScript。
首先,需要import脚本引擎:
然后,调用ScriptEngineManager
来获取Nashorn引擎:
现在就可以任意时候对JavaScript代码求值了:
eval
方法也可以用Filereader
类型做输入参数:
这样就可以引入并运行任何JavaScript代码。然而,需要知道的是,浏览器中提供的典型变量(窗口,文档等)将不可用。
6.4 引入
在JavaScript中,可以通过JavaImporter引入并使用Java类和包。
例如,引入java.util
、IO和NIO的包:
以上展示了paths
是LinkedList
的实例,并打印list。
之后,就可以添加如下代码来把文本写入文件:
我们可以使用已有的Java类,也可以创建新的类。
6.5 扩展
可以使用Java.type
和Java.extend
方法来扩展Java类和接口。例如,可以扩展Callable接口并实现call
方法:
6.6 Invocable
也可以直接从Java中调用JavaScript方法。
首先,需要将引擎的类型转换为Invocable接口:
然后,调用任何方法只要简单的使用invokeFunction
方法,例如:
最后,就可以调用getInterface
方法用JavaScript来实现任意接口。
例如,已有如下的JPrinter
接口,可以如下调用:
7 新的Date和Time API
Java 8引入了新的Date/Time API,这些API线程安全、易用、比之前的API更加全面。Java的Calendar实现没有很多变化,这是因为它是首次引入,且Joda-Time广泛的被认为是一个很好的替代。Java 8的新Date/Time API与Joda-Time非常相似。
7.1 新的类
引人注意的最注意差别是有多个不同的类来表示时间、日期、时间段、和特定时区的数据,也有一些不同日期类和时间类之间的转换器。
对不含时区信息的日期和时间,使用如下类:
LocalDate
– 日、月、年。LocalTime
– 仅含时间。LocalDateTime
– 含日期和时间。
对特定时区的时间,可以用ZonedDateTime
。
在Java 8之前,为了计算之后8消失的时间,需要像下面这样写:
在Java 8中,可以更简单的这样写:
也有命名清晰的方法,如plusDays
、plusMonths
、minusDays
、minusMonths
。如下:
注意,每个方法都返回不同的LocalDate
实例,原本的LocalDate对象today
并未变化。这是因为新的Date-Time类型是不可变的,是它们变得线程安全和可缓存的。
7.2 创建
创建性的日期和时间对象在Java 8中更加容易也更加不易犯错。每个类型都是不可变的,且有静态工厂方法。
例如,创建新的LocalDate在2014-03-15这天,可以如下简单的创建:
考虑跟多类型安全的话,可以使用新的枚举类型Month
:
也可以通过结合LocalDate和LocalTime的实例来简单的创建LocalDateTime对象:
也能调用(LocalDate的)以下方法:
atTime(int hour, int minute)
atTime(int hour, int minute, int second)
atTime(int hour, int minute, int second, int nanoOfSecond)
每个类都有now()
方法,相应的返回调用时瞬间的时间(或日期)。
7.3 枚举类型
Java 8增加了一些枚举类型,如java.time.temporal.ChronoUnit
用来表示类似"天"或"小时"的概念替换掉Calendar API中的整数常量,例如:
也有java.time.DayOfWeek
、java.time.Month
枚举类型。
Month
枚举类型可以用来创建LocalDates,也可以由LocalDate::getMonth
返回。如,以下是创建LocalDate并打印月份的例子:
以上代码会打印出"MARCH"。
7.4 Clock(时钟)
Clock
类可以用于连接日期和时间以构建测试。在生成环境可以用普通时钟,在测试环境可以用另一个时钟。
获取默认的时钟,可以用以下代码:
然后clock就可以传入进工厂方法,如下:
7.5 时间区间和时间长度
模拟人的理解,Java 8有两个类型来表示时间差,时间区间和时间长度(Period and Duration)。
时间长度是基于时间的时间量,例如"34.5秒";时间区间是基于日期的时间量,例如"2年3个月4天"。
时间区间和时间长度可以通过between
方法来确定:
也可以通过静态方法来创建,例如,时间长度可以通过任意值的天、小时、分、秒来创建:
Java 8的LocalTime类型可以加减时间区间和时间长度,例如:
7.6 时间调整(TemporalAdjusters
)
TemporalAdjusters
可以用来做很麻烦的日期"数学计算",这在业务功能中很常用。例如,可以用来获取"某月的第一天"和"下个周二"。
java.time.temporal.TemporalAdjusters
类包含了一批有用的方法来创建TemporalAdjuster,以下是其中一部分:
firstDayOfMonth()
firstDayOfNextMonth()
firstInMonth(DayOfWeek)
lastDayOfMont()
next(DayOfWeek)
nextOrSame(DayOfWeek)
previous(DayOfWeek)
previousOrSame(DayOfWeek)
用TemporalAdjuster
的with
方法,该方法返回date-time或date对象调整后的副本,例如:
7.7 Instant(即时)
Instant
类表示精确到纳秒的时间点,它构成了Java 8的date-time API中计算时间的基础。
跟老的Date类很像,Instant
也是从"纪元"(1970-01-01)开始计算时间的,且不考虑时区。
7.8 时区
时区是用java.time.ZoneId
类来表示的。共有两种时区标识,基于固定偏移的和基于地理区域的。这可以用来补偿类似"夏令时"之类复杂情况的时间。
可以通过很多方法来获取时区标识的实例,以下是两个示例:
如果要打印所有可用的标识,可以调用getAvailableZoneIds()
:
7.9 向后兼容性
原始的Date和Calendar对象包含toInstant()
方法来转换到新的Date-Time API,可以调用ofInstant(Insant,ZoneId)
方法来获取LocalDateTime
或ZonedDateTime
对象,例如:
8 再也没有永久代了
发布的实现将把类的元数据放在本地内存,并将内部的字符串和静态类移至Java堆中。http://openjdk.java.net/jeps/122
大多数情况的类元数据内存分配现在被分配在了本地内存。这意味着不用再设置"XX:PermSize"选项了(实际上也没有了)。
这也意味着,在内存溢出的时候,你会得到"java.lang.OutOfMemoryError: Metadata space"的错误信息,而不是之前的"java.lang.OutOfMemoryError: Permgen space"
这是某种程度上Oracle JRockit和HotSpot两个JVM的一致性。
9 杂项
Java 8有大量你可能会忽略的新功能,因为你的注意了都被lambda吸引去了。以下是这些功能的部分:
java.util.Base64
- 加密算法更新(很多)
- JDBC 4.2
- 可重复的注解
- 类型的注解
如果想获得更完整的列表,请参考官方列表。
9.1 Base64
知道现在,Java开发者必须依赖第三方库来编码和解码Base-64。由于这是个很常用的操作,大型的项目通常会包含多个不同的Base64实现。例如:Apache common-codec、Spring和Guava都有独立的实现。
出于这个原因,Java 8引入了java.util.Base64
,其行为类似Base64的编码和解码器,有以下方法:
getEncoder()
getDecoder()
getUrlEncoder()
getUrlDecoder()
每个工厂方法返回编码器或者解码器。
URL Base64编码器提供URL和文件地址安全(62是-,63是_)的编码。
9.2 Java类型的注解
Java 8之前,注解可以用于任意的申明。在Java 8中,注解可以用于类型的使用,以下是一些示例:
新功能注意目标在于支持类型检查的框架,如Checker。这些框架在编译期就可以协助找到代码中的错误。
9.3 可重复的注解
Java 8允许使用@Repeatable
注解的注解重复使用。
例如,假设你在编写一个游戏,并且想使用注解来调度方法何时被调用,你可以使用多个注解申明多个调度策略:
为了将这些变得可能,你需要:
Schedule
注解需要使用元注解@Repeatable
。- 需要另一个注解通过
@Repeatable
注解来申明。
由于Java注重向后兼容性,重复的注解实际上是和另一个注解(即你的注解)一起保存的。@Repeatable
注解的输入是一个包含注解的类,例如:
现在Schedule就是一个可重复的注解
。
可以使用反射在运行期访问可重复的注解。完成这些的新方法是getAnnotationsByType
(Class annotationClass),在Class
、Constructor
和Method
等上都有。他返回所有这样注解的数组(如果没有的话,返回空数组)。
10 Java 8中的函数式编程
Java 8计划添加很多函数式语言的特性却不很显著的改动Java语言。
当lambda表达式、方法引用、Stream接口和不可变的数据类型结合在一起,Java就可以进行所谓的"函数式编程"(“functional programming” (FP))了。
处于本书的目的,函数式编程的三大支柱是:
- 函数
- 不可变性
- 并发性
10.1 函数
当然,如其名所示,函数式编程是基于函数是第一类型的特性。Java 8可以说通过Lambda项目和函数接口把函数提升到了第一类型。
Function
接口(包括相关的接口IntFunction
、DoubleFunction
、LongFunction
、BiFunction
等)体现了Java 8在提升函数到对象过程中做出的妥协。该接口允许函数像参数一样传递,像变量一样保存,以及可以由方法返回。
Function
接口有以下默认方法:
andThen(Function)
: 返回一个合成函数,该函数先在输入上调用本函数,在在结果上调用给定的函数。compose(Function)
: 和andThen
类似,但是顺序不一样(即,先在输入上调用给定的函数,再调用本函数)。identity()
: 返回一个函数,该函数总是返回其输入值。
你可以使用这些方法来创建一个创建函数的链,例如:
返回的函数输入一个整数,乘以2,然后在前面添加"str"。
可以使用andThen
任意多次来创建一个函数,记住,函数可以被传递进和返回自方法。以下是一个使用新的Date-Time API的例子:
该方法输入是一个操作LocalDate
的函数,并转换为输出LocalDateTime
(在时间上午02:02
)的函数。
Tuple(元组)
如果需要一个有多于两个参数方法的函数接口(如,"TriFunction"),那么你需要使用库自己生成。另一个处理这个问题的方法是使用一个叫Tuple的数据结构。
Tuple是一个有类型的数据结构,用于保存一列元素。一些语言,如Scala,对Tuple有内建的支持。Tuple在处理多个相关的值,但却不希望有创建新类的开销的时候很有用。
以下一个非常简单的实现两个元素Tuple的例子:
元组也能让你近似返回多个值。
Java中有多个可用的Tuple的实现,例如javatuples和totallylazy。
10.2 不可变性
在函数式编程中,状态被认为是有害的,需要尽可能去避免,相反,immutable(不可变的)数据结构很受推荐。例如,String
就是Java中的一个不可变类型。
正如你所知,Java 8的新Date-Time类是不可变的。而你可能没有意识到的是,几乎所有新加入Java 8的类都是不可变的(如Optional和Stream)。
然而,在使用Java 8的函数式模式的时候,必须小心防止又陷入可变模式的思维定势。例如,以下代码是应当避免的:
虽然你可能很聪明,但是这样的代码会导致问题,相反,你应该使用类似下面的做法:
如果发现你自己又要求助于可变性的时候,考虑是否可以使用“filter”、“map”、“reduce”和“collect”的结合做替代。
10.3 并发性
由于多核处理器越来越普及,并发编程变得更加重要。函数式编程为并发编程创建了坚实的基础,Java 8也使用多种方式支持并发性。
第一种方式是Collection的parallelStream()
方法。它提供了一条并发使用Stream的捷径,然而,和所有优化一样,你需要测试来确认代码实际上变得更快了,并保守是使用它。太多的并发性,实际上会导致程序变慢。
第二种Java 8支持并发的方式是使用新的CompletableFuture
类。它包含supplyAsync
静态方法,其输入是函数接口Supplier
(生产者);它还包含方法thenAccept
,其输入是Consumer
(消费者),用于处理任务的完成。CompletableFuture
在另一个线程中调用给定的Supplier
,并在完成时执行Consumer
。
当和类似CountDownLatch
、AtomicInteger
、AtomicLong
、AtomicReference
等的类连接起来,就可以实现线程安全,并发的类似函数式的代码,例如:
上例找到最近的龙的位置(假设Dragon的distance
方法会导致耗时的计算)。
然而,这可以用parallelStream()
默认方法来简化(因为过程中只有一种计算),如下所示:
以上代码进行了和之前的例子实质上相同的任务,但是更加简洁(函数式)。
10.4 尾调用优化
函数式编程的一个标志是尾调用递归7。它用迭代的方式(函数式编程中没有迭代)处理相同的问题,不幸的是,如果没有编译器适当的优化,它会导致栈溢出。
尾调用优化指编译器将递归的函数调用转化为循环来避免栈溢出。例如,Lisp中使用尾调用递归的函数会自动进行这样的优化。
Java 8和很多其他语言一样不支持尾调用优化(目前为止)。然而,使用类似下面这样的接口来预估是可能的:
Tail
接口有3个默认方法和1个抽象方法(apply
),invoke()
方法包含了"尾调用优化"的主体:
- 它使用了Stream的
iterate
方法带来的便利来创建无限Stream,这回持续的调用尾的apply
方法。 - 然后,直到
isDone()
返回真的时候,调用filter
和findFirst
来停止Stream。 - 最后,返回结果。
为了实现"完成"条件,Tail需要有以下额外的静态方法:
使用Tail
接口,你就可以在Java 8中轻易的模拟尾调用递归。以下是使用这个接口计算阶乘的示例:
使用这个方法,就可以获取极快的程序运行速度而仍然使用函数式风格。
当然,JVM本身已经做了很多优化,因此这可能不总是最佳的方法。但是,这值得记在脑子里。
11 结论
感谢你阅读了这个Java 8的简短介绍。希望你已经学到很多,并已经准备好开始自己使用。
综上,Java 8包含以下特性:
- lambda表达式
- 方法引用
- 默认方法(Defender方法)
- 新的Stream API
- Optional
- 新的Date/Time API
- 新的JavaScript引擎Nashorn
- 移除永久代
如果要更总Java未来可能的加入的特性,可能需要参考JEPS
反向移植
如果处于一些原因无法立即更新到Java 8。也有一些方法反向移植一些Java 8的特性到之前版本。
对每个特性,以下是反向移植或类似的库:
- Lambdas – Retrolambda
- Lazily Evaluated Sequences – totallylazy
- Optional – guava
- Date/Time – ThreeTen
- Nashorn – nashorn-backport
请谨慎使用反向移植。