JDK 1.8 新特性
预计阅读 17 分钟

JDK 1.8 新特性

JDK 1.8 新特性

一、抽象类回顾(作为铺垫)

在 JDK 1.8 之前,接口里只能声明方法,不能有方法实现。因此当我们想要提供一段"所有子类共享的默认逻辑"时,通常会借助于抽象类:把公共逻辑写成一个非抽象方法,子类直接调用即可,不必每个都重复实现。

Java
1// 定义抽象类:抽象方法强制子类实现,非抽象方法提供默认逻辑
2public abstract class AFormula {
3    // 抽象方法:求值逻辑交给子类
4    abstract double calculate(int a);
5
6    // 非抽象方法:所有子类共享,不必重复实现
7    // 平方根
8    double sqrt(int a) {
9        return Math.sqrt(a);
10    }
11}

使用时,通过匿名内部类给出 calculate 的具体实现,sqrt 则直接复用:

Java
1@Test
2public void test_00() {
3    AFormula aFormula = new AFormula() {
4        @Override
5        double calculate(int a) {
6            return a * a;   // 实现求平方
7        }
8    };
9    System.out.println(aFormula.calculate(2)); // 求平方: 4.0
10    System.out.println(aFormula.sqrt(2));      // 求开方: 1.4142135623730951
11}

可以看到,抽象类承担了"默认实现"的角色。但抽象类是单继承的,使用成本较高。JDK 8 的接口默认方法,正是为了把这种"默认实现"下放到接口里。


二、接口回顾:default 默认方法

JDK 1.8 起,接口不仅可以定义抽象方法,还可以用 default 关键字提供默认实现。这一个小小的改变,让整个抽象设计变得更灵活。

Java
1// 定义:default 关键字必须有,表示该方法带方法体(默认实现)
2public interface IFormula {
3    double calculate(int a);
4
5    // 平方根 —— 默认实现,实现类可直接复用
6    default double sqrt(int a) {
7        return Math.sqrt(a);
8    }
9}

方式一:匿名内部类

Java
1@Test
2public void test_01() {
3    IFormula formula = new IFormula() {
4        @Override
5        public double calculate(int a) {
6            return a * a;
7        }
8    };
9    System.out.println(formula.calculate(2)); // 4.0
10    System.out.println(formula.sqrt(2));      // 1.4142135623730951
11}

方式二:Lambda 表达式

由于 IFormula 只有一个抽象方法 calculate,可以用 Lambda 大幅简化:

Java
1@Test
2public void test_02() {
3    // a 是入参名称(可以改成任何名字),-> 后面是具体实现
4    // 注意:这样写省略了日志,如果需要加日志还是要用代码块写法
5    IFormula formula = a -> a * a;
6    System.out.println(formula.calculate(2)); // 4.0
7    System.out.println(formula.sqrt(2));      // 1.4142135623730951
8}

三、Lambda 表达式

接口中可以提供默认方法实现,本质上是为了简化开发。因此你会看到 ListSet 等接口里也新增了许多默认方法。Lambda 表达式则是配合函数式接口的一把利器。

先看一个排序的演进过程。准备数据:

Java
1List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

第 0 步:传统匿名内部类(JDK 8 之前)

Collections.sort 接收一个 List 和一个 Comparator。过去我们需要传一个匿名内部类:

Java
1Collections.sort(names, new Comparator<String>() {
2    @Override
3    public int compare(String a, String b) {
4        return b.compareTo(a); // 降序
5    }
6});

第 1 步:改写为 Lambda

Java
1Collections.sort(names, (String a, String b) -> {
2    return b.compareTo(a);
3});

第 2 步:去掉类型声明

java.util.List 已经新增了 sort 方法,而且编译器能根据上下文(目标类型)推断参数类型,所以连入参类型都可以省略:

Java
1names.sort((a, b) -> b.compareTo(a));

对应的 List.sort 源码就是一个默认方法:

Java
1// java.util.List.sort —— 接口里的 default 方法
2default void sort(Comparator<? super E> c) {
3    Object[] a = this.toArray();
4    Arrays.sort(a, (Comparator) c);
5    ListIterator<E> i = this.listIterator();
6    for (Object e : a) {
7        i.next();
8        i.set((E) e);
9    }
10}

第 3 步:直接使用现成的静态方法

得益于 Comparator 还提供了 static 方法(接口中除了 default,还可以有 static 方法),代码可以更短:

Java
1names.sort(Comparator.reverseOrder()); // 等价于上面的降序排序
2
3// java.util.Comparator.reverseOrder
4public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
5    return Collections.reverseOrder();
6}

💡 小结:同一个功能,从 7 行匿名内部类,简化到了一行方法调用。Lambda 的价值在于"把行为当数据传递",而不是替换所有匿名内部类。


四、函数式接口(Functional Interfaces)

官方定义

每个 Lambda 表达式都对应一个类型,这个类型由接口指定。所谓的函数式接口必须包含且仅包含一个抽象方法。该类型的每个 Lambda 表达式都会匹配到这个抽象方法上。由于 default 方法不是抽象方法,所以你可以自由地在函数式接口里添加 default 方法。

为什么只能有一个抽象方法?

JVM 之所以能做"类型推断",靠的就是"接口里只有一个抽象方法"这个约束——编译器要把 Lambda 的逻辑精准地映射到这个唯一的抽象方法上。如果有两个抽象方法,编译器就不知道该映射给谁了。

哪些方法不计入抽象方法数量?

  1. default 方法:带方法体的默认实现,不算抽象方法。
  2. static 方法:静态方法,不算抽象方法。
  3. Object 类公共方法的覆盖:接口里对 java.lang.Object 公共方法(如 equalshashCodetoString)的声明不算抽象方法,因为任何实现类最终都会从 Object 继承到实现。

@FunctionalInterface 注解的作用

为了明确地声明一个接口是函数式接口,建议加上 @FunctionalInterface 注解。它的作用是编译期校验:一旦你(或同事)无意中添加了第二个抽象方法,编译器会立刻报错。

当然,即使不加注解,只要接口满足"仅有一个抽象方法"的条件,它依然是函数式接口。注解只是起到"明确意图 + 编译期校验"的作用。

Comparator 为例

Comparator 在 Java 8 中被标记了 @FunctionalInterface。查看其源码会发现,它内部不仅有多个 default 方法(如 reversed())和静态方法(如 comparing()),甚至显式声明了 boolean equals(Object obj);。但:

  • default 方法和 static 方法不计入抽象方法数量;
  • equals 是对 Object 公共方法的覆盖,也不计入。

所以 Comparator 实际上只有 一个核心抽象方法 int compare(T o1, T o2),完全符合函数式接口的定义。排序时传入的 Lambda,JVM 就会自动映射到这个唯一的 compare 上。

自己写一个函数式接口

Java
1// 加上注解,编译器帮你盯着"只能有一个抽象方法"
2@FunctionalInterface
3public interface IConverter<F, T> {
4    T convert(F from);
5}

使用方式,逐步简化:

Java
1// 1. 传统匿名内部类写法
2IConverter<String, Integer> converter01 = new IConverter<String, Integer>() {
3    @Override
4    public Integer convert(String from) {
5        return Integer.valueOf(from);
6    }
7};
8
9// 2. Lambda 写法:保留参数括号和方法体大括号
10IConverter<String, Integer> converter02 = (from) -> {
11    return Integer.valueOf(from);
12};
13
14// 3. 进一步简化:单参数可省略括号,单行可省略 return 和大括号
15IConverter<String, Integer> converter03 = from -> Integer.valueOf(from);
16
17// 4. 方法引用(下文详解):连参数和 -> 都省了
18IConverter<Integer, String> converter04 = String::valueOf;

五、方法与构造函数的便捷引用(::

上文出现了 String::valueOf,这就是方法引用:: 关键字不只是能引用静态方法,还能引用实例方法、构造函数。

1. 引用普通实例方法

先定义一个普通类:

Java
1public class Something {
2    // 取字符串的第一个字符
3    public String startsWith(String s) {
4        return String.valueOf(s.charAt(0));
5    }
6
7    // 无参静态方法(供 Supplier 演示使用)
8    public static String test01() {
9        return "hi";
10    }
11
12    // 无参实例方法(供 Supplier 演示使用)
13    public String test02() {
14        return "hello";
15    }
16}

两种等价写法:

Java
1// 方式 A:直接用 Lambda 写出逻辑
2IConverter<String, String> converter01 = s -> String.valueOf(s.charAt(0));
3
4// 方式 B:通过方法引用复用已有方法
5// 引用的方法体里可以放更复杂的逻辑,而不只是一句话
6IConverter<String, String> converter02 = new Something()::startsWith;
7
8System.out.println(converter01.convert("Java")); // J
9System.out.println(converter02.convert("Java")); // J

2. 引用构造函数

先定义 Person

Java
1public class Person {
2    String firstName;
3    String lastName;
4
5    Person() {}
6
7    Person(String firstName, String lastName) {
8        this.firstName = firstName;
9        this.lastName = lastName;
10    }
11}

再定义一个工厂接口(注意:函数式接口里依然只能有一个抽象方法,否则报错):

Java
1@FunctionalInterface
2public interface IPersonFactory<P extends Person> {
3    P create(String firstName, String lastName);
4}

使用时,Person::new 让编译器根据方法签名自动选择匹配的构造函数:

Java
1// 等价于:(firstName, lastName) -> new Person(firstName, lastName);
2// 编译器会根据 create 的签名,自动选中带有两个参数的构造函数
3IPersonFactory<Person> personFactory = Person::new;
4Person person = personFactory.create("Peter", "Parker");
5// person.firstName = "Peter", person.lastName = "Parker"

💡 :: 的本质:它是 Lambda 的"极简版"——当 Lambda 体里只是调用一个已存在的方法时,就可以用方法引用代替。


六、Lambda 作用范围

Lambda 表达式访问外部变量(局部变量、成员变量、静态变量、接口默认方法)的方式,与匿名内部类非常相似,但有几个关键区别。

1. 访问局部变量

可以从外部读取最终局部变量

Java
1int num = 1;
2IConverter<Integer, String> stringConverter = from -> String.valueOf(from + num);
3String convert = stringConverter.convert(2);
4System.out.println(convert); // 3

但这个 num 在被 Lambda 引用后,就不能再被修改(effectively final 约束)。下面两种写法都会报错:

Java
1// ❌ 错误一:Lambda 引用后,外部又修改了变量
2int num = 1;
3IConverter<Integer, String> stringConverter = from -> String.valueOf(from + num);
4num = 3;
5// 报错:Variable used in lambda expression should be final or effectively final
6
7// ❌ 错误二:在 Lambda 内部修改变量也不允许
8int num = 1;
9IConverter<Integer, String> converter = from -> {
10    String value = String.valueOf(from + num);
11    num = 3; // 报错:同上
12    return value;
13};

为什么必须是 final? 因为 Lambda 可能在另一个线程、另一个时刻执行(比如 Stream 并行流),如果允许修改捕获的局部变量,就会出现"读到的是哪一时刻的值"这种并发可见性问题。所以 Java 干脆要求它不可变。

2. 访问成员变量和静态变量

与局部变量不同,Lambda 对成员变量和静态变量拥有完整的读写权限(因为它们是通过对象实例/类本身访问的,不存在"哪一时刻快照"的问题):

Java
1public class Lambda4 {
2    // 静态变量
3    static int outerStaticNum;
4
5    // 成员变量
6    int outerNum;
7
8    void testScopes() {
9        IConverter<Integer, String> stringConverter1 = from -> {
10            outerNum = 23;        // ✅ 可写成员变量
11            return String.valueOf(from);
12        };
13
14        IConverter<Integer, String> stringConverter2 = from -> {
15            outerStaticNum = 72;  // ✅ 可写静态变量
16            return String.valueOf(from);
17        };
18    }
19}

3. 访问接口默认方法(Lambda 与匿名内部类的关键区别)

在接口中定义带默认实现的方法时,匿名内部类与 Lambda 对默认方法的访问权限存在本质区别

匿名内部类可以正常访问默认方法(因为它的 this 指向自身实例):

Java
1public interface IFormula {
2    double calculate(int a);
3
4    default double sqrt(int a) {
5        return Math.sqrt(a);
6    }
7}
8
9IFormula formula = new IFormula() {
10    @Override
11    public double calculate(int a) {
12        // ✅ 编译通过:匿名内部类持有自身 this,可直接调用继承来的默认方法
13        return sqrt(a * a);
14    }
15};

但 Lambda 表达式无法访问接口的默认方法

Java
1// ❌ 编译报错
2IFormula formula = a -> sqrt(a * a);

原因:Lambda 不创建新的作用域,它内部的 this 指向的是外部宿主类,而不是接口实现类。编译器会在外部类中查找 sqrt 方法,找不到自然无法通过编译。

🧠 结论:默认方法只能通过"实现了接口的匿名内部类"或"实现类对象"来调用,不能直接在 Lambda 里访问。Lambda 只关心如何实现那个唯一的抽象方法,并不继承接口的默认方法。


七、内置的函数式接口

JDK 1.8 API 内置了大量函数式接口。除了我们早就熟悉的 ComparatorRunnable(Java 8 都给它们加了 @FunctionalInterface 注解),还新增了一批工具型接口,其中很多借鉴自 Google Guava 库。

下面介绍最常用的几个。

1. Predicate 断言

Predicate<T> 接收一个入参,返回 boolean。它内部提供了组合方法(andornegate),可以拼装复杂的判断逻辑。

Java
1@Test
2public void test11() {
3    Predicate<String> predicate = s -> s.length() > 0;
4
5    boolean foo0 = predicate.test("foo");             // true
6    boolean foo1 = predicate.negate().test("foo");    // false(negate 相当于取反 !)
7
8    Predicate<Boolean> nonNull = Objects::nonNull;     // 判非空
9    Predicate<Boolean> isNull = Objects::isNull;       // 判空
10    Predicate<String> isEmpty = String::isEmpty;       // 判空串
11    Predicate<String> isNotEmpty = isEmpty.negate();   // 判非空串
12}

2. Function 函数

Function<T, R> 接收一个"原料" T,生产一个"产品" R。它提供了 compose(先执行参数函数)、andThen(后执行参数函数)来串行处理:

Java
1@Test
2public void test12() {
3    Function<String, Integer> toInteger = Integer::valueOf;                       // String -> Integer
4    Function<String, String> backToString = toInteger.andThen(String::valueOf);   // String -> Integer -> String
5    Function<String, String> afterToStartsWith = backToString.andThen(new Something()::startsWith); // 再截取第一位
6
7    // 执行链路:"123" -> 123 -> "123" -> "1"(取第一位字符)
8    String apply = afterToStartsWith.apply("123");
9    System.out.println(apply); // "1"
10}

3. Supplier 生产者

Supplier<T> 不接收入参,直接生产一个结果,类似生产者模式(工厂、惰性求值常用):

Java
1@Test
2public void test13() {
3    // 引用构造函数:每次 get 都 new 一个 Person
4    Supplier<Person> personSupplier0 = Person::new;
5    personSupplier0.get();
6
7    // 引用无参静态方法(Something.test01 见上文定义)
8    Supplier<String> personSupplier1 = Something::test01;
9    System.out.println(personSupplier1.get()); // hi
10
11    // 引用无参实例方法:先 new 出实例,再绑定其方法
12    Supplier<String> personSupplier2 = new Something()::test02;
13    System.out.println(personSupplier2.get()); // hello
14}

4. Consumer 消费者

Consumer<T> 需要提供入参,被"消费"掉(通常有副作用,比如打印、写库),没有返回值:

Java
1@Test
2public void test14() {
3    // 参照物:匿名内部类写法
4    Consumer<Person> greeter01 = new Consumer<Person>() {
5        @Override
6        public void accept(Person p) {
7            System.out.println("Hello, " + p.firstName);
8        }
9    };
10
11    // Lambda 简化写法
12    Consumer<Person> greeter02 = p -> System.out.println("Hello, " + p.firstName);
13    greeter02.accept(new Person("Luke", "Skywalker")); // Hello, Luke
14
15    // 实际开发更常见的姿势:引用已有类的方法
16    Consumer<Person> greeter03 = new MyConsumer<Person>()::accept;
17    greeter03.accept(new Person("Luke", "Skywalker")); // Hello, Luke
18}
19
20// 配套定义:MyConsumer(实际开发中可以把逻辑写在这里)
21class MyConsumer<T> implements Consumer<T> {
22    @Override
23    public void accept(T t) {
24        if (t instanceof Person) {
25            System.out.println("Hello, " + ((Person) t).firstName);
26        }
27    }
28}

5. Comparator 比较器

Comparator 在 Java 8 中除了升级为函数式接口,还新增了 comparingreversed 等默认/静态方法:

Java
1@Test
2public void test15() {
3    // 先定义参与比较的两个对象
4    Person p1 = new Person("Bob", "Builder");
5    Person p2 = new Person("Alice", "Wonderland");
6
7    // 方式 A:直接用 Lambda 写比较逻辑
8    Comparator<Person> comparator01 = (a, b) -> a.firstName.compareTo(b.firstName);
9    // 方式 B:等价写法,用 comparing 提取比较键
10    Comparator<Person> comparator02 = Comparator.comparing(p -> p.firstName);
11
12    System.out.println(comparator01.compare(p1, p2));              // > 0(Bob > Alice)
13    System.out.println(comparator02.reversed().compare(p1, p2));   // < 0(反转后 Bob < Alice)
14}

6. Optional 容器

首先澄清:Optional 不是函数式接口。它被设计出来的目的是防范 NullPointerException。可以把它看作"包装对象(可能是 null,也可能非 null)的容器"。当一个方法返回的对象可能为空时,推荐用 Optional 包装。

Java
1@Test
2public void test16() {
3    Optional<String> optional = Optional.of("bam");
4
5    System.out.println(optional.isPresent());                 // true(是否有值)
6    System.out.println(optional.get());                       // "bam"(取值,为空会抛异常)
7    System.out.println(optional.orElse("fallback"));          // "bam"(有值取值,无值取默认值)
8    optional.ifPresent(s -> System.out.println(s.charAt(0))); // "b"(有值才执行)
9
10    Optional<Person> optionalPerson = Optional.of(new Person());
11    optionalPerson.ifPresent(s -> System.out.println(s.firstName)); // null(Person 无参构造,firstName 为 null)
12}

💡 记忆口诀:Predicate 断言(出 boolean)、Function 转换(T→R)、Supplier 供给(无参→T)、Consumer 消费(T→void)。


八、Stream 流

什么是 Stream 流?

简单说,java.util.Stream 让我们能够对一个包含一个或多个元素的集合做各种操作。这些操作分为两类:

  • 中间操作:返回一个新的 Stream,可以继续链式调用(如 filtermapsorted)。
  • 终端操作:返回一个结果或副作用,流到此结束(如 forEachcountreduce)。

三个要点:

  1. 只能对实现了 java.util.Collection 接口的类做流操作。
  2. Stream 流既支持顺序执行,也支持并发执行(并行流)。
  3. Map 本身不支持 Stream,但它的 keySetvaluesentrySet 是支持的。

下面示例统一使用这份数据:

Java
1List<String> stringCollection = new ArrayList<>();
2stringCollection.add("ddd2");
3stringCollection.add("aaa2");
4stringCollection.add("bbb1");
5stringCollection.add("aaa1");
6stringCollection.add("bbb3");
7stringCollection.add("ccc");
8stringCollection.add("bbb2");
9stringCollection.add("ddd1");

1. Filter 过滤

filter 的入参是一个 Predicate,用来筛选出符合条件的元素。它返回的还是一个 Stream,再用 forEach 终端操作打印:

Java
1stringCollection
2        .stream()
3        .filter(s -> s.startsWith("a"))
4        .forEach(System.out::println);
5// 输出:aaa2、aaa1

2. Sorted 排序

sorted 同样是中间操作,返回一个 Stream。可以传入 Comparator 自定义排序,不传则用默认排序规则:

Java
1stringCollection
2        .stream()
3        .sorted()
4        .filter(s -> s.startsWith("a"))
5        .forEach(System.out::println);
6// 输出:aaa1、aaa2
7
8// ⚠️ 注意:sorted 只是产生一个"排序后的视图"用于本次计算,
9// 并不会修改原 List 的数据顺序。
10System.out.println(stringCollection);
11// 仍是原始顺序:ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

3. Map 转换

中间操作 map 通过给定的函数,把每个元素转换成另一个对象。下面的例子把每个字符串转成大写:

Java
1stringCollection
2        .stream()
3        .map(String::toUpperCase)
4        .sorted(Comparator.reverseOrder())
5        .forEach(System.out::println);
6// 输出全部元素的大写形式,按降序排列

💡 这个操作在工程中非常常用,比如 DTO(数据传输对象)与 DO(领域对象)之间的互相转换。

4. Match 匹配

match 用于匹配判断,返回 boolean。有三个变体:anyMatchallMatchnoneMatch

Java
1// anyMatch:只要有任意一个以 a 开头就返回 true
2boolean anyStartsWithA = stringCollection.stream().anyMatch(s -> s.startsWith("a"));
3System.out.println(anyStartsWithA); // true
4
5// allMatch:是否全部都以 a 开头
6boolean allStartsWithA = stringCollection.stream().allMatch(s -> s.startsWith("a"));
7System.out.println(allStartsWithA); // false
8
9// noneMatch:是否全都不以 z 开头
10boolean noneStartsWithZ = stringCollection.stream().noneMatch(s -> s.startsWith("z"));
11System.out.println(noneStartsWithZ); // true

5. Count 计数

count 是终端操作,统计 Stream 中的元素总数,返回 long

Java
1long startsWithB = stringCollection.stream().filter(s -> s.startsWith("b")).count();
2System.out.println(startsWithB); // 3

6. Reduce 归约

reduce 通过入参的函数,把整个列表归约成一个值,返回 Optional

Java
1Optional<String> reduced = stringCollection
2        .stream()
3        .sorted()
4        .reduce((s1, s2) -> s1 + "#" + s2);
5reduced.ifPresent(System.out::println);
6// 输出:aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2

7. Parallel-Streams 并行流

流可以是顺序的,也可以是并行的。顺序流在单个线程上执行,并行流在多个线程上并发执行。对于 CPU 密集型的大数据量操作,并行流可以显著提升性能。

顺序流排序

Java
1@Test
2public void test23() {
3    int max = 1_000_000;
4    List<String> values = new ArrayList<>(max);
5    for (int i = 0; i < max; i++) {
6        values.add(UUID.randomUUID().toString());
7    }
8
9    long t0 = System.nanoTime();
10    long count = values.stream().sorted().count();
11    System.out.println(count);
12    long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t0);
13    System.out.println(String.format("顺序流排序耗时: %d ms", millis));
14    // 参考耗时: 712 ms(视机器配置而定)
15}

并行流排序

Java
1@Test
2public void test24() {
3    int max = 1_000_000;
4    List<String> values = new ArrayList<>(max);
5    for (int i = 0; i < max; i++) {
6        values.add(UUID.randomUUID().toString());
7    }
8
9    long t0 = System.nanoTime();
10    long count = values.parallelStream().sorted().count(); // 仅这里不同
11    System.out.println(count);
12    long millis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t0);
13    System.out.println(String.format("并行流排序耗时: %d ms", millis));
14    // 参考耗时: 385 ms(比顺序流快约 50%)
15}

两段代码几乎相同,只把 stream() 换成 parallelStream(),并行排序大约快 50%。 ⚠️ 注意:并行流不总是更快。数据量小、任务有锁竞争或依赖顺序时,反而可能更慢,需要按实际场景取舍。


九、Map 集合

如前所述,Map 不支持 Stream(因为 Map 接口没有定义 stream() 方法)。但可以对它的 key、value、entry 使用流操作:map.keySet().stream()map.values().stream()map.entrySet().stream()

此外,JDK 8 还为 Map 增加了一系列便利方法。

1. putIfAbsent 与 forEach

Java
1@Test
2public void test25() {
3    Map<Integer, String> map = new HashMap<>();
4    for (int i = 0; i < 10; i++) {
5        // putIfAbsent:key 不存在时才放入;存在则直接返回原 value
6        // 省去了旧写法里 "if (map.get(i) == null) continue" 的判断
7        map.putIfAbsent(i, "val" + i);
8    }
9    // forEach:非常方便的遍历方式
10    map.forEach((key, value) -> System.out.println(value));
11}

2. 用 Stream 做 Map → 对象转换

配合上文定义的 BeanABeanB

Java
1// 数据源对象
2public class BeanA {
3    private int id;
4    private String name;
5    private int age;
6    private String idCard;
7
8    public BeanA(int id, String name, int age, String idCard) {
9        this.id = id; this.name = name; this.age = age; this.idCard = idCard;
10    }
11    public String getName() { return name; }
12    public int getAge() { return age; }
13}
14
15// 目标对象(DTO)
16public class BeanB {
17    private String name;
18    private int age;
19
20    public BeanB(String name, int age) {
21        this.name = name; this.age = age;
22    }
23
24    @Override
25    public String toString() {
26        return "BeanB{name='" + name + "', age=" + age + "}";
27    }
28}

转换过程(两种等价写法):

Java
1@Test
2public void test26() {
3    Map<Integer, BeanA> map = new HashMap<>();
4    for (int i = 0; i < 10; i++) {
5        map.putIfAbsent(i, new BeanA(i, "明明" + i, i + 20, "89021839021830912809" + i));
6    }
7
8    // 写法 A:匿名内部类(便于理解 map 的入参类型)
9    Stream<BeanB> beanBStream00 = map.values().stream()
10            .map(new Function<BeanA, BeanB>() {
11                @Override
12                public BeanB apply(BeanA beanA) {
13                    return new BeanB(beanA.getName(), beanA.getAge());
14                }
15            });
16
17    // 写法 B:Lambda 简化版(实际开发推荐)
18    Stream<BeanB> beanBStream01 = map.values().stream()
19            .map(beanA -> new BeanB(beanA.getName(), beanA.getAge()));
20
21    beanBStream01.forEach(System.out::println);
22}

3. computeIfPresent / computeIfAbsent

对某个 key 的值做条件化操作:

Java
1@Test
2public void test27() {
3    // 先准备一份基础数据(沿用 test25 的填充方式)
4    Map<Integer, String> map = new HashMap<>();
5    for (int i = 0; i < 10; i++) {
6        map.putIfAbsent(i, "val" + i);
7    }
8
9    // computeIfPresent:key 存在时才执行函数,并用返回值替换 value
10    map.computeIfPresent(3, (num, val) -> val + num);
11    System.out.println(map.get(3)); // "val33"(原 "val3" + key "3")
12
13    // 函数返回 null,则直接删除该 entry
14    map.computeIfPresent(9, (num, val) -> null);
15    System.out.println(map.containsKey(9)); // false(被删除了)
16
17    // computeIfAbsent:key 不存在时才执行函数
18    map.computeIfAbsent(23, num -> "val" + num);
19    System.out.println(map.containsKey(23)); // true(新增了)
20
21    // key 已存在时,函数不会执行,原值不变
22    map.computeIfAbsent(3, num -> "bam");
23    System.out.println(map.get(3)); // "val33"(仍是原值)
24}

4. remove(按 key+value 精确匹配)

JDK 8 提供了新的 remove 重载:只有 key 和 value 完全匹配时才删除

Java
1@Test
2public void test28() {
3    Map<Integer, String> map = new HashMap<>();
4    map.put(3, "val33");
5
6    map.remove(3, "val3");      // value 不匹配,不删除
7    System.out.println(map.get(3)); // "val33"
8
9    map.remove(3, "val33");     // key+value 完全匹配,删除成功
10    System.out.println(map.get(3)); // null
11}

5. getOrDefault

带有默认值的取值方法:

Java
1@Test
2public void test29() {
3    Map<Integer, String> map = new HashMap<>();
4    map.put(1, "val1");
5
6    // 若 key 不存在,返回指定的默认值,而不是 null
7    System.out.println(map.getOrDefault(42, "not found")); // "not found"
8}

6. merge 合并

merge 会先判断 key 是否存在,不存在则直接放入;存在则按函数合并 value:

Java
1@Test
2public void test30() {
3    Map<Integer, String> map = new HashMap<>();
4
5    // key 9 不存在 → 直接放入 "val9"
6    map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
7    System.out.println(map.get(9)); // "val9"
8
9    // key 9 已存在 → 执行拼接函数
10    map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
11    System.out.println(map.get(9)); // "val9concat"
12}

十、日期 Date API

Java 8 在 java.time 包下新增了日期 API。它和 Joda-Time 库相似,但又不完全相同。所有新类型都是不可变且线程安全的。

1. Clock 时钟

Clock 提供对当前日期和时间的访问,可用来替代 System.currentTimeMillis()

Java
1@Test
2public void test31() {
3    Clock clock = Clock.systemDefaultZone();
4    long millis = clock.millis();         // 当前时间毫秒数
5    Instant instant = clock.instant();    // Instant 实例
6    Date legacyDate = Date.from(instant); // 转成老版本 java.util.Date
7}

2. Timezones 时区

ZoneId 代表时区。可用静态方法传入时区编码获取:

Java
1@Test
2public void test32() {
3    // 打印所有可用时区 ID
4    System.out.println(ZoneId.getAvailableZoneIds());
5
6    ZoneId zone1 = ZoneId.of("Europe/Berlin");
7    ZoneId zone2 = ZoneId.of("Brazil/East");
8    System.out.println(zone1.getRules()); // ZoneRules[currentStandardOffset=+01:00]
9    System.out.println(zone2.getRules()); // ZoneRules[currentStandardOffset=-03:00]
10}

3. LocalTime 本地时间

LocalTime 表示一个不带时区的时间,例如 10:0017:30:15

Java
1@Test
2public void test33() {
3    ZoneId zone1 = ZoneId.of("Europe/Berlin");
4    ZoneId zone2 = ZoneId.of("Brazil/East");
5    LocalTime now1 = LocalTime.now(zone1);
6    LocalTime now2 = LocalTime.now(zone2);
7
8    System.out.println(now1.isBefore(now2)); // false
9    long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
10    long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
11    System.out.println(hoursBetween);   // -3
12    System.out.println(minutesBetween); // -239
13}

LocalTime 提供多个静态工厂方法,简化创建和解析:

Java
1@Test
2public void test34() {
3    LocalTime late = LocalTime.of(23, 59, 59);
4    System.out.println(late); // 23:59:59
5
6    DateTimeFormatter germanFormatter = DateTimeFormatter
7            .ofLocalizedTime(FormatStyle.SHORT)
8            .withLocale(Locale.GERMAN);
9    LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
10    System.out.println(leetTime); // 13:37
11}

4. LocalDate 本地日期

LocalDate 是一个日期对象(如 2014-03-11),同样是 final 不可变类型。可通过加减日、月、年来计算新日期:

Java
1@Test
2public void test35() {
3    LocalDate today = LocalDate.now();
4    LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);  // 今天 +1 天
5    LocalDate yesterday = tomorrow.minusDays(2);          // 明天 -2 天
6    LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4); // 2014 年 7 月 4 日
7
8    DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
9    System.out.println(dayOfWeek); // FRIDAY(星期五)
10}

也可以直接解析日期字符串:

Java
1@Test
2public void test36() {
3    DateTimeFormatter germanFormatter = DateTimeFormatter
4            .ofLocalizedDate(FormatStyle.MEDIUM)
5            .withLocale(Locale.GERMAN);
6    LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
7    System.out.println(xmas); // 2014-12-24
8}

5. LocalDateTime 本地日期时间

LocalDateTimeLocalDateLocalTime 的结合体,操作方式也大致相同:

Java
1@Test
2public void test37() {
3    LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
4    System.out.println(sylvester.getDayOfWeek());   // WEDNESDAY(星期三)
5    System.out.println(sylvester.getMonth());       // DECEMBER(十二月)
6
7    // 一天中的第几分钟
8    long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
9    System.out.println(minuteOfDay); // 1439
10}

加上时区信息后,LocalDateTime 可转换为 Instant,进而转成老版本的 Date

Java
1@Test
2public void test38() {
3    LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
4    Instant instant = sylvester.atZone(ZoneId.systemDefault()).toInstant();
5    Date legacyDate = Date.from(instant);
6    System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
7}

格式化 LocalDateTime 和格式化 LocalDate/LocalTime 一样,既可用预定义格式,也可自定义:

Java
1@Test
2public void test39() {
3    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd, yyyy - HH:mm");
4    LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
5    String string = formatter.format(parsed);
6    System.out.println(string); // Nov 03, 2014 - 07:13
7}

十一、注解 Annotations

Java 8 中的注解是可重复的。直接看例子。

首先定义一个包装器注解(包含实际注解的数组),再给实际注解标注 @Repeatable

Java
1// 实际注解:标记为可重复
2@Repeatable(Hints.class)
3public @interface Hint {
4    String value();
5}
6
7// 容器注解:持有 Hint 数组
8public @interface Hints {
9    Hint[] value();
10}

形态一:使用注解容器(老方法)

Java
1@Test
2public void test40() {
3    @Hints({@Hint("hint1"), @Hint("hint2")})
4    class Person {
5    }
6}

形态二:使用可重复注解(新方法)

Java
1@Test
2public void test41() {
3    @Hint("hint1")
4    @Hint("hint2")
5    class Person {
6    }
7}

Java 编译器会自动把形态二的多个 @Hint 收拢到一个 @Hints 容器里。通过反射读取时,这一点很重要:

Java
1@Test
2public void test42() {
3    @Hint("hint1")
4    @Hint("hint2")
5    class Person {
6    }
7
8    // getAnnotation 拿不到直接重复标注的 Hint(因为它实际被包在 Hints 里)
9    Hint hint = Person.class.getAnnotation(Hint.class);
10    System.out.println(hint); // null
11
12    // 通过容器注解拿到
13    Hints hints1 = Person.class.getAnnotation(Hints.class);
14    System.out.println(hints1.value().length); // 2
15
16    // 更推荐:getAnnotationsByType 能跨容器直接拿到所有 Hint
17    Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
18    System.out.println(hints2.length); // 2
19}

尽管我们没在 Person 上显式声明 @Hints,它的信息依然可以通过 getAnnotation(Hints.class) 读取到。而 getAnnotationsByType 更方便,可以直接获得所有 @Hint

此外,Java 8 还新增了两个注解使用位置(ElementType):

Java
1// 可用于泛型类型参数声明 和 任意类型使用处
2@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
3@interface MyAnnotation {}

十二、总结

  • JDK 8 的新特性包括:Lambda、函数式接口、方法引用(::)、内置函数式接口(Predicate/Function/Supplier/Consumer)、Stream 流、Map 集合新方法、新日期 API、可重复注解等。
  • 合理组合使用这些特性,可以显著减少编码量,同时让代码更整洁、更易读。
  • 在新框架(如 SpringBoot)的源码中,这些新特性几乎随处可见,掌握它们是阅读现代 Java 代码的基础。

学习建议:不要死记语法,而是抓住每个特性的"它解决了什么痛点"——default 解决接口演进、Lambda 解决行为传递、Stream 解决集合运算的声明式表达、Optional 解决空指针、新日期 API 解决老 Date 的可变性与线程安全问题。理解了动机,语法自然就记住了。

继续阅读

推荐阅读

技术2026年6月26日JDK1.8 新特性 · 知识卡片技术2026年6月18日逻辑帧阅读总结AI 时代程序员的转变 过去更注重编程细节以及如何实现,现在 AI 在实现上和细节上已经更好,现在程序员更看重以下两点: 1. 实现什么:由认知和想象力决定,认知决定上限,AI 能写出优雅的代码但是不知道用户的痛点、业务的瓶颈,这些来自于对行业的理解、用户的洞察、技术的敏感度等等... 1. 如何组合:由工程能力决定,过去工程能力体现在代码的细节实现上,现在体现在技术的组合架构上,需要理解每个组件技术2026年6月3日实现多线程的方法是 1 种还是 2 种还是 4 种?实现多线程的方法是 1 种还是 2 种还是 4 种? 网上的说法 正确的说法 实现多线程的官方正确方法: 2 种。 Oracle 官网的文档说明 方法小结 方法一: 实现 Runnable 接口。 方法二: 继承 Thread 类。 代码示例 java package cn.xilikeli.threadcoreknowledge.createthreads; / <p 实现 Runnable 接
JDK 1.8 新特性 | 博击长空