其他现代特性速览

这一章是第八阶段的收官——前面四章讲了模块系统、Records、Sealed Classes、Pattern Matching 这些”重磅”特性,本章把剩下的”小而美”特性一口气讲完。它们每一个都没有三件套那么革命性,但合在一起让 Java 写起来舒服多了。

这一章会快速过完六个特性:

  1. 接口私有方法(Java 9+)
  2. Stream 增强(Java 9+ / 16+)
  3. HttpClient(Java 11+)
  4. Text Blocks 文本块(Java 15+)
  5. Sequenced Collections 序列化集合(Java 21+)
  6. Foreign Function & Memory API(Java 22+,正式)

一、接口私有方法(Java 9+)

1.1 痛点:接口的代码复用

Java 8 引入了接口的 default 方法和 static 方法——接口终于能带”实现”了。但很快出现一个问题:多个 default 方法之间想复用代码怎么办?

interface Logger {
    default void logInfo(String msg) {
        // 想复用这个格式化逻辑
        System.out.println("[INFO] " + msg);
    }
    default void logError(String msg) {
        // 又要写一遍格式化?
        System.out.println("[ERROR] " + msg);
    }
}

Java 8 没办法——只能把”共用逻辑”也写成 default 方法,但这样就暴露给接口的使用者了。用户能看到 logInternal 这种”内部方法”,破坏抽象。

1.2 Java 9 的解法:接口私有方法

Java 9(JEP 213)允许接口里有 private 方法——只能在接口内部被 default/static 方法调用:

interface Logger {
    default void logInfo(String msg) {
        log("INFO", msg);   // 调用私有方法
    }
    default void logError(String msg) {
        log("ERROR", msg);
    }
    default void logWarn(String msg) {
        log("WARN", msg);
    }

    // 私有方法, 复用逻辑, 不暴露给实现类
    private void log(String level, String msg) {
        System.out.println("[" + level + "] " + msg + " @" + System.currentTimeMillis());
    }

    // 也可以是 static 私有
    private static String format(String level, String msg) {
        return "[" + level + "] " + msg;
    }
}

特点:

  • private 方法:实例方法,被 default 方法调用。
  • private static 方法:静态方法,被 static/default 方法调用。
  • 不能是 abstract——私有方法必须有实现。
  • 不被实现类继承——只在接口内部可见。

这是个小特性,但解决了接口代码复用的”最后一公里”。

二、Stream 增强(Java 9+ / 16+)

Java 8 引入了 Stream API,后续版本持续在”打补丁”。几个最常用的增强:

2.1 takeWhile / dropWhile(Java 9+)

takeWhile:取满足条件的前缀,遇到第一个不满足的就停。

dropWhile:跳过满足条件的前缀,遇到第一个不满足的就开始取。

Stream.of(1, 2, 3, 4, 5, 2, 1)
    .takeWhile(n -> n < 4)   // 取 1, 2, 3, 遇到 4 停
    .collect(Collectors.toList());   // [1, 2, 3]

Stream.of(1, 2, 3, 4, 5, 2, 1)
    .dropWhile(n -> n < 4)   // 跳 1, 2, 3, 从 4 开始
    .collect(Collectors.toList());   // [4, 5, 2, 1]

filter 的区别——filter 是”全程过滤”,takeWhile/dropWhile 是”前缀操作”。对有序且按某种条件分段的流特别有用:日志按时间分页、序列按阈值分段。

2.2 ofNullable(Java 9+)

Stream.ofNullable(null) 返回空流,不抛 NPE:

Stream.ofNullable(null).count();   // 0
Stream.ofNullable("hello").count();   // 1

// 配合 flatMap 处理可能为 null 的元素
Stream.of("a", null, "b", null, "c")
    .flatMap(Stream::ofNullable)   // 过滤掉 null
    .collect(Collectors.toList());   // [a, b, c]

2.3 Stream.toList()(Java 16+)

Java 16 之前,把 Stream 收集成 List 要写 .collect(Collectors.toList())——又长又容易写错。Java 16 加了 Stream.toList() 直接收集:

// 老写法
List<String> list = stream.collect(Collectors.toList());

// 新写法
List<String> list = stream.toList();

注意toList() 返回的是不可变 List——不能 add/remove。如果要可变 List,还得用 collect(Collectors.toCollection(ArrayList::new))

List<Integer> nums = Stream.of(1, 2, 3).toList();
nums.add(4);   // 抛 UnsupportedOperationException

2.4 其他小增强

  • Stream.iterate(seed, hasNext, next)——支持终止条件的 iterate,类似 for 循环。
  • IntStream.takeWhile/dropWhile——基本类型流也支持。
  • Collectors.teeing——双下游收集器,同时算两个统计(如同时算 sum 和 count 求平均)。
// Java 9: 带终止条件的 iterate
Stream.iterate(1, n -> n <= 100, n -> n + 1)
    .forEach(System.out::println);   // 等价于 for (int i=1; i<=100; i++)

// Java 12+: teeing 同时算总和和数量
double avg = Stream.of(1, 2, 3, 4, 5)
    .collect(Collectors.teeing(
        Collectors.summingDouble(i -> i),
        Collectors.counting(),
        (sum, count) -> sum / count
    ));   // 3.0

三、HttpClient(Java 11+)

Java 9 引入了新的 HTTP Client API(java.net.http),Java 11 正式发布(JEP 321)。它替代了老旧的 HttpURLConnection——支持 HTTP/2、WebSocket、同步异步、连接池。

3.1 基本用法

import java.net.URI;
import java.net.http.*;
import java.time.Duration;

HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)        // 默认 HTTP/2, 自动降级 HTTP/1.1
    .connectTimeout(Duration.ofSeconds(10))
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .timeout(Duration.ofSeconds(30))
    .header("Accept", "application/json")
    .GET()
    .build();

// 同步
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());

// 异步
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println)
    .join();   // 等待完成

3.2 优势对比

维度HttpURLConnectionHttpClient
HTTP/2不支持原生支持
异步需自己开线程内建 sendAsync
连接池无(每次新建)内建连接池
API 设计API 古老、配置繁琐Builder 模式,清晰
WebSocket不支持原生支持
HTTPS配置繁琐默认支持

3.3 POST JSON

String json = "{\"name\":\"Alice\",\"age\":30}";
HttpRequest postReq = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

3.4 注意

HttpClient 本身是线程安全的、可以复用——一个应用通常建一个 client 全局共享。每个请求建一个 HttpRequest 对象(不可变,可重用)。

四、Text Blocks 文本块(Java 15+)

4.1 痛点:字符串里的”转义地狱”

写 SQL、JSON、HTML 时,Java 字符串的转义让人崩溃:

String json = "{\\n" +
    "  \\"name\\": \\"Alice\\",\\n" +
    "  \\"age\\": 30\\n" +
    "}";

每一行 + 拼接,每个 " 要转义成 \"——10 行 JSON 写出来像天书。

4.2 文本块语法

Java 15(JEP 378)引入 Text Blocks——用三个引号 """ 包裹多行字符串:

String json = """
        {
          "name": "Alice",
          "age": 30
        }
        """;

特点:

  • 多行——直接换行,不用 \n
  • 不用转义引号——" 直接写。
  • 缩进控制——编译器自动去掉”共同最小缩进”,让代码对齐。
  • 结尾 """ 单独一行表示字符串结束。

4.3 缩进规则

文本块的缩进是”智能”的——以结尾 """ 的位置为基准:

String s = """
        hello
        world
        """;   // 结尾 """ 在这里
// s = "hello\\nworld\\n"  (缩进被去掉了)

如果结尾 """ 在更左的位置,会保留更多缩进:

String s = """
            hello
            world
        """;   // 结尾 """ 在更左
// s = "    hello\\n    world\\n"  (保留了 4 空格)

4.4 转义和插值

文本块里仍然可以用 \s(保留尾部空格)、\<换行>(行尾续行):

String sql = """
        SELECT id, name \\
               age \\
        FROM   users \\
        WHERE  age > 18
        """;
// 实际是 "SELECT id, name age FROM users WHERE age > 18" (一行)

String colors = """
        red   \\s
        green \\s
        blue  \\s
        """;   // \\s 保留尾部空格, 否则编译器会去掉

Java 没有内建的字符串插值(如 ${var}),但可以用 String.formatformatted

String name = "Alice";
int age = 30;
String json = """
        {
          "name": "%s",
          "age": %d
        }
        """.formatted(name, age);

五、Sequenced Collections 序列化集合(Java 21+)

5.1 痛点:拿”最后一个元素”那么难?

Java 21 之前,不同集合”拿最后一个元素”的 API 完全不统一:

List<Integer> list = List.of(1, 2, 3);
list.get(list.size() - 1);   // List 的最后

Deque<Integer> deque = new ArrayDeque<>(list);
deque.getLast();   // Deque 的最后

SortedSet<Integer> set = new TreeSet<>(list);
set.last();   // SortedSet 的最后

// Set 没有"顺序", 拿最后没意义 (但 LinkedHashSet 有顺序...)

每个集合类型自己一套 API,记不住。Map 更惨——拿最后一个 entry 要 map.entrySet().iterator() 一直 next

5.2 Sequenced Collections 接口

Java 21(JEP 431)引入了三个新接口统一”有顺序的集合”:

  • SequencedCollection<E> —— 有顺序的 Collection
  • SequencedSet<E> —— 有顺序的 Set
  • SequencedMap<K,V> —— 有顺序的 Map

新方法:

interface SequencedCollection<E> extends Collection<E> {
    SequencedCollection<E> reversed();   // 反转
    void addFirst(E e);                  // 头部添加
    void addLast(E e);                   // 尾部添加
    E getFirst();                        // 取第一个
    E getLast();                         // 取最后一个
    E removeFirst();                     // 删第一个
    E removeLast();                      // 删最后一个
}
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
list.getFirst();      // 1
list.getLast();       // 5
list.addFirst(0);     // [0, 1, 2, 3, 4, 5]
list.reversed();      // [5, 4, 3, 2, 1, 0]

LinkedHashSet<Integer> set = new LinkedHashSet<>(List.of(1, 2, 3));
set.getFirst();       // 1
set.getLast();        // 3   (以前做不到!)

LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1); map.put("b", 2); map.put("c", 3);
map.firstEntry();     // a=1
map.lastEntry();      // c=3   (以前做不到!)
map.pollLastEntry();  // 删除并返回 c=3
map.reversed();       // 反转的 Map 视图

终于——所有”有顺序的集合”用同一套 API。代码可读性、可移植性大幅提升。

5.3 哪些集合实现了

  • ListDequeQueue → 实现 SequencedCollection
  • LinkedHashSet → 实现 SequencedSet
  • LinkedHashMap → 实现 SequencedMap
  • TreeSet / TreeMap 也实现了(按比较顺序)
  • HashSet / HashMap 没实现(无顺序)

六、Foreign Function & Memory API(Java 22+)

6.1 痛点:JNI 的折磨

Java 调用 C/C++ 库一直用 JNI(Java Native Interface)——但它有几个大问题:

  • 写 C 桥接代码——要写 .h 头文件、.c 实现文件,编译成 .so/.dll,部署复杂。
  • 内存不安全——C 的指针、内存分配,JVM 管不到,容易段错误。
  • 性能开销——JNI 调用有上下文切换开销。
  • 不能调用系统函数——想调 printfgetpid 这种 libc 函数也要写 C 包装。

6.2 FFM API 是什么

Foreign Function & Memory API(FFM) 是 Java 22 正式发布的特性(JEP 454),让 Java 直接调用 C/C++ 库的函数、直接操作堆外内存,不用写 JNI 桥接代码

核心 API:

  • Linker —— 平台链接器,找 C 函数。
  • SymbolLookup —— 在 .so/.dll 里找符号。
  • MemorySegment —— 堆外内存段,替代 sun.misc.UnsafeByteBuffer
  • Arena —— 内存分配器,管理生命周期。

6.3 调用 C 函数示例

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();

// 找到 C 的 printf 函数
MethodHandle printf = stdlib.find("printf")
    .orElseThrow();

// 调用
try (Arena arena = Arena.ofConfined()) {
    MemorySegment format = arena.allocateUtf8String("Hello %s!\\n");
    MemorySegment arg = arena.allocateUtf8String("World");
    printf.invoke(format, arg);   // 输出 "Hello World!"
}

不写一行 C 代码,直接调 printf——这是 JNI 时代不可想象的。Arena.ofConfined() 管理的内存会在 try-with-resources 结束时自动释放,内存安全

6.4 性能与场景

FFM 的设计目标之一是性能接近 JNI、内存更安全。它替代了三个老 API:

  • sun.misc.Unsafe(内部 API,未来移除)
  • java.nio.ByteBuffer 的直接内存(API 限制多)
  • JNI(开发复杂)

适合场景:高性能计算、调原生库(OpenSSL、SQLite、CUDA)、游戏引擎、数据库驱动。Java 22 起官方数据库驱动、TLS 库已陆续用 FFM 重写。

七、实战:综合演示

下面的例子综合演示这六个特性。

Java · 在线运行

观察重点

  • Logger 接口的 private void log——三个 default 方法复用,外部看不到。
  • takeWhile vs filter——前者遇到第一个不满足就停,后者过滤全部。
  • toList() 不可变——add 抛 UnsupportedOperationException
  • Text Blocks 缩进自动去除——结尾 """ 的位置决定保留多少缩进。
  • list.getFirst() / set.getLast() / map.firstEntry()——所有”有序集合”统一 API。
  • HttpClient sendAsync 返回 CompletableFuture——和 CompletableFuture API(第 44 章)无缝衔接。
  • FFM API 在 Java 22+ 才能用——Java 21 之前是预览特性。

八、其他值得一提的特性

除了上面六大特性,还有一些值得了解的现代特性:

  • var 局部变量类型推断(Java 10+)——var list = new ArrayList<String>()
  • switch 表达式(Java 14+)——switch 能作为表达式返回值,yield 关键字。
  • String.isBlank()/strip()/repeat()/lines()(Java 11+)——字符串增强。
  • Files.readString/writeString(Java 11+)——一行读写文本文件。
  • Optional.isEmpty()(Java 11+)——!isPresent() 的对称方法。
  • String.format 的实例版 formatted(Java 15+)——"%s".formatted(name)
  • Record Patterns 嵌套(Java 21)——上一章讲过。
  • Virtual Threads(Java 21)——第七阶段已讲。
  • Generational ZGC(Java 21)——下个阶段讲。
  • Unnamed Patterns/Variables(Java 22)——case Point(_, _)var _ = ...
  • Statements Before super/this(Java 22)——构造器里 super 前可以写语句。

Java 6 个月一次的发布节奏让特性”小步快跑”——每个版本都有几个小特性,累积起来就是质变。

九、本章小结

特性版本核心要点
接口私有方法Java 9+复用 default 方法间逻辑,不暴露给实现类
takeWhile/dropWhileJava 9+Stream 前缀操作,遇条件停止
ofNullableJava 9+null 转 empty stream
iterate(seed, hasNext, next)Java 9+带终止条件的迭代
teeingJava 12+双下游收集器
Stream.toList()Java 16+直接收集为不可变 List
HttpClientJava 11+HTTP/2、同步异步、连接池、WebSocket
Text BlocksJava 15+三引号多行字符串,自动缩进控制
Sequenced CollectionsJava 21+统一 getFirst/getLast/reversed
FFM APIJava 22+无 JNI 调 C,安全操作堆外内存

记忆口诀

  • 接口私有方法——default 间的”内部工具”。
  • takeWhile 取前缀,dropWhile 跳前缀——和 filter 的”全程过滤”不同。
  • toList 不可变——要可变得用 toCollection(ArrayList::new)
  • HttpClient 三件套——HttpClient、HttpRequest、HttpResponse,Builder 模式。
  • Text Blocks 三引号——缩进看结尾 """ 的位置。
  • Sequenced Collections——所有有序集合的 getFirst/getLast/reversed 统一了。
  • FFM——Java 22+ 才正式,调 C 不用 JNI。

结语:第八阶段完结——现代 Java 的全景

第八阶段到这里就结束了。回顾这 5 章,现代 Java 的”现代化”体现在三个层面:

  1. 模块化(第 46 章) —— 强封装、可靠配置、可定制 JRE。Java 终于有了”大型工程”的边界控制。
  2. 数据建模三件套(第 47-49 章) —— Records + Sealed + Pattern Matching。Java 终于能像函数式语言一样优雅建模 ADT。
  3. API 现代化(本章) —— HttpClient、Text Blocks、Sequenced Collections、FFM。日常 API 终于不再”上世纪”。

这三个层面合起来,让 Java 在保持向后兼容的同时,写起来和 Kotlin、Scala 一样舒服。如果你还在用 Java 8,强烈建议升级——光是 Records + switch 模式匹配 + HttpClient + Text Blocks,就能让你的代码量减少 30%。

下一阶段我们进入 第九阶段:JVM 深度。从语法回到运行时——JVM 内存模型、垃圾回收、类加载机制、性能调优、字节码基础。这是 Java 工程师”内功”的最后一关——会写 Java 不算什么,懂 JVM 才是真正的”Java 老兵”。我们下一阶段见。