本文概述
Java平台的最新版本Java 8于一年多以前发布。许多公司和开发人员仍在使用以前的版本, 这是可以理解的, 因为从一个平台版本迁移到另一个平台存在很多问题。即使这样, 许多开发人员仍在使用旧版本的Java启动新应用程序。这样做的理由很少, 因为Java 8对语言进行了一些重要的改进。
Java 8有许多新功能。我将向你展示一些最有用和最有趣的功能:
- Lambda表达式
- 用于使用集合的Stream API
- 具有CompletableFuture的异步任务链
- 全新的Time API
Lambda表达式
Lambda是一个代码块, 可以引用该代码块并将其传递给另一段代码, 以供将来执行一次或多次。例如, 其他语言中的匿名函数是lambda。像函数一样, lambda可以在执行时传递参数, 从而修改其结果。 Java 8引入了lambda表达式, 该表达式提供了创建和使用lambda的简单语法。
让我们来看一个如何改善代码的示例。在这里, 我们有一个简单的比较器, 该比较器通过对两个Integer值的模2进行比较:
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传递给sort()方法的方式与在先前示例中传递BinaryComparator实例的方式相同。 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, 传入不同的lambda来实现不同的行为, 如下所示:
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方法。我们也可以引用另一个对象或类的方法。
Stream 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;
}
假设我们需要打印出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对象, 该对象封装了该collection的所有元素。可以使用Stream API中的不同修饰符(例如filter()和map())来操纵此属性。每个修改器都返回一个新的Stream对象, 其中包含修改的结果, 可以进一步对其进行操作。 .forEach()方法允许我们对结果流的每个实例执行一些操作。
此示例还演示了函数编程与lambda表达式之间的紧密关系。请注意, 传递给流中每个方法的参数是自定义lambda或方法引用。从技术上讲, 每个修改器都可以接收任何功能接口, 如上一节所述。
Stream API帮助开发人员从新的角度看待Java集合。现在想象一下, 我们需要获取每个国家/地区可用语言的地图。如何在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对象以跟踪其进度。
- 创建一个异步可运行任务。
- 在ExecutorService中运行任务, 该任务将提供一个Future来访问状态或结果。
为了利用异步任务的结果, 有必要使用Future接口的方法从外部监视其进度, 并在就绪时显式检索结果并对其执行进一步的操作。要实现而没有错误, 这可能相当复杂, 尤其是在具有大量并发任务的应用程序中。
但是, 在Java 8中, 通过CompletableFuture <T>接口进一步引入了Future概念, 该接口允许创建和执行异步任务链。这是在Java 8中创建异步应用程序的强大机制, 因为它使我们能够在完成后自动处理每个任务的结果。
让我们来看一个例子:
import java.util.concurrent.CompletableFuture;
...
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage())
.thenApply(this::getLinks)
.thenAccept(System.out::println);
方法CompletableFuture.supplyAsync在默认的执行程序(通常为ForkJoinPool)中创建一个新的异步任务。任务完成后, 其结果将自动作为参数提供给函数this :: getLinks, 该函数也在新的异步任务中运行。最后, 第二阶段的结果将自动打印到System.out。 thenApply()和thenAccept()只是可用于帮助你构建并发任务而无需手动使用执行程序的几种有用方法中的两种。
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)提供。阳历日历。这些类有几个缺点, 包括Calendar类不是类型安全的。由于这些类是可变的, 因此无法在多线程应用程序中使用它们。由于异常的月份数和缺乏类型安全性, 应用程序代码中的错误很常见。”
Java 8最终通过新的java.time包解决了这些长期存在的问题, 该包包含用于处理日期和时间的类。它们都是不可变的, 并且具有类似于流行的框架Joda-Time的API, 几乎所有Java开发人员都在其应用程序中使用该API, 而不是本机的Date, Calendar和TimeZone。
这是此程序包中的一些有用的类:
- 时钟-时钟, 用于告知当前时间, 包括当前时间, 日期和带时区的时间。
- 持续时间和期间-时间量。持续时间使用基于时间的值, 例如” 76.8秒”, 以及基于期限的日期, 例如” 4年, 6个月和12天”。
- 即时-具有多种格式的即时时间点。
- 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欧洲/巴黎”。
有时, 我们需要找到一些相对日期, 例如”每月的第一个星期二”。对于这些情况, java.time提供了一个特殊的类TemporalAdjuster。 TemporalAdjuster类包含一组标准的调节器, 可以作为静态方法使用。这些使我们能够:
- 查找该月的第一天或最后一天。
- 查找下个月或上个月的第一天或最后一天。
- 查找一年中的第一天或最后一天。
- 查找下一年或上一年的第一天或最后一天。
- 查找一个月内一周中的第一天或最后一天, 例如”六月的第一个星期三”。
- 查找一周的下一天或前一天, 例如”下周四”。
这是一个简短的示例, 说明如何获取每月的第一个星期二:
LocalDate getFirstTuesday(int year, int month) {
return LocalDate.of(year, month, 1)
.with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));
}
还在使用Java 7吗?获得该程序! #Java8
鸣叫
Java 8总结
如我们所见, Java 8是Java平台的划时代版本。语言发生了很多变化, 特别是在引入了lambda之后, 这代表了将更多功能编程功能引入Java的举动。 Stream API是一个很好的示例, lambda如何改变我们使用已经习惯的标准Java工具的方式。
而且, Java 8带来了一些用于异步编程的新功能以及对其日期和时间工具的急需的全面修改。
这些变化共同代表了Java语言的一大进步, 使Java开发更加有趣和高效。