1. 项目概述:为什么Java 8值得你“反复研读”?

如果你是一名Java开发者,或者正准备踏入这个领域,那么“Java 8”这四个字对你来说,绝对不是一个简单的版本号。它更像是一个分水岭,将Java的编程范式从传统的命令式、面向对象,悄然引入了函数式编程的广阔天地。我至今还记得第一次接触Java 8新特性时的震撼——原来代码可以写得如此简洁、优雅,原来处理集合数据可以像流水线一样清晰高效。然而,当时的学习过程也伴随着大量的困惑:Lambda表达式到底怎么用?Stream流操作顺序错了为什么结果天差地别?Optional真的能杜绝空指针吗?

市面上关于Java 8的教程很多,但要么是蜻蜓点水地罗列特性,要么是过于学术化地讲解理论,缺少将特性串联起来、并结合真实开发场景与“坑点”的系统性解读。这正是我写下这篇“最全讲解”的初衷。它不是一份冰冷的API文档,而是一位老司机在踩过无数坑、重构过大量旧代码后,为你梳理出的一份实战指南。无论你是想应对面试中高频的“Java 8新特性”问题,还是希望在现有项目中优雅地引入新特性提升代码质量,甚至是单纯地好奇函数式编程的魅力,这篇文章都将为你提供一个从“知道”到“精通”的清晰路径。我会围绕Lambda、Stream、Optional、新日期API等核心,不仅告诉你“是什么”,更重点剖析“为什么”和“怎么用得好”,以及那些官方文档里不会写的“避坑指南”。

2. Java 8核心新特性深度解析

Java 8的变革是系统性的,它引入了一系列相互关联的特性,共同构建了新的编程模型。理解这些特性背后的设计思想,比单纯记忆语法更重要。

2.1 Lambda表达式:从匿名内部类到行为参数化

Lambda表达式的本质,是一个可传递的匿名函数。它没有名称,但有参数列表、函数主体、返回类型,可能还有可以抛出的异常列表。它的出现,极大地简化了匿名内部类的书写。

语法与演变 最经典的例子莫过于线程创建。Java 8之前:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
}).start();

使用Lambda后:

new Thread(() -> System.out.println("Hello World")).start();

() 对应 run 方法的参数列表(无参数), -> 是Lambda操作符, System.out.println(“Hello World”) 是函数主体。当主体只有一条语句时,花括号和return关键字(如果有返回值)都可以省略。

函数式接口:Lambda的类型归属 Lambda表达式需要“类型”,这个类型就是函数式接口——一个仅包含一个抽象方法的接口。Java 8专门在 java.util.function 包中提供了四大核心函数式接口,理解它们是掌握Lambda的关键:

  1. Consumer<T> :消费型接口 。接受一个参数,无返回。 void accept(T t) 。常用于遍历、打印等消费操作。
    List<String> list = Arrays.asList("A", "B", "C");
    list.forEach(str -> System.out.println(str)); // forEach 接受一个Consumer
    
  2. Supplier<T> :供给型接口 。无参数,返回一个结果。 T get() 。常用于对象生成、懒加载。
    Supplier<LocalDate> dateSupplier = () -> LocalDate.now();
    LocalDate today = dateSupplier.get();
    
  3. Function<T, R> :函数型接口 。接受一个参数,返回一个结果。 R apply(T t) 。最常用的转换接口。
    Function<String, Integer> strToLength = str -> str.length();
    Integer length = strToLength.apply("Hello");
    
  4. Predicate<T> :断言型接口 。接受一个参数,返回布尔值。 boolean test(T t) 。用于条件判断、过滤。
    Predicate<Integer> isEven = num -> num % 2 == 0;
    boolean test = isEven.test(4); // true
    

方法引用与构造器引用:再次简化 当Lambda体仅仅是调用一个已有方法时,可以用方法引用进一步简化。其本质是Lambda的语法糖。

  • 静态方法引用 ClassName::staticMethod
  • 实例方法引用 instance::method ClassName::method (非静态,第一个参数作为方法调用者)
  • 构造器引用 ClassName::new

例如:

// Lambda
Function<String, Integer> parser = s -> Integer.parseInt(s);
// 方法引用
Function<String, Integer> parser = Integer::parseInt;

List<String> names = Arrays.asList("Alice", "Bob");
// Lambda
names.forEach(s -> System.out.println(s));
// 方法引用
names.forEach(System.out::println);

实操心得 :不要为了用Lambda而用Lambda。在简单的循环体(例如仅一行调用)中,方法引用往往比Lambda更清晰。但在逻辑稍复杂(包含条件判断或简单运算)时,Lambda的表达能力更强。优先考虑代码的可读性。

2.2 Stream API:声明式的数据处理流水线

Stream(流)是Java 8处理集合数据的抽象概念。它允许你以声明式的方式,对数据集合进行复杂的查询、筛选、转换、归约等操作。其核心思想是“做什么”,而不是“怎么做”。

流的构成与操作分类 一个流的使用包含三个步骤: 创建流 -> 中间操作 -> 终端操作

  • 创建流 :从集合、数组、I/O通道、生成器函数等创建。
    // 从集合创建
    List<String> list = new ArrayList<>();
    Stream<String> stream1 = list.stream();
    // 从数组创建
    Stream<String> stream2 = Arrays.stream(new String[]{"A", "B", "C"});
    // 使用Stream.of
    Stream<String> stream3 = Stream.of("A", "B", "C");
    // 生成无限流
    Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2);
    
  • 中间操作 :返回一个新的Stream,可以连续调用。这些操作是 惰性求值 的,即在终端操作被调用前,中间操作不会执行任何实际处理。常见的有:
    • filter(Predicate) :过滤
    • map(Function) :元素转换
    • distinct() :去重
    • sorted() :排序
    • limit(long) :截断
    • skip(long) :跳过
  • 终端操作 :触发流的遍历并产生结果或副作用。一个流有且只能有一个终端操作,执行后流被消耗,不可再用。常见的有:
    • forEach(Consumer) :遍历消费
    • count() :计数
    • collect(Collector) :将流转换为集合或其他形式(最常用、最强大)
    • reduce(...) :归约,将流元素组合成一个结果
    • anyMatch(Predicate) allMatch noneMatch :短路匹配

核心操作详解与组合 map filter 是最常用的组合。例如,从一个字符串列表中,找出所有长度大于3的字符串,并将其转换为大写:

List<String> result = list.stream()
                          .filter(s -> s.length() > 3) // 过滤
                          .map(String::toUpperCase)    // 转换
                          .collect(Collectors.toList()); // 收集为List

flatMap 用于将流中的每个元素转换为一个流,然后把所有流连接起来。这在处理嵌套集合时非常有用:

List<List<String>> nestedList = ...;
List<String> flatList = nestedList.stream()
                                  .flatMap(Collection::stream) // 将每个List<String>展平为String流
                                  .collect(Collectors.toList());

收集器(Collectors):流的终点站 Collectors 工具类提供了大量静态工厂方法,用于生成常见的收集器,将流规约成汇总结果。

  • toList() toSet() toCollection(Supplier) :收集到集合。
  • toMap(Function, Function) :收集到Map。
  • joining() :连接字符串。
  • groupingBy(Function) :分组,返回 Map<K, List<T>>
  • partitioningBy(Predicate) :分区(分为true和false两组)。
  • summarizingInt(ToIntFunction) :统计汇总(数量、总和、最小值、最大值、平均值)。

分组操作的威力示例:按部门对员工分组。

Map<String, List<Employee>> employeesByDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

注意事项 :Stream流一旦被终端操作消费,就不能再被使用。试图再次使用会抛出 IllegalStateException 。因此,如果你需要对同一数据源进行多次不同的终端操作,应该每次都重新创建流。另外,并行流( parallelStream() )并非银弹,它适用于数据量大、且任务可独立并行处理的场景,但会引入线程安全、顺序不确定性等额外复杂度,使用前需仔细评估。

2.3 Optional:优雅的空值防御

NullPointerException 是Java程序中最常见的异常之一。 Optional<T> 是一个容器对象,它可能包含一个非空的T类型值,也可能什么都不包含(为空)。它的设计目的是 强制调用者显式地处理值可能缺失的情况 ,而不是隐式地假设它存在。

创建Optional对象

  • Optional.of(T value) :创建一个包含非空值的Optional。 如果传入null,会立即抛出NullPointerException 。用于你明确知道值不为null的场景。
  • Optional.ofNullable(T value) :创建一个Optional,value可以为null。这是最常用的工厂方法。
  • Optional.empty() :创建一个空的Optional实例。

使用Optional而非直接判空 传统方式:

public String getUppercaseName(User user) {
    if (user != null) {
        String name = user.getName();
        if (name != null) {
            return name.toUpperCase();
        }
    }
    return "DEFAULT";
}

使用Optional后:

public String getUppercaseName(User user) {
    return Optional.ofNullable(user)
                   .map(User::getName)
                   .map(String::toUpperCase)
                   .orElse("DEFAULT");
}

链式调用 map 方法,如果前一步结果为 Optional.empty() ,则后续 map 不会执行,直接返回 empty orElse 提供默认值。

核心方法解析

  • isPresent() :判断值是否存在。 但应尽量避免直接使用 ,因为它又回到了命令式判空的老路。
  • ifPresent(Consumer) :如果值存在,则执行给定的消费操作。
  • orElse(T other) :值存在则返回值,否则返回 other
  • orElseGet(Supplier) :值存在则返回值,否则由Supplier函数生成一个。 惰性求值 ,推荐在构造默认值成本较高时使用。
  • orElseThrow(Supplier) :值存在则返回值,否则抛出由Supplier创建的异常。
  • flatMap(Function) :与 map 类似,但映射函数返回的本身就是一个Optional对象,用于避免Optional的嵌套( Optional<Optional<T>> )。

避坑指南 Optional 的初衷是作为 返回类型 ,用于明确告知调用者方法可能没有返回值,迫使调用者处理空情况。它 不应被用作类的字段类型 ,也不应作为方法参数类型(这会让API变得笨拙)。另外,不要用 Optional 去包装集合,一个空的集合(如 Collections.emptyList() )本身就是完美的“无值”表示,比 Optional<List> 更合适。

2.4 新的日期时间API:告别混乱的Date和Calendar

旧的 java.util.Date java.util.Calendar API饱受诟病:可变性、糟糕的月份设计(一月是0)、非线程安全等。Java 8在 java.time 包下引入了全新的日期时间API,其设计清晰、线程安全、且遵循ISO-8601标准。

核心类介绍

  1. LocalDate LocalTime LocalDateTime :表示不带时区的日期、时间、日期时间。它们是不可变对象,也是日常使用最频繁的类。
    LocalDate today = LocalDate.now(); // 当前日期,如 2023-10-27
    LocalDate specificDate = LocalDate.of(2023, Month.OCTOBER, 27);
    LocalTime now = LocalTime.now(); // 当前时间,如 14:30:00.123
    LocalDateTime currentDateTime = LocalDateTime.now(); // 2023-10-27T14:30:00.123
    
  2. Instant :时间戳。表示自1970-01-01T00:00:00Z(UTC)开始的纳秒数。用于机器时间。
  3. Duration Period Duration 用于计算两个 Instant LocalTime 之间的时间量(基于时间)。 Period 用于计算两个 LocalDate 之间的时间量(基于日期)。
    Duration betweenTime = Duration.between(startTime, endTime);
    Period betweenDate = Period.between(startDate, endDate);
    
  4. ZonedDateTime :带时区的日期时间。
    ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
    

格式化与解析 使用 DateTimeFormatter 替代旧的 SimpleDateFormat ,它是线程安全的。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.now();
String formatted = dateTime.format(formatter); // 格式化
LocalDateTime parsed = LocalDateTime.parse("2023-10-27 14:30:00", formatter); // 解析

实操心得 :在涉及跨时区的业务(如全球化应用、航班时刻)时,务必使用 ZonedDateTime 。而在仅需要记录一个确定的时刻点(如日志时间戳、数据创建时间)时,通常使用 LocalDateTime 就足够了,并约定好系统默认时区(如UTC或东八区)。将 LocalDateTime 存入数据库时,对应的字段类型通常是 TIMESTAMP (不带时区)或 DATETIME

3. 其他重要特性与底层优化

除了上述四大核心,Java 8还有一些深刻影响开发效率和性能的特性。

3.1 接口的默认方法与静态方法

在Java 8之前,接口只能有抽象方法。这导致了一个问题:如果想给一个广泛使用的接口(如 Collection )添加新方法,所有实现类都必须实现它,否则会编译失败,这在实际中是灾难性的。

默认方法(Default Methods) 使用 default 关键字,可以在接口中提供方法的默认实现。实现类可以继承它,也可以覆盖它。

public interface Vehicle {
    void run();
    default void honk() {
        System.out.println("Beep beep!");
    }
}

这使得接口的演化成为可能。Java 8的 Collection 接口就添加了 stream() parallelStream() 等默认方法,而所有现有实现类无需修改就能获得这些新功能。

静态方法(Static Methods) 接口中也可以定义静态方法,通常作为该接口的通用工具方法。

public interface MathUtils {
    static int add(int a, int b) {
        return a + b;
    }
}
// 调用
int sum = MathUtils.add(5, 3);

多重继承的冲突解决 如果一个类实现了两个接口,而这两个接口有同名的默认方法,则实现类必须通过覆盖该方法来解决冲突,可以选择调用某个父接口的默认方法:

interface A { default void foo() { System.out.println("A"); } }
interface B { default void foo() { System.out.println("B"); } }
class C implements A, B {
    @Override
    public void foo() {
        A.super.foo(); // 显式选择调用A接口的默认实现
    }
}

3.2 方法引用与构造器引用进阶

除了之前提到的简单形式,方法引用在处理构造函数和数组时也有妙用。

数组引用 Type[]::new

Function<Integer, String[]> arrayGenerator = String[]::new;
String[] arr = arrayGenerator.apply(10); // 创建一个长度为10的String数组

带参数的构造器引用 :需要配合函数式接口使用。例如, Function<Integer, MyClass> 可以指向 MyClass 的接受一个Integer参数的构造函数。

public class MyClass {
    public MyClass(Integer id) { /* ... */ }
}
Function<Integer, MyClass> constructorRef = MyClass::new;
MyClass obj = constructorRef.apply(123);

3.3 并行流与性能考量

通过 Collection.parallelStream() Stream.parallel() 可以获取一个并行流。Fork/Join框架会在底层将流元素拆分到多个线程上处理,最后合并结果。

适用场景

  • 数据量非常大(例如,超过十万级)。
  • 处理每个元素的成本较高(CPU密集型计算)。
  • 数据源易于拆分(如 ArrayList ),结果顺序无关紧要或易于合并。

陷阱与注意事项

  1. 线程安全 :传递给流操作的函数(如 filter map 中的Lambda)必须是 无状态 的,且不干扰外部可变状态。
  2. 性能开销 :并行化本身有开销(线程创建、通信、结果合并)。对于小数据量,串行流往往更快。
  3. 顺序依赖 findFirst limit 等依赖于顺序的操作在并行流中性能可能下降。
  4. 共享资源竞争 :避免在并行流中修改共享的集合(如 ArrayList ),应使用线程安全的收集器或规约操作。

一个简单的性能对比示例(计算大整数列表的和):

List<Long> numbers = LongStream.rangeClosed(1, 10_000_000L)
                               .boxed()
                               .collect(Collectors.toList());

long start = System.currentTimeMillis();
long sum1 = numbers.stream().reduce(0L, Long::sum); // 串行
long end = System.currentTimeMillis();
System.out.println("串行耗时: " + (end - start) + "ms");

start = System.currentTimeMillis();
long sum2 = numbers.parallelStream().reduce(0L, Long::sum); // 并行
end = System.currentTimeMillis();
System.out.println("并行耗时: " + (end - start) + "ms");

在我的测试环境(8核)下,并行版本通常有数倍的加速,但具体效果需实测。

4. 实战应用:用Java 8重构常见代码模式

理论最终要服务于实践。让我们看看如何用Java 8的特性,让日常代码脱胎换骨。

4.1 集合处理的重构

场景一:过滤并收集符合条件的对象 旧模式:

List<Employee> developers = new ArrayList<>();
for (Employee emp : allEmployees) {
    if ("研发部".equals(emp.getDepartment()) && emp.getSalary() > 10000) {
        developers.add(emp);
    }
}

新模式:

List<Employee> developers = allEmployees.stream()
    .filter(e -> "研发部".equals(e.getDepartment()))
    .filter(e -> e.getSalary() > 10000)
    .collect(Collectors.toList());

清晰地将“过滤条件”声明出来,链式调用易于阅读和修改。

场景二:将对象列表转换为Map(ID -> 对象) 旧模式:

Map<Long, Employee> employeeMap = new HashMap<>();
for (Employee emp : allEmployees) {
    employeeMap.put(emp.getId(), emp);
}

新模式:

Map<Long, Employee> employeeMap = allEmployees.stream()
    .collect(Collectors.toMap(Employee::getId, Function.identity()));

一行代码搞定,且通过 toMap 可以处理键冲突(提供第三个参数 mergeFunction )。

场景三:复杂分组与聚合 需求:按部门分组,并计算每个部门的平均工资。 旧模式:需要手动创建Map,遍历列表,为每个部门维护一个工资列表和计数,最后计算平均。 新模式:

Map<String, Double> avgSalaryByDept = allEmployees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

groupingBy 结合下游收集器 averagingDouble ,逻辑表达极其简洁。

4.2 空值安全链式调用

使用 Optional map / flatMap 可以构建安全的深层属性访问链,避免多层 if-null 检查。 假设有 User -> Address -> City 这样的嵌套关系,要安全地获取城市名。 旧模式:

String cityName = "未知";
if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        City city = address.getCity();
        if (city != null) {
            cityName = city.getName();
        }
    }
}

新模式:

String cityName = Optional.ofNullable(user)
                         .map(User::getAddress)
                         .map(Address::getCity)
                         .map(City::getName)
                         .orElse("未知");

代码变成线性的、声明式的,空值检查被隐藏在了 Optional 的机制里。

4.3 资源管理与Try-with-Resources的增强

Java 7引入了 try-with-resources 来自动关闭实现了 AutoCloseable 的资源。Java 8允许在 try 语句中声明最终变量(effectively final),使得在Lambda表达式内部使用资源对象更方便。

// Java 7/8
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    // br 是 effectively final,可以在Lambda中使用
    List<String> lines = br.lines().collect(Collectors.toList());
    // 处理lines
} catch (IOException e) {
    // 处理异常
}

这里 br.lines() 返回一个 Stream<String> ,它同样需要被及时消费(在try块内)。流本身不是资源,但底层的 BufferedReader 会在try块结束时自动关闭。

5. 性能调优、常见陷阱与最佳实践

拥抱新特性的同时,也需要了解其背后的成本与限制。

5.1 Stream API的性能考量

  1. 短路操作优先 anyMatch findFirst findAny limit 是短路操作。在可能的情况下,尽早使用它们可以避免处理整个流。例如,检查列表中是否有大于100的数:
    // 好:找到第一个就停止
    boolean hasLarge = numbers.stream().anyMatch(n -> n > 100);
    // 不够好:会处理所有元素
    boolean hasLarge = numbers.stream().filter(n -> n > 100).findFirst().isPresent();
    
  2. 避免在流中执行副作用 :流操作(尤其是并行流)应专注于转换和计算,避免修改外部状态(如修改集合、写入文件)。这违反了函数式编程的无状态原则,并可能导致线程安全问题或难以调试的行为。
  3. 原始类型流 :对于 int long double ,使用 IntStream LongStream DoubleStream 可以避免装箱/拆箱开销,提升性能。
    // 有装箱开销
    int sum = list.stream().mapToInt(Integer::intValue).sum();
    // 更优:直接使用IntStream
    int sum = IntStream.rangeClosed(1, 100).sum();
    

5.2 Optional的误用与正用

误用示例

// 错误:用Optional.isPresent()又回到命令式风格
Optional<User> userOpt = findUserById(id);
if (userOpt.isPresent()) {
    return userOpt.get().getName();
} else {
    return "Unknown";
}
// 正确:使用函数式风格
return findUserById(id).map(User::getName).orElse("Unknown");

// 错误:将Optional作为方法参数,使调用方代码冗长
public void process(Optional<String> data) { ... }
process(Optional.ofNullable(someString)); // 调用方很别扭
// 正确:方法参数应为普通类型,在方法内部处理null
public void process(String data) {
    Optional.ofNullable(data).ifPresent(...);
}

最佳实践

  • 作为返回类型 :这是 Optional 最核心的用途。
  • 避免使用 get() :尽量使用 orElse orElseGet orElseThrow ifPresent 来安全地获取值。
  • 不要用它包装集合或数组 :空集合本身已是很好的表示。
  • 谨慎用于字段 :这会增加序列化的复杂性,且通常有更好的设计替代(如空对象模式)。

5.3 日期时间API的序列化与数据库映射

在Web应用和持久化层中使用新的日期时间API时,需要注意兼容性。

JSON序列化(如Jackson) : 默认情况下,Jackson可能无法正确序列化/反序列化 java.time 对象。需要添加相关模块:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

并在 ObjectMapper 中注册模块:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// 禁用将日期写为时间戳,按字符串格式输出
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

数据库映射(如JPA/Hibernate) : 对于 LocalDate LocalDateTime 等类型,现代版本的Hibernate(5.2+)通常能自动映射到合适的SQL类型( DATE TIMESTAMP )。如果使用旧版本或需要自定义,可以使用 @Column 注解指定列类型,或使用属性转换器( AttributeConverter )。

5.4 调试Lambda和Stream

调试Lambda表达式和流管道可能比调试传统循环更困难,因为异常堆栈跟踪可能更长、更复杂,且IDE的逐步调试在流内部可能不直观。

调试技巧

  1. 使用 peek() 方法 :这是一个中间操作,接受一个 Consumer ,对流中的每个元素执行操作,同时将元素传递下去。常用于日志打印,观察流经管道的元素。
    List<String> result = list.stream()
        .filter(s -> s.length() > 3)
        .peek(s -> System.out.println("After filter: " + s)) // 调试点
        .map(String::toUpperCase)
        .peek(s -> System.out.println("After map: " + s)) // 调试点
        .collect(Collectors.toList());
    
  2. 将复杂的Lambda提取为方法 :如果一个Lambda表达式逻辑复杂,将其提取成一个命名清晰的私有方法,然后用方法引用替代。这不仅能提高可读性,也便于单独测试和调试该方法。
  3. 在IDE中设置断点 :现代IDE(如IntelliJ IDEA)对Stream调试的支持越来越好,可以在流操作行设置断点,并查看当时的流内容。

6. 总结与资源推荐

经过上面数万字的梳理,我们从Lambda的简洁、Stream的声明式力量、Optional的空安全哲学,到新日期API的严谨,再到接口演化和并行计算的考量,系统地遍历了Java 8的核心疆域。我个人的体会是,学习Java 8不是一个一蹴而就的事件,而是一个持续的过程。初期你可能会觉得语法陌生,但一旦你习惯了这种声明式的思考方式,就再也回不去了。代码会变得更清晰、更易维护,许多原本需要多行循环和临时变量的操作,现在可以用一行流畅的链式调用表达。

最后,再分享两个小技巧:一是,在团队中推广Java 8时,可以从代码审查入手,当看到传统的 for 循环和 if-null 检查时,可以友好地提出“这里是否可以用Stream和Optional来改写?”。二是,对于遗留的大型项目,不必强求一次性全部重构。可以采取“新人新代码,老人老代码”的策略,在新开发的模块或类中率先使用新特性,让团队成员逐渐感受到其好处,从而自发地进行渐进式重构。

如果你想进一步深入,我推荐阅读Brian Goetz的《Java并发编程实战》(虽然主要讲并发,但对理解函数式思想有帮助)和Richard Warburton的《Java 8实战》。官方文档永远是第一手资料,多翻看 java.util.function java.util.stream java.time 包的API文档,你会不断有新的发现。记住,最好的学习方式就是动手实践,尝试用Java 8的风格去重写你项目中的一小段代码,亲自体验它带来的改变。

更多推荐