JDK 8 介绍:Oracle JDK 8 指南 – wiki基地


Oracle JDK 8 指南:里程碑式变革的详细解读

引言:JDK 8 的时代意义

2014年3月18日,Java 开发工具包(JDK)的第八个主要版本,即 Java SE 8,由 Oracle 公司正式发布。这次发布不仅仅是一次简单的版本更新,而是 Java 平台历史上最具革命性和影响力的版本之一。JDK 8 引入了大量开创性的新特性,极大地提升了 Java 语言的表达能力、开发效率和运行时性能。它标志着 Java 从传统的面向对象编程范式开始,融入了函数式编程的理念,为开发者提供了更强大、更灵活的工具。

即便在 JDK 9、10、11、17 等后续版本相继问世的今天,JDK 8 仍然是企业级应用中最广泛使用的 Java 版本之一。无数现有的系统和框架都构建在 JDK 8 的基础之上。因此,深入理解 JDK 8 的核心特性,对于任何 Java 开发者来说,都具有至关重要的意义。本指南将带您详细探索 Oracle JDK 8 的关键亮点,揭示它如何改变了 Java 的开发方式。

核心变革一:拥抱函数式编程——Lambda 表达式与函数式接口

JDK 8 引入的最重磅特性无疑是 Lambda 表达式(Lambda Expressions)。它让 Java 语言能够以一种紧凑、简洁的方式表达行为,是函数式编程思想在 Java 中的重要体现。

1. Lambda 表达式(Lambda Expressions)

是什么?

Lambda 表达式可以被看作是一个匿名函数(Anonymous Function),它没有名称,但有参数列表、函数体以及返回类型(通常是推断出来的)。它的核心作用是允许将代码块作为参数传递给方法,或者将其存储在变量中。

为什么需要?

在 JDK 8 之前,如果想传递行为(例如一个回调函数),通常需要使用匿名内部类。匿名内部类语法冗余,特别是在处理简单的功能时,代码会显得非常臃肿。Lambda 表达式的出现极大地简化了这一过程。

语法:

Lambda 表达式的基本语法是:(parameters) -> expression(parameters) -> { statements; }

  • parameters:参数列表。如果只有一个参数,可以省略括号;如果没有参数,必须使用空括号 ()。参数的类型通常可以由编译器根据上下文推断出来。
  • ->:箭头符号,分隔参数列表和函数体。
  • expression{ statements; }:函数体。如果函数体只有一条表达式,可以直接写,返回值就是表达式的结果;如果函数体包含多条语句,需要使用花括号 {} 包围,并且如果需要返回值,必须使用 return 关键字。

示例:

比较匿名内部类和 Lambda 表达式:

“`java
// 使用匿名内部类创建一个 Runnable 对象
Runnable oldRunnable = new Runnable() {
@Override
public void run() {
System.out.println(“Hello from anonymous inner class!”);
}
};

// 使用 Lambda 表达式创建一个 Runnable 对象
Runnable newRunnable = () -> System.out.println(“Hello from lambda expression!”);

// 使用匿名内部类创建一个 Comparator 对象
Comparator oldComparator = new Comparator() {
@Override
public int compare(Integer a, Integer b) {
return a.compareTo(b);
}
};

// 使用 Lambda 表达式创建一个 Comparator 对象
Comparator newComparator = (a, b) -> a.compareTo(b);
“`

可以看到,Lambda 表达式的语法更加简洁明了。

** Lambda 表达式与变量捕获(Variable Capture)**

Lambda 表达式可以访问其所在作用域的局部变量。这些变量必须是 final 的,或者事实上是 final 的(effectively final),即在 Lambda 表达式的生命周期内不会被修改。这一规则保证了多线程环境下 Lambda 表达式的安全性。

2. 函数式接口(Functional Interfaces)

是什么?

函数式接口是 JDK 8 为配合 Lambda 表达式引入的一个概念。它是一个只有一个抽象方法的接口。Lambda 表达式的类型就是函数式接口。

@FunctionalInterface 注解:

为了明确地标记一个接口是函数式接口,并让编译器进行检查,JDK 8 引入了 @FunctionalInterface 注解。这不是强制性的,但强烈推荐使用,因为它可以防止在接口中意外添加了第二个抽象方法,从而导致其不再是一个函数式接口。

java
@FunctionalInterface
interface MyFunction {
void apply(); // 只有一个抽象方法
// void doSomethingElse(); // 如果添加这一行,编译器会报错,因为这不是一个函数式接口了
}

JDK 内置的函数式接口:

JDK 8 在 java.util.function 包中提供了大量常用的函数式接口,极大地便利了 Lambda 表达式的使用。主要类别包括:

  • Consumer<T>:接受一个输入参数并且无返回的操作 (void accept(T t))。
  • Supplier<T>:无参数,返回一个结果 (T get())。
  • Predicate<T>:接受一个输入参数,返回一个布尔值 (boolean test(T t))。
  • Function<T, R>:接受一个输入参数,返回一个结果 (R apply(T t))。
  • 以及它们的变种(如 BiConsumer, BiFunction, IntConsumer, LongSupplier 等)和组合接口(如 andThen, compose)。

重要性:

函数式接口是 Lambda 表达式的基石。Lambda 表达式并不是孤立存在的,它必须被“赋值”给一个与其签名匹配的函数式接口类型的引用。这使得 Java 可以在保持其强类型特性的同时,引入函数式编程的便利。

核心变革二:数据处理的革命——Streams API

与 Lambda 表达式紧密相连的是 Streams API(流 API),它提供了对集合(Collections)和其他数据源进行函数式风格操作的能力。Streams API 极大地简化了集合数据的处理,使其更易于并行化。

1. Streams API(java.util.stream)

是什么?

Stream 是一个元素序列,它支持串行和并行聚合操作。与集合不同,Stream 本身并不存储元素,它只是数据源(如集合、数组、I/O 通道等)的一个视图或管道。Streams API 提供了丰富的操作,可以用来过滤、映射、排序、归约等数据。

为什么需要?

在 JDK 8 之前,处理集合数据通常需要使用迭代器或增强 for 循环。这种方式是命令式的(我们告诉计算机每一步要做什么),代码通常冗长且难以并行化。Streams API 采用声明式风格(我们告诉计算机我们想要什么结果),代码更简洁、更易读,并且天然支持并行处理。

核心概念:

一个典型的 Stream 操作序列包含三个部分:

  1. 数据源(Source): 创建 Stream 的源,可以是集合(通过 stream()parallelStream() 方法)、数组(通过 Arrays.stream())、I/O 通道、生成器等。
  2. 中间操作(Intermediate Operations): 这些操作会返回一个新的 Stream,它们是“懒惰的”(lazy),这意味着它们在终端操作之前不会真正执行。常见的中间操作包括:
    • filter(Predicate):根据条件过滤元素。
    • map(Function):将每个元素转换成另一个元素。
    • flatMap(Function):将每个元素转换成一个 Stream,然后将所有 Stream 连接成一个 Stream。
    • distinct():去除重复元素。
    • sorted():排序元素。
    • peek(Consumer):对每个元素执行某个操作,主要用于调试。
    • limit(n):截断 Stream,只保留前 n 个元素。
    • skip(n):跳过 Stream 的前 n 个元素。
  3. 终端操作(Terminal Operation): 这些操作会消耗 Stream,产生一个最终结果(如集合、值、副作用)。终端操作会触发整个 Stream 管道的执行。常见的终端操作包括:
    • forEach(Consumer):对每个元素执行一个动作。
    • collect(Collector):将 Stream 的元素收集到一个集合或其他结构中(如 List, Set, Map)。Collectors 类提供了大量的预定义收集器。
    • reduce(accumulator)reduce(identity, accumulator):将 Stream 中的元素组合成一个单一结果。
    • count():返回元素的数量。
    • min(Comparator) / max(Comparator):返回 Stream 中的最小/最大元素。
    • anyMatch(Predicate) / allMatch(Predicate) / noneMatch(Predicate):检查 Stream 中是否有元素匹配给定的条件。
    • findFirst() / findAny():返回 Stream 中的第一个或任意一个元素(返回 Optional)。

示例:

“`java
List names = Arrays.asList(“Alice”, “Bob”, “Charlie”, “David”, “Alice”);

// 找出所有名字长度大于3且不重复,并按字母顺序排序,最后收集到列表中
List filteredSortedNames = names.stream() // 数据源
.filter(name -> name.length() > 3) // 中间操作:过滤
.distinct() // 中间操作:去重
.sorted() // 中间操作:排序
.collect(Collectors.toList()); // 终端操作:收集到 List

System.out.println(filteredSortedNames); // 输出: [Alice, Charlie, David]

// 计算所有名字的总长度
int totalLength = names.stream() // 数据源
.mapToInt(String::length) // 中间操作:将 String Stream 映射为 IntStream
.sum(); // 终端操作:求和

System.out.println(“Total length: ” + totalLength); // 输出: Total length: 27
“`

并行流(Parallel Streams):

Streams API 的另一个强大之处在于可以轻松地转换为并行流,利用多核处理器的能力来加速数据处理。只需将 stream() 替换为 parallelStream() 即可。

java
names.parallelStream()
.filter(...)
.map(...)
.forEach(...); // 操作会自动在多个线程中并行执行

然而,并行流并非总是更快,它引入了线程管理的开销。对于少量数据或某些类型的操作(如 reduce 在非关联操作时),串行流可能更快。需要根据具体情况进行性能测试。

重要性:

Streams API 极大地改变了 Java 中集合数据处理的方式,使其变得更加声明式、函数式,并且更容易实现并行处理。它与 Lambda 表达式结合,构成了 JDK 8 函数式编程的核心支柱。

核心变革三:接口的进化——默认方法与静态方法

在 JDK 8 之前,接口是纯粹的抽象契约,只能包含抽象方法和常量。一旦发布了一个接口,就很难在不破坏现有实现类的情况下添加新的方法。JDK 8 通过引入默认方法(Default Methods)和接口静态方法解决了这个问题。

1. 默认方法(Default Methods / Defender Methods)

是什么?

默认方法允许在接口中拥有具有默认实现的方法。它们使用 default 关键字修饰。

为什么需要?

主要目的是为了接口的向前兼容性。当需要在现有接口中添加新方法时,如果使用默认方法,则已有的实现了该接口的类无需修改代码也能正常编译和运行,它们将继承默认实现。

语法:

“`java
interface MyInterface {
void abstractMethod(); // 抽象方法

default void defaultMethod() { // 默认方法
    System.out.println("Default implementation of defaultMethod");
}

}

class MyClass implements MyInterface {
@Override
public void abstractMethod() {
System.out.println(“Implementation of abstractMethod”);
}
// 无需实现 defaultMethod
}
“`

多继承问题(Diamond Problem)的解决:

如果一个类实现了多个接口,并且这些接口中包含签名相同的默认方法,Java 采用以下规则解决冲突:

  1. 类优先: 如果类本身提供了同名同签名的具体方法,则使用类中的方法。
  2. 子接口优先: 如果一个接口继承了另一个接口,并且定义了签名相同的默认方法或抽象方法,子接口的方法优先。
  3. 明确指定: 如果上述规则无法解决冲突(例如,类实现了两个互不继承的接口,它们都有同名同签名的默认方法),编译器会报错,要求实现类明确指定使用哪个接口的默认方法,或者提供自己的实现。

2. 接口静态方法(Static Methods in Interfaces)

是什么?

JDK 8 允许在接口中定义静态方法。这些方法与类中的静态方法类似,可以直接通过接口名调用,无需接口的实现类或对象。

为什么需要?

可以将一些与接口相关的工具方法或工厂方法直接放在接口中,而不是放在一个单独的工具类中(例如,Collections 类)。这提高了代码的内聚性。

语法:

“`java
interface MyInterface {
void abstractMethod();

default void defaultMethod() {
    System.out.println("Default implementation");
}

static void staticMethod() { // 接口静态方法
    System.out.println("Static method in interface");
}

}

// 调用静态方法
MyInterface.staticMethod(); // 直接通过接口名调用
“`

重要性:

默认方法是 JDK 8 中支持 Lambda 表达式和 Streams API 的一个关键技术(例如,Collection 接口中的 stream()forEach() 方法就是默认方法)。它们共同增强了接口的功能,使其在保持抽象契约角色的同时,也能够包含一些实现细节和工具方法,提高了代码的灵活性和可维护性。

核心变革四:告别旧时光——新的日期与时间 API(java.time)

在 JDK 8 之前,Java 的日期和时间处理一直是开发者的痛点。java.util.Datejava.util.Calendar 类存在许多问题:它们是可变的、非线程安全的、设计复杂且不直观。JDK 8 引入了全新的日期和时间 API (java.time 包),彻底解决了这些问题。

1. 新的日期与时间 API

是什么?

这是一个基于 Joda-Time 库设计的新 API,提供了强大、清晰、易用的日期、时间、时间戳、时区、周期和持续时间处理能力。

为什么需要?

为了解决旧 API 的痛点:

  • 可变性: 旧 API 的对象可以被修改,容易导致多线程问题。新 API 的所有核心类都是不可变的,天生线程安全。
  • 清晰性: 旧 API 中 Date 类既包含日期又包含时间,且年份从1900开始,月份从0开始,非常容易出错。新 API 提供了针对不同用途的清晰类(如 LocalDate, LocalTime, LocalDateTime)。
  • 设计: 旧 API 的设计复杂且方法命名不一致。新 API 设计优雅,方法命名直观。
  • 时区处理: 旧 API 的时区处理复杂且容易出错。新 API 提供了专门的类(如 ZoneId, ZonedDateTime)来简化时区操作。

核心类:

  • LocalDate:表示一个日期(年、月、日),没有时间和时区信息。
  • LocalTime:表示一天中的时间(小时、分钟、秒、纳秒),没有日期和时区信息。
  • LocalDateTime:表示日期和时间的组合,没有时区信息。
  • Instant:表示时间线上的一个瞬时点,通常以 Unix 时间戳的形式存储(自1970-01-01T00:00:00Z 以来的秒数和纳秒数)。
  • ZonedDateTime:表示带时区的日期和时间。它是 LocalDateTimeZoneId 的组合。
  • OffsetDateTime:表示带偏移量(而不是时区规则)的日期和时间。
  • Duration:表示时间上的持续时间,精确到纳秒(例如:2小时30分钟)。
  • Period:表示日期上的一个周期(例如:2年3个月5天)。
  • ZoneId:表示一个时区标识符(例如:”Asia/Shanghai”, “Europe/Paris”)。
  • ZoneOffset:表示与 Greenwich/UTC 的固定时间偏移量(例如:+08:00)。
  • DateTimeFormatter:用于日期和时间的格式化和解析,它是线程安全的。

示例:

“`java
// 获取当前日期、时间、日期时间
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime todayNow = LocalDateTime.now();

System.out.println(“Today: ” + today); // 例如: 2023-10-27
System.out.println(“Now: ” + now); // 例如: 10:30:55.123
System.out.println(“Today and Now: ” + todayNow); // 例如: 2023-10-27T10:30:55.123

// 创建特定日期和时间
LocalDate specificDate = LocalDate.of(2024, 1, 1); // 2024-01-01
LocalTime specificTime = LocalTime.of(14, 30); // 14:30

// 日期和时间操作(不可变,返回新对象)
LocalDate nextMonth = today.plusMonths(1);
LocalDateTime modifiedDateTime = todayNow.withHour(15).minusMinutes(10);

// 时区
ZoneId paris = ZoneId.of(“Europe/Paris”);
ZonedDateTime parisTime = ZonedDateTime.now(paris);

// 持续时间和周期
Duration duration = Duration.between(LocalTime.of(9, 0), LocalTime.of(17, 0)); // 8小时
Period period = Period.between(LocalDate.of(2023, 1, 1), LocalDate.of(2024, 1, 1)); // 1年

// 格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(“yyyy/MM/dd HH:mm:ss”);
String formattedDateTime = todayNow.format(formatter);
System.out.println(“Formatted: ” + formattedDateTime); // 例如: 2023/10/27 10:30:55
“`

重要性:

新的日期和时间 API 是 JDK 8 中最受欢迎的改进之一。它彻底改变了 Java 中日期和时间处理的体验,使其变得更加直观、安全和强大,是现代 Java 开发中不可或缺的一部分。

核心变革五:解决空指针烦恼——Optional

NullPointerException(NPE)是 Java 开发中最常见的异常之一,被戏称为“十亿美元的错误”。JDK 8 引入了 java.util.Optional<T> 类,旨在以更优雅的方式处理可能为空的值,从而减少 NPE 的发生。

1. Optional

是什么?

Optional 是一个容器对象,它可能包含也可能不包含非 null 的值。如果值存在,isPresent() 方法返回 true,并且 get() 方法会返回该值。

为什么需要?

为了提供一种更明确的方式来表示一个值是否存在。通过方法的返回类型是 Optional<T>,调用者可以清楚地知道返回值可能是空的,从而被鼓励以一种显式的方式处理空值情况,而不是依赖于容易遗漏的 null 检查。

核心方法:

  • Optional.empty():创建一个空的 Optional 对象。
  • Optional.of(value):创建一个包含非 null 值的 Optional 对象。如果 value 是 null,会抛出 NPE。
  • Optional.ofNullable(value):创建一个包含指定值的 Optional 对象,如果 value 是 null,则返回一个空的 Optional
  • isPresent():如果 Optional 中包含值,返回 true
  • get():如果 Optional 中有值,返回该值;否则抛出 NoSuchElementException应谨慎使用,通常配合 isPresent() 或替代方法使用。
  • orElse(other):如果 Optional 中有值,返回该值;否则返回指定的 other 值。
  • orElseGet(supplier):如果 Optional 中有值,返回该值;否则调用 supplierget() 方法并返回其结果。这比 orElse 更高效,特别是当默认值计算成本较高时,因为 supplier 只在需要时被调用。
  • orElseThrow()orElseThrow(supplier):如果 Optional 中有值,返回该值;否则抛出指定的异常(默认为 NoSuchElementException)。
  • ifPresent(Consumer):如果 Optional 中有值,则对该值执行指定的 Consumer 操作。
  • filter(Predicate):如果 Optional 中有值且该值符合 Predicate 条件,则返回包含该值的 Optional;否则返回一个空的 Optional
  • map(Function):如果 Optional 中有值,则对其应用 Function 函数,并返回结果包含在 Optional 中的对象;否则返回一个空的 Optional。这是链式处理 Optional 值的常用方法。
  • flatMap(Function):类似于 map,但要求 Function 返回一个 Optional。用于处理嵌套的 Optional 结构,避免出现 Optional<Optional<T>>

示例:

“`java
String name = “Java”;
Optional optionalName = Optional.ofNullable(name);

String nullName = null;
Optional optionalNullName = Optional.ofNullable(nullName);

// 安全地获取值
optionalName.ifPresent(n -> System.out.println(“Value is: ” + n)); // 输出: Value is: Java
optionalNullName.ifPresent(n -> System.out.println(“Value is: ” + n)); // 无输出

// 提供默认值
String result1 = optionalName.orElse(“Default”); // Result1: Java
String result2 = optionalNullName.orElse(“Default”); // Result2: Default

// 链式操作 with map
String upperCaseName = optionalName.map(String::toUpperCase).orElse(“UNKNOWN”); // UpperCaseName: JAVA
String upperCaseNullName = optionalNullName.map(String::toUpperCase).orElse(“UNKNOWN”); // UpperCaseNullName: UNKNOWN
“`

重要性:

Optional 并不是万能的 NPE 终结者,它并不能替代所有的 null 检查。但它提供了一种清晰、函数式的方式来表达“值可能不存在”的情况,特别适用于方法的返回值。合理使用 Optional 可以显著提高代码的可读性和健壮性,减少因遗漏 null 检查导致的运行时错误。然而,不恰当地使用 Optional(例如将其作为字段或方法参数类型)反而会增加代码复杂度,需谨慎使用。

其他重要改进

除了上述五大核心特性,JDK 8 还带来了许多其他重要的改进和新功能:

1. Nashorn JavaScript 引擎

JDK 8 中集成了 Nashorn JavaScript 引擎,取代了之前的 Rhino 引擎。Nashorn 提供了更好的 ECMAScript 规范兼容性,更优秀的性能,以及与 Java 代码更紧密的集成能力。可以在 JVM 上直接运行和调用 JavaScript 代码。

2. PermGen 空间的移除与 Metaspace

在 JDK 8 之前,JVM 使用一块名为 PermGen(Permanent Generation)的内存区域来存储类的元数据(如类定义、字节码、方法信息等)。PermGen 的大小是固定的,如果加载的类过多,容易发生 OutOfMemoryError: PermGen space 错误,且调整大小不灵活。

JDK 8 移除了 PermGen 空间,取而代之的是 Metaspace(元空间)。Metaspace 使用本地内存(Native Memory),其大小默认只受限于操作系统的可用内存。虽然也可能发生 OutOfMemoryError: Metaspace,但通常是因为加载了过多类且没有对 Metaspace 进行合理限制,或者存在类加载器泄漏。Metaspace 的引入使得 JVM 的内存管理更加灵活和高效。

3. Base64 编码/解码支持

JDK 8 在 java.util.Base64 类中提供了官方的 Base64 编码和解码支持,无需再依赖第三方库或手动实现,更加便捷和规范。

4. 无符号整数支持

在包装类(如 IntegerLong)中添加了新的静态方法,支持无符号整数的操作,例如比较、除法、转换为字符串等。虽然 Java 本身没有无符号基本类型,但这些方法提供了处理无符号整数二进制表示的能力。

5. 并行数组排序(Parallel Array Sorting)

java.util.Arrays 类中新增了 parallelSort() 方法,可以利用 Fork/Join 框架并行地对数组进行排序,在处理大型数组时可以显著提高性能。

6. 并发性改进

java.util.concurrent 包中新增了一些类,如 CompletableFuture(用于异步编程)和一些与 Fork/Join 框架相关的改进。

7. IO 改进

尽管大部分 NIO.2 的改进在 JDK 7 中,但 JDK 8 也包含了一些小的改进,例如 Files.list()Files.walk() 方法,方便以 Stream 的方式遍历文件系统。

8. 注解增强

JDK 8 引入了类型注解(Type Annotations),允许在任何可以使用类型的地方(如类声明、方法参数、返回类型、泛型参数、数组元素等)使用注解。这增强了类型检查的能力,并为一些框架(如 JSR 308 JSR 308 Annotated Types)提供了基础。同时,支持重复注解(Repeating Annotations),允许在同一个元素上应用同一个注解多次。

9. 反射改进

java.lang.reflect.Parameter 类提供了访问方法参数名称的能力(需要在编译时使用 -parameters 标志)。

JDK 8 的影响与遗产

JDK 8 的发布对 Java 生态系统产生了深远的影响。

  • 提升开发者效率: Lambda 表达式和 Streams API 极大地减少了编写常用代码的冗余,使得代码更简洁、更具表现力,提高了开发效率。
  • 推动函数式编程普及: JDK 8 将函数式编程的思想引入主流 Java 开发,改变了许多开发者思考和解决问题的方式。
  • 改善现有问题: 新的日期时间 API 和 Optional 类解决了长期困扰开发者的痛点。
  • 为未来奠定基础: JDK 8 的这些核心特性成为了后续 Java 版本继续演进的基础,许多新功能都是在其之上构建的。

尽管 Oracle 对 JDK 8 的公共更新已停止(除非购买商业支持),但许多其他 OpenJDK 发行版(如 Adoptium Temurin, Azul Zulu, Amazon Corretto 等)仍提供 JDK 8 的免费长期支持(LTS)更新。因此,即使新的 Java 版本不断涌现,JDK 8 仍然在许多企业和项目中发挥着重要作用,理解它依然是 Java 开发者的必备技能。

总结

Oracle JDK 8 是 Java 发展历程中一个里程碑式的版本。它通过引入 Lambda 表达式、Streams API、默认方法、新的日期时间 API 和 Optional 等重量级特性,显著增强了 Java 语言的功能和表达力,使其更加现代化和高效。这些特性不仅解决了旧版本的一些痛点,还为 Java 引入了函数式编程范式,极大地改变了开发者的编程习惯。

对于任何希望深入理解 Java 语言或维护基于 JDK 8 构建的系统的开发者来说,掌握这些核心概念至关重要。通过本指南的详细介绍,希望您能对 JDK 8 的强大功能有一个全面和深入的认识,并在您的开发实践中灵活运用这些特性,编写出更优雅、更高效、更健壮的 Java 代码。JDK 8 的时代虽然已过去近十年,但它的影响将持续存在,它是我们通往现代 Java 世界的关键一步。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部