Java 8 終于要被淘汰了!帶你速通 Java 8~24 新特性 | 又能跟面試官吹牛皮了
Java 8 終于要被淘汰了!
記得我從大一開始學的就是 Java 8,當時還叫做新特性;后來 Java 11 出了,我用 Java 8;Java 17 出了,我用 Java 8;Java 21 出了,我還用 Java 8。
隨你怎么更新,我用 Java 8!

我之前帶大家做項目的時候,還是強烈建議大家用 Java 8 的,為什么現在說 Java 8 要被淘汰了呢?
在我看來主要是因為業務和生態變了,尤其是這幾年 AI 發展,很多老項目都要接入 AI、新項目直接面向 AI 開發,為了追求開發效率,我們要用 AI 開發框架(比如 Spring AI、LangChain4j),而這些框架要求的版本幾乎都是 >= 17, 所以我們團隊自己的業務也從 Java 8 遷到 Java 21 了。
另外也是因為有些新版本的 Java 特性確實很香,學會之后無論是開發效率還是性能都能提升一大截。
所以我做了本期干貨內容,講通 Java 8 ~ Java 24 的新特性,洋洋灑灑一萬多字!建議收藏,看完后你就約等于學完了十幾個 Java 版本~
?? 推薦觀看視頻版,體驗更佳:https://bilibili.com/video/BV1haamzUE8m
?? 免費 Java 教程 + 新特性大全:https://codefather.cn/course/java

?? Java 8
Java 8 絕對是 Java 歷史上最重要的穩定版本,也是這么多年來最受歡迎的 Java 版本,甚至有專門的書籍來講解 Java 8。這個版本最大的變化就是引入了函數式編程的概念,給 Java 這門傳統的面向對象語言增加了新的玩法。

【必備】Lambda 表達式
什么是 Lambda 表達式?
Lambda 表達式可以說是 Java 8 的殺手級特性。在這個特性出現之前,我們要實現一個簡單的回調函數,只能通過匿名內部類的方式,代碼又臭又長。
舉些例子,比如給按鈕添加點擊事件、或者創建一個新線程執行操作,必須要自己 new 接口并且編寫接口的定義和實現代碼。
// Java 8 之前的寫法,給按鈕添加點擊事件
button.addActionListener(new ActionListener() {
Lambda 表達式的出現,讓代碼變得簡潔優雅,告別匿名內部類!
// Java 8 Lambda 寫法
button.addActionListener(e -> System.out.println("按鈕被點擊了"));
Thread thread = new Thread(() -> System.out.println("線程正在運行"));
Lambda 表達式的語法非常靈活,可以根據參數個數和方法代碼的復雜度選擇不同的寫法:
// 無參數的 Lambda
Runnable r = () -> System.out.println("Hello Lambda!");
?
// 單個參數(可以省略括號)
Consumer<String> printer = s -> System.out.println(s);
?
// 多個參數
BinaryOperator<Integer> add = (a, b) -> a + b;
Comparator<String> comparator = (a, b) -> a.compareTo(b);
?
// 復雜的方法體(需要大括號和 return)
Function<String, String> processor = input -> {
String processed = input.trim().toLowerCase();
if (processed.isEmpty()) {
return "空字符串";
}
return "處理后的字符串:" + processed;
};
方法引用
Lambda 表達式還有一個實用特性叫做 方法引用,可以看作是 Lambda 表達式的一種簡寫形式。當 Lambda 表達式只是調用一個已存在的方法時,使用方法引用代碼會更簡潔。
舉個例子:
List<String> names = Arrays.asList("魚皮", "編程導航", "面試鴨");
?
// 使用 Lambda 表達式
names.forEach(name -> System.out.println(name));
?
// 使用方法引用(更簡潔)
names.forEach(System.out::println);
實際開發中,方法引用經常用于獲取某個 Java 對象的屬性。比如使用 MyBatis Plus 來構造數據庫查詢條件時,經常會看到下面這種代碼:
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getName, "魚皮");
方法引用有幾種不同的形式,包括靜態方法引用、實例方法引用、構造器引用,適用于不同的場景。
// 靜態方法引用
List<String> strings = Arrays.asList("1", "2", "3");
List<Integer> numbers = strings.stream()
.map(Integer::parseInt) // 等于 s -> Integer.parseInt(s)
.collect(Collectors.toList());
?
// 實例方法引用
List<String> words = Arrays.asList("hello", "world", "java");
List<String> upperWords = words.stream()
.map(String::toUpperCase) // 等于 s -> s.toUpperCase()
.collect(Collectors.toList());
?
// 構造器引用
List<String> nameList = Arrays.asList("魚皮", "編程導航", "面試鴨");
List<Person> persons = nameList.stream()
.map(Person::new) // 等于 name -> new Person(name)
.collect(Collectors.toList());
【必備】函數式接口
什么是函數式接口?
函數式接口是 只有一個抽象方法的接口。要玩轉 Lambda 表達式,就必須了解函數式接口,因為 Lambda 表達式的本質是函數式接口的匿名實現。
展開來說,函數式接口定義了 Lambda 表達式的參數和返回值類型,而 Lambda 表達式提供了這個接口的具體實現。兩者相輔相成,讓 Java 函數式編程偉大!
常用的函數式接口
Java 8 為我們提供了很多內置的函數式接口,讓函數式編程變得簡單直觀。列舉一些常用的函數式接口:
1)Predicate 用于條件判斷:
// Predicate<T> 用于條件判斷
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate(); // 取反
?
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(isEven)
.collect(Collectors.toList());
2)Function 接口用于數據轉換,支持函數組合,讓代碼邏輯更清晰:
// Function<T, R> 用于轉換
Function<String, Integer> stringLength = String::length;
Function<Integer, String> intToString = Object::toString;
?
// 函數組合
Function<String, String> addPrefix = s -> "前綴-" + s;
Function<String, String> addSuffix = s -> s + "-后綴";
Function<String, String> combined = addPrefix.andThen(addSuffix);
String result = combined.apply("魚皮"); // "前綴-魚皮-后綴"
3)Consumer 和 Supplier 接口分別用于消費和提供數據:
// Consumer<T> 用于消費數據(無返回值)
Consumer<String> printer = System.out::println;
Consumer<String> logger = s -> log.info("處理數據:{}", s);
// 組合消費
Consumer<String> combinedConsumer = printer.andThen(logger);
?
// Supplier<T> 用于提供數據
Supplier<String> randomId = () -> UUID.randomUUID().toString();
Supplier<LocalDateTime> now = LocalDateTime::now;
4)BinaryOperator 接口用于二元操作,比如數學運算:
// BinaryOperator<T> 用于二元操作
BinaryOperator<Integer> max = Integer::max;
BinaryOperator<String> concat = (a, b) -> a + b;
自定義函數式接口
雖然實際開發中,我們更多的是使用 Java 內置的函數式接口,但大家還是要了解一下自定義函數式接口的寫法,有個印象。
// 創建自定義函數式接口
使用自定義函數式接口,代碼會更簡潔:
// 使用自定義函數式接口
Calculator addition = (a, b) -> a + b;
Calculator subtraction = (a, b) -> a - b;
?? 自定義函數式接口時,需要注意:
1)函數式接口必須是接口類型,不能是類、抽象類或枚舉。
2)必須且只能包含一個抽象方法。否則 Lambda 表達式可能無法匹配接口。
3)建議使用 @FunctionalInterface注解。
雖然這個注解不是強制的,但加上后編譯器會幫你檢查是否符合函數式接口的規范(是否只有一個抽象方法),如果不符合會報錯。
4)可以包含默認方法 default 和靜態方法 static
函數式接口允許有多個默認方法和靜態方法,因為它們不是抽象方法,不影響單一抽象方法的要求。
// 創建自定義函數式接口
【必備】Stream API
什么是 Stream API?
Stream API 是 Java 8 另一個重量級特性,它讓集合處理變得既優雅又高效。(學大數據的同學應該對它不陌生)
在 Stream API 出現之前,我們處理集合數據只能通過傳統的循環,需要大量的樣板代碼。
比如過濾列表中的數據、將小寫轉為大寫并排序:
List<String> words = Arrays.asList("apple", "banana", "cherry");
?
// 傳統的處理方式
List<String> result = new ArrayList<>();
for (String word : words) {
if (word.length() > 5) {
String upperCase = word.toUpperCase();
result.add(upperCase);
}
}
Collections.sort(result);
如果使用 Stream API,可以讓同樣的邏輯變得更簡潔直觀:
// 使用 Stream 的方式
List<String> result = words.stream()
.filter(word -> word.length() > 5) // 過濾長度大于 5 的單詞
.map(String::toUpperCase) // 轉換為大寫
.sorted() // 排序
.collect(Collectors.toList()); // 收集結果
這就是 Stream 的作用。Stream 不是數據結構,而是 像工廠流水線 一樣處理數據的工具。數據從一端進入,經歷過濾、轉換、排序等一系列加工步驟后,最終輸出我們想要的結果。這種 鏈式調用 讓代碼讀起來就像自然語言一樣流暢。

Stream 操作類型
Stream 的操作分為中間操作和終端操作。中間操作是 “懶惰” 的,只有在遇到終端操作時才會真正執行。
filter 過濾和 map 映射都是中間操作,比如下面這段代碼,并不會對列表進行過濾和轉換:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
?
numbers.stream()
.filter(n -> n > 3) // 中間操作:過濾大于3的數字
.map(n -> n * n) // 中間操作:平方
一些常用的中間操作:
-
filter()- 過濾元素 -
map()- 轉換元素 -
sorted()- 排序 -
distinct()- 去重 -
limit()- 限制數量 -
skip()- 跳過元素
給上面的代碼加上一個終端操作 collect 后,才會真正執行:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
?
// 演示中間操作和終端操作
numbers.stream()
.filter(n -> n > 3) // 中間操作:過濾大于3的數字
.map(n -> n * n) // 中間操作:平方
.collect(Collectors.toList()); // 終端操作:收集結果
一些常用的終端操作:
-
collect()- 收集到集合 -
forEach()- 遍歷每個元素 -
count()- 統計數量 -
findFirst()- 查找第一個 -
anyMatch()- 是否有匹配的 -
reduce()- 歸約操作
實際應用
分享一些 Stream API 在開發中的典型用例。
1)對列表進行分組(List 轉為 Map):
Map<Boolean, List<Integer>> partitioned = numbers.stream()
.filter(n -> n > 3) // 中間操作:過濾大于3的數字
.map(n -> n * n) // 中間操作:平方
.collect(Collectors.partitioningBy(n -> n % 2 == 0)); // 終端操作:按奇偶分組
2)使用 Stream 內置的統計功能,對數據進行統計:
// 統計操作
IntSummaryStatistics stats = numbers.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
?
System.out.println("數量:" + stats.getCount());
System.out.println("總和:" + stats.getSum());
System.out.println("平均值:" + stats.getAverage());
System.out.println("最大值:" + stats.getMax());
System.out.println("最小值:" + stats.getMin());
3)按照對象的某個字段進行分組計算:
List<Person> people = Arrays.asList(
new Person("張三", 25, "北京"),
new Person("魚皮", 18, "上海"),
new Person("李四", 25, "北京"),
new Person("老二", 35, "上海")
);
?
// 按城市分組
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity));
?
// 按城市分組并統計年齡
Map<String, Double> avgAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.averagingInt(Person::getAge)
));
?
// 按城市分組并收集姓名
Map<String, List<String>> namesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::getCity,
Collectors.mapping(Person::getName, Collectors.toList())
));
學過數據庫的同學應該對這種操作并不陌生,其實 SQL 語句中的很多操作都可以通過 Stream 實現。這也是 Stream 的典型應用場景 —— 對數據庫中查出的數據進行業務層面的運算。
并行流
并行流是 Stream API 的另一個強大特性,它可以自動利用多核 CPU 處理器加速數據處理任務的執行。
在此之前,我們要實現并行處理集合數據,需要手動管理線程池和任務分割,代碼復雜且容易出錯。
但有了 Stream API,一行代碼就能創建并行流,比如過濾并計算數據的總和:
List<Integer> largeList = IntStream.rangeClosed(1, 1000000)
.boxed()
.collect(Collectors.toList());
?
// 并行處理,只需要改一個方法調用
long parallelCount = largeList.parallelStream()
.filter(n -> isPrime(n))
.count();
并行流底層使用了 Fork/Join 框架,簡單來說就是把大任務拆分成小任務,分配給多個線程同時執行,最后把結果合并起來。這個過程對開發者完全透明,只需要調用 parallelStream() 即可。

但也正因如此,實際開發中,要謹慎使用并行流!
因為它使用的是 JVM 全局的 ForkJoinPool.commonPool(),默認線程數等于 CPU 核心數減 1。如果某個并行流任務阻塞了線程,會影響其他并行流的性能。
而且并行流不一定就更快,特別是對于簡單操作或小數據集,切換線程的開銷可能超過并行帶來的收益。
因此,并行流更適合大數據量、CPU 密集型任務(如復雜計算、圖像處理),不適合 I/O 密集型任務(如網絡請求)。而且只要涉及到并發場景,就要考慮到線程安全問題。
【實用】Optional
Optional 的作用
NullPointerException(NPE)一直是 Java 程序員的噩夢,學 Java 的同學應該都被它折磨過。

之前,我們只能通過大量的 if 語句檢查 null 來避免空指針異常,不僅代碼又臭又長,而且稍微不注意就漏掉了。
// 傳統的空值檢查
public String getDefaultName(User user) {
if (user != null) {
String name = user.getName();
if (name != null && !name.isEmpty()) {
return name.toUpperCase();
}
}
return "unknown";
}
Optional 類的引入就是為了優雅地處理可能為空的值,可以先把它理解為 “包裝器”,把可能為空的對象封裝起來。
創建 Optional 對象:
// 創建 Optional 對象
Optional<String> optional1 = Optional.of("Hello"); // 不能為 null
Optional<String> optional2 = Optional.ofNullable(getName()); // 可能為 null
Optional<String> optional3 = Optional.empty(); // 空的 Optional
Optional 提供了多種處理空值的方法:
// 檢查是否有值
if (optional.isPresent()) {
System.out.println(optional.get());
}
?
// 更優雅的方式,如果對象存在則輸出
optional.ifPresent(System.out::println);
還可以設置默認值策略,比如空值時拋出異常:
// 提供默認值
String result1 = optional.orElse("默認值");
String result2 = optional.orElseGet(() -> generateDefaultValue());
String result3 = optional.orElseThrow(() -> new IllegalStateException("值不能為空"));
除了前面這些基本方法外,Optional 甚至提供了一套完整的 API 來處理空值場景!
跟 Stream API 類似,你可以對 Optional 封裝的數據進行過濾、映射等操作:
optional
.filter(s -> s.length() > 5)
.map(String::toUpperCase)
.ifPresentOrElse(
System.out::println, // 有值時執行
() -> System.out.println("沒有值") // 無值時執行
);
應用場景
魚皮經常使用 Optional 來簡化空值判斷:
int pageNum = Optional.ofNullable(params.getPageNum())
.orElseThrow(() -> new RuntimeException("pageNum不能為空"));
如果不用 Optional,就要寫下面這段代碼:
int pageNum;
if (params.getPageNum() != null) {
pageNum = params.getPageNum();
} else {
throw new RuntimeException("pageNum不能為空");
}
此外,Optional 的一個典型應用場景是在集合中進行安全查找:
List<String> names = Arrays.asList("張三", null, "李四", "", "王五");
?
// 使用 Optional 進行安全的查找
Optional<String> foundName = names.stream()
.filter(Objects::nonNull)
.filter(name -> name.startsWith("張"))
.findFirst();
?
foundName.ifPresentOrElse(
name -> System.out.println("找到了:" + name),
() -> System.out.println("沒有找到匹配的名字")
);
【必備】新的日期時間 API
Java 8 引入的新日期時間 API 解決了舊版 Date 和 Calendar 類的很多問題,比如線程安全、可變性、時區處理等等。
傳統的日期處理方式:
// 舊版本的復雜日期處理
Calendar cal = Calendar.getInstance();
cal.set(2024, Calendar.JANUARY, 15); // 注意月份從0開始
Date date = cal.getTime();
?
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = sdf.format(date); // 線程不安全
使用新的日期時間 API,代碼會更簡潔:
// 當前日期時間
LocalDate today = LocalDate.now(); // 2025-09-01
LocalTime now = LocalTime.now(); // 14:30:25.123
LocalDateTime dateTime = LocalDateTime.now(); // 2025-09-01T14:30:25.123
?
// 指定的日期時間
LocalDate specificDate = LocalDate.of(2025, 09, 01);
LocalTime specificTime = LocalTime.of(14, 30, 0);
LocalDateTime specificDateTime = LocalDateTime.of(2025, 09, 01, 14, 30, 0);
典型的應用場景是從字符串解析日期,一行代碼就能搞定:
// 從字符串解析
LocalDate parsedDate = LocalDate.parse("2025-09-01");
LocalDateTime parsedDateTime = LocalDateTime.parse("2025-09-01T14:30:25");
?
// 自定義格式解析
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
LocalDateTime customParsed = LocalDateTime.parse("2025/09/01 14:30:25", formatter);
還有日期和時間的計算,也變得更直觀、見名知意:
LocalDate today = LocalDate.now();
?
// 基本的日期計算
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);
?
// 時間段計算
LocalDate startDate = LocalDate.of(2024, 1, 28);
LocalDate endDate = LocalDate.of(2025, 9, 1);
Period period = Period.between(startDate, endDate);
System.out.println("相差 " + period.getMonths() + " 個月 " + period.getDays() + " 天");
?
// 精確時間差計算
LocalDateTime start = LocalDateTime.now();
LocalDateTime end = LocalDateTime.of(2025, 09, 01, 14, 30, 0);
Duration duration = Duration.between(start, end);
System.out.println("執行時間:" + duration.toMillis() + " 毫秒");
還支持時區處理和時間戳處理,不過這段代碼就沒必要記了,現在有了 AI,直接讓它生成時間日期操作就好。
// 帶時區的日期時間
ZonedDateTime beijingTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
?
// 時區轉換
ZonedDateTime beijingToNewYork = beijingTime.withZoneSameInstant(ZoneId.of("America/New_York"));
?
// 獲取所有可用時區
ZoneId.getAvailableZoneIds().stream()
.filter(zoneId -> zoneId.contains("Shanghai"))
.forEach(System.out::println);
?
// 時間戳處理
Instant instant = Instant.now();
long epochSecond = instant.getEpochSecond();
ZonedDateTime fromInstant = instant.atZone(ZoneId.systemDefault());
總之,有了這套 API,我們不需要使用第三方的時間日期處理庫,也能解決大多數問題。
【必備】接口默認方法
Java 8 引入的接口默認方法解決了接口演化的問題。
在默認方法出現之前,如果你想給一個被廣泛使用的接口添加新方法,就會影響所有已有的實現類。想象一下,如果要給 Collection 接口添加一個新方法,ArrayList、LinkedList 等所有的實現類都需要修改,成本很大。
默認方法讓接口可以在 不破壞現有代碼的情況下添加新功能。
舉個例子,如果想要給接口增加一個 drawWithBorder 方法:
public interface Drawable {
// 已有抽象方法
void draw();
// 默認方法
default void drawWithBorder() {
System.out.println("繪制邊框");
draw();
System.out.println("邊框繪制完成");
}
}
使用默認方法后,實現類可以選擇重寫默認方法,也可以直接使用:
// 實現類可以選擇重寫默認方法
public class Circle implements Drawable {
Java 8 為 Collection 接口添加了 stream、removeIf 等方法,都是默認方法:

需要注意的是,如果一個類實現多個接口,并且這些接口有相同的默認方法時,需要顯式解決沖突:
interface A {
default void hello() {
System.out.println("Hello from A");
}
}
?
interface B {
default void hello() {
System.out.println("Hello from B");
}
}
?
// 實現類必須重寫沖突的方法
class C implements A, B {
類似的,Java 8 還支持接口的靜態方法,前面講函數式接口的時候有提到。
Java 9
【了解】模塊系統
在模塊系統出現之前,傳統 Java 應用只能依賴 classpath 來管理依賴,所有的類都在同一個類路徑下,任何類都可以訪問任何其他類,這種 “全局可見性” 在大型項目中會導致代碼耦合嚴重、依賴關系混亂、運行時才發現 ClassNotFoundException 等問題。
模塊系統允許我們將代碼組織成模塊,每個模塊都有明確的依賴關系和導出接口,讓大型應用的架構變得更加清晰和可維護。
模塊系統通過 module-info.java 文件來定義模塊的邊界,明確聲明哪些包對外開放,哪些依賴是必需的,這樣就形成了強封裝的架構。
比如一個用戶管理模塊只暴露用戶服務接口,而內部的數據訪問層對其他模塊完全不可見,這種設計讓系統的層次結構更加清晰,也避免了意外的跨層調用。
module user.management {
// 只導出 service 包,dao 包對外不可見
exports com.company.user.service;
// 依賴其他模塊
requires java.base;
requires database.connection;
}
此外,模塊系統還帶來了更好的性能優化,JVM 可以在啟動時只加載必需的模塊,減少內存占用和啟動時間(適合云原生應用)。
但是,模塊系統在企業中用的比較少,目前大多數企業還是使用傳統的 Maven/Gradle + JAR 包的方式管理依賴,改造項目的成本 > 模塊系統帶來的實際收益,所以僅作了解就好。
【了解】JShell 交互工具
JShell 是 Java 9 引入的一個交互式工具,在這個工具出現之前,我們要測試一小段 Java 代碼,必須創建完整的類和 main 方法,編譯后才能運行。
有了 JShell,我們可以像使用 Python 解釋器一樣使用 Java,對于學習調試有點兒用(但不多)。
直接在命令行輸入 jshell 就能使用了:

【必備】集合工廠方法
Java 9 為集合類添加了便捷的工廠方法,能夠輕松創建不可變集合。
在這之前,創建不可變集合還是比較麻煩的,很多開發者會選擇依賴第三方庫(比如 Google Guava)。
傳統的不可變集合創建方式:
// Java 9 之前創建不可變集合的方式
List<String> oldList = new ArrayList<>();
oldList.add("蘋果");
oldList.add("香蕉");
oldList.add("魚皮");
List<String> immutableList = Collections.unmodifiableList(oldList);
?
// 或者使用 Google Guava 等第三方庫
List<String> guavaList = ImmutableList.of("蘋果", "香蕉", "魚皮");
有了 Java 9 的工廠方法,創建不可變集合簡直不要太簡單!
// Java 9 的簡潔寫法
List<String> fruits = List.of("蘋果", "香蕉", "魚皮");
Set<Integer> numbers = Set.of(1, 2, 3, 4, 5);
Map<String, Integer> scores = Map.of(
"張三", 85,
"魚皮", 92,
"狗剩", 78
);
這些集合是真正不可變的,任何修改操作都會拋出 UnsupportedOperationException 異常。

如果想創建包含大量元素的不可變 Map,可以使用 ofEntries 方法:
Map<String, String> largeMap = Map.ofEntries(
Map.entry("key1", "value1"),
Map.entry("key2", "value2"),
Map.entry("key3", "value3")
// ... 可以有任意多個
);
【了解】接口私有方法
思考一個問題,如果某個接口中的默認方法需要復用代碼,你會怎么做呢?
比如讓你來優化下面這段代碼:
public interface Calculator {
default double calculateRectangleArea(double width, double height) {
// 重復的驗證邏輯
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("寬度和高度必須為正數");
}
return width * height;
}
default double calculateTriangleArea(double base, double height) {
// 重復的驗證邏輯
if (base <= 0 || height <= 0) {
throw new IllegalArgumentException("底邊和高度必須為正數");
}
return base * height / 2;
}
}
你會把重復的驗證邏輯寫在哪里呢?
答案很簡單,寫在一個外部工具類里,或者在接口內再寫一個通用的驗證方法:
public interface Calculator {
// 通用的驗證方法
default void validate(double x, double y) {
if (x <= 0 || y <= 0) {
throw new IllegalArgumentException("必須為正數");
}
}
}
但這種方式存在一個問題,validate 作為 default 方法,它會成為接口的公共 API,所有實現類都能訪問到!其實這個方法只需要在接口內可以使用就夠了。
Java 9 解決了這個問題,允許在接口中定義私有方法(以及私有靜態方法)。
public interface Calculator {
// 私有方法
private void validate(double x, double y) {
if (x <= 0 || y <= 0) {
throw new IllegalArgumentException("必須為正數");
}
}
?
// 私有靜態方法
private static void validatePositive(double x, double y) {
if (x <= 0 || y <= 0) {
throw new IllegalArgumentException("必須為正數");
}
}
}
這樣一來,接口內部可以優雅地復用代碼,同時保持接口對外的簡潔性。
?? 這里也能看出 Java 的演進很謹慎,先允許 default 方法(Java 8),再允許 private 方法(Java 9),每一步都有明確的設計考量。
【了解】改進的 try-with-resources
Java 9 改進了 try-with-resources 語句,在這之前,我們不能在 try 子句中使用外部定義的變量,必須在 try 括號內重新聲明,會讓代碼變得冗余。
// Java 9 之前
public void readFile(String filename) throws IOException {
BufferedReader reader = Files.newBufferedReader(Paths.get(filename));
try (BufferedReader br = reader) { // 需要重新賦值
br.lines().forEach(System.out::println);
}
}
Java 9 的改進讓代碼更加簡潔:
// Java 9
public void readFile(String filename) throws IOException {
BufferedReader reader = Files.newBufferedReader(Paths.get(filename));
try (reader) { // 直接使用 effectively final 變量
reader.lines().forEach(System.out::println);
}
}
而且還可以同時使用多個變量:
public void processFiles(String file1, String file2) throws IOException {
var reader1 = Files.newBufferedReader(Paths.get(file1));
var reader2 = Files.newBufferedReader(Paths.get(file2));
try (reader1; reader2) { // 可以使用多個變量
String line1 = reader1.readLine();
String line2 = reader2.readLine();
while (line1 != null && line2 != null) {
System.out.println(line1 + " | " + line2);
line1 = reader1.readLine();
line2 = reader2.readLine();
}
}
}
Java 10
【實用】var 關鍵字
用過弱類型編程語言的朋友應該知道,不用自己聲明變量的類型有多爽。
但是對于 Java 這種強類型語言,我們經常要寫下面這種代碼,一個變量類型寫老長(特別是在泛型場景下):
Map<String, List<Integer>> complexMap = new HashMap<String, List<Integer>>();
ArrayList<String> list = new ArrayList<String>();
Iterator<Map.Entry<String, List<Integer>>> iterator = complexMap.entrySet().iterator();
好在 Java 10 引入了 var 關鍵字,支持局部變量的類型推斷,編譯器會根據初始化表達式自動推斷變量的類型,讓代碼可以變得更簡潔。
var complexMap = new HashMap<String, List<Integer>>();
var list = new ArrayList<String>();
var iterator = complexMap.entrySet().iterator();
?
// 使用 var 的 for-each 循環
for (var entry : complexMap.entrySet()) {
var key = entry.getKey();
var value = entry.getValue();
// 處理邏輯
}
但是,var 關鍵字是一把雙刃劍,不是所有程序員都喜歡它。畢竟代碼中都是 var,丟失了一定的可讀性,尤其是下面這種代碼,你不能直觀地了解變量的類型:
var data = getData();
而且使用 var 時,還要確保編譯器能正確推斷類型,下面這幾種寫法都是錯誤的:

所以我個人其實是沒那么喜歡用這個關鍵字的,純個人偏好。
【了解】應用程序類數據共享
Java 10 擴展了類數據共享功能,允許應用程序類也參與共享(Application Class-Data Sharing)。在此之前,只有 JDK 核心類可以進行類數據共享,應用程序類每次啟動都需要重新加載和解析。
類數據共享的核心思路是:將 JDK 核心類和應用程序類的元數據都打包到共享歸檔文件中,多個 JVM 實例同時映射同一個歸檔文件,通過 共享讀取 優化應用啟動時間和減少內存占用。

?? Java 11
Java 11 是繼 Java 8 之后的第二個 LTS 版本,這個版本的重點是提供更好的開發體驗和更強大的標準庫功能,特別是在字符串處理、文件操作和 HTTP 客戶端方面,增加了不少新方法。
【實用】HTTP 客戶端 API
HTTP 請求是后端開發常用的能力,之前我們只能基于內置的 HttpURLConnection 自己封裝,或者使用 Apache HttpClient、OkHttp 第三方庫。
還記得我第一次去公司實習的時候,就看到代碼倉庫內有很多老員工自己封裝的 HTTP 請求代碼,寫法各異。。。
// 傳統的 HttpURLConnection 使用方式
URL url = new URL("https://codefather.cn");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");
?
int responseCode = connection.getResponseCode();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
// 更多繁瑣的代碼...
Java 11 將 HTTP 客戶端 API 正式化,新的 HTTP 客戶端提供了現代化的、支持 HTTP/2 和 WebSocket 的客戶端實現,讓網絡編程變得簡單。
// 創建 HTTP 客戶端
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
?
// 構建 GET 請求
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(URI.create("https://codefather.cn"))
.header("Accept", "application/json")
.header("User-Agent", "Java-HttpClient")
.timeout(Duration.ofSeconds(30))
.GET()
.build();
?
// POST 請求
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonData))
.build();
支持發送同步和異步請求,能夠輕松獲取響應結果:
// 同步發送請求
HttpResponse<String> response = client.send(getRequest,
HttpResponse.BodyHandlers.ofString());
System.out.println("狀態碼: " + response.statusCode());
System.out.println("響應頭: " + response.headers().map());
System.out.println("響應體: " + response.body());
?
// 異步發送請求
client.sendAsync(getRequest, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);
還支持自定義響應處理和 WebSocket 請求:
// 自定義響應處理
HttpResponse<String> customResponse = client.send(getRequest,
responseInfo -> {
if (responseInfo.statusCode() == 200) {
return HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
} else {
return HttpResponse.BodySubscribers.discarding();
}
});
?
// WebSocket 支持
WebSocket webSocket = HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(URI.create("ws://localhost:8080/websocket"), new WebSocket.Listener() {
上面這些代碼都不用記,現在直接把接口文檔甩給 AI,讓它來幫你生成請求代碼就好。
【實用】String 類的新方法
Java 11 為 String 類添加了許多實用的方法,讓字符串處理變得更加方便。
我估計很多現在學 Java 的同學都已經區分不出來哪些是新增的方法、哪些是老方法了,反正能用就行~
1)基本的字符串檢查和處理:
String text = " Hello World \n\n";
String emptyText = " ";
String multiLine = "第一行\n第二行\n第三行";
?
// isBlank() 檢查字符串是否為空或只包含空白字符
System.out.println(emptyText.isBlank()); // true
System.out.println("hello".isBlank()); // false
System.out.println("".isBlank()); // true
2)strip() 系列方法
相比傳統的 trim() 更加強大,能夠處理 Unicode 空白字符:
// strip() 系列方法,去除空白字符
System.out.println("'" + text.strip() + "'"); // 'Hello World'
System.out.println("'" + text.stripLeading() + "'"); // 'Hello World \n\n'
System.out.println("'" + text.stripTrailing() + "'"); // ' Hello World'
3)lines() 方法,讓多行字符串處理更簡單:
// 將字符串按行分割成 Stream
multiLine.lines()
.map(line -> "處理: " + line)
.forEach(System.out::println);
?
long lineCount = multiLine.lines().count();
System.out.println("總行數: " + lineCount);
4)repeat() 方法,可以重復字符串:
System.out.println("Java ".repeat(3)); // "Java Java Java "
System.out.println("=".repeat(50)); // 50個等號
System.out.println("*".repeat(0)); // 空字符串
即便如此,我還是更喜歡使用 Hutool 或者 Apache Commons 提供的字符串工具類。
?? 提到字符串處理,魚皮建議大家安裝 StringManipulation 插件,便于我們開發時對字符串進行各種轉換(比如小寫轉為駝峰):

【實用】Files 類的新方法
Java 11 為文件操作新增了更便捷的方法,不需要使用 FileReader / FileWriter 這種復雜的操作了。
基本的文件讀寫操作,一個方法搞定:
// 寫入文件
String content = "這是一個測試文件\n包含多行內容\n中文支持測試";
Path tempFile = Files.writeString(
Paths.get("temp.txt"),
content,
StandardCharsets.UTF_8
);
?
// 讀取文件
String readContent = Files.readString(tempFile, StandardCharsets.UTF_8);
System.out.println("讀取的內容:\n" + readContent);
支持流式讀取文件,適合文件較大的場景:
try (Stream<String> lines = Files.lines(tempFile)) {
lines.filter(line -> !line.isBlank())
.map(String::trim)
.forEach(System.out::println);
}
【了解】Optional 的新方法
Java 11 為 Optional 類添加了 isEmpty() 方法,和之前的 isPresent 正好相反,讓空值檢查更直觀。

Java 12 ~ 13
Java 12 和 13 主要引入了一些預覽特性,其中最重要的是 Switch 表達式和文本塊,這些特性在后續版本中得到了完善和正式化。
Java 14
Java 14 將 Switch 表達式正式化,并引入了 Records、instanceof 模式匹配作為預覽特性。
【必備】Switch 表達式
Java 14 將 Switch 表達式轉正了,讓條件判斷變得更簡潔和安全。
在這之前,傳統的 switch 語句存在不少問題,比如需要手動添加 break 防止穿透、賦值不方便等:
String dayType;
switch (day) {
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
dayType = "工作日";
break;
case SATURDAY:
case SUNDAY:
dayType = "周末";
break;
default:
dayType = "未知";
break;
}
?
// 賦值不方便
int score;
switch (grade) {
case 'A':
System.out.println("優秀!");
score = 90;
break;
case 'B':
score = 80;
break;
default:
score = 0;
}
在 Java 14 之后,可以直接這么寫:
// Java 14 的簡潔寫法
String dayType = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "工作日";
case SATURDAY, SUNDAY -> "周末";
default -> "未知";
};
?
// 支持復雜邏輯的 yield 關鍵字
int score = switch (grade) {
case 'A' -> {
System.out.println("優秀!");
yield 90; // 使用 yield 返回值
}
case 'B' -> 80;
default -> 0;
};
上述代碼中,我們使用了 Switch 表達式增強的幾個特性:
-
箭頭語法:使用
->替代冒號,自動防止 fall-through(不用寫 break 了) -
多標簽支持:
case A, B, C ->一行處理多個條件 -
表達式求值:可以直接使用 yield 關鍵字返回值并賦給變量
這樣一來,多條件判斷變得更優雅了!還能避免忘記 break 導致的邏輯錯誤。
// 實際應用示例:根據月份判斷季節
String season = switch (month) {
case 12, 1, 2 -> "冬季";
case 3, 4, 5 -> "春季";
case 6, 7, 8 -> "夏季";
case 9, 10, 11 -> "秋季";
default -> throw new IllegalArgumentException("無效月份: " + month);
};
【了解】有用的空指針異常
Java 14 改進了 NullPointerException 的錯誤信息。JVM 會提供更詳細的堆棧跟蹤信息,指出導致異常的具體位置和原因,讓調試變得更加容易。

Java 15
Java 15 將文本塊正式化,新增了 Hidden 隱藏類,并引入了 Sealed 類作為預覽特性。
【必備】文本塊
這可能是我最喜歡的特性之一了,因為之前每次復制多行文本到代碼中,都會給我轉成這么一坨:

需要大量的字符串拼接、轉義字符,對于 HTML、SQL 和 JSON 格式來說簡直是噩夢了。
有了 Java 15 的文本塊特性,多行字符串簡直不要太爽!直接用三個引號 """ 括起來,就能以字符串本來的格式展示。

文本塊會保持代碼的縮進、而且內部的引號不需要轉義。
配合 String 的格式化方法,就能輕松傳入參數生成復雜的字符串模板:

【了解】Hidden 隱藏類
Java 15 引入了 Hidden 隱藏類特性,這是一個 專為框架和運行時環境設計 的底層機制,主要是為了優化 動態生成短期類(比如 Lambda 表達式、動態代理)的性能問題,普通開發者無需關心。
在 Lambda 表達式、AOP 動態代理、ORM 映射等場景中,框架會動態生成代碼載體(比如方法句柄、臨時代理類),這些載體需要關聯類的元數據才能運行。如果生成頻繁,傳統類的元數據會被類加載器追蹤,需要等待類加載器卸載才能回收,導致元空間堆積和 GC 壓力。
Hidden 類的特點是對其定義類加載器之外的所有代碼都不可見,由于不可發現且鏈接微弱,JVM 垃圾回收器能夠更高效地卸載隱藏類及其元數據,從而防止短期類堆積對元空間造成壓力,優化了需要動態生成大量類的性能。
Java 16
Java 16 正式發布了 Records 和 instanceof 模式匹配這 2 大特性,讓代碼更簡潔易讀。
【必備】Records
以前,我們如果想創建一個 POJO 對象來存一些數據,需要編寫大量的樣板代碼,包括構造函數、getter 方法、equals、hashCode 等等,比較麻煩。
// Java 16 之前創建數據類的方式
public class Person {
private final String name;
private final int age;
private final String email;
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
即使通過 Lombok 插件簡化了代碼,估計也要十幾行。
有了 Java 16 的 Records,創建數據包裝類簡直不要太簡單,一行代碼搞定:
public record Person(String name, int age, String email) {}
Records 自動提供了所有必需的方法,使用方式完全一樣!
Person person = new Person("魚皮", 25, "yupi@yuyuanweb.com");
System.out.println(person.name()); // 自動生成的訪問器
System.out.println(person.age());
System.out.println(person.email());
System.out.println(person.toString()); // 自動生成的 toString
此外,Records 還支持自定義方法和驗證邏輯,只不過個人建議這種情況下不如老老實實用 “類” 了。
public record BankAccount(String accountNumber, double balance) {
// 構造函數中添加驗證
public BankAccount {
if (balance < 0) {
throw new IllegalArgumentException("余額不能為負數");
}
if (accountNumber == null || accountNumber.isBlank()) {
throw new IllegalArgumentException("賬號不能為空");
}
}
// 自定義方法
public boolean isVIP() {
return balance > 100000;
}
// 靜態工廠方法
public static BankAccount createSavingsAccount(String accountNumber) {
return new BankAccount(accountNumber, 0.0);
}
}
【了解】instanceof 模式匹配
Java 16 正式推出了 instanceof 的模式匹配,讓類型檢查和轉換變得更優雅。
傳統的 instanceof 使用方式,需要顯示轉換對象類型:
Object obj = xxx;
if (obj instanceof String) {
String str = (String) obj; // 需要顯式轉換
return "字符串長度: " + str.length();
}
有了 instanceof 模式匹配,可以直接在匹配類型時聲明變量:
if (obj instanceof String str) {
return "字符串長度: " + str.length();
}
但是要注意,str 變量的作用域被限定在 if 條件為 true 的代碼塊中,符合最小作用域原則。
【了解】Stream 新增方法
Java 16 為 Stream API 添加了 toList() 方法,可以用更簡潔的代碼將流轉換為不可變列表。
// 傳統寫法
List<String> result = stream
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
?
// Java 16 簡化寫法
List<String> result = stream
.filter(s -> s.length() > 3)
.toList(); // 返回不可變 List
還提供了 mapMulti() 方法,跟 flatMap 的作用一樣,將一個元素映射為 0 個或多個元素,但是某些場景下比 flatMap 更靈活高效。
當需要從一個元素生成多個元素時,flatMap 需要先創建一個中間 Stream,而 mapMulti() 可以通過傳入的 Consumer 直接 “推送” 多個元素,避免了中間集合或 Stream 的創建開銷。
// flatMap 傳統方式
List<String> words = List.of("hello", "world", "java");
List<Character> chars = words.stream()
.flatMap(word -> word.chars()
.mapToObj(c -> (char) c))
.toList();
?
// Java 16 的 mapMulti 方式
List<Character> chars = words.stream()
.<Character>mapMulti((word, consumer) -> {
for (char c : word.toCharArray()) {
consumer.accept(c); // 直接向下游推送元素
}
})
.toList();
?? Java 17
Java 17 是目前 Java 最主流的 LTS 版本,比例已經超越了 Java 8!現在很多新的 Java 開發框架和類庫支持的最低 JDK 版本就是 17(比如 AI 開發框架 LangChain4j)。

【實用】Sealed 密封類
在很多 Java 開發者的印象中,一個類要么完全開放繼承(任何類都能繼承),要么完全禁止繼承(final 類)。
// 選擇1:完全開放繼承
public class Shape {
// 問題:不知道會有哪些子類,難以進行窮舉
}
?
// 選擇2:完全禁止繼承
public final class Circle {
// 問題:即使在同一個模塊內也無法繼承
}
其實這樣是沒辦法精確控制繼承關系的,在設計 API 或領域模型時可能會遇到問題。
Java 17 將 Sealed 密封類轉正,讓類的繼承關系變得更可控和安全。
比如我可以只允許某幾個類繼承:
public sealed class Shape
permits Circle, Rectangle, Triangle {
// 只允許這三個類繼承
}
但是,被允許繼承的子類必須選擇一種繼承策略:
1)final:到我為止,不能再繼承了
public final class Circle extends Shape {
}
2)sealed:我也要控制誰能繼承我
public sealed class Triangle extends Shape
permits RightTriangle {
}
3)non-sealed:我開放繼承,任何人都可以繼承我
public non-sealed class Rectangle extends Shape {
}
強制聲明繼承策略是為了 確保設計控制權的完整傳遞。如果不強制聲明,sealed 類精確控制繼承的價值就會被破壞,任何人都可以通過繼承子類來繞過原始設計的限制。
注意,雖然看起來 non-sealed 打破了這個設計,但這也是設計者的主動選擇。如果不需要強制聲明,設計者可能會無意中失去控制權。
有了 Sealed 類后,某個接口可能的實現類型就盡在掌握了,可以讓 switch 模式匹配變得更加安全:
// 編譯器知道所有可能的子類型,可以進行完整性檢查
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.getRadius() * c.getRadius();
case Rectangle r -> r.getWidth() * r.getHeight();
case Triangle t -> 0.5 * t.getBase() * t.getHeight();
// 編譯器確保我們處理了所有情況,無需 default 分支
};
}
【了解】新的隨機數生成器
Java 17 引入了全新的隨機數生成器 API,提供了更優的性能和更多的算法選擇:
// 傳統的隨機數
Random oldRandom = new Random();
int oldValue = oldRandom.nextInt(100);
?
// 新的隨機數生成器
RandomGenerator generator = RandomGenerator.of("L32X64MixRandom");
int newValue = generator.nextInt(100);
【了解】強封裝 JDK 內部 API
Java 17 進一步強化了對 JDK 內部 API 的封裝,一些之前可以通過反射訪問的內部類現在完全不可訪問,比如:
-
sun.misc.Unsafe -
com.sun.*包下的類 -
jdk.internal.*包下的類
雖然這提高了 JDK 的安全性和穩定性,但可能需要遷移一些依賴內部 API 的老代碼。
Java 18
個人感覺 Java 18 提供的功能都沒什么用,簡單了解一下就好。
【了解】簡單 Web 服務器
Java 18 引入了一個簡單的 Web 服務器,主要用于開發和測試。
# 啟動簡單的文件服務器,服務當前目錄
jwebserver
?
# 指定端口和目錄
jwebserver -p 8080 -d /path/to/your/files
?
# 綁定到特定地址
jwebserver -b 127.0.0.1 -p 9000
Nginx 不香么,我要用這個東西?
【了解】UTF-8 作為默認字符集
Java 18 將 UTF-8 設為默認字符集,解決了很多字符編碼相關的問題,Java 程序在不同平臺上的行為會更加一致。
// 這些操作現在默認使用 UTF-8 編碼
FileReader reader = new FileReader("file.txt");
FileWriter writer = new FileWriter("file.txt");
PrintStream out = new PrintStream("output.txt");
在這之前,Java 使用的是 系統默認字符集,會導致同一段代碼在不同操作系統上可能產生完全不同的結果。
【了解】JavaDoc 代碼片段
Java 18 引入了 @snippet 標簽,可以讓 JavaDoc 生成的代碼示例更美觀,而且支持從外部文件引入代碼片段。
/**
* 計算兩個數的最大公約數
*
* {@snippet :
* int a = 48;
* int b = 18;
* int gcd = MathUtils.gcd(a, b);
* System.out.println("GCD: " + gcd); // @highlight substring="GCD"
* }
*
* @param a 第一個數
* @param b 第二個數
* @return 最大公約數
*/
public static int gcd(int a, int b) {
// 實現代碼...
}
?
/**
* 從外部文件引入代碼片段
*
* {@snippet file="examples/QuickSort.java" region="main-algorithm"}
*
* @param arr 要排序的數組
*/
public static void quickSort(int[] arr) {
// 實現代碼...
}
不過這年頭還有開發者閱讀 JavaDoc 么?

Java 19 ~ 20
Java 19 和 20 主要是為一些重大特性做準備,包括虛擬線程、Record 模式、Switch 模式匹配等。
Java 21
Java 21 是魚皮做新項目時使用的首選 LTS 版本。這個版本發布了很多重要特性,其中最重要的是 Virtual Threads 虛擬線程。
【必備】Virtual Threads 虛擬線程
這是 Java 并發編程的革命性突破,也是很多 Java 開發者選擇 21 的理由。
什么是虛擬線程呢?
想象一下,你是一家餐廳的老板。傳統的線程就像是餐廳的服務員,假設每個服務員同時只能服務一桌客人。如果有 1000 桌客人,你就需要 1000 個服務員,但這顯然不現實。餐廳地方不夠,也負擔不起那么多員工的工錢。
在傳統的 Java 線程模型中也是如此。如果每個線程都對應操作系統的一個真實線程,創建成本很高、內存占用也大。當需要處理大量并發請求時,系統可能很快就會被拖垮。
舉個例子,假設開 1000 個線程同時處理網絡請求:
public void handleRequests() {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
// 發送網絡請求,需要等待響應
String result = httpClient.get("https://codefather.cn");
System.out.println("收到響應: " + result);
}).start();
}
}
創建 1000 個線程會消耗大量系統資源(因為對應 1000 個操作系統線程),而且大部分時間線程都在等待網絡響應,很浪費。
而虛擬線程就像是給餐廳引入了一個智能調度系統。服務員不再需要傻傻地等在客人桌邊等菜上桌,而是可以在等待的時候去服務其他客人。當某桌的菜準備好了,系統會自動安排一個空閑的服務員去處理。
我們可以開一個虛擬線程執行器執行同樣的一批任務,這里我用的執行器會為每個任務生成一個虛擬線程來處理:
public void handleRequestsWithVirtualThreads() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
// 同樣的網絡請求代碼
String result = httpClient.get("https://codefather.cn");
System.out.println("收到響應: " + result);
});
}
}
}
同樣是 1000 個,但是 1000 個虛擬線程只需要很少的系統資源(比如映射到 8 個操作系統線程上);而且當虛擬線程等待網絡響應時,會讓出底層的操作系統線程,操作系統線程就會自動切換去執行其他虛擬線程和任務。

總結一下 Virtual Threads 的核心優勢。首先是 超級輕量。一個傳統線程可能需要幾 MB 的內存,而一個虛擬線程只需要幾 KB。你可以輕松創建百萬級別的虛擬線程而不用擔心系統資源。
其次是 編程簡單。你不需要學習復雜的異步編程模式,跟創建一個普通線程的代碼類似,一行代碼就能提交異步任務。當遇到阻塞的 I/O 操作時,虛擬線程會自動讓出底層的操作系統線程。
// 直接創建虛擬線程
public void handleSingleUser(Long userId) {
Thread.ofVirtual().start(() -> {
// 要異步執行的任務
User user = userService.findById(userId);
processUser(user);
});
}
相關面試題:
【必備】Switch 模式匹配
Java 14 版本推出了 Switch 表達式,能夠一行處理多個條件;Java 21 版本進一步優化了 Switch 的能力,新增了模式匹配特性,能夠更輕松地根據對象的類型做不同的處理。
沒有 Switch 模式匹配時,我們需要利用 instanceof 匹配類型:
public String processMessage(Object message) {
if (message instanceof String) {
String textMessage = (String) message;
return "文本消息:" + textMessage;
} else if (message instanceof Integer) {
Integer numberMessage = (Integer) message;
return "數字消息:" + numberMessage;
} else if (message instanceof List) {
List<?> listMessage = (List<?>) message;
return "列表消息,包含 " + listMessage.size() + " 個元素";
} else {
return "未知消息類型";
}
}
有了模式匹配,這段代碼可以變得很優雅,直接在匹配對象類型的同時聲明了變量(跟 instanceof 模式匹配有點像):
public String processMessage(Object message) {
return switch (message) {
case String text -> "文本消息:" + text;
case Integer number -> "數字消息:" + number;
case List<?> list -> "列表消息,包含 " + list.size() + " 個元素";
case null -> "空消息";
default -> "未知消息類型";
};
}
此外,模式匹配還支持 條件判斷,讓處理邏輯更加精細,相當于在 case ... when ... 中寫 if 條件表達式(感覺有點像 SQL 的語法)。
// 根據字符串長度采用不同處理策略
public String processText(String text) {
return switch (text) {
case String s when s.length() < 10 -> "短文本:" + s;
case String s when s.length() < 100 -> "中等文本:" + s.substring(0, 5);
case String s -> "長文本:" + s.substring(0, 10);
};
}
【實用】Record 模式
Record 模式讓數據的解構變得更簡單直觀,可以一次性取出 record 中所有需要的信息。
舉個例子,先定義一些簡單的 Record:
public record Person(String name, int age) {}
public record Address(String city, String street) {}
public record Employee(Person person, Address address, double salary) {}
使用 Record 模式可以直接解構這些數據,不用一層一層取了:
public String analyzeEmployee(Employee emp) {
return switch (emp) {
// 一次性提取所有需要的信息
case Employee(Person(var name, var age), Address(var city, var street), var salary)
when salary > 50000 ->
String.format("%s(%d歲)是高薪員工,住在%s%s,月薪%.0f",
name, age, city, street, salary);
case Employee(Person(var name, var age), var address, var salary) ->
String.format("%s(%d歲)月薪%.0f,住在%s",
name, age, salary, address.city());
};
}
這種寫法適合追求極致簡潔代碼的程序員,可以在一行代碼中同時完成 類型檢查、數據提取 和 條件判斷。
【了解】有序集合
Java 21 的有序集合為我們提供了更直觀的方式來操作集合的頭尾元素,說白了就是補了幾個方法:
List<String> tasks = new ArrayList<>();
tasks.addFirst("魚皮的任務"); // 添加到開頭
tasks.addLast("小阿巴的任務"); // 添加到結尾
?
String firstStr = tasks.getFirst(); // 獲取第一個
String lastStr = tasks.getLast(); // 獲取最后一個
?
String removedFirst = tasks.removeFirst(); // 刪除并返回第一個
String removedLast = tasks.removeLast(); // 刪除并返回最后一個
?
List<String> reversed = tasks.reversed(); // 反轉列表
除了 List 之外,SequencedMap 接口(比如 LinkedHashMap)和 SequencedSet 接口(比如 LinkedHashSet)也新增了類似的方法。本質上都是實現了有序集合接口:

【了解】分代 ZGC
Java 21 中的分代 ZGC 可以說是垃圾收集器領域的一個重大突破。ZGC 從 Java 11 開始就以其超低延遲而聞名,但是它并沒有采用分代的設計思路。
在這之前,ZGC 對所有對象一視同仁,無論是剛創建的新對象還是存活了很久的老對象,都使用同樣的收集策略。這雖然保證了一致的低延遲,但在內存分配密集的應用中,效率并不是最優的。
分代 ZGC 的核心思想是基于一個現象 —— 大部分對象都是 “朝生夕死” 的。它將堆內存劃分為年輕代和老年代兩個區域,年輕代的垃圾收集可以更加頻繁和高效,因為大部分年輕對象很快就會死亡,收集器可以快速清理掉這些垃圾;而老年代的收集頻率相對較低,減少了對長期存活對象的不必要掃描。

Java 22
【了解】外部函數和內存 API
長期以來,Java 程序員想要調用 C/C++ 編寫的本地庫,只能依賴 JNI(Java Native Interface)。但說實話,JNI 的使用體驗并不好,需要手寫膠水代碼、維護頭文件和構建腳本、處理 JNIEnv 和復雜類型轉換,一旦接口頻繁變更,維護成本較高。
外部函數與內存 API(FFM API)提供了標準化、類型安全的方式來從 Java 直接調用本地代碼。FFM API 現在支持幾乎所有主流平臺,性能相比 JNI 可能有一定提升,特別是在頻繁調用本地函數的場景下。
大家不用記憶具體是怎么使用的,只要知道有這個特性就足夠了。
【了解】未命名變量和模式
在開發中,我們可能會遇到這樣的情況:有些變量我們必須聲明,但實際上并不會使用到它們的值。
在這之前,我們只能給這些不使用的變量起一個名字,代碼會顯得有些多余。舉些例子:
try {
processData();
} catch (IOException ignored) { // 只關心異常發生,不關心異常對象
System.out.println("處理數據時出錯了");
}
?
String result = switch (obj) {
case Integer i -> "這是整數: " + i;
case String s -> "這是字符串: " + s;
case Double unused -> "這是浮點數"; // 不需要使用具體的值
default -> "未知類型";
};
有了未命名變量特性,可以使用下劃線 _ 表示不使用的變量代碼,意圖更清晰:
try {
processData();
} catch (IOException _) { // 不關心異常對象
System.out.println("處理數據時出錯了");
}
?
String result = switch (obj) {
case Integer i -> "這是整數: " + i;
case String s -> "這是字符串: " + s;
case Double _ -> "這是浮點數"; // 只關心類型,不關心值
default -> "未知類型";
};
?
// 在解構中也很有用
if (point instanceof Point(var x, var _)) { // 只關心 x 坐標
System.out.println("x 坐標是: " + x);
}
Java 23
【了解】ZGC 默認分代模式
Java 22 引入了分代 ZGC,但當時你需要通過特殊的 JVM 參數來啟用它:
java -XX:+UseZGC -XX:+UnlockExperimentalVMOptions -XX:+UseGenerationalZGC MyApp
而在 Java 23 中,分代模式成為了 ZGC 的默認行為。
雖然聽起來只是個小改動,但這個改變的背后是大量的性能測試和實際應用驗證的結果。Oracle 的工程師們發現,分代 ZGC 在絕大多數應用場景中都能帶來顯著的性能改善,特別是在內存分配密集的應用中,性能提升可能達到數倍之多。
Java 24
【了解】類文件 API
類文件 API 是一個專為框架和工具開發者設計的強大特性。長期以來,如果你想要在運行時動態生成、分析或修改 Java 字節碼,就必須依賴像 ASM、Javassist 或者 CGLIB 這樣的第三方庫。
而且操作字節碼需要深入了解底層細節,學習難度很大,我只能借助 AI 來搞定。
有了類文件 API,操作字節碼變得簡單了一些:
public byte[] generateClass() {
return ClassFile.of().build(ClassDesc.of("com.example.GeneratedClass"), cb -> {
// 添加默認構造函數
cb.withMethod("<init>", MethodTypeDesc.of(ConstantDescs.CD_void), ACC_PUBLIC, mb -> {
mb.withCode(codeb -> {
codeb.aload(0)
.invokespecial(ConstantDescs.CD_Object, "<init>", MethodTypeDesc.of(ConstantDescs.CD_void))
.return_();
});
});
// 添加 sayHello 方法
cb.withMethod("sayHello", MethodTypeDesc.of(ConstantDescs.CD_String), ACC_PUBLIC, mb -> {
mb.withCode(codeb -> {
codeb.ldc("Hello from generated class!")
.areturn();
});
});
});
}
讀取和分析現有的類文件也很簡單:
public void analyzeClass(byte[] classBytes) {
ClassModel cm = ClassFile.of().parse(classBytes);
System.out.println("類名: " + cm.thisClass().asInternalName());
System.out.println("方法列表:");
for (MethodModel method : cm.methods()) {
System.out.println(" - " + method.methodName().stringValue() +
method.methodType().stringValue());
}
}
第三方字節碼庫可能需要一段時間才能跟上新特性的變化,而官方的類文件 API 則能夠與語言特性同步發布,確保開發者能夠使用最新的字節碼功能。
【了解】Stream Gatherers 流收集器
Stream API 自 Java 8 引入以來,極大地改變了我們處理集合數據的方式,但是在一些特定的場景中,傳統的 Stream 操作就顯得力不從心了。Stream Gatherers 正是對 Stream API 的一個重要擴展,它解決了現有 Stream API 在某些復雜數據處理場景中的局限性,補齊了 Stream API 的短板。
如果你想實現一些復雜的數據聚合操作,比如滑動窗口或固定窗口分析,可以直接使用 Java 24 內置的 Gatherers。
// 1. 滑動窗口 - windowSliding(size)
List<Double> prices = Arrays.asList(100.0, 102.0, 98.0, 105.0, 110.0);
?
List<Double> movingAverages = prices.stream()
.gather(Gatherers.windowSliding(3)) // 創建大小為 3 的滑動窗口
.map(window -> {
// window 是 List<Double> 類型,包含 3 個連續元素
return window.stream()
.mapToDouble(Double::doubleValue)
.average()
.orElse(0.0);
})
.collect(Collectors.toList());
?
System.out.println("移動平均值: " + movingAverages);
// 移動平均值: [100.0, 101.66666666666667, 104.33333333333333]
還有更多方法,感興趣的同學可以自己嘗試:

除了內置的 Gatherers 外,還可以自定義 Gatherer,舉一個最簡單的例子 —— 給每個元素添加前綴。先自定義一個 Gatherer:
Gatherer<String, ?, String> addPrefix = Gatherer.ofSequential(
() -> null, // 不需要狀態,所以初始化為 null
(state, element, downstream) -> {
// 給每個元素添加 "前綴-" 并推送到下游
downstream.push("前綴-" + element);
return true; // 繼續處理下一個元素
}
// 不需要 finisher,省略第三個參數
);
Gatherer.ofSequential 方法會返回 Gatherer 接口的實現類:

然后就可以愉快地使用了:
List<String> names = Arrays.asList("魚皮", "編程", "導航");
List<String> prefixedNames = names.stream()
.gather(addPrefix)
.collect(Collectors.toList());
?
System.out.println(prefixedNames);
// 輸出: [前綴-魚皮, 前綴-編程, 前綴-導航]
這個例子展示了 Gatherer 的最基本形態:
-
不需要狀態:第一個參數返回 null,因為我們不需要維護任何狀態
-
簡單轉換:第二個參數接收每個元素,做簡單處理后推送到下游
-
無需收尾:省略第三個參數,因為不需要最終處理
雖然這個例子用 map() 也能實現,但它幫助我們理解了 Gatherer 的基本工作機制。
這就是 Stream Gatherers 強大之處,它能夠維護復雜的內部狀態,并根據業務邏輯靈活地向下游推送結果,讓原本需要手動循環的復雜邏輯變得簡潔優雅。
Stream Gatherers 的另一個優勢是它和現有的 Stream API 完全兼容。你可以在 Stream 管道中的任何位置插入 Gatherer 操作,就像使用 map、filter 或 collect 一樣自然,讓復雜的數據處理變得既強大又優雅。
OK 以上就是本期內容,學會的話記得點贊三連支持,我們下期見。


記得我從大一開始學的就是 Java 8,當時還叫做新特性;后來 Java 11 出了,我用 Java 8;Java 17 出了,我用 Java 8;Java 21 出了,我還用 Java 8。
浙公網安備 33010602011771號