为什么您已经需要升级到 Java 8
已发表: 2022-03-11Java 平台的最新版本 Java 8 于一年多前发布。 许多公司和开发人员仍在使用以前的版本,这是可以理解的,因为从一个平台版本迁移到另一个版本存在很多问题。 即便如此,许多开发人员仍在使用旧版本的 Java 开发新应用程序。 这样做的充分理由很少,因为 Java 8 为该语言带来了一些重要的改进。
Java 8 中有许多新特性。我将向您展示一些最有用和最有趣的特性:
- Lambda 表达式
- 用于处理集合的流 API
- 使用
CompletableFuture
进行异步任务链 - 全新的时间 API
Lambda 表达式
lambda是一个代码块,它可以被引用并传递给另一段代码以供将来执行一次或多次。 例如,其他语言中的匿名函数是 lambda。 与函数一样,lambda 可以在执行时传递参数,从而修改其结果。 Java 8 引入了lambda 表达式,它提供了一种简单的语法来创建和使用 lambda。
让我们看一个如何改进我们的代码的示例。 这里我们有一个简单的比较器,它通过模 2 比较两个Integer
数值:
class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }
将来可能会在需要此比较器的代码中调用此类的实例,如下所示:
... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...
新的 lambda 语法让我们可以更简单地做到这一点。 这是一个简单的 lambda 表达式,它与BinaryComparator
的compare
方法做同样的事情:
(Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
该结构与函数有许多相似之处。 在括号中,我们设置了一个参数列表。 语法->
表明这是一个 lambda。 在这个表达式的右侧,我们设置了 lambda 的行为。
现在我们可以改进前面的例子:
... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...
我们可以用这个对象定义一个变量。 让我们看看它的外观:
Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
现在我们可以重用这个功能,像这样:
... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...
请注意,在这些示例中,lambda 以与前面示例中传递BinaryComparator
实例相同的方式传递给sort()
方法。 JVM 如何知道正确解释 lambda?
为了允许函数将 lambda 作为参数,Java 8 引入了一个新概念:函数式接口。 函数式接口是只有一个抽象方法的接口。 事实上,Java 8 将 lambda 表达式视为函数式接口的特殊实现。 这意味着,为了接收一个 lambda 作为方法参数,该参数的声明类型只需要是一个函数式接口。
当我们声明一个函数式接口时,我们可以添加@FunctionalInterface
符号来向开发人员展示它是什么:
@FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }
现在,我们可以调用方法sendDTO
,传入不同的 lambdas 来实现不同的行为,如下所示:
sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));
方法参考
Lambda 参数允许我们修改函数或方法的行为。 正如我们在最后一个示例中看到的,有时 lambda 仅用于调用另一个方法( sendToAndroid
或sendToIos
)。 对于这种特殊情况,Java 8 引入了一种方便的简写方式:方法引用。 这种缩写语法表示调用方法的 lambda,并具有objectName::methodName
形式。 这使我们可以使前面的示例更加简洁易读:
sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);
在这种情况下,方法sendToAndroid
和sendToIos
在this
中实现。 我们也可以引用另一个对象或类的方法。
流 API
Java 8 以全新的 Stream API 的形式带来了使用Collections
的新功能。 这个新功能由java.util.stream
包提供,旨在为使用集合进行编程提供更实用的方法。 正如我们将看到的,这在很大程度上要归功于我们刚刚讨论的新的 lambda 语法。
Stream API 提供了对集合的简单过滤、计数和映射,以及从中获取信息切片和子集的不同方法。 由于函数式语法,Stream API 允许使用更短、更优雅的代码来处理集合。
让我们从一个简短的例子开始。 我们将在所有示例中使用此数据模型:
class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }
假设我们需要打印books
收藏中的所有作者,这些作者在 2005 年之后写了一本书。在 Java 7 中我们将如何做到这一点?
for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }
我们将如何在 Java 8 中做到这一点?
books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element
这只是一种表达方式! 在任何Collection
上调用方法stream()
会返回一个Stream
对象,该对象封装了该集合的所有元素。 这可以使用 Stream API 中的不同修饰符进行操作,例如filter()
和map()
。 每个修改器都返回一个带有修改结果的新Stream
对象,可以进一步对其进行操作。 .forEach()
方法允许我们对结果流的每个实例执行一些操作。
这个例子还展示了函数式编程和 lambda 表达式之间的密切关系。 请注意,传递给流中每个方法的参数要么是自定义 lambda,要么是方法引用。 从技术上讲,每个修饰符都可以接收任何功能接口,如上一节所述。
Stream API 帮助开发人员从一个新的角度看待 Java 集合。 现在想象一下,我们需要获取每个国家/地区可用语言的Map
。 这将如何在 Java 7 中实现?
Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }
在 Java 8 中,事情变得更加简洁:
import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));
collect()
方法允许我们以不同的方式收集流的结果。 在这里,我们可以看到它首先按国家分组,然后按语言映射每个组。 ( groupingBy()
和toSet()
都是Collectors
类的静态方法。)

Stream API 还有很多其他功能。 完整的文档可以在这里找到。 我建议进一步阅读以更深入地了解该软件包必须提供的所有强大工具。
带有CompletableFuture
的异步任务链
在 Java 7 的java.util.concurrent
包中,有一个接口Future<T>
,它允许我们获取未来某个异步任务的状态或结果。 要使用此功能,我们必须:
- 创建一个
ExecutorService
,它管理异步任务的执行,并且可以生成Future
对象来跟踪它们的进度。 - 创建一个
Runnable
可运行任务。 - 在
ExecutorService
中运行任务,这将提供一个Future
来访问状态或结果。
为了利用异步任务的结果,有必要使用Future
接口的方法从外部监视其进度,并在准备好时显式检索结果并使用它们执行进一步的操作。 这在没有错误的情况下实现起来可能相当复杂,尤其是在具有大量并发任务的应用程序中。
然而,在 Java 8 中, Future
概念得到了进一步的发展,使用CompletableFuture<T>
接口,它允许创建和执行异步任务链。 它是在 Java 8 中创建异步应用程序的强大机制,因为它允许我们在完成后自动处理每个任务的结果。
让我们看一个例子:
import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);
CompletableFuture.supplyAsync
方法在默认的Executor
(通常是ForkJoinPool
)中创建一个新的异步任务。 任务完成后,其结果将自动作为参数提供给函数this::getLinks
,该函数也在新的异步任务中运行。 最后,第二阶段的结果会自动打印到System.out
。 thenApply()
和thenAccept()
只是几个有用的方法中的两个,它们可以帮助您在不手动使用Executors
的情况下构建并发任务。
CompletableFuture
使管理复杂异步操作的顺序变得容易。 假设我们需要创建一个包含三个任务的多步数学运算。 任务 1和任务 2使用不同的算法来寻找第一步的结果,我们知道其中只有一个会工作,而另一个会失败。 然而,哪一个有效取决于输入数据,我们事先并不知道。 这些任务的结果必须与任务 3的结果相加。 因此,我们需要找到任务 1或任务 2的结果,以及任务 3的结果。 为了实现这一点,我们可以这样写:
import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution
如果我们检查 Java 8 如何处理这个问题,我们将看到所有三个任务将同时异步运行。 尽管任务 2因异常而失败,但最终结果将被成功计算和打印。
CompletableFuture
使构建具有多个阶段的异步任务变得更加容易,并为我们提供了一个简单的界面,用于准确定义在每个阶段完成时应采取的操作。
Java 日期和时间 API
正如Java自己承认的那样:
在 Java SE 8 发布之前,Java 日期和时间机制由
java.util.Date
、java.util.Calendar
和java.util.TimeZone
类以及它们的子类(例如java.util.GregorianCalendar
。 这些类有几个缺点,包括
- Calendar 类不是类型安全的。
- 因为这些类是可变的,所以它们不能在多线程应用程序中使用。
- 由于不寻常的月份数和缺乏类型安全性,应用程序代码中的错误很常见。”
Java 8 终于通过新的java.time
包解决了这些长期存在的问题,其中包含用于处理日期和时间的类。 它们都是不可变的,并且具有类似于流行的框架 Joda-Time 的 API,几乎所有 Java 开发人员在他们的应用程序中都使用它,而不是原生的Date
、 Calendar
和TimeZone
。
下面是这个包中一些有用的类:
-
Clock
- 显示当前时间的时钟,包括当前时刻、日期和带时区的时间。 -
Duration
和Period
- 时间量。Duration
使用基于时间的值,例如“76.8 秒”,而Period
使用基于日期的值,例如“4 年 6 个月和 12 天”。 -
Instant
- 瞬时时间点,有多种格式。 -
LocalDate
,LocalDateTime
,LocalTime
,Year
,YearMonth
- 日期、时间、年份、月份或它们的某种组合,在 ISO-8601 日历系统中没有时区。 -
OffsetDateTime
,OffsetTime
- 在 ISO-8601 日历系统中与 UTC/格林威治有偏移的日期时间,例如“2015-08-29T14:15:30+01:00”。 -
ZonedDateTime
- 在 ISO-8601 日历系统中具有关联时区的日期时间,例如“1986-08-29T10:15:30+01:00 Europe/Paris”。
有时,我们需要找到一些相对日期,例如“每月的第一个星期二”。 对于这些情况java.time
提供了一个特殊的类TemporalAdjuster
。 TemporalAdjuster
类包含一组标准的调整器,可作为静态方法使用。 这些使我们能够:
- 查找该月的第一天或最后一天。
- 查找下个月或上个月的第一天或最后一天。
- 查找一年中的第一天或最后一天。
- 查找下一年或上一年的第一天或最后一天。
- 查找一个月内的第一天或最后一天,例如“六月的第一个星期三”。
- 查找一周中的下一天或上一天,例如“下周四”。
这是一个简短的示例,如何获取每月的第一个星期二:
LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Java 8 总结
正如我们所见,Java 8 是 Java 平台的划时代版本。 语言发生了很多变化,尤其是引入了 lambda,这代表了将更多函数式编程能力带入 Java 的举措。 Stream API 是一个很好的例子,lambdas 如何改变我们使用我们已经习惯的标准 Java 工具的方式。
此外,Java 8 还带来了一些用于处理异步编程的新特性,并对其日期和时间工具进行了急需的检修。
这些变化共同代表了 Java 语言向前迈出的一大步,使 Java 开发更有趣、更高效。