JDK 8(Java Development Kit 8)是 Java 发展史上最具里程碑意义的版本,被广泛认为是自 JDK 5 以来最重要的更新。即使在多年后的今天,JDK 8 依然是企业级开发中最主流的版本之一。

我为你将 JDK 8 的特性梳理为以下五大核心板块

Lambda 表达式与函数式编程

这是 JDK 8 最具标志性的特性,它让 Java 代码变得更加简洁、可读性更高。

Lambda 表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用 Lambda 表达式可以使代码变的更加简洁紧凑。

示例

首先定义三个接口,每个接口都包含一个方法

public interface MathOperation {
	int operation(int a, int b);
}
 
public interface PlaceOperation {
	void operation(String str);
}
  
public interface MessageOperation {
	String operation(String str);
}

测试类

public static void main(String[] args) {
	MathOperation mathOperation = (a, b) -> a + b;
	int operation = mathOperation.operation(1, 4);
	System.out.println(operation);
 
	PlaceOperation placeOperation = str -> System.out.println(str);
	placeOperation.operation("我是谁?");
 
	MessageOperation messageOperation = (str) -> {
		System.out.println("hello" + str);
		return "写完了";
	};
	String operation2 = messageOperation.operation("java");
	System.out.println(operation2);
}

运行结果

tips

lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。

我们也可以直接在 lambda 表达式中访问外层的局部变量:lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)

int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
num = 5;  
//报错信息:Local variable num defined in an enclosing scope must be final or effectively 
// final

在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

String first = "";  
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());  
//编译会出错 

函数式接口

在接口上添加注解@FunctionalInterface,这样做可以检查它是否是一个函数式接口,同时javac也会包含一条声明,说明这个接口是函数接口。

@FunctionalInterface
public interface MyInter {
	void getOut();
}
 
//接口中使用泛型
@FunctionalInterface
public interface MyInter<T> {
	T getOut(T t);
}

其他类型的接口

方法引用

当 Lambda 表达式只是简单地调用一个现成的方法时,可以用方法引用(::)来进一步简化代码;

//示例:类名::方法名 或 对象::实例方法名。
System.out::println 代替 (x) -> System.out.println(x)。

接口的默认方法和静态方法

接口中可以包含带有实现的方法(使用 default 关键字)。这使得接口可以在不破坏现有实现类的前提下进行升级,接口中也可以定义静态方法,直接通过接口名调用。

为了让你更直观地理解,我为你编写了一个完整的示例。我们将创建一个接口,并展示如何在不修改旧有实现类的情况下,通过 default 方法给接口增加新功能。

假设我们有一个 Vehicle(交通工具)接口。起初,它只有一个抽象方法 start()。后来,我们想给所有交通工具增加一个“鸣笛”的功能 honk(),但我们不想去修改所有已经实现了 Vehicle 接口的类(比如 Car 和 Bike)。

// 1. 定义接口:Vehicle
interface Vehicle {

    // 抽象方法(以前的传统写法)
    void start();

    // ==========================================
    // ✅ 新特性 1:Default 默认方法
    // ==========================================
    // 这个方法有具体实现,且使用了 default 关键字
    // 实现类如果不重写这个方法,就会使用这里的默认实现
    default void honk() {
        System.out.println("Vehicle is making a sound... 嘟嘟!");
    }

    // ==========================================
    // ✅ 新特性 2:静态方法
    // ==========================================
    // 静态方法属于接口本身,不能被实现类继承或重写
    // 只能通过 接口名.方法名 的方式调用
    static void describe() {
        System.out.println("这是一个描述交通工具的接口,包含默认方法和静态方法。");
    }
}

// 2. 实现类 A:汽车
// 注意:这里我们并没有在 Car 类中实现 honk() 方法
class Car implements Vehicle {
    @Override
    public void start() {
        System.out.println("Car is starting... 发动引擎!");
    }

    // Car 类可以选择重写 default 方法
    @Override
    public void honk() {
        System.out.println("Car is honking... 嘟~~~~嘟~~~~ (汽车喇叭声)");
    }
}

// 3. 实现类 B:自行车
// 注意:Bike 类完全没有提到 honk() 方法
class Bike implements Vehicle {
    @Override
    public void start() {
        System.out.println("Bike is starting... 蹬车!");
    }

    // Bike 使用接口提供的默认 honk() 实现
}

// 4. 测试主类
public class InterfaceExample {
    public static void main(String[] args) {
        
        // 调用接口的静态方法
        // ⚠️ 注意:只能通过 接口名.方法名 调用
        System.out.println("=== 静态方法调用 ===");
        Vehicle.describe(); 

        System.out.println("\n=== 实例方法测试 ===");
        
        Vehicle car = new Car();
        Vehicle bike = new Bike();

        // --- 测试抽象方法 start() ---
        car.start();  // 输出: Car is starting...
        bike.start(); // 输出: Bike is starting...

        System.out.println("---");

        // --- 测试 Default 方法 honk() ---
        // Car 重写了 honk,所以执行 Car 的版本
        car.honk();   // 输出: Car is honking... 嘟~~~~嘟~~~~
        
        // Bike 没有重写 honk,所以执行接口默认的版本
        bike.honk();  // 输出: Vehicle is making a sound... 嘟嘟!
    }
}

Stream 流式数据处理

JDK 8 引入的 Stream API 确实是 Java 开发史上的一次“革命”。它彻底改变了我们处理集合数据的方式——从以前的“告诉 JVM 怎么做”(命令式编程,写 for 循环),转变为“告诉 JVM 做什么”(声明式编程,写 SQL 风格的语句)。简单来说,Stream 不是集合,而是一种“计算流程”或“管道”。以下我为你详细拆解 Stream API 的核心概念、操作类型以及实战用法。

核心概念:Stream 到底是什么?

在深入代码之前,你需要理解 Stream 的三个核心特征:

  1. 不存储数据:Stream 本身不保存元素,它只是对数据源(集合、数组)进行计算的“视图”。
  2. 不可变性:Stream 操作不会修改原始数据源。例如 filter 过滤后,原集合还在,返回的是一个全新的 Stream。
  3. 惰性求值:中间操作(如 filter)不会立即执行,只有当终端操作(如 collect)被调用时,整个流水线才会“开闸放水”,一次性执行完成。

三大操作步骤

使用 Stream API 通常遵循以下三个步骤

1. 创建 Stream(数据源)

你需要一个起点,常见的数据源有集合、数组等。

// 1. 从集合创建(最常用)
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

// 2. 从数组创建
String[] arr = {"a", "b", "c"};
Stream<String> arrayStream = Arrays.stream(arr);

// 3. 使用 of 静态方法
Stream<String> ofStream = Stream.of("a", "b", "c");

// 4. 生成无限流(需配合 limit 使用)
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1); // 迭代
Stream<Double> randomStream = Stream.generate(Math::random);   // 生成
2. 中间操作(加工处理)

中间操作是“流水线”上的工序,它们会返回一个新的 Stream,支持链式调用。因为它们是惰性的,所以此时代码并没有运行。

方法 作用 示例
filter(Predicate) 过滤:保留符合条件的元素 stream.filter(s -> s.length() > 3)
map(Function) 映射:将元素转换成另一种形式 stream.map(String::toUpperCase)
flatMap(Function) 扁平化:将流中的每个元素“摊平”并合并成一个流 将二维列表转为一维
distinct() 去重:根据 hashCode() 和 equals() 去除重复元素 stream.distinct()
sorted() 排序:自然排序或自定义排序 stream.sorted((a, b) -> b - a)
limit(n) 截断:使元素数量不超过 n 个 stream.limit(10)
skip(n) 跳过:跳过前 n 个元素 stream.skip(2)
3. 终端操作(消费结果)

终端操作是流水线的终点。一旦执行终端操作,中间操作链才会真正开始计算,计算完成后该 Stream 就被“消费”掉了,不可复用34。

  • 聚合操作:count()max()min()findFirst()anyMatch()
  • 遍历操作:forEach()
  • 收集操作(最常用):collect()。它可以将流转换回集合、字符串等。

实战:代码对比

为了让你感受 Stream 的威力,我们来看一个经典的例子:

需求:从一个用户列表中,筛选出年龄大于 18 岁的用户,提取他们的姓名,转换为大写,并按字母排序,最后收集到一个新的列表中。

方式一:JDK 7 传统写法(命令式)

代码冗长,且充斥着“怎么做”的模板代码(循环、if 判断、集合初始化)。

List<String> result = new ArrayList<>();
for (User user : userList) {
    if (user.getAge() > 18) {
        result.add(user.getName().toUpperCase());
    }
}
Collections.sort(result);

方式二:JDK 8 Stream 写法(声明式)

代码简洁,逻辑清晰,一眼就能看出业务意图。

List<String> result = userList.stream()          // 1. 获取流
    .filter(user -> user.getAge() > 18)          // 2. 中间操作:过滤
    .map(user -> user.getName().toUpperCase())    // 2. 中间操作:映射+转大写
    .sorted()                                    // 2. 中间操作:排序
    .collect(Collectors.toList());               // 3. 终端操作:收集结果

高阶特性

1. 并行流

如果你的数据量非常大,想利用多核 CPU 的性能,只需将 stream() 改为 parallelStream(),或者在流上调用 .parallel()

// 自动利用 Fork/Join 框架进行多线程并行处理
long count = list.parallelStream()
                 .filter(s -> s.length() > 3)
                 .count();

注意:并行流不是万能的,如果操作涉及线程安全问题(如修改共享变量),或者数据量很小,使用并行流反而会因为线程开销导致性能下降410。

2. Optional 与 Stream 结合

Stream 中经常出现 findFirst()findAny() 等方法,它们返回的是 Optional<T>,这能有效避免空指针异常。

Optional<String> first = list.stream()
    .filter(s -> s.startsWith("A"))
    .findFirst();

// 安全地获取值或提供默认值
String result = first.orElse("Default Value");
3. Collectors 收集器

Collectors 类提供了非常强大的归约功能,是 Stream 的“瑞士军刀”。

  • 分组:Collectors.groupingBy(User::getCity) // 按城市分组
  • 分区:Collectors.partitioningBy(User::isAdult) // 按是否成年分区
  • 连接字符串:Collectors.joining(", ") // 将元素用逗号连接

总结

Stream API 的核心价值在于:代码优雅:链式调用,可读性强,像写 SQL 一样处理数据;性能优化:惰性求值机制会优化中间过程,减少不必要的内存占用;易于并行:轻松切换并行流,无需手动管理线程。在日常开发中,当你面对复杂的集合处理逻辑时,优先考虑使用 Stream API。

java.time新的日期时间 API

JDK 8 引入了一套全新的日期和时间 API(java.time 包),这套 API 是为了解决旧版 java.util.Date 和 java.util.Calendar 存在的线程安全问题、设计混乱以及时区处理麻烦等缺陷而设计的。这套新 API 的设计灵感来自于 Joda-Time 库,遵循了不可变对象、函数式编程和职责分离的原则。

以下我为你详细介绍 JDK 8 中最核心的时间类及其用法。

核心思想与特点

  • 不可变性(Immutable):所有的核心类(如 LocalDateTime, LocalDate)都是 final 的,一旦创建就不能修改。任何修改操作(如加减日期)都会返回一个新的对象,这保证了线程安全。
  • 职责分离:将日期(LocalDate)、时间(LocalTime)、日期+时间(LocalDateTime)拆分为不同的类,而不是像旧版 Date 那样混杂在一起。
  • ISO-8601 标准:默认遵循国际标准格式(例如 yyyy-MM-dd)。
     

以下是 java.time 包中最常用的类

类名 作用 格式示例 适用场景
LocalDate 仅表示日期(年月日) 2024-05-12 生日、纪念日等不需要时间的场合
LocalTime 仅表示时间(时分秒) 16:38:54.158 记录一天中的某个时刻
LocalDateTime 日期+时间(最常用) 2024-05-12T16:38:54.158 替代旧版 Date,用于记录事件发生的时间
ZonedDateTime 带时区的日期时间 2024-05-12T16:38:54.158+08:00[Asia/Shanghai] 需要处理全球不同时区的业务
Instant 时间戳(瞬时点) 2024-05-12T08:38:54.158Z 机器时间,用于记录日志、时间戳计算
Duration 时间间隔(秒/纳秒) - 计算两个时间点之间的差(如耗时)
Period 日期间隔(年/月/日) - 计算两个日期之间的差(如年龄)
DateTimeFormatter 日期格式化与解析 - 替代旧版 SimpleDateFormat,线程安全

JVM 与底层架构优化

JDK 8 在虚拟机层面做了一些影响深远的改动,主要是为了解决内存溢出问题。

元空间取代永久代:

旧版:JDK 7 及之前使用“永久代”存储类的元数据,容易出现 java.lang.OutOfMemoryError: PermGen space 错误。

新版:JDK 8 移除了永久代,引入了元空间。元空间使用的是本地内存(Native Memory),不再占用 JVM 的堆内存,从而大大减少了内存溢出的风险。

新的垃圾回收器:

G1 收集器:虽然 G1 在 JDK 7 中已引入,但在 JDK 8 中得到了广泛应用和优化。它旨在替代 CMS 收集器,适合大内存、多核处理器的机器,能提供更可控的停顿时间。

其他重要特性与工具

特性 描述
Optional 类 一个容器对象,用于优雅地处理 null 值,强制开发者显式地处理空指针情况,从而避免 NullPointerException
Nashorn JavaScript 引擎 取代了旧的 Rhino 引擎,提供了在 JVM 上运行 JavaScript 代码的能力,且性能更好。
HashMap 优化 当链表长度超过阈值(默认为 8)时,链表会转换为红黑树,将查询时间从 O(n) 优化到 O(log n),极大地提升了极端情况下的性能。
Base64 支持 在标准库中内置了 Base64 编码和解码的支持,不再需要依赖第三方库。

总结

JDK 8 的核心在于“化繁为简”和“与时俱进”。

Lambda + Stream 让你的业务逻辑代码行数减少一半,且更具可读性。

java.time 解决了困扰 Java 开发者多年的日期处理痛点。

Metaspace 解决了运维层面的内存溢出隐患。

如果你正在使用或计划深入学习 Java,掌握 JDK 8 的这些特性(特别是 Lambda、Stream 和新日期 API)是绝对必要的基础。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐