<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      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() {
         @Override
         public void actionPerformed(ActionEvent e) {
             System.out.println("按鈕被點擊了");
        }
      });
      ?
      // 使用線程的傳統寫法
      Thread thread new Thread(new Runnable() {
         @Override
         public void run() {
             System.out.println("線程正在運行");
        }
      });

       

      Lambda 表達式的出現,讓代碼變得簡潔優雅,告別匿名內部類!

      // Java 8 Lambda 寫法
      button.addActionListener(-> System.out.println("按鈕被點擊了"));
      Thread thread new Thread(() -> System.out.println("線程正在運行"));

       

      Lambda 表達式的語法非常靈活,可以根據參數個數和方法代碼的復雜度選擇不同的寫法:

      // 無參數的 Lambda
      Runnable = () -> System.out.println("Hello Lambda!");
      ?
      // 單個參數(可以省略括號)
      Consumer<Stringprinter -> System.out.println(s);
      ?
      // 多個參數
      BinaryOperator<Integeradd = (a, b) -> b;
      Comparator<Stringcomparator = (a, b) -> a.compareTo(b);
      ?
      // 復雜的方法體(需要大括號和 return)
      Function<String, Stringprocessor input -> {
         String processed input.trim().toLowerCase();
         if (processed.isEmpty()) {
             return "空字符串";
        }
         return "處理后的字符串:" processed;
      };

       

      方法引用

      Lambda 表達式還有一個實用特性叫做 方法引用,可以看作是 Lambda 表達式的一種簡寫形式。當 Lambda 表達式只是調用一個已存在的方法時,使用方法引用代碼會更簡潔。

      舉個例子:

      List<Stringnames Arrays.asList("魚皮", "編程導航", "面試鴨");
      ?
      // 使用 Lambda 表達式
      names.forEach(name -> System.out.println(name));
      ?
      // 使用方法引用(更簡潔)
      names.forEach(System.out::println);

       

      實際開發中,方法引用經常用于獲取某個 Java 對象的屬性。比如使用 MyBatis Plus 來構造數據庫查詢條件時,經常會看到下面這種代碼:

      LambdaQueryWrapper<UserlambdaQueryWrapper new LambdaQueryWrapper<>();
      lambdaQueryWrapper.eq(User::getName, "魚皮");

       

      方法引用有幾種不同的形式,包括靜態方法引用、實例方法引用、構造器引用,適用于不同的場景。

      // 靜態方法引用
      List<Stringstrings Arrays.asList("1", "2", "3");
      List<Integernumbers strings.stream()
        .map(Integer::parseInt)  // 等于 s -> Integer.parseInt(s)
        .collect(Collectors.toList());
      ?
      // 實例方法引用
      List<Stringwords Arrays.asList("hello", "world", "java");
      List<StringupperWords words.stream()
        .map(String::toUpperCase)  // 等于 s -> s.toUpperCase()
        .collect(Collectors.toList());
      ?
      // 構造器引用
      List<StringnameList Arrays.asList("魚皮", "編程導航", "面試鴨");
      List<Personpersons nameList.stream()
        .map(Person::new)  // 等于 name -> new Person(name)
        .collect(Collectors.toList());

       

      【必備】函數式接口

      什么是函數式接口?

      函數式接口是 只有一個抽象方法的接口。要玩轉 Lambda 表達式,就必須了解函數式接口,因為 Lambda 表達式的本質是函數式接口的匿名實現

      展開來說,函數式接口定義了 Lambda 表達式的參數和返回值類型,而 Lambda 表達式提供了這個接口的具體實現。兩者相輔相成,讓 Java 函數式編程偉大!

       

      常用的函數式接口

      Java 8 為我們提供了很多內置的函數式接口,讓函數式編程變得簡單直觀。列舉一些常用的函數式接口:

      1)Predicate 用于條件判斷:

      // Predicate<T> 用于條件判斷
      Predicate<IntegerisEven -> == 0;
      Predicate<StringisEmpty String::isEmpty;
      Predicate<StringisNotEmpty isEmpty.negate();  // 取反
      ?
      List<Integernumbers Arrays.asList(1, 2, 3, 4, 5, 6);
      List<IntegerevenNumbers numbers.stream()
        .filter(isEven)
        .collect(Collectors.toList());

       

      2)Function 接口用于數據轉換,支持函數組合,讓代碼邏輯更清晰:

      // Function<T, R> 用于轉換
      Function<String, IntegerstringLength String::length;
      Function<Integer, StringintToString Object::toString;
      ?
      // 函數組合
      Function<String, StringaddPrefix -> "前綴-" s;
      Function<String, StringaddSuffix -> "-后綴";
      Function<String, Stringcombined addPrefix.andThen(addSuffix);
      String result combined.apply("魚皮"); // "前綴-魚皮-后綴"

       

      3)Consumer 和 Supplier 接口分別用于消費和提供數據:

      // Consumer<T> 用于消費數據(無返回值)
      Consumer<Stringprinter System.out::println;
      Consumer<Stringlogger -> log.info("處理數據:{}", s);
      // 組合消費
      Consumer<StringcombinedConsumer printer.andThen(logger);
      ?
      // Supplier<T> 用于提供數據
      Supplier<StringrandomId = () -> UUID.randomUUID().toString();
      Supplier<LocalDateTimenow LocalDateTime::now;

       

      4)BinaryOperator 接口用于二元操作,比如數學運算:

      // BinaryOperator<T> 用于二元操作
      BinaryOperator<Integermax Integer::max;
      BinaryOperator<Stringconcat = (a, b) -> b;

       

      自定義函數式接口

      雖然實際開發中,我們更多的是使用 Java 內置的函數式接口,但大家還是要了解一下自定義函數式接口的寫法,有個印象。

      // 創建自定義函數式接口
      @FunctionalInterface
      public interface Calculator {
         double calculate(double a, double b);
      }

       

      使用自定義函數式接口,代碼會更簡潔:

      // 使用自定義函數式接口
      Calculator addition = (a, b) -> b;
      Calculator subtraction = (a, b) -> b;

       

      ?? 自定義函數式接口時,需要注意:

      1)函數式接口必須是接口類型,不能是類、抽象類或枚舉。

      2)必須且只能包含一個抽象方法。否則 Lambda 表達式可能無法匹配接口。

      3)建議使用 @FunctionalInterface注解。

      雖然這個注解不是強制的,但加上后編譯器會幫你檢查是否符合函數式接口的規范(是否只有一個抽象方法),如果不符合會報錯。

      4)可以包含默認方法 default 和靜態方法 static

      函數式接口允許有多個默認方法和靜態方法,因為它們不是抽象方法,不影響單一抽象方法的要求。

      // 創建自定義函數式接口
      @FunctionalInterface
      public interface Calculator {
         double calculate(double a, double b);
      ?
         // 可以有默認方法
         default double add(double a, double b) {
             return b;
        }
         
         // 可以有靜態方法
         static Calculator multiply() {
             return (a, b) -> b;
        }
      }

       

      【必備】Stream API

      什么是 Stream API?

      Stream API 是 Java 8 另一個重量級特性,它讓集合處理變得既優雅又高效。(學大數據的同學應該對它不陌生)

      在 Stream API 出現之前,我們處理集合數據只能通過傳統的循環,需要大量的樣板代碼。

      比如過濾列表中的數據、將小寫轉為大寫并排序:

      List<Stringwords Arrays.asList("apple", "banana", "cherry");
      ?
      // 傳統的處理方式
      List<Stringresult new ArrayList<>();
      for (String word : words) {
         if (word.length() 5) {
             String upperCase word.toUpperCase();
             result.add(upperCase);
        }
      }
      Collections.sort(result);

       

      如果使用 Stream API,可以讓同樣的邏輯變得更簡潔直觀:

      // 使用 Stream 的方式
      List<Stringresult words.stream()
        .filter(word -> word.length() 5)    // 過濾長度大于 5 的單詞
        .map(String::toUpperCase)             // 轉換為大寫
        .sorted()                             // 排序
        .collect(Collectors.toList());        // 收集結果

       

      這就是 Stream 的作用。Stream 不是數據結構,而是 像工廠流水線 一樣處理數據的工具。數據從一端進入,經歷過濾、轉換、排序等一系列加工步驟后,最終輸出我們想要的結果。這種 鏈式調用 讓代碼讀起來就像自然語言一樣流暢。

       

      Stream 操作類型

      Stream 的操作分為中間操作和終端操作。中間操作是 “懶惰” 的,只有在遇到終端操作時才會真正執行。

      filter 過濾和 map 映射都是中間操作,比如下面這段代碼,并不會對列表進行過濾和轉換:

      List<Integernumbers Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
      ?
      numbers.stream()
        .filter(-> 3) // 中間操作:過濾大于3的數字
        .map(-> n)    // 中間操作:平方

      一些常用的中間操作:

      • filter() - 過濾元素

      • map() - 轉換元素

      • sorted() - 排序

      • distinct() - 去重

      • limit() - 限制數量

      • skip() - 跳過元素

       

      給上面的代碼加上一個終端操作 collect 后,才會真正執行:

      List<Integernumbers Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
      ?
      // 演示中間操作和終端操作
      numbers.stream()
        .filter(-> 3)            // 中間操作:過濾大于3的數字
        .map(-> n)               // 中間操作:平方
        .collect(Collectors.toList()); // 終端操作:收集結果

      一些常用的終端操作:

      • collect() - 收集到集合

      • forEach() - 遍歷每個元素

      • count() - 統計數量

      • findFirst() - 查找第一個

      • anyMatch() - 是否有匹配的

      • reduce() - 歸約操作

       

      實際應用

      分享一些 Stream API 在開發中的典型用例。

      1)對列表進行分組(List 轉為 Map):

      Map<Boolean, List<Integer>> partitioned numbers.stream()
        .filter(-> 3) // 中間操作:過濾大于3的數字
        .map(-> n)    // 中間操作:平方
        .collect(Collectors.partitioningBy(-> == 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<Personpeople 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, DoubleavgAgeByCity 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<IntegerlargeList IntStream.rangeClosed(1, 1000000)
        .boxed()
        .collect(Collectors.toList());
      ?
      // 并行處理,只需要改一個方法調用
      long parallelCount largeList.parallelStream()
        .filter(-> 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<Stringoptional1 Optional.of("Hello");          // 不能為 null
      Optional<Stringoptional2 Optional.ofNullable(getName()); // 可能為 null
      Optional<Stringoptional3 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.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<Stringnames Arrays.asList("張三", null, "李四", "", "王五");
      ?
      // 使用 Optional 進行安全的查找
      Optional<StringfoundName 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 {
         @Override
         public void draw() {
             System.out.println("繪制圓形");
        }
         
         // 可以重寫默認方法
         @Override
         public void drawWithBorder() {
             System.out.println("繪制圓形邊框");
             draw();
        }
      }

       

      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 implements A, B {
         @Override
         public void hello() {
             // 可以調用特定接口的默認方法
             A.super.hello();
             B.super.hello();
             // 或者提供自己的實現
             System.out.println("Hello from C");
        }
      }

       

      類似的,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<StringoldList new ArrayList<>();
      oldList.add("蘋果");
      oldList.add("香蕉");
      oldList.add("魚皮");
      List<StringimmutableList Collections.unmodifiableList(oldList);
      ?
      // 或者使用 Google Guava 等第三方庫
      List<StringguavaList ImmutableList.of("蘋果", "香蕉", "魚皮");

       

      有了 Java 9 的工廠方法,創建不可變集合簡直不要太簡單!

      // Java 9 的簡潔寫法
      List<Stringfruits List.of("蘋果", "香蕉", "魚皮");
      Set<Integernumbers Set.of(1, 2, 3, 4, 5);
      Map<String, Integerscores Map.of(
         "張三", 85, 
         "魚皮", 92, 
         "狗剩", 78
      );

       

      這些集合是真正不可變的,任何修改操作都會拋出 UnsupportedOperationException 異常。

       

      如果想創建包含大量元素的不可變 Map,可以使用 ofEntries 方法:

      Map<String, StringlargeMap 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 <= || height <= 0) {
                 throw new IllegalArgumentException("寬度和高度必須為正數");
            }
             return width height;
        }
         
         default double calculateTriangleArea(double base, double height) {
             // 重復的驗證邏輯
             if (base <= || height <= 0) {
                 throw new IllegalArgumentException("底邊和高度必須為正數");
            }
             return base height 2;
        }
      }

      你會把重復的驗證邏輯寫在哪里呢?

      答案很簡單,寫在一個外部工具類里,或者在接口內再寫一個通用的驗證方法:

      public interface Calculator {
         // 通用的驗證方法
         default void validate(double x, double y) {
             if (<= || <= 0) {
                 throw new IllegalArgumentException("必須為正數");
            }
        }
      }

      但這種方式存在一個問題,validate 作為 default 方法,它會成為接口的公共 API,所有實現類都能訪問到!其實這個方法只需要在接口內可以使用就夠了。

      Java 9 解決了這個問題,允許在接口中定義私有方法(以及私有靜態方法)。

      public interface Calculator {
         // 私有方法
         private void validate(double x, double y) {
             if (<= || <= 0) {
                 throw new IllegalArgumentException("必須為正數");
            }
        }
      ?
         // 私有靜態方法
         private static void validatePositive(double x, double y) {
             if (<= || <= 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<Stringlist 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<Stringresponse 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<StringcustomResponse 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() {
             @Override
             public void onOpen(WebSocket webSocket) {
                 System.out.println("WebSocket 連接已打開");
                 webSocket.sendText("Hello WebSocket!", true);
            }
             @Override
             public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
                 System.out.println("收到消息: " data);
                 return null;
            }
        })
        .join();

       

      上面這些代碼都不用記,現在直接把接口文檔甩給 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<Stringlines 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, -> "冬季";
         case 3, 4, -> "春季"; 
         case 6, 7, -> "夏季";
         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; }
         
         @Override
         public boolean equals(Object obj) {
             if (this == obj) return true;
             if (obj == null || getClass() != obj.getClass()) return false;
             Person person = (Person) obj;
             return age == person.age && 
                    Objects.equals(name, person.name) && 
                    Objects.equals(email, person.email);
        }
         
         @Override
         public int hashCode() {
             return Objects.hash(name, age, 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<Stringresult stream
        .filter(-> s.length() 3)
        .collect(Collectors.toList());
      ?
      // Java 16 簡化寫法
      List<Stringresult stream
        .filter(-> s.length() 3)
        .toList();  // 返回不可變 List

       

      還提供了 mapMulti() 方法,跟 flatMap 的作用一樣,將一個元素映射為 0 個或多個元素,但是某些場景下比 flatMap 更靈活高效。

      當需要從一個元素生成多個元素時,flatMap 需要先創建一個中間 Stream,而 mapMulti() 可以通過傳入的 Consumer 直接 “推送” 多個元素,避免了中間集合或 Stream 的創建開銷。

      // flatMap 傳統方式
      List<Stringwords List.of("hello", "world", "java");
      List<Characterchars words.stream()
        .flatMap(word -> word.chars()
            .mapToObj(-> (char) c))
        .toList();
      ?
      // Java 16 的 mapMulti 方式
      List<Characterchars 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 -> Math.PI c.getRadius() c.getRadius();
             case Rectangle -> r.getWidth() r.getHeight();
             case Triangle -> 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 0; 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 0; 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);
        });
      }

      相關面試題:什么是協程?Java 支持協程嗎?

       

      【必備】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 when s.length() 10 -> "短文本:" s;
             case String when s.length() 100 -> "中等文本:" s.substring(0, 5);
             case String -> "長文本:" 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<Stringtasks new ArrayList<>();
      tasks.addFirst("魚皮的任務");    // 添加到開頭
      tasks.addLast("小阿巴的任務");   // 添加到結尾
      ?
      String firstStr tasks.getFirst();  // 獲取第一個
      String lastStr tasks.getLast();   // 獲取最后一個
      ?
      String removedFirst tasks.removeFirst();  // 刪除并返回第一個
      String removedLast tasks.removeLast();    // 刪除并返回最后一個
      ?
      List<Stringreversed 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;
         case String -> "這是字符串: " s;
         case Double unused -> "這是浮點數";  // 不需要使用具體的值
         default -> "未知類型";
      };

       

      有了未命名變量特性,可以使用下劃線 _ 表示不使用的變量代碼,意圖更清晰:

      try {
         processData();
      catch (IOException _) { // 不關心異常對象
         System.out.println("處理數據時出錯了");
      }
      ?
      String result switch (obj) {
         case Integer -> "這是整數: " i;
         case String -> "這是字符串: " 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<Doubleprices Arrays.asList(100.0, 102.0, 98.0, 105.0, 110.0);
      ?
      List<DoublemovingAverages 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, ?, StringaddPrefix Gatherer.ofSequential(
        () -> null,  // 不需要狀態,所以初始化為 null
        (state, element, downstream) -> {
             // 給每個元素添加 "前綴-" 并推送到下游
             downstream.push("前綴-" element);
             return true;  // 繼續處理下一個元素
        }
         // 不需要 finisher,省略第三個參數
      );

       

      Gatherer.ofSequential 方法會返回 Gatherer 接口的實現類:

       

      然后就可以愉快地使用了:

      List<Stringnames Arrays.asList("魚皮", "編程", "導航");
      List<StringprefixedNames 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 以上就是本期內容,學會的話記得點贊三連支持,我們下期見。

      更多編程學習資源

      posted @ 2025-09-05 11:18  程序員魚皮  閱讀(499)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产精品国三级国产av| 欧美性潮喷xxxxx免费视频看| 黑人猛精品一区二区三区| 狠狠色噜噜狠狠亚洲AV| 国产v亚洲v天堂a无码| 国产精品99久久免费| 成人自拍小视频在线观看| 午夜福利国产精品视频| 乾安县| 亚洲高清国产自产拍av| 视频一区二区三区四区久久| 91亚洲免费视频| 国产av国片精品一区二区| 久久人人97超碰国产精品| 日本一道一区二区视频| 国产无套内射又大又猛又粗又爽 | 国产精品小仙女自拍视频| 亚洲精品日本久久一区二区三区| 一二三四日本高清社区5| 高清无码爆乳潮喷在线观看| 国产网友愉拍精品视频手机| 在线欧美精品一区二区三区| 国产精品七七在线播放| 亚洲中文字幕成人综合网| 毛片av中文字幕一区二区| 亚洲一区二区偷拍精品| 熟女精品视频一区二区三区| 久久精品国产亚洲欧美| 日韩有码中文在线观看| 日韩高清砖码一二区在线| 欧洲极品少妇| 又粗又硬又黄a级毛片| 蜜臀久久99精品久久久久久| 亚洲岛国成人免费av| 人妻人人澡人人添人人爽 | 亚洲av成人区国产精品| 国内精品伊人久久久久影院对白| 岢岚县| 少妇爽到呻吟的视频| 丰满少妇又爽又紧又丰满在线观看| 亚洲av无码精品色午夜蛋壳|