文章目录
  1. 1. 前言
  2. 2. 1 概览
  3. 3. 2 lambda表达式
    1. 3.1. 2.1 语法
    2. 3.2. 2.2 范围
    3. 3.3. 2.3 方法引用
    4. 3.4. 2.4 函数接口
    5. 3.5. 2.5 与Java 7的比较
  4. 4. 3 默认方法
    1. 4.1. 3.1 默认的和函数的(接口)
    2. 4.2. 3.2 多个默认方法
    3. 4.3. 3.3 接口中的静态方法
  5. 5. 4 Stream(流)
    1. 5.1. 4.1 什么是Stream?
    2. 5.2. 4.2 生成Stream
    3. 5.3. 4.3 For Each
    4. 5.4. 4.4 Map/Filter/Reduce
    5. 5.5. 4.5 Parallel Array(并行数组)
    6. 5.6. 4.6 Peek(偷看)
    7. 5.7. 4.7 Limit(限制)
    8. 5.8. 4.8 Sort(排序)
    9. 5.9. 4.9 Collector(收集器)和统计量
    10. 5.10. 4.10 分组和分块
    11. 5.11. 4.11 与Java 7的比较
  6. 6. 5 Optional类
  7. 7. 6 Nashorn
    1. 7.1. 6.1 jjs
    2. 7.2. 6.2 脚本
    3. 7.3. 6.3 脚本引擎
    4. 7.4. 6.4 引入
    5. 7.5. 6.5 扩展
    6. 7.6. 6.6 Invocable
  8. 8. 7 新的Date和Time API
    1. 8.1. 7.1 新的类
    2. 8.2. 7.2 创建
    3. 8.3. 7.3 枚举类型
    4. 8.4. 7.4 Clock(时钟)
    5. 8.5. 7.5 时间区间和时间长度
    6. 8.6. 7.6 时间调整(TemporalAdjusters)
    7. 8.7. 7.7 Instant(即时)
    8. 8.8. 7.8 时区
    9. 8.9. 7.9 向后兼容性
  9. 9. 8 再也没有永久代了
  10. 10. 9 杂项
    1. 10.1. 9.1 Base64
    2. 10.2. 9.2 Java类型的注解
    3. 10.3. 9.3 可重复的注解
  11. 11. 10 Java 8中的函数式编程
    1. 11.1. 10.1 函数
    2. 11.2. 10.2 不可变性
    3. 11.3. 10.3 并发性
    4. 11.4. 10.4 尾调用优化
  12. 12. 11 结论
  13. 13. 反向移植

本文翻译自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关键字是非强制的。

这里是一些语法的示例:

1
2
3
4
5
() -> System.out.println(this)
(String str) -> System.out.println(str)
str -> System.out.println(str)
(String s1, String s2) -> { return s2.length() - s1.length(); }
(s1, s2) -> s2.length() - s1.length()

最后一个表达式可以用来做list的排序,如下:

1
2
Arrays.sort(strArray,
(String s1, String s2) -> s2.length() - s1.length());

在此例中,lambda表达式实现了Comparator接口来按长度排序字符串。

2.2 范围

这是一个使用lambda及Runnable接口的短例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import static java.lang.System.out;
public class Hello {
Runnable r1 = () -> out.println(this);
Runnable r2 = () -> out.println(toString());
public String toString() { return "Hello, world!"; }
public static void main(String... args) {
new Hello().r1.run(); //Hello, world!
new Hello().r2.run(); //Hello, world!
}
}

值得关注的是r1r2两个lambda调用Hello类的toStrin()方法,这展示了lambda可用的范围。

也可以引用常量或实际上的常量(effectively final variables),变量如果只被赋值一次,就是实际上的常量。

例如,使用Spring的Hibernate模板:

1
2
3
String sql = "delete * from User";
getHibernateTemplate().execute(session ->
session.createSQLQuery(sql).uniqueResult());

以上代码中,你可以引用变量sql因为它只被赋值了一次,如果它再被赋值一次的话,就会导致编译错误。

2.3 方法引用

lambda表达式类似一个非对象的方法,如果我们可以引用已有方法来替代lambda表达式岂非更好?这正是方法引用所能做的。

例如,如果你需要经常根据文件的类型来过滤一系列文件,假定你已有以下一些用于确定文件类型的方法:

1
2
3
4
5
public class FileFilters {
public static boolean fileIsPdf(File file) {/*code*/}
public static boolean fileIsTxt(File file) {/*code*/}
public static boolean fileIsRtf(File file) {/*code*/}
}

在需要过滤文件的时候,你可以用方法引用,如下例所示(假设已经定义了方法getFiles返回Stream):

1
2
3
Stream<File> pdfs = getFiles().filter(FileFilters::fileIsPdf);
Stream<File> txts = getFiles().filter(FileFilters::fileIsTxt);
Stream<File> rtfs = getFiles().filter(FileFilters::fileIsRtf);

方法引用可以指向:

  • 静态方法
  • 实例方法
  • 特定实例上的方法
  • 构造器(如,TreeSet::new)

例如,使用新的java.nio.file.Files.lines方法:

1
2
3
Files.lines(Paths.get("Nio.java"))
.map(String::trim)
.forEach(System.out::println);

以上代码读入文件"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

函数接口最屌的是可以用任何能完成其职责的对象来赋值给它,正如以下代码示例,

1
2
3
Function<String, String> atr = (name) -> {return "@" + name;};
Function<String, Integer> leng = (name) -> name.length();
Function<String, Integer> leng2 = String::length;

这些代码在Java 8中是完全合法的,第一行定义了在String前插入'@'的函数;后两行是相同的,定义了获取String长度的函数。

Java编译器已经足够聪明到可以将String的length()的方法引用转换为Function(函数接口),其apply方法输入String并返回Integer。例如:

1
for (String s : args) out.println(leng2.apply(s));

会打印所有输入字符串的长度。

任意接口都可以是函数接口,而不仅仅是哪些由Java引入的方法。可以使用注解@FunctionalInterface来表示你认为一个接口是函数接口。尽管不必要,如果你的接口不满足要求(如,只有一个抽象方法),将会导致编译错误。

Github

更多例子请参考jdk8 lambda示例

2.5 与Java 7的比较

为了更好的阐述lambda表达式的优势,这里的一些例子展示了如何在Java 8中简化Java 7的代码。

创建ActionListener

1
2
3
4
5
6
7
8
9
// Java 7
ActionListener al = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println(e.getActionCommand());
}
};
// Java 8
ActionListener al8 = e -> System.out.println(e.getActionCommand());

打印一列字符串

1
2
3
4
5
6
// Java 7
for (String s : list) {
System.out.println(s);
}
//Java 8
list.forEach(System.out::println);

排序一列字符串

1
2
3
4
5
6
7
8
9
10
11
// Java 7
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
//Java 8
Collections.sort(list, (s1, s2) -> s1.length() - s2.length());
// or
list.sort(Comparator.comparingInt(String::length));

排序

对排序的例子,假定已有如下的Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class Person {
String firstName;
String lastName;
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}

以下展示了你如何在Java 7中按姓和名来排序:

1
2
3
4
5
6
7
8
9
10
Collections.sort(list, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
int n = p1.getLastName().compareTo(p2.getLastName());
if (n == 0) {
return p1.getFirstName().compareTo(p2.getFirstName());
}
return n;
}
});

在Java 8中,代码可以减短为如下这样:

1
2
list.sort(Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName));

本例使用了接口(comparing)的静态方法和下一章讨论的默认方法(thenComparing)。

3 默认方法

为了在核心的Collection API中加入stream方法,Java需要另一个新特性——默认方法(也称作Defender方法,或虚拟扩展方法Virtual Extension methods)。这样,就可以为List接口增加新的方法而不破坏所有已有的实现(向后兼容性)。

默认方法可以加入到任何接口中,如默认方法这个名称表达的意思,任何实现了接口单没有重写方法的类会获得默认实现。

例如,Collection接口中的stream方法就类似如下的定义:

1
2
3
default public Stream stream() {
return StreamSupport.stream(spliterator());
}

更多分隔符(spliterator)的信息可以参考the Java docs

如果你需要其他行为的话,你可以重载默认方法。

3.1 默认的和函数的(接口)

接口可以有一个或多个默认方法,并且仍然是函数接口。

例如,来看看Iterable接口:

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Iterable {
Iterator iterator();
default void forEach(Consumer< ? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}

它包含了iterator()方法和forEach方法。

3.2 多个默认方法

在一些罕见的例子里,你的类实现了2个或多个接口,这些接口中可能定义了相同的默认方法,此时Java会跑出编译错误。你必须重载这些方法或选择其中一个接口的实现。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Foo {
default void talk() {
out.println("Foo!");
}
}
interface Bar {
default void talk() {
out.println("Bar!");
}
}
class FooBar implements Foo, Bar {
@Override
void talk() { Foo.super.talk(); }
}

以上代码中,talk被重载了,它调用了Footalk方法。这与你引用一个Java 8之前的超类的语法相类似。

3.3 接口中的静态方法

尽管与默认方法不是强相关,可以给接口加入静态方法对Java语言来说也是一个类似的改动。

例如,Stream接口中有很多静态方法。这让"帮助"方法很容易就能找到,因为他们能很容易在接口中直接定位,而不是在另一个类中,如StreamUtilStreams

这是一个新的Stream接口的示例:

1
2
3
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}

以上方法根据给定值创建一个新的流。

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

1
2
3
4
try (FileReader fr = new FileReader("file");
BufferedReader br = new BufferedReader(fr)) {
br.lines().forEach(System.out::println);
}

也可以使用Files.lines(Path filePath)来把文件当做Stream来读取,例如:

1
2
3
try (Stream st = Files.lines(Paths.get("file"))) {
st.forEach(System.out::println);
}

注意,这会惰性求值,它不会读取整个文件,仅在你调用时读取。

!!!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,例如:

1
2
3
4
5
import java.util.regex.Pattern;
// later on...
Pattern patt = Pattern.compile(",");
patt.splitAsStream("a,b,c")
.forEach(System.out::println);

以上代码使用了一个简易的模式,逗号',',将文本拆分成Stream并打印。这会产生以下输出:

1
2
3
a
b
c

无限Stream

使用Stream的generateiterate静态方法,你可以创建Stream包含无穷的对象。例如,可以调用generate来创建提供无穷对象的Stream,如下所示:

1
Stream.generate(() -> new Dragon());

例如,你可以使用这样的技术来产生CPU负载或内存使用信息的Stream。但是,你必须小心使用,它和无穷循环很类似。

你也可以使用generate来创建无穷随机数源的Stream,例如:

1
Stream.generate(() -> Math.random());

然而,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。例如,你可以用以下代码来便利整数:

1
2
Stream.iterate(1, i -> i+1)
.forEach(System.out::print);

这会持续打印出"1234......"直到你停止程序。

我们之后会讨论一些停止无穷Stream的方法(filterlimit)。

Range

还有一些方法是用于创建一段有限的整数Stream。

例如,IntStream接口的静态方法range

1
2
IntStream.range(1, 11)
.forEach(System.out::println);

以上代码会打印数字1到10。

每个基本Stream(IntStream、DoubleStream和LongStream)都有一个相应的range方法。

Stream化任何对象

使用以下两个方法,就可以从任意个元素或者数组创建Stream:

1
2
Stream<Integer> s = Stream.of(1, 2, 3);
Stream<Object> s2 = Arrays.stream(array);

Stream.of可以输入任意类型的任意个参数。

4.3 For Each

对Stream可以进行最基础操作就是循环,可以使用forEach方法来完成。

例如,打印当前目录下的所有文件,可以如下操作:

1
2
Files.list(Paths.get("."))
.forEach(System.out::println);

对大多数情况而言,可以替代"for循环",而且更加简洁,并且更加面向对象,因为代理了实际循环的实现。

4.4 Map/Filter/Reduce

lambda表达式和默认方法让我们在Java 8中可以实现map/filter/reduce,实际上,标准库中已经实现了这些。

例如,设想你从一列运动员姓名中获取他们的当前分数,并找出其中最高的分数。一个简单的PlayerPoints类和getPoints方法可以如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class PlayerPoints {
public final String name;
public final long points;
public PlayerPoints(String name, long points) {
this.name = name;
this.points = points;
}
public String toString() {
return name + ":" + points;
}
}
public static long getPoints(final String name) {
// gets the Points for the Player
}

找出最高分运动员可以使用Java 8非常简单的实现,如下:

1
2
3
4
PlayerPoints highestPlayer =
names.stream().map(name -> new PlayerPoints(name, getPoints(name)))
.reduce(new PlayerPoints("", 0.0),
(s1, s2) -> (s1.points > s2.points) ? s1 : s2);

在Java 7中也可以使用dollar库(或其他类似Guava和Functional-Java的库)来实现,但是可能会非常的冗长,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
PlayerPoints highestPlayer =
$(names).map(new Function<String, PlayerPoints>() {
public PlayerPoints call(String name) {
return new PlayerPoints(name, getPoints(name));
}
})
.reduce(new PlayerPoints("", 0.0),
new BiFunction<PlayerPoints, PlayerPoints, PlayerPoints>() {
public PlayerPoints call(PlayerPoints s1, PlayerPoints s2) {
return (s1.points > s2.points) ? s1 : s2;
}
});

用这种方法编程的最大益处(除了代码行数减少)是可以隐藏map/reduce的内在实现的能力。例如,map和reduce可能是并发实现的,允许你容易的发挥多处理器的优势。我们将在下面的章节介绍一种这么做的方法(ParallelArray)。

4.5 Parallel Array(并行数组)

ParallelArray是JSR-166的一部分,但最终被排除在标准的Java库。它确实存在,并被发布到公共领域(可以通过JSR网站下载)。

虽然它早就在那,但是实在不易使用,直到闭包出现在Java语言中才改变了这一点。在Java 7中可以如下使用ParallelArray:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// with this class
public class Student {
String name;
int graduationYear;
double gpa;
}
// this predicate
final Ops.Predicate<Student> isSenior =
new Ops.Predicate<>() {
public boolean op(Student s) {
return s.graduationYear == Student.THIS_YEAR;
}
};
// and this conversion operation
final Ops.ObjectToDouble<Student> selectGpa =
new Ops.ObjectToDouble<>() {
public double op(Student student) {
return student.gpa;
}
};
// create a fork-join-pool
ForkJoinPool fjPool = new ForkJoinPool();
ParallelArray<Student> students = new ParallelArray<>(fjPool, data);
// find the best GPA:
double bestGpa = students.withFilter(isSenior)
.withMapping(selectGpa)
.max();

在Java 8中,可以这么做:

1
2
3
4
5
6
7
8
// create a fork-join-pool
ForkJoinPool pool = new ForkJoinPool();
ParallelArray<Student> students = new ParallelArray<>(pool,data);
// find the best GPA:
double bestGpa = students
.withFilter((Student s) -> (s.graduationYear == THIS_YEAR))
.withMapping((Student s) -> s.gpa)
.max();

然而,Java 8提供了stream()parallelStream()使这项工作更加容易:

1
2
3
4
5
double bestGpa = students
.parallelStream()
.filter(s -> (s.graduationYear == THIS_YEAR))
.mapToDouble(s -> s.gpa)
.max().getAsDouble();

这使从顺序执行的实现转为并行实现变得格外简单。

Groovy GPars

如果使用Groovy和GPars库,现在可以类似的使用,如下所示:

1
2
3
4
5
6
7
GParsPool.withPool {
// a map-reduce functional style (students is a Collection)
def bestGpa = students.parallel
.filter{ s -> s.graduationYear == Student.THIS_YEAR }
.map{ s -> s.gpa }
.max()
}

静态方法GParsPool.withPool输入一个闭包并使用多个方法增强任意Collection(使用Groovy的类别方法)。parallel方法实际上从给定的Collection创建了ParallelArray,并通过一个薄包装来使用它。

4.6 Peek(偷看)

你可以"偷看"Stream来做一些操作但却不中断Stream。

例如,可以打印元素来调试代码:

1
2
3
4
Files.list(Paths.get("."))
.map(Path::getFileName)
.peek(System.out::println)
.forEach(p -> doSomething(p));

可以使用任何想要的操作,但是不能修改元素,如果想修改的话,可以使用map来替代。

4.7 Limit(限制)

limit(int n)方法可以用来限制Stream中元素为给定个数,例如:

1
2
3
Random rnd = new Random();
rnd.ints().limit(10)
.forEach(System.out::println);

以上代码打印10个随机整数。

4.8 Sort(排序)

Stream也有sort()方法来给流排序。像所有Stream的中间方法(例如map、filter和peek),sort()方法是惰性执行的,在中止操作调用(如reduce和forEach)之前,什么也不做。但是,你必须在对无限流调用sort()之前调用限制操作如limit

例如,以下代码会抛出运行时异常(使用构建版本1.8.0-b132):

1
2
rnd.ints().sorted().limit(10)
.forEach(System.out::println);

然而,以下代码就工作正常:

1
2
rnd.ints().limit(10).sorted()
.forEach(System.out::println);

也可以在调用filter()之后调用sorted()。例如,以下代码打印当前目录下的前5个Java文件的文件名:

1
2
3
4
5
6
7
Files.list(Paths.get("."))
.map(Path::getFileName) // still a path
.map(Path::toString) // convert to Strings
.filter(name -> name.endsWith(".java"))
.sorted() // sort them alphabetically
.limit(5) // first 5
.forEach(System.out::println);

以上代码做了这些事情:

  • 列出当前目录下的所有文件。
  • 将这些文件映射到文件名(译者注:即获取文件名)。
  • 获取那些以".java"结尾的文件名。
  • 只取前5个文件名(按字母排序)。
  • 打印这些文件名。

4.9 Collector(收集器)和统计量

正因Stream是惰性求值,并支持并行执行,因此需要特别的方法来汇总结果,这就是Collector(收集器)。

Collector表示汇总Stream的元素成一个结果的方法,它包含3个部分:

  • 初始值。
  • 将值加到初始值上的累加器。
  • 将两个结果合并成一个的归并器。

有两个方法来完成:collect(supplier,accumulator,combiner)collect(Collector)(省略类型)。

可喜的是,Java 8提供的多个内建的Collector。可以通过如下方法Import这些类:

1
import static java.util.stream.Collectors.*;

简单的Collector

最简单的collector是像toList()toCollection()那样的:

1
2
3
4
5
6
7
8
9
// Accumulate names into a List
List<String> list = dragons.stream()
.map(Dragon::getName)
.collect(toList());
// Accumulate names into a TreeSet
Set<String> set = dragons.stream()
.map(Dragon::getName)
.collect(toCollection(TreeSet::new));

Join(合并)

如果你熟悉Apache Common的StringUtil.joinjoiningcollector与其很相似。它可以使用给定的分隔符合并Stream,如下:

1
2
3
String names = dragons.stream()
.map(Dragon::getName)
.collect(joining(","));

以上代码合并所有的名字为一个字符串,并使用逗号分割。

统计量

更加复杂的collector合并成单一值,例如,可以使用"averaging"Collector来获取平均值,如下:

1
2
3
4
5
6
7
System.out.println("\n----->Average line length:");
System.out.println(
Files.lines(Paths.get("Nio.java"))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(averagingInt(String::length))
);

以上代码计算文件"Nio.java"中的所有非空行长度的平均值。

有些情况下需要获取集合的多个统计量,但是因为Stream会因为调用collect而被消费,所以,必须一次性计算所有的统计量。这正是SummaryStatistics的功能,如果要使用的话,需要先import:

1
import java.util.IntSummaryStatistics;

然后就可以使用summarizingIntcollector,如下:

1
2
3
4
5
6
7
8
9
IntSummaryStatistics stats = Files.lines(Paths.get("Nio.java"))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(summarizingInt(String::length));
System.out.println(stats.getAverage());
System.out.println("count=" + stats.getCount());
System.out.println("max=" + stats.getMax());
System.out.println("min=" + stats.getMin());

以上代码得到了和之前一样的平均值,并且同时也计算出了最大值、最小值和元素个数。

也提供了summarizingLongsummarizingDouble

另一个等价的方法是,把Stream map到基础类型,然后调用summaryStatistics(),如下:

1
2
3
4
5
IntSummaryStatistics stats = Files.lines(Paths.get("Nio.java"))
.map(String::trim)
.filter(s -> !s.isEmpty())
.mapToInt(String::length)
.summaryStatistics();

4.10 分组和分块

groupingBy collector根据提供的方法把元素分组,例如:

1
2
3
4
// Group by first letter of name
List<Dragon> dragons = getDragons();
Map<Character,List<Dragon>> map = dragons.stream()
.collect(groupingBy(dragon -> dragon.getName().charAt(0)));

类似的,partitioningBy方法创建一个布尔类型为键的映射,例如:

1
2
3
// Group by whether or not the dragon is green
Map<Boolean,List<Dragon>> map = dragons.stream()
.collect(partitioningBy(Dragon::isGreen));

并行分组

为了并行的执行分组(如果不关心顺序的话),可以使用groupingByConcurrent方法。被操作的Stream应该是无序的,这样分组才能并行执行,例如:

dragons.parallelStream().unordered().collect(groupingByConcurrent(Dragon::getColor));.

4.11 与Java 7的比较

为了更好的展示Java 8的Stream的优势,以下是一些Java 7里的示例代码和新版代码的比较:

求最大值

1
2
3
4
5
6
7
8
9
10
11
12
// Java 7
double max = 0;
for (Double d : list) {
if (d > max) {
max = d;
}
}
//Java 8
max = list.stream().reduce(0.0, Math::max);
// or
max = list.stream().mapToDouble(Number::doubleValue).max().getAsDouble();

计算平均值

1
2
3
4
5
6
7
8
9
double total = 0;
double ave = 0;
// Java 7
for (Double d : list) {
total += d;
}
ave = total / ((double) list.size());
//Java 8
ave = list.stream().mapToDouble(Number::doubleValue).average().getAsDouble();

打印数字1到10

1
2
3
4
5
6
7
8
9
10
// Java 7
for (int i = 1; i < 11; i++) {
System.out.println(i);
}
// Java 8
IntStream.range(1, 11)
.forEach(System.out::println);
//or
Stream.iterate(1, i -> i+1).limit(10)
.forEach(System.out::println);

合并多个字符串

1
2
3
4
5
6
7
8
9
// Java 7 using commons-util
List<String> names = new LinkedList<>();
for (Dragon dragon : dragons)
names.add(dragon.getName());
String names = StringUtils.join(names, ",");
// Java 8
String names = dragons.stream()
.map(Dragon::getName)
.collect(Collectors.joining(","));

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里面):

1
$ jjs script.js

这对运行脚本很有用,例如,假如你想很快求出几个数的和,如下:

1
2
3
var data = [1, 3, 5, 7, 11]
var sum = data.reduce(function(x, y) {return x + y}, 0)
print(sum)

运行上述代码会打印27

6.2 脚本

使用-scripting参数运行jjs进入交互的shell,然后就可以键入并执行JavaScript。

可以在字符串中嵌入变量并对它们求值,例如:

1
2
jjs> var date = new Date()
jjs> print("${date}")

以上代码会打印出当前的日期和时间。

6.3 脚本引擎

也可以在Java中动态的运行JavaScript。

首先,需要import脚本引擎:

1
2
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

然后,调用ScriptEngineManager来获取Nashorn引擎:

1
2
ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("nashorn");

现在就可以任意时候对JavaScript代码求值了:

1
2
engine.eval("function p(s) { print(s) }");
engine.eval("p('Hello Nashorn');");

eval方法也可以用Filereader类型做输入参数:

1
engine.eval(new FileReader('library.js'));

这样就可以引入并运行任何JavaScript代码。然而,需要知道的是,浏览器中提供的典型变量(窗口,文档等)将不可用。

6.4 引入

在JavaScript中,可以通过JavaImporter引入并使用Java类和包。

例如,引入java.util、IO和NIO的包:

1
2
3
4
5
6
7
8
9
var imports = new JavaImporter(java.util, java.io, java.nio.file);
with (imports) {
var paths = new LinkedList();
print(paths instanceof LinkedList); //true
paths.add(Paths.get("file1"));
paths.add(Paths.get("file2"));
paths.add(Paths.get("file3"));
print(paths) // [file1, file2, file3]
}

以上展示了pathsLinkedList的实例,并打印list。

之后,就可以添加如下代码来把文本写入文件:

1
2
3
for (var i=0; i < paths.size(); i++)
Files.newOutputStream(paths.get(i))
.write("test\n".getBytes());

我们可以使用已有的Java类,也可以创建新的类。

6.5 扩展

可以使用Java.typeJava.extend方法来扩展Java类和接口。例如,可以扩展Callable接口并实现call方法:

1
2
3
4
5
6
7
8
9
10
11
12
var concurrent = new JavaImporter(java.util, java.util.concurrent);
var Callable = Java.type("java.util.concurrent.Callable");
with (concurrent) {
var executor = Executors.newCachedThreadPool();
var tasks = new LinkedHashSet();
for (var i=0; i < 200; i++) {
var MyTask = Java.extend(Callable, {call: function() {print("task " + i)}})
var task = new MyTask();
tasks.add(task);
executor.submit(task);
}
}

6.6 Invocable

也可以直接从Java中调用JavaScript方法。

首先,需要将引擎的类型转换为Invocable接口:

1
Invocable inv = (Invocable) engine;

然后,调用任何方法只要简单的使用invokeFunction方法,例如:

1
2
engine.eval("function p(s) { print(s) }");
inv.invokeFunction("p", "hello");

最后,就可以调用getInterface方法用JavaScript来实现任意接口。

例如,已有如下的JPrinter接口,可以如下调用:

1
2
3
4
5
6
public static interface JPrinter {
void p(String s);
}
// later on...
JPrinter printer = inv.getInterface(JPrinter.class);
printer.p("Hello again!");

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消失的时间,需要像下面这样写:

1
2
3
Calendar cal = Calendar.getInstance();
cal.add(Calendar.HOUR, 8);
cal.getTime(); // actually returns a Date

在Java 8中,可以更简单的这样写:

1
2
LocalTime now = LocalTime.now();
LocalTime later = now.plus(8, HOURS);

也有命名清晰的方法,如plusDaysplusMonthsminusDaysminusMonths。如下:

1
2
3
4
LocalDate today = LocalDate.now();
LocalDate thirtyDaysFromNow = today.plusDays(30);
LocalDate nextMonth = today.plusMonths(1);
LocalDate aMonthAgo = today.minusMonths(1);

注意,每个方法都返回不同的LocalDate实例,原本的LocalDate对象today并未变化。这是因为新的Date-Time类型是不可变的,是它们变得线程安全和可缓存的。

7.2 创建

创建性的日期和时间对象在Java 8中更加容易也更加不易犯错。每个类型都是不可变的,且有静态工厂方法。

例如,创建新的LocalDate在2014-03-15这天,可以如下简单的创建:

1
LocalDate date = LocalDate.of(2014, 3, 15);

考虑跟多类型安全的话,可以使用新的枚举类型Month

1
date = LocalDate.of(2014, Month.MARCH, 15);

也可以通过结合LocalDate和LocalTime的实例来简单的创建LocalDateTime对象:

1
2
LocalTime time = LocalTime.of(12, 15, 0);
LocalDateTime datetime = date.atTime(time);

也能调用(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中的整数常量,例如:

1
2
3
4
5
LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plus(1, ChronoUnit.WEEKS);
LocalDate nextMonth = today.plus(1, ChronoUnit.MONTHS);
LocalDate nextYear = today.plus(1, ChronoUnit.YEARS);
LocalDate nextDecade = today.plus(1, ChronoUnit.DECADES);

也有java.time.DayOfWeekjava.time.Month枚举类型。

Month枚举类型可以用来创建LocalDates,也可以由LocalDate::getMonth返回。如,以下是创建LocalDate并打印月份的例子:

1
2
3
// import java.time.Month;
LocalDate date = LocalDate.of(2014, Month.MARCH, 27);
System.out.println(date.getMonth());

以上代码会打印出"MARCH"。

7.4 Clock(时钟)

Clock类可以用于连接日期和时间以构建测试。在生成环境可以用普通时钟,在测试环境可以用另一个时钟。

获取默认的时钟,可以用以下代码:

1
Clock.systemDefaultZone();

然后clock就可以传入进工厂方法,如下:

1
LocalTime time = LocalTime.now(clock);

7.5 时间区间和时间长度

模拟人的理解,Java 8有两个类型来表示时间差,时间区间和时间长度(Period and Duration)。

时间长度是基于时间的时间量,例如"34.5秒";时间区间是基于日期的时间量,例如"2年3个月4天"。

时间区间和时间长度可以通过between方法来确定:

1
2
Period p = Period.between(date1, date2);
Duration d = Duration.between(time1, time2);

也可以通过静态方法来创建,例如,时间长度可以通过任意值的天、小时、分、秒来创建:

1
2
3
Duration twoHours = Duration.ofHours(2);
Duration tenMinutes = Duration.ofMinutes(10);
Duration thirtySecs = Duration.ofSeconds(30);

Java 8的LocalTime类型可以加减时间区间和时间长度,例如:

1
LocalTime t2 = time.plus(twoHours);

7.6 时间调整(TemporalAdjusters)

TemporalAdjusters可以用来做很麻烦的日期"数学计算",这在业务功能中很常用。例如,可以用来获取"某月的第一天"和"下个周二"。

java.time.temporal.TemporalAdjusters类包含了一批有用的方法来创建TemporalAdjuster,以下是其中一部分:

  • firstDayOfMonth()
  • firstDayOfNextMonth()
  • firstInMonth(DayOfWeek)
  • lastDayOfMont()
  • next(DayOfWeek)
  • nextOrSame(DayOfWeek)
  • previous(DayOfWeek)
  • previousOrSame(DayOfWeek)

TemporalAdjusterwith方法,该方法返回date-time或date对象调整后的副本,例如:

1
2
3
import static java.time.temporal.TemporalAdjusters.*;
//...
LocalDate nextTuesday = LocalDate.now().with(next(DayOfWeek.TUESDAY));

7.7 Instant(即时)

Instant类表示精确到纳秒的时间点,它构成了Java 8的date-time API中计算时间的基础。

跟老的Date类很像,Instant也是从"纪元"(1970-01-01)开始计算时间的,且不考虑时区。

7.8 时区

时区是用java.time.ZoneId类来表示的。共有两种时区标识,基于固定偏移的和基于地理区域的。这可以用来补偿类似"夏令时"之类复杂情况的时间。

可以通过很多方法来获取时区标识的实例,以下是两个示例:

1
2
ZoneId mountainTime = ZoneId.of("America/Denver");
ZoneId myZone = ZoneId.systemDefault();

如果要打印所有可用的标识,可以调用getAvailableZoneIds()

1
System.out.println(ZoneId.getAvailableZoneIds());

7.9 向后兼容性

原始的Date和Calendar对象包含toInstant()方法来转换到新的Date-Time API,可以调用ofInstant(Insant,ZoneId)方法来获取LocalDateTimeZonedDateTime对象,例如:

1
2
3
4
Date date = new Date();
Instant now = date.toInstant();
LocalDateTime dateTime = LocalDateTime.ofInstant(now, myZone);
ZonedDateTime zdt = ZonedDateTime.ofInstant(now, myZone);

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中,注解可以用于类型的使用,以下是一些示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Class instance creation:
new @Interned RocketShip();
// Type cast:
notNullString = (@NonNull String) str;
// implements clause:
class ImmutableSet<T> implements
@Readonly Set<@Readonly T> { ... }
// Thrown exception declaration:
void launchRocket() throws
@Critical FireException { ... }

新功能注意目标在于支持类型检查的框架,如Checker。这些框架在编译期就可以协助找到代码中的错误。

9.3 可重复的注解

Java 8允许使用@Repeatable注解的注解重复使用。

例如,假设你在编写一个游戏,并且想使用注解来调度方法何时被调用,你可以使用多个注解申明多个调度策略:

1
2
3
4
// the first of the month and every monday at 7am
@Schedule(dayOfMonth="first")
@Schedule(dayOfWeek="Monday", hour=7)
public void doGoblinInvasion() { ... }

为了将这些变得可能,你需要:

  • Schedule注解需要使用元注解@Repeatable
  • 需要另一个注解通过@Repeatable注解来申明。

由于Java注重向后兼容性,重复的注解实际上是和另一个注解(即你的注解)一起保存的。@Repeatable注解的输入是一个包含注解的类,例如:

1
2
3
4
5
6
7
// Schedule.java
@Repeatable(Schedules.class)
public @interface Schedule {...}
// Schedules.java
public @interface Schedules {
Schedule[] value;
}

现在Schedule就是一个可重复的注解

可以使用反射在运行期访问可重复的注解。完成这些的新方法是getAnnotationsByType(Class annotationClass),在ClassConstructorMethod等上都有。他返回所有这样注解的数组(如果没有的话,返回空数组)。

10 Java 8中的函数式编程

Java 8计划添加很多函数式语言的特性却不很显著的改动Java语言。

当lambda表达式、方法引用、Stream接口和不可变的数据类型结合在一起,Java就可以进行所谓的"函数式编程"(“functional programming” (FP))了。

处于本书的目的,函数式编程的三大支柱是:

  • 函数
  • 不可变性
  • 并发性

10.1 函数

当然,如其名所示,函数式编程是基于函数是第一类型的特性。Java 8可以说通过Lambda项目和函数接口把函数提升到了第一类型。

Function接口(包括相关的接口IntFunctionDoubleFunctionLongFunctionBiFunction等)体现了Java 8在提升函数到对象过程中做出的妥协。该接口允许函数像参数一样传递,像变量一样保存,以及可以由方法返回。

Function接口有以下默认方法:

  • andThen(Function): 返回一个合成函数,该函数先在输入上调用本函数,在在结果上调用给定的函数。
  • compose(Function): 和andThen类似,但是顺序不一样(即,先在输入上调用给定的函数,再调用本函数)。
  • identity(): 返回一个函数,该函数总是返回其输入值。

你可以使用这些方法来创建一个创建函数的链,例如:

1
2
Function<Integer,String> f = Function.<Integer>identity()
.andThen(i -> 2*i).andThen(i -> "str" + i);

返回的函数输入一个整数,乘以2,然后在前面添加"str"。

可以使用andThen任意多次来创建一个函数,记住,函数可以被传递进和返回自方法。以下是一个使用新的Date-Time API的例子:

1
2
3
4
5
public Function<LocalDate,LocalDateTime> dateTimeFunction(
final Function<LocalDate,LocalDate> f) {
return f.andThen(d -> d.atTime(2, 2));
}

该方法输入是一个操作LocalDate的函数,并转换为输出LocalDateTime(在时间上午02:02)的函数。

Tuple(元组)

如果需要一个有多于两个参数方法的函数接口(如,"TriFunction"),那么你需要使用库自己生成。另一个处理这个问题的方法是使用一个叫Tuple的数据结构。

Tuple是一个有类型的数据结构,用于保存一列元素。一些语言,如Scala,对Tuple有内建的支持。Tuple在处理多个相关的值,但却不希望有创建新类的开销的时候很有用。

以下一个非常简单的实现两个元素Tuple的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Tuple2<A, B> {
public final A _1;
public final B _2;
public Tuple2(A a, B b) {
this._1 = a;
this._2 = b;
}
@Override
public A get_1() {
return _1;
}
@Override
public B get_2() {
return _2;
}
}

元组也能让你近似返回多个值。

Java中有多个可用的Tuple的实现,例如javatuplestotallylazy

10.2 不可变性

在函数式编程中,状态被认为是有害的,需要尽可能去避免,相反,immutable(不可变的)数据结构很受推荐。例如,String就是Java中的一个不可变类型。

正如你所知,Java 8的新Date-Time类是不可变的。而你可能没有意识到的是,几乎所有新加入Java 8的类都是不可变的(如Optional和Stream)。

然而,在使用Java 8的函数式模式的时候,必须小心防止又陷入可变模式的思维定势。例如,以下代码是应当避免的:

1
2
3
4
int[] myCount = new int[1];
list.forEach(dragon -> {
if (dragon.isGreen()) myCount[0]++;
}

虽然你可能很聪明,但是这样的代码会导致问题,相反,你应该使用类似下面的做法:

1
list.stream().filter(Dragon::isGreen).count();

如果发现你自己又要求助于可变性的时候,考虑是否可以使用“filter”、“map”、“reduce”和“collect”的结合做替代。

10.3 并发性

由于多核处理器越来越普及,并发编程变得更加重要。函数式编程为并发编程创建了坚实的基础,Java 8也使用多种方式支持并发性。

第一种方式是Collection的parallelStream()方法。它提供了一条并发使用Stream的捷径,然而,和所有优化一样,你需要测试来确认代码实际上变得更快了,并保守是使用它。太多的并发性,实际上会导致程序变慢。

第二种Java 8支持并发的方式是使用新的CompletableFuture类。它包含supplyAsync静态方法,其输入是函数接口Supplier(生产者);它还包含方法thenAccept,其输入是Consumer(消费者),用于处理任务的完成。CompletableFuture在另一个线程中调用给定的Supplier,并在完成时执行Consumer

当和类似CountDownLatchAtomicIntegerAtomicLongAtomicReference等的类连接起来,就可以实现线程安全,并发的类似函数式的代码,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Dragon closestDragon(Location location) {
AtomicReference<DragonDistance> closest =
new AtomicReference<>(DragonDistance.worstMatch());
CountDownLatch latch = new CountDownLatch(dragons.size());
dragons.forEach(dragon -> {
CompletableFuture.supplyAsync(() -> dragon.distance(location))
.thenAccept(result -> {
closest.accumulateAndGet(result, DragonDistance::closest);
latch.countDown();
});
});
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted during calculations", e);
}
return closest.get().getDragon();
}

上例找到最近的龙的位置(假设Dragon的distance方法会导致耗时的计算)。

然而,这可以用parallelStream()默认方法来简化(因为过程中只有一种计算),如下所示:

1
2
3
4
5
6
public Dragon closestDragon(Location location) {
return dragons.parallelStream()
.map(dragon -> dragon.distance(location))
.reduce(DistancePair.worstMatch(), DragonDistance::closest)
.getDragon();
}

以上代码进行了和之前的例子实质上相同的任务,但是更加简洁(函数式)。

10.4 尾调用优化

函数式编程的一个标志是尾调用递归7。它用迭代的方式(函数式编程中没有迭代)处理相同的问题,不幸的是,如果没有编译器适当的优化,它会导致栈溢出。

尾调用优化指编译器将递归的函数调用转化为循环来避免栈溢出。例如,Lisp中使用尾调用递归的函数会自动进行这样的优化。

Java 8和很多其他语言一样不支持尾调用优化(目前为止)。然而,使用类似下面这样的接口来预估是可能的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@FunctionalInterface
public interface Tail<T> {
Tail<T> apply();
default boolean isDone() {
return false;
}
default T result() {
throw new UnsupportedOperationException("Not done yet.");
}
default T invoke() {
return Stream.iterate(this, Tail::apply)
.filter(Tail::isDone)
.findFirst()
.get()
.result();
}

Tail接口有3个默认方法和1个抽象方法(apply),invoke()方法包含了"尾调用优化"的主体:

  • 它使用了Stream的iterate方法带来的便利来创建无限Stream,这回持续的调用尾的apply方法。
  • 然后,直到isDone()返回真的时候,调用filterfindFirst来停止Stream。
  • 最后,返回结果。

为了实现"完成"条件,Tail需要有以下额外的静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static <T> Tail<T> done(final T value) {
return new Tail<T>() {
@Override
public T result() {
return value;
}
@Override
public boolean isDone() {
return true;
}
@Override
public Tail<T> apply() {
throw new UnsupportedOperationException("Not supported.");
}
};
}

使用Tail接口,你就可以在Java 8中轻易的模拟尾调用递归。以下是使用这个接口计算阶乘的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Long fastFactorial(int n) {
return fastFactorial(1L, n).invoke();
}
private static Tail<Long> fastFactorial(long x, int n) {
return () -> {
switch (n) {
case 1:
return Tail.done(x);
default:
return fastFactorial(x * n, n - 1);
}
};
}

使用这个方法,就可以获取极快的程序运行速度而仍然使用函数式风格。

当然,JVM本身已经做了很多优化,因此这可能不总是最佳的方法。但是,这值得记在脑子里。

11 结论

感谢你阅读了这个Java 8的简短介绍。希望你已经学到很多,并已经准备好开始自己使用。

综上,Java 8包含以下特性:

  • lambda表达式
  • 方法引用
  • 默认方法(Defender方法)
  • 新的Stream API
  • Optional
  • 新的Date/Time API
  • 新的JavaScript引擎Nashorn
  • 移除永久代

如果要更总Java未来可能的加入的特性,可能需要参考JEPS

反向移植

如果处于一些原因无法立即更新到Java 8。也有一些方法反向移植一些Java 8的特性到之前版本。

对每个特性,以下是反向移植或类似的库:

请谨慎使用反向移植。


  1. 译者注:本文原地址在https://leanpub.com/whatsnewinjava8/read

  2. 译者注:这里指Optional类

  3. lambda表达式不是匿名类,实际上它在字节码中使用了invokedynamic(译者注:动态调用)。

  4. 下一节中介绍"函数接口"的含义。

  5. 当然,你这里需要加一个catch语句处理异常。

  6. 实际的方法签名是walk(Path start, FileVisitOption... options),但是可能用walk(Path start)就可以。

  7. 尾调用递归是一个函数的调用作为这个函数的最后动作发生。

文章目录
  1. 1. 前言
  2. 2. 1 概览
  3. 3. 2 lambda表达式
    1. 3.1. 2.1 语法
    2. 3.2. 2.2 范围
    3. 3.3. 2.3 方法引用
    4. 3.4. 2.4 函数接口
    5. 3.5. 2.5 与Java 7的比较
  4. 4. 3 默认方法
    1. 4.1. 3.1 默认的和函数的(接口)
    2. 4.2. 3.2 多个默认方法
    3. 4.3. 3.3 接口中的静态方法
  5. 5. 4 Stream(流)
    1. 5.1. 4.1 什么是Stream?
    2. 5.2. 4.2 生成Stream
    3. 5.3. 4.3 For Each
    4. 5.4. 4.4 Map/Filter/Reduce
    5. 5.5. 4.5 Parallel Array(并行数组)
    6. 5.6. 4.6 Peek(偷看)
    7. 5.7. 4.7 Limit(限制)
    8. 5.8. 4.8 Sort(排序)
    9. 5.9. 4.9 Collector(收集器)和统计量
    10. 5.10. 4.10 分组和分块
    11. 5.11. 4.11 与Java 7的比较
  6. 6. 5 Optional类
  7. 7. 6 Nashorn
    1. 7.1. 6.1 jjs
    2. 7.2. 6.2 脚本
    3. 7.3. 6.3 脚本引擎
    4. 7.4. 6.4 引入
    5. 7.5. 6.5 扩展
    6. 7.6. 6.6 Invocable
  8. 8. 7 新的Date和Time API
    1. 8.1. 7.1 新的类
    2. 8.2. 7.2 创建
    3. 8.3. 7.3 枚举类型
    4. 8.4. 7.4 Clock(时钟)
    5. 8.5. 7.5 时间区间和时间长度
    6. 8.6. 7.6 时间调整(TemporalAdjusters)
    7. 8.7. 7.7 Instant(即时)
    8. 8.8. 7.8 时区
    9. 8.9. 7.9 向后兼容性
  9. 9. 8 再也没有永久代了
  10. 10. 9 杂项
    1. 10.1. 9.1 Base64
    2. 10.2. 9.2 Java类型的注解
    3. 10.3. 9.3 可重复的注解
  11. 11. 10 Java 8中的函数式编程
    1. 11.1. 10.1 函数
    2. 11.2. 10.2 不可变性
    3. 11.3. 10.3 并发性
    4. 11.4. 10.4 尾调用优化
  12. 12. 11 结论
  13. 13. 反向移植

欢迎来到Valleylord的博客!

本博的文章尽量原创。