Java8 新特性介绍
Java 8 于 2014 年 3 月份发布,新版本增添很多不错的特性,本篇文章我们一起纵览一下这些新特性,后面的文章我们将会一一详细讨论。
Java8 介绍
促使 Java 做出重大改变的动力主要来源于:
- 代码可读性
- 多核运行
代码可读性
Java 代码非常的啰嗦冗长,导致了代码可读性的下降,换句话说,需要很多额外的代码来解释一段很小的内容。例如,现在有个需求,要按照发票金额的数量倒序排序发票列表。在 Java 8 之前,你可能需要这么处理:
1 | Collections.sort(invoices, new Comparator<Invoice>() { |
而在 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 | List<Integer> ids = invoices.stream() |
后面的章节会做详细介绍
多核
Java 8 的第二个重大变化是多核处理器时代所必须的。在过去,你的电脑只有一个处理单元。想要更快地运行应用程序通常意味着提高处理单元的性能。 不幸的是,处理单元的时钟速度不会再更快了。 今天,绝大多数计算机和移动设备都有多个处理单元(称为核)来以并行的方式进行工作。
应用程序应该利用不同的处理单元来提高性能。 Java 应用程序通常通过线程来实现这一点。 不幸的是,与线程一起工作往往是困难且容易出错的,并且通常专供专家使用。
Java 8 中的 Streams API 能够让你非常方便地以并行方式来处理数据。例如,将前面的代码改为并行运行,只需要使用 parallelStream() 接口即可:
1 | List<Integer> ids = invoices.parallelStream() |
后面的章节会做详细介绍
新特性纵览
Lambda 表达式 (Lambda Expressions)
Lambda 表达式可以让您以简洁的方式传递一段代码。 例如,假设你需要获得一个线程来执行任务。 你可以通过创建一个 Runnable 对象来实现,然后将其作为参数传递给 Thread:
1 | Runnable runnable = new Runnable() { |
使用 lambda 表达式,你可以用可读性更强的方式重构前面的代码:
1 | new Thread(() -> System.out.println("Hello world")).start(); |
方法引用 (Method references)
方法引用构成了一个与 Lambda 表达式密切相关的新特性。他们可以让你选择一个在类中定义的方法并且传递它。 例如,假设您需要通过忽略大小写来比较字符串列表。 目前,您会编写如下所示的代码:
1 | List<String> strs = Arrays.asList("C", "a", "A", "b"); |
使用方法引用,代码简洁如下:
1 | Collections.sort(strs, String::compareToIgnoreCase); |
String::compareToIgnoreCase
就是方法引用,它使用一种特殊的语法 ::
.
流 (Streams)
几乎每一个 Java 应用程序都会去创建和处理数据集合。它们是诸多应用程序的基础,因为它们可以让你对数据进行分组,并且处理数据。然而,使用 Java 集合去编写程序,代码可能会非常冗长,并且难以并行化运行。下面的例子,充分说明了使用集合,导致冗余的可能性。
找出与培训相关的发票的 ID,并且按照发票的金额进行排序:
1 | List<Invoice> trainingInvoices = new ArrayList<>(); |
使用 Java 8 中的 Stream API 就可将上述代码简化为:
1 | List<Integer> invoiceIds = invoices.stream() |
接口增强 (Enhanced Interfaces)
Java 8 中的接口现在可以通过两个改进来声明带有实现代码的方法。
默认方法
首先,Java 8 引入了默认方法,它允许您在接口中声明具有实现代码的方法。它们是作为一种以向后兼容的方式发展 Java API 的机制而引入的。例如,您将看到在 Java 8 中,List 接口现在支持一种排序方法,其定义如下:
1 | default void sort(Comparator<? super E> c) { |
默认方法也可以作为行为的多重继承机制。 事实上,在 Java 8 之前,一个类可能已经实现了多个接口。 现在,您可以从多个不同的接口继承默认方法。 请注意,Java 8 具有明确的规则来防止 C ++ 中常见的继承问题(例如 diamond problem)。
静态方法
其次,接口现在也可以有静态方法。 定义用于处理接口实例的静态方法的接口和伴随类是一种常见模式。 例如,Java 具有 Collection 接口和 Collections 类,它定义了实用程序的静态方法。 这种实用静态方法现在可以存在于接口中。 例如,Java 8 中的 Stream 接口声明了一个像这样的静态方法:
1 | public static<T> Stream<T> of(T... values) { |
CompletableFuture
Java 8 通过一个新的 Class 类 CompletableFuture 来考虑一种新的异步编程方式。 这是对旧有的 Future 类的改进,其操作灵感来源于新 Streams API 中的类似设计选择(即声明式风格和流畅链接方法的能力)。 换句话说,您可以声明式地处理和编写多个异步任务。
以下是一个同时查询两个阻塞任务的示例:价格查找服务以及汇率计算器。 一旦两项服务的结果可用,您可以将其结果合并计算并以英镑打印价格:
1 | findBestPrice("iPhone6") |
Optional
Java 8 引入了一个名为 Optional 的新类。 受到函数式编程语言的启发,当值可能存在或缺失时,允许在代码库中更好地建模。 把它看作一个单值容器,因为它包含一个值或是空的。 Optional 已经在替代集合框架(如 Guava)中可用,但现在可作为 Java API 的一部分。 Optional 的另一个好处是它可以保护你免受 NullPointerExceptions 的侵害。 事实上,Optional 定义了一些方法来强制你明确地检查一个值是否存在。 以下面的代码为例:
1 | getEventWithId(10).getLocation().getCity(); |
getEventWithId (10) 返回 null,或者 getLocation () 返回 null,都会导致程序抛出异常:NullPointerException。为了避免异常发生,需要做以下判断:
1 | public String getCityForEvent(int id) { |
在这段代码中,一个 Event
可能有一个关联的 Location
。 然而,一个 Location
总是有一个相关的 City
。 不幸的是,我们常常忘记检查 null。 另外,代码也会变得更加冗长。 使用 Optional,您可以重构代码,使其变得更加简单明了,如下所示:
1 | public String getCityForEvent(int id) { |
新 Date 和 Time API
java 8 引入了一个全新的 Date
和 Time
的 API,修复了旧 Date
和 Calendar
中存在的许多问题。新的 Date 和 Time API 主要根据以下两个原则进行设计:
领域驱动设计
新的 Date 和 Time API 通过引入新的 class 对象来精确模式各种日期和时间的概念。例如,您可以使用 Period 类来表示类似 “2 个月和 3 天” 的值,并使用 ZonedDateTime 来表示具有时区的日期时间。每个类都提供了基于流畅式编码风格的特定领域的方法。 因此,您可以使用方法链来编写更多可读性强的代码。 例如,以下代码显示如何创建一个新的 LocalDateTime 对象并添加 2 小时 30 分钟:
1 | LocatedDateTime coffeeBreak = LocalDateTime.now() |
不可变性
Date 和 Calender 类的另一个问题就是它们是线程不安全的。另外,使用日期作为其 API 的开发人员可能会意外地更新时间值。为了预防这种潜在 Bug 的产生,在新的 Date 和 Time API 中,所有的类都是不可变的。换句话说,在新的 Date 和 Time API 中,你无法修改对象的状态,相反,你需要调用新的方法来更新值,并且会返回一个新的对象。
例如:
1 | ZoneId london = ZoneId.of("Europe/London"); |
输出结果:
1 | 2014-07-04T08:45+01:00[Europe/London] |