Java 8 于 2014 年 3 月份发布,新版本增添很多不错的特性,本篇文章我们一起纵览一下这些新特性,后面的文章我们将会一一详细讨论。

Java8 介绍

促使 Java 做出重大改变的动力主要来源于:

  • 代码可读性
  • 多核运行

代码可读性

Java 代码非常的啰嗦冗长,导致了代码可读性的下降,换句话说,需要很多额外的代码来解释一段很小的内容。例如,现在有个需求,要按照发票金额的数量倒序排序发票列表。在 Java 8 之前,你可能需要这么处理:

1
2
3
4
5
Collections.sort(invoices, new Comparator<Invoice>() {
public int compare(Invoice inv1, Invoice inv2) {
return Double.compare(inv2.getAmount(), inv1.getAmount());
}
});

而在 Java8 中,你只需要这样即可:

1
invoices.sort(comparingDouble(Invoice::getAmount).reversed());

后面的章节会做详细介绍

此外,Java 8 引入了一种名为 Streams API 的新 API,可以让你编写可读性良好的代码来处理数据。Streams API 支持多种内置操作,以更简单的方式来处理数据。在业务运营环境中,你可能希望生成一个结束日期报表,以过滤和汇总来自各个部门的发票。 好消息是,使用 Streams API,您无需担心如何实现查询本身。
这种方法与您习惯使用 SQL 的方法类似。 事实上,在 SQL 中,您可以指定查询而不用担心其内部实现。 例如,假设您想要查找金额大于 1,000 的发票的所有 ID:

1
SELECT id FROM invoices WHERE amount > 1000

这种编写查询所做的风格通常被称为声明式编程。 以下是如何使用 Streams API 并行地解决这个问题的方法:

1
2
3
4
List<Integer> ids = invoices.stream()
.filter(inv -> inv.getAmount() > 1000)
.map(Invoice::getId)
.collect(Collectors.toList());

后面的章节会做详细介绍

多核

Java 8 的第二个重大变化是多核处理器时代所必须的。在过去,你的电脑只有一个处理单元。想要更快地运行应用程序通常意味着提高处理单元的性能。 不幸的是,处理单元的时钟速度不会再更快了。 今天,绝大多数计算机和移动设备都有多个处理单元(称为核)来以并行的方式进行工作。

应用程序应该利用不同的处理单元来提高性能。 Java 应用程序通常通过线程来实现这一点。 不幸的是,与线程一起工作往往是困难且容易出错的,并且通常专供专家使用。

Java 8 中的 Streams API 能够让你非常方便地以并行方式来处理数据。例如,将前面的代码改为并行运行,只需要使用 parallelStream() 接口即可:

1
2
3
4
List<Integer> ids = invoices.parallelStream()
.filter(inv -> inv.getAmount() > 1000)
.map(Invoice::getId)
.collect(Collectors.toList());

后面的章节会做详细介绍

新特性纵览

Lambda 表达式 (Lambda Expressions)

Lambda 表达式可以让您以简洁的方式传递一段代码。 例如,假设你需要获得一个线程来执行任务。 你可以通过创建一个 Runnable 对象来实现,然后将其作为参数传递给 Thread:

1
2
3
4
5
6
7
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello world");
}
};
new Thread(runnable).start();

使用 lambda 表达式,你可以用可读性更强的方式重构前面的代码:

1
new Thread(() -> System.out.println("Hello world")).start();

方法引用 (Method references)

方法引用构成了一个与 Lambda 表达式密切相关的新特性。他们可以让你选择一个在类中定义的方法并且传递它。 例如,假设您需要通过忽略大小写来比较字符串列表。 目前,您会编写如下所示的代码:

1
2
3
4
5
6
7
List<String> strs = Arrays.asList("C", "a", "A", "b");
Collections.sort(strs, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
});

使用方法引用,代码简洁如下:

1
Collections.sort(strs, String::compareToIgnoreCase);

String::compareToIgnoreCase 就是方法引用,它使用一种特殊的语法 :: .

流 (Streams)

几乎每一个 Java 应用程序都会去创建和处理数据集合。它们是诸多应用程序的基础,因为它们可以让你对数据进行分组,并且处理数据。然而,使用 Java 集合去编写程序,代码可能会非常冗长,并且难以并行化运行。下面的例子,充分说明了使用集合,导致冗余的可能性。

找出与培训相关的发票的 ID,并且按照发票的金额进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<Invoice> trainingInvoices = new ArrayList<>();
for(Invoice inv: invoices) {
if(inv.getTitle().contains("Training")) {
trainingInvoices.add(inv);
}
}

Collections.sort(trainingInvoices, new Comparator() {
public int compare(Invoice inv1, Invoice inv2) {
return inv2.getAmount().compareTo(inv1.getAmount());
}
});

List<Integer> invoiceIds = new ArrayList<>();

for(Invoice inv: trainingInvoices) {
invoiceIds.add(inv.getId());
}

使用 Java 8 中的 Stream API 就可将上述代码简化为:

1
2
3
4
5
List<Integer> invoiceIds = invoices.stream()
.filter(inv -> inv.getTitle().contains("Training"))
.sorted(comparingDouble(Invoice::getAmount).reversed())
.map(Invoice::getId)
.collect(Collectors.toList());

接口增强 (Enhanced Interfaces)

Java 8 中的接口现在可以通过两个改进来声明带有实现代码的方法。

默认方法

首先,Java 8 引入了默认方法,它允许您在接口中声明具有实现代码的方法。它们是作为一种以向后兼容的方式发展 Java API 的机制而引入的。例如,您将看到在 Java 8 中,List 接口现在支持一种排序方法,其定义如下:

1
2
3
4
5
6
7
8
9
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}

默认方法也可以作为行为的多重继承机制。 事实上,在 Java 8 之前,一个类可能已经实现了多个接口。 现在,您可以从多个不同的接口继承默认方法。 请注意,Java 8 具有明确的规则来防止 C ++ 中常见的继承问题(例如 diamond problem)。

静态方法

其次,接口现在也可以有静态方法。 定义用于处理接口实例的静态方法的接口和伴随类是一种常见模式。 例如,Java 具有 Collection 接口和 Collections 类,它定义了实用程序的静态方法。 这种实用静态方法现在可以存在于接口中。 例如,Java 8 中的 Stream 接口声明了一个像这样的静态方法:

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

CompletableFuture

Java 8 通过一个新的 Class 类 CompletableFuture 来考虑一种新的异步编程方式。 这是对旧有的 Future 类的改进,其操作灵感来源于新 Streams API 中的类似设计选择(即声明式风格和流畅链接方法的能力)。 换句话说,您可以声明式地处理和编写多个异步任务。
以下是一个同时查询两个阻塞任务的示例:价格查找服务以及汇率计算器。 一旦两项服务的结果可用,您可以将其结果合并计算并以英镑打印价格:

1
2
3
4
5
6
7
8
9
10
11
findBestPrice("iPhone6")
.thenCombine(lookupExchangeRate(Currency.GBP), this::exchange)
.thenAccept(localAmount -> System.out.printf("It will cost you %f GBP\n", localAmount));

private CompletableFuture<Price> findBestPrice(String productName) {
return CompletableFuture.supplyAsync(() -> priceFinder.findBestPrice(productName));
}

private CompletableFuture<Double> lookupExchangeRate(Currency localCurrency) {
return CompletableFuture.supplyAsync(() -> exchangeService.lookupExchangeRate(Currency.USD, localCurrency));
}

Optional

Java 8 引入了一个名为 Optional 的新类。 受到函数式编程语言的启发,当值可能存在或缺失时,允许在代码库中更好地建模。 把它看作一个单值容器,因为它包含一个值或是空的。 Optional 已经在替代集合框架(如 Guava)中可用,但现在可作为 Java API 的一部分。 Optional 的另一个好处是它可以保护你免受 NullPointerExceptions 的侵害。 事实上,Optional 定义了一些方法来强制你明确地检查一个值是否存在。 以下面的代码为例:

1
getEventWithId(10).getLocation().getCity();

getEventWithId (10) 返回 null,或者 getLocation () 返回 null,都会导致程序抛出异常:NullPointerException。为了避免异常发生,需要做以下判断:

1
2
3
4
5
6
7
8
9
10
public String getCityForEvent(int id) {
Event event = getEventWithId(id);
if(event != null) {
Location location = event.getLocation();
if(location != null) {
return location.getCity();
}
}
return "TBC";
}

在这段代码中,一个 Event 可能有一个关联的 Location。 然而,一个 Location 总是有一个相关的 City。 不幸的是,我们常常忘记检查 null。 另外,代码也会变得更加冗长。 使用 Optional,您可以重构代码,使其变得更加简单明了,如下所示:

1
2
3
4
5
6
public String getCityForEvent(int id) {
Optional.ofNullable(getEventWithId(id))
.flatMap(this::getLocation)
.map(this::getCity)
.orElse("TBC");
}

新 Date 和 Time API

java 8 引入了一个全新的 DateTime 的 API,修复了旧 DateCalendar 中存在的许多问题。新的 Date 和 Time API 主要根据以下两个原则进行设计:

领域驱动设计

新的 Date 和 Time API 通过引入新的 class 对象来精确模式各种日期和时间的概念。例如,您可以使用 Period 类来表示类似 “2 个月和 3 天” 的值,并使用 ZonedDateTime 来表示具有时区的日期时间。每个类都提供了基于流畅式编码风格的特定领域的方法。 因此,您可以使用方法链来编写更多可读性强的代码。 例如,以下代码显示如何创建一个新的 LocalDateTime 对象并添加 2 小时 30 分钟:

1
2
3
LocatedDateTime coffeeBreak = LocalDateTime.now()
.plusHours(2)
.plusMinutes(30);

不可变性

Date 和 Calender 类的另一个问题就是它们是线程不安全的。另外,使用日期作为其 API 的开发人员可能会意外地更新时间值。为了预防这种潜在 Bug 的产生,在新的 Date 和 Time API 中,所有的类都是不可变的。换句话说,在新的 Date 和 Time API 中,你无法修改对象的状态,相反,你需要调用新的方法来更新值,并且会返回一个新的对象。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ZoneId london = ZoneId.of("Europe/London");
LocalDate july4 = LocalDate.of(2014, Month.JULY, 4);
LocalTime early = LocalTime.parse("08:45");
ZonedDateTime flightDeparture = ZonedDateTime.of(july4, early, london);
System.out.println(flightDeparture);

LocalTime from = LocalTime.from(flightDeparture);
System.out.println(from);

ZonedDateTime touchDown = ZonedDateTime.of(july4,
LocalTime.of(11, 35),
ZoneId.of("Europe/Stockholm"));
Duration flightLength = Duration.between(flightDeparture, touchDown);
System.out.println(flightLength);

// How long have I been in continental Europe?
ZonedDateTime now = ZonedDateTime.now();
Duration timeHere = Duration.between(touchDown, now);
System.out.println(timeHere);

输出结果:

1
2
3
4
2014-07-04T08:45+01:00[Europe/London]
08:45
PT1H50M
PT33879H12M25.546S
参考资料