<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>沉潜飞动</title>
  
  <subtitle>君子藏器于身，待时而动。</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://www.howardliu.cn/"/>
  <updated>2025-01-14T15:06:10.000Z</updated>
  <id>https://www.howardliu.cn/</id>
  
  <author>
    <name>Howard Liu</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>正则表达式实用指南（三）：边界匹配、Pattern模式、Matcher方法</title>
    <link href="https://www.howardliu.cn/regular-expressions-java-3/"/>
    <id>https://www.howardliu.cn/regular-expressions-java-3/</id>
    <published>2025-01-14T15:06:10.000Z</published>
    <updated>2025-01-14T15:06:10.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/20250114225842777.jpg" alt="正则表达式实用指南（三）：边界匹配、Pattern模式、Matcher方法"></p><blockquote><p> 本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>你好，我是看山。</p><p>正则表达式是Java变成中一把利器，常出现在文本检查、替换等逻辑中。本文中，我们将深入探讨 Java 正则表达式 API，并研究如何在 Java 编程语言中运用正则表达式。</p><p>在正则表达式的领域中，存在多种不同的“风格”可供选择，例如 grep、Perl、Python、PHP、awk 等等。这就意味着，在一种编程语言中有效的正则表达式，在另一种语言中可能无法正常工作。Java 中的正则表达式语法与 Perl 中的最为相似。</p><a id="more"></a><ul><li><a href="https://mp.weixin.qq.com/s/i1i9dc1IQknVni0K-4Ydtw" target="_blank" rel="noopener">一、元字符及关系运算</a></li><li><a href="https://mp.weixin.qq.com/s/KoOKKMV2YT3WKDMUhB_14A" target="_blank" rel="noopener">转义字符、量词和捕获小能手</a></li></ul><h2 id="八、边界匹配器"><a href="#八、边界匹配器" class="headerlink" title="八、边界匹配器"></a>八、边界匹配器</h2><p>Java 正则表达式 API 还支持边界匹配，如果我们需要判断匹配的位置，就可以使用边界匹配器。</p><p>要仅在文本开头满足所需正则表达式时进行匹配，我们使用“^”。下面的测试将通过，因为“dog”在文本开头：</p><pre><code class="java">int matches = runTest(&quot;^dog&quot;, &quot;dogs are friendly&quot;);assertTrue(matches &gt; 0);</code></pre><p>下面示例就会匹配失败：</p><pre><code class="java">int matches = runTest(&quot;^dog&quot;, &quot;are dogs are friendly?&quot;);assertFalse(matches &gt; 0);</code></pre><p>要仅在文本结尾满足所需正则表达式时进行匹配，我们使用“$”。在下面的情况下，我们会找到匹配：</p><pre><code class="java">int matches = runTest(&quot;dog$&quot;, &quot;Man&#39;s best friend is a dog&quot;);assertTrue(matches &gt; 0);</code></pre><p>下面示例就会匹配失败：</p><pre><code class="java">int matches = runTest(&quot;dog$&quot;, &quot;is a dog man&#39;s best friend?&quot;);assertFalse(matches &gt; 0);</code></pre><p><code>\\b</code>表示单词边界，正则表达是中所说的单词是指<code>\w</code>+空格，即数字、大小写字母、下划线、空格。</p><pre><code class="java">int matches = runTest(&quot;\\bdog\\b&quot;, &quot;a dog is friendly&quot;);assertEquals(matches, 1);</code></pre><p>行首的空字符串也是一个单词边界：</p><pre><code class="java">int matches = runTest(&quot;\\bdog\\b&quot;, &quot;dog is man&#39;s best friend&quot;);assertEquals(matches, 1);</code></pre><p>下面这个示例则会无匹配字符：</p><pre><code class="java">int matches = runTest(&quot;\\bdog\\b&quot;, &quot;snoop dogg is a rapper&quot;);assertEquals(matches, 0);</code></pre><p>两个连续的单词字符不标记为单词边界，但我们可以通过更改正则表达式的结尾来查找非单词边界，使其通过测试：</p><pre><code class="java">int matches = runTest(&quot;\\bdog\\B&quot;, &quot;snoop dogg is a rapper&quot;);assertEquals(matches, 1);</code></pre><p>这里用到了<code>\B</code>，表示非边界单词，与<code>\b</code>正好相反。</p><h2 id="九、Pattern-类方法"><a href="#九、Pattern-类方法" class="headerlink" title="九、Pattern 类方法"></a>九、Pattern 类方法</h2><p><code>Pattern</code>的<code>compile</code>方法可以传入一组标志，这些标志会影响匹配方式。</p><p>让我们在测试类中重载<code>runTest</code>方法，使其能够接受一个标志：</p><pre><code class="java">public static int runTest(String regex, String text, int flags) {    Pattern pattern = Pattern.compile(regex, flags);    Matcher matcher = pattern.matcher(text);    int matches = 0;    while (matcher.find()) {        matches++;    }    return matches;}</code></pre><p><code>Pattern</code>提供了标记的常量，我们一一看一下。</p><h3 id="Pattern-CANON-EQ"><a href="#Pattern-CANON-EQ" class="headerlink" title="Pattern.CANON_EQ"></a>Pattern.CANON_EQ</h3><p>当指定此标志时，两个字符只有在其完整的规范分解匹配时才被视为匹配。主要用于组合码与组成元素之间的匹配。</p><p>比如，带重音的 Unicode 字符“é”。它的组合码点是“u00E9”，Unicode 也为其组成字符“e”（“u0065”）和锐音符（“u0301”）提供了单独的码点。如果启用<code>CANON_EQ</code>模式，组合字符“u00E9”与两个字符序列“u0065 u0301”可以算作匹配的。</p><p>默认情况下，下面示例是不会命中：</p><pre><code class="java">int matches = runTest(&quot;\u00E9&quot;, &quot;\u0065\u0301&quot;);assertEquals(matches, 0);</code></pre><p>如果我们设置了<code>CANON_EQ</code>标志，测试将通过：</p><pre><code class="java">int matches = runTest(&quot;\u00E9&quot;, &quot;\u0065\u0301&quot;, Pattern.CANON_EQ);assertTrue(matches &gt; 0);</code></pre><h3 id="Pattern-CASE-INSENSITIVE"><a href="#Pattern-CASE-INSENSITIVE" class="headerlink" title="Pattern.CASE_INSENSITIVE"></a>Pattern.CASE_INSENSITIVE</h3><p>此标志启用不区分大小写的匹配。</p><p>默认情况下，匹配是区分大小写的：</p><pre><code class="java">int matches = runTest(&quot;dog&quot;, &quot;This is a Dog&quot;);assertEquals(matches, 0);</code></pre><p>我们可以使用<code>CASE_INSENSITIVE</code>设置为不区分大小写：</p><pre><code class="java">int matches = runTest(&quot;dog&quot;, &quot;This is a Dog&quot;, Pattern.CASE_INSENSITIVE);assertTrue(matches &gt; 0);</code></pre><p>我们可以使用内联修饰符<code>(?i)</code>，可以使正则表达式的匹配过程忽略大小写，使用<code>(?-i)</code>关闭。</p><pre><code class="java">int matches = runTest(&quot;(?i)d(?-i)og&quot;, &quot;This is a Dog&quot;);assertTrue(matches &gt; 0);</code></pre><h3 id="Pattern-COMMENTS"><a href="#Pattern-COMMENTS" class="headerlink" title="Pattern.COMMENTS"></a>Pattern.COMMENTS</h3><p>Java API 允许我们在正则表达式中使用“#”添加注释。这有助于为复杂的正则表达式添加文档说明，使其对其他程序员更易理解。</p><p><code>COMMENTS</code>标志可以忽略正则表达式中的任何空白字符或注释。</p><p>在默认匹配模式下，下面这种会无匹配：</p><pre><code class="java">int matches = runTest(&quot;dog$  #check for word dog at end of text&quot;, &quot;This is a dog&quot;);assertEquals(matches, 0);</code></pre><p>这是因为匹配器会在输入文本中查找整个正则表达式，包括空格和“#”字符。但是当我们使用<code>COMMENTS</code>标志时，它会忽略多余的空格，并且每行中以“#”开头的所有文本都将被视为注释而被忽略：</p><pre><code class="java">int matches = runTest(&quot;dog$  #check end of text&quot;, &quot;This is a dog&quot;, Pattern.COMMENTS);assertTrue(matches &gt; 0);</code></pre><p>这里也有一个内联修饰符<code>(?x)</code>，可以启用扩展模式，在这种模式下：白空格（如空格、制表符、换行符等）在正则表达式中被忽略；可以使用 # 开始的注释，直到行尾。</p><p>比如：</p><pre><code class="java">int matches = runTest(&quot;(?x)dog$  #check end of text&quot;, &quot;This is a dog&quot;);assertTrue(matches &gt; 0);</code></pre><h3 id="Pattern-DOTALL"><a href="#Pattern-DOTALL" class="headerlink" title="Pattern.DOTALL"></a>Pattern.DOTALL</h3><p>默认情况下，当我们在正则表达式中使用点“.”表达式时，它会匹配输入字符串中的每个字符，直到遇到换行符为止。使用<code>DOTALL</code>标志后，可以匹配换行符。</p><p>首先，来看默认行为：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;(.*)&quot;);Matcher matcher = pattern.matcher(&quot;this is a text&quot; + System.getProperty(&quot;line.separator&quot;)                + &quot; continued on another line&quot;);matcher.find();assertEquals(&quot;this is a text&quot;, matcher.group(1));</code></pre><p>如我们所见，匹配了换行符之前的部分。如果在<code>DOTALL</code>模式下，整个文本，包括换行符，都会被匹配：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;(.*)&quot;, Pattern.DOTALL);Matcher matcher = pattern.matcher(&quot;this is a text&quot; + System.getProperty(&quot;line.separator&quot;)                + &quot; continued on another line&quot;);matcher.find();assertEquals(&quot;this is a text&quot; + System.getProperty(&quot;line.separator&quot;)                + &quot; continued on another line&quot;, matcher.group(1));</code></pre><p>我们也可以使用内联表达式<code>(?s)</code>开启单行模式，与<code>DOTALL</code>标志行为相同。</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;(?s)(.*)&quot;);Matcher matcher = pattern.matcher(&quot;this is a text&quot; + System.getProperty(&quot;line.separator&quot;)                + &quot; continued on another line&quot;);matcher.find();assertEquals(&quot;this is a text&quot; + System.getProperty(&quot;line.separator&quot;)                + &quot; continued on another line&quot;, matcher.group(1));</code></pre><h3 id="Pattern-LITERAL"><a href="#Pattern-LITERAL" class="headerlink" title="Pattern.LITERAL"></a>Pattern.LITERAL</h3><p>在这种模式下，匹配器不会给任何元字符、转义字符或正则表达式语法赋予特殊含义。</p><p>在没有此标志时，匹配器会将以下正则表达式与任何输入字符串进行匹配：</p><pre><code class="java">int matches = runTest(&quot;(.*)&quot;, &quot;text&quot;);assertTrue(matches &gt; 0);</code></pre><p>使用<code>LITERAL</code>标志时，如果输入字符串与正则表达式字面相同才会匹配，否则不会找到匹配，因为匹配器会查找<code>(.*)</code>而不是对其进行解释：</p><pre><code class="java">int matches = runTest(&quot;(.*)&quot;, &quot;text&quot;, Pattern.LITERAL);assertEquals(matches, 0);</code></pre><p>上面示例中，如果待匹配字符串变为<code>text(.*)</code>，<code>matches</code>就是1。</p><h3 id="Pattern-MULTILINE"><a href="#Pattern-MULTILINE" class="headerlink" title="Pattern.MULTILINE"></a>Pattern.MULTILINE</h3><p>默认情况下，“^”和“$”元字符分别绝对匹配整个输入字符串的开头和结尾。匹配器会忽略任何换行符：</p><pre><code class="java">int matches = runTest(&quot;dog$&quot;,        &quot;This is a dog&quot; + System.getProperty(&quot;line.separator&quot;) + &quot;this is a fox&quot;);assertEquals(matches, 0);</code></pre><p>这个匹配会失败，因为匹配器在整个字符串的末尾查找“dog”，但是示例中文本是分两行，会认为并没有结束。</p><p>使用<code>MULTILINE</code>标志后，匹配器会考虑换行符，遇到换行符即认为结束：</p><pre><code class="java">int matches = runTest(&quot;dog$&quot;,        &quot;This is a dog&quot; + System.getProperty(&quot;line.separator&quot;) + &quot;this is a fox&quot;,        Pattern.MULTILINE);assertTrue(matches &gt; 0);</code></pre><p>我们也可以使用内联表达式<code>(?m)</code>实现相同逻辑：</p><pre><code class="java">int matches = runTest(&quot;(?m)dog$&quot;,        &quot;This is a dog&quot; + System.getProperty(&quot;line.separator&quot;) + &quot;this is a fox&quot;);assertTrue(matches &gt; 0);</code></pre><h2 id="十、Matcher-类方法"><a href="#十、Matcher-类方法" class="headerlink" title="十、Matcher 类方法"></a>十、Matcher 类方法</h2><h3 id="（一）索引方法"><a href="#（一）索引方法" class="headerlink" title="（一）索引方法"></a>（一）索引方法</h3><p>索引方法提供了有用的索引值，能准确地告诉我们在输入字符串中匹配项的位置。</p><p>在以下测试中，我们将确认输入字符串中“dog”的匹配起始和结束索引：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;dog&quot;);Matcher matcher = pattern.matcher(&quot;This dog is mine&quot;);matcher.find();assertEquals(5, matcher.start());assertEquals(8, matcher.end());</code></pre><h3 id="（二）查找方法"><a href="#（二）查找方法" class="headerlink" title="（二）查找方法"></a>（二）查找方法</h3><p>查找方法遍历输入字符串，并返回一个布尔值，指示是否找到模式。常用的方法是<code>matches</code>和<code>lookingAt</code>。</p><p><code>matches</code>和<code>lookingAt</code>方法都尝试将输入序列与模式进行匹配。区别在于<code>matches</code>要求整个输入序列都匹配，而<code>lookingAt</code>不需要。</p><p>这两个方法都从输入字符串的开头开始：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;dog&quot;);Matcher matcher = pattern.matcher(&quot;dogs are friendly&quot;);assertTrue(matcher.lookingAt());assertFalse(matcher.matches());</code></pre><p>在以下情况下，<code>matches</code>方法将返回 true：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;dog&quot;);Matcher matcher = pattern.matcher(&quot;dog&quot;);assertTrue(matcher.matches());</code></pre><h3 id="（三）替换方法"><a href="#（三）替换方法" class="headerlink" title="（三）替换方法"></a>（三）替换方法</h3><p>替换方法用于替换输入字符串中的文本。常用的方法是<code>replaceFirst</code>和<code>replaceAll</code>。</p><p><code>replaceFirst</code>和<code>replaceAll</code>方法替换与给定正则表达式匹配的文本。顾名思义，<code>replaceFirst</code>替换第一次出现的匹配项，<code>replaceAll</code>替换所有匹配项：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;dog&quot;);Matcher matcher = pattern.matcher(&quot;dogs are domestic animals, dogs are friendly&quot;);String newStr = matcher.replaceFirst(&quot;cat&quot;);assertEquals(&quot;cats are domestic animals, dogs are friendly&quot;, newStr);</code></pre><p>替换所有匹配项：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;dog&quot;);Matcher matcher = pattern.matcher(&quot;dogs are domestic animals, dogs are friendly&quot;);String newStr = matcher.replaceAll(&quot;cat&quot;);assertEquals(&quot;cats are domestic animals, cats are friendly&quot;, newStr);</code></pre><p><code>replaceAll</code>方法允许我们用相同的替换内容替换所有匹配项。</p><p><code>String</code>类的<code>replaceFirst</code>和<code>replaceAll</code>方法啊，底层也是使用了<code>Matcher</code>的方法。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>正则表达式是一个开发利器，用的好的话，会大大提升我们的开发效率。本文介绍了边界匹配、Pattern模式、Matcher方法。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=3757675060425670656#wechat_redirect" target="_blank" rel="noopener">并发编程</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=3809802644269318145#wechat_redirect" target="_blank" rel="noopener">MapStruct实用手册</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1750195552933199874#wechat_redirect" target="_blank" rel="noopener">一个架构师的素养修炼</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/4lOmXnKMHf3YUkYWfK_PYg" target="_blank" rel="noopener">原来OpenFeign功能这么强大</a></li><li><a href="https://mp.weixin.qq.com/s/iVo-Cr8shxjmh9UK_rWQMw" target="_blank" rel="noopener">原来OpenFeign功能这么强大（第二弹）</a></li><li><a href="https://mp.weixin.qq.com/s/_032A7UPP4SgqrpYolSyiQ" target="_blank" rel="noopener">使用OpenFeign的5个步骤和7个高级功能</a></li><li><a href="https://mp.weixin.qq.com/s/Ahsz70-30IYO-9hQ0VDefw" target="_blank" rel="noopener">由浅入深掌握CompletableFuture的七种用法</a></li><li><a href="https://mp.weixin.qq.com/s/mAthOXL3eZNUiuV7SPp7Cw" target="_blank" rel="noopener">全新的HttpClient，现代高效的网络通信利器</a></li><li><a href="https://mp.weixin.qq.com/s/b2WKGF9Z0gUkYYRP7PdybA" target="_blank" rel="noopener">Java 中反射、内省的性能差距居然如此</a></li><li><a href="https://mp.weixin.qq.com/s/vXJxyTm2BWZtUV8Kiwp3NQ" target="_blank" rel="noopener">比反射更好用的内省</a></li><li><a href="https://mp.weixin.qq.com/s/ak_t3qQ7OPI116xHL2RO1w" target="_blank" rel="noopener">异常处理的9条建议</a></li><li><a href="https://mp.weixin.qq.com/s/nkS8bPfyQ1L9knPrpGrxbA" target="_blank" rel="noopener">Java原生支持Lombok了</a></li><li><a href="https://mp.weixin.qq.com/s/ZCm71il_n5FG8gAHSiKCzQ" target="_blank" rel="noopener">MapStruct教程</a></li><li><a href="https://mp.weixin.qq.com/s/KXTzTcxvf3pqXzEe2tdYOg" target="_blank" rel="noopener">手写重试器</a></li><li><a href="https://mp.weixin.qq.com/s/ETg09gegy4a5i_fdgUUx7w" target="_blank" rel="noopener">等待线程完成的三种常用方式</a></li><li><a href="https://mp.weixin.qq.com/s/F3Pv1B5-2XErTxK6qqkoTA" target="_blank" rel="noopener">如何在线程完成时执行回调</a></li><li><a href="https://mp.weixin.qq.com/s/N-cLbHHqBKxUz4bqptMSJg" target="_blank" rel="noopener">由浅入深掌握Future</a></li><li><a href="https://mp.weixin.qq.com/s/Z0hjAKuiRZI2JOd-CUPETQ" target="_blank" rel="noopener">由浅入深掌握ExecutorService</a></li><li><a href="https://mp.weixin.qq.com/s/my64BSuMnJzX8bfhIblwPQ" target="_blank" rel="noopener">Fork/Join框架快速上手指南</a></li><li><a href="https://mp.weixin.qq.com/s/L5NUIde7li1sayF6hTqncw" target="_blank" rel="noopener">如何延迟执行一段代码</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><ul><li>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a></li><li>个人博文：<a href="https://www.howardliu.cn/">正则表达式实用指南（三）：边界匹配、Pattern模式、Matcher方法</a></li><li>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a></li><li>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">正则表达式实用指南（三）：边界匹配、Pattern模式、Matcher方法</a></li></ul><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;img src=&quot;https://static.howardliu.cn/20250114225842777.jpg&quot; alt=&quot;正则表达式实用指南（三）：边界匹配、Pattern模式、Matcher方法&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt; 本文收录在 &lt;a href=&quot;https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&amp;action=getalbum&amp;album_id=1732392238946533378&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;《从小工到专家的 Java 进阶之旅》&lt;/a&gt; 系列专栏中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;你好，我是看山。&lt;/p&gt;
&lt;p&gt;正则表达式是Java变成中一把利器，常出现在文本检查、替换等逻辑中。本文中，我们将深入探讨 Java 正则表达式 API，并研究如何在 Java 编程语言中运用正则表达式。&lt;/p&gt;
&lt;p&gt;在正则表达式的领域中，存在多种不同的“风格”可供选择，例如 grep、Perl、Python、PHP、awk 等等。这就意味着，在一种编程语言中有效的正则表达式，在另一种语言中可能无法正常工作。Java 中的正则表达式语法与 Perl 中的最为相似。&lt;/p&gt;
    
    </summary>
    
    
      <category term="regular" scheme="https://www.howardliu.cn/categories/regular/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="regular" scheme="https://www.howardliu.cn/tags/regular/"/>
    
  </entry>
  
  <entry>
    <title>正则表达式实用指南（二）：转移字符、量词和捕获小能手</title>
    <link href="https://www.howardliu.cn/regular-expressions-java-2/"/>
    <id>https://www.howardliu.cn/regular-expressions-java-2/</id>
    <published>2025-01-13T15:35:47.000Z</published>
    <updated>2025-01-13T15:35:47.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/20250113233104754.jpg" alt="正则表达式实用指南（二）：转移字符、量词和捕获小能手"></p><blockquote><p> 本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>你好，我是看山。</p><p>正则表达式是Java变成中一把利器，常出现在文本检查、替换等逻辑中。本文中，我们将深入探讨 Java 正则表达式 API，并研究如何在 Java 编程语言中运用正则表达式。</p><p>在正则表达式的领域中，存在多种不同的“风格”可供选择，例如 grep、Perl、Python、PHP、awk 等等。这就意味着，在一种编程语言中有效的正则表达式，在另一种语言中可能无法正常工作。Java 中的正则表达式语法与 Perl 中的最为相似。</p><a id="more"></a><ul><li><a href="https://mp.weixin.qq.com/s/i1i9dc1IQknVni0K-4Ydtw" target="_blank" rel="noopener">一、元字符及关系运算</a></li></ul><h2 id="五、预定义字符类"><a href="#五、预定义字符类" class="headerlink" title="五、预定义字符类"></a>五、预定义字符类</h2><p>Java 正则表达式 API 也接受预定义字符类。Java 版本正则表达式的一个特殊之处在于转义字符。</p><p>如我们所见，大多数预定义字符类以反斜杠“\”开头，而反斜杠在 Java 中有特殊含义。为了让<code>Pattern</code>类能够正确编译这些字符类，必须对开头的反斜杠进行转义，即<code>\d</code>要写成<code>\\d</code>。</p><p>匹配数字，等同于<code>[0-9]</code>：</p><pre><code class="java">int matches = runTest(&quot;\\d&quot;, &quot;123&quot;);assertEquals(matches, 3);</code></pre><p>匹配非数字，等同于<code>[^0-9]</code>：</p><pre><code class="java">int matches = runTest(&quot;\\D&quot;, &quot;a6c&quot;);assertEquals(matches, 2);</code></pre><p>匹配空白字符：</p><pre><code class="java">int matches = runTest(&quot;\\s&quot;, &quot;a c&quot;);assertEquals(matches, 1);</code></pre><p>匹配非空白字符：</p><pre><code class="java">int matches = runTest(&quot;\\S&quot;, &quot;a c&quot;);assertEquals(matches, 2);</code></pre><p>匹配单词字符，等同于“[a-zA-Z_0-9]”：</p><pre><code class="java">int matches = runTest(&quot;\\w&quot;, &quot;hi!&quot;);assertEquals(matches, 2);</code></pre><p>匹配非单词字符：</p><pre><code class="java">int matches = runTest(&quot;\\W&quot;, &quot;hi!&quot;);assertEquals(matches, 1);</code></pre><h2 id="六、量词"><a href="#六、量词" class="headerlink" title="六、量词"></a>六、量词</h2><p>Java 正则表达式 API 还允许我们使用量词。通过量词，我们可以指定匹配的次数，进一步调整匹配行为。</p><p>要匹配文本零次或一次，我们使用<code>?</code>量词：</p><pre><code class="java">int matches = runTest(&quot;\\a?&quot;, &quot;hi&quot;);assertEquals(matches, 3);</code></pre><p>或者，我们也可以使用花括号语法，Java 正则表达式 API 也支持这种方式：</p><pre><code class="java">int matches = runTest(&quot;\\a{0,1}&quot;, &quot;hi&quot;);assertEquals(matches, 3);</code></pre><p>这个例子引入了零长度匹配的概念。如果量词的匹配阈值为零，它将匹配文本中的所有内容，包括每个输入末尾的空字符串。这意味着，即使输入为空，它也会返回一个零长度匹配。</p><p>这就解释了为什么在上面的例子中，尽管输入字符串的长度为 2，但我们得到了三个匹配。第三个匹配就是零长度的空字符串。</p><p>要匹配文本零次或无数次，我们使用<code>*</code>量词，它与``?`类似：</p><pre><code class="java">int matches = runTest(&quot;\\a*&quot;, &quot;hi&quot;);assertEquals(matches, 3);</code></pre><p>支持的替代方式：</p><pre><code class="java">int matches = runTest(&quot;\\a{0,}&quot;, &quot;hi&quot;);assertEquals(matches, 3);</code></pre><p>与<code>*</code>和<code>?</code>不同的量词是<code>+</code>，它的匹配阈值为 1。如果所需字符串根本不出现，就不会有匹配，甚至连零长度字符串也不会匹配：</p><pre><code class="java">int matches = runTest(&quot;\\a+&quot;, &quot;hi&quot;);assertEquals(matches, 0);</code></pre><p>支持的替代方式：</p><pre><code class="java">int matches = runTest(&quot;\\a{1,}&quot;, &quot;hi&quot;);assertEquals(matches, 0);</code></pre><p>与 Perl 和其他语言一样，我们可以使用花括号语法来指定文本的匹配次数：</p><pre><code class="java">int matches = runTest(&quot;a{3}&quot;, &quot;aaaaaa&quot;);assertEquals(matches, 2);</code></pre><p>在上述例子中，我们得到两个匹配，因为只有当“a”连续出现三次时才会有匹配。然而，在接下来的测试中，由于文本中“a”只连续出现两次，所以不会有匹配：</p><pre><code class="java">int matches = runTest(&quot;a{3}&quot;, &quot;aa&quot;);assertFalse(matches &gt; 0);</code></pre><p>当我们在花括号中使用范围时，匹配将是贪婪的，会从范围的较大端开始匹配：</p><pre><code class="java">int matches = runTest(&quot;a{2,3}&quot;, &quot;aaaa&quot;);assertEquals(matches, 1);</code></pre><p>这里我们指定至少出现两次，但不超过三次，所以匹配器会将“aaaa”视为一个“aaa”和一个单独的“a”，只得到一个匹配。</p><p>然而，API 允许我们指定一种懒惰或非贪婪的方式，使得匹配器可以从范围的较小端开始匹配，将“aaaa”匹配为“aa”和“aa”：</p><pre><code class="java">int matches = runTest(&quot;a{2,3}?&quot;, &quot;aaaa&quot;);assertEquals(matches, 2);</code></pre><h2 id="七、捕获组"><a href="#七、捕获组" class="headerlink" title="七、捕获组"></a>七、捕获组</h2><p>API 还允许我们通过捕获组将多个字符视为一个单元。它会为捕获组分配编号，并允许使用这些编号进行反向引用。</p><p>在本节中，我们将通过几个示例来了解如何在 Java 正则表达式 API 中使用捕获组。</p><p>让我们使用一个捕获组，使其仅在输入文本包含两个相邻数字时进行匹配：</p><pre><code class="java">int matches = runTest(&quot;(\\d\\d)&quot;, &quot;12&quot;);assertEquals(matches, 1);</code></pre><p>上述匹配的编号为 1，通过反向引用，我们可以告诉匹配器我们希望匹配文本中已匹配部分的另一次出现。这样，对于输入“1212”，我们可以将其视为一个匹配，而不是两个单独的匹配：</p><pre><code class="java">int matches = runTest(&quot;(\\d\\d)&quot;, &quot;1212&quot;);assertEquals(matches, 2);</code></pre><p>我们可以有一个匹配，并通过反向引用使相同的正则表达式匹配扩展到整个输入长度：</p><pre><code class="java">int matches = runTest(&quot;(\\d\\d)\\1&quot;, &quot;1212&quot;);assertEquals(matches, 1);</code></pre><p>如果不使用反向引用，我们需要重复正则表达式来实现相同的结果：</p><pre><code class="java">int matches = runTest(&quot;(\\d\\d)(\\d\\d)&quot;, &quot;1212&quot;);assertEquals(matches, 1);</code></pre><p>同样，对于任何其他重复次数，反向引用都可以使匹配器将输入视为一个匹配：</p><pre><code class="java">int matches = runTest(&quot;(\\d\\d)\\1\\1\\1&quot;, &quot;12121212&quot;);assertEquals(matches, 1);</code></pre><p>但是，如果我们更改最后一个数字，匹配将失败：</p><pre><code class="java">int matches = runTest(&quot;(\\d\\d)\\1&quot;, &quot;1213&quot;);assertFalse(matches &gt; 0);</code></pre><p>在 Java 语法中，不要忘记转义反斜杠，这一点至关重要。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>正则表达式是一个开发利器，用的好的话，会大大提升我们的开发效率。本文介绍了转移字符、量词和捕获组，后续慢慢展开，把正则表达式的各种功能展示出来。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=3757675060425670656#wechat_redirect" target="_blank" rel="noopener">并发编程</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=3809802644269318145#wechat_redirect" target="_blank" rel="noopener">MapStruct实用手册</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1750195552933199874#wechat_redirect" target="_blank" rel="noopener">一个架构师的素养修炼</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/4lOmXnKMHf3YUkYWfK_PYg" target="_blank" rel="noopener">原来OpenFeign功能这么强大</a></li><li><a href="https://mp.weixin.qq.com/s/iVo-Cr8shxjmh9UK_rWQMw" target="_blank" rel="noopener">原来OpenFeign功能这么强大（第二弹）</a></li><li><a href="https://mp.weixin.qq.com/s/_032A7UPP4SgqrpYolSyiQ" target="_blank" rel="noopener">使用OpenFeign的5个步骤和7个高级功能</a></li><li><a href="https://mp.weixin.qq.com/s/Ahsz70-30IYO-9hQ0VDefw" target="_blank" rel="noopener">由浅入深掌握CompletableFuture的七种用法</a></li><li><a href="https://mp.weixin.qq.com/s/mAthOXL3eZNUiuV7SPp7Cw" target="_blank" rel="noopener">全新的HttpClient，现代高效的网络通信利器</a></li><li><a href="https://mp.weixin.qq.com/s/b2WKGF9Z0gUkYYRP7PdybA" target="_blank" rel="noopener">Java 中反射、内省的性能差距居然如此</a></li><li><a href="https://mp.weixin.qq.com/s/vXJxyTm2BWZtUV8Kiwp3NQ" target="_blank" rel="noopener">比反射更好用的内省</a></li><li><a href="https://mp.weixin.qq.com/s/ak_t3qQ7OPI116xHL2RO1w" target="_blank" rel="noopener">异常处理的9条建议</a></li><li><a href="https://mp.weixin.qq.com/s/nkS8bPfyQ1L9knPrpGrxbA" target="_blank" rel="noopener">Java原生支持Lombok了</a></li><li><a href="https://mp.weixin.qq.com/s/ZCm71il_n5FG8gAHSiKCzQ" target="_blank" rel="noopener">MapStruct教程</a></li><li><a href="https://mp.weixin.qq.com/s/KXTzTcxvf3pqXzEe2tdYOg" target="_blank" rel="noopener">手写重试器</a></li><li><a href="https://mp.weixin.qq.com/s/ETg09gegy4a5i_fdgUUx7w" target="_blank" rel="noopener">等待线程完成的三种常用方式</a></li><li><a href="https://mp.weixin.qq.com/s/F3Pv1B5-2XErTxK6qqkoTA" target="_blank" rel="noopener">如何在线程完成时执行回调</a></li><li><a href="https://mp.weixin.qq.com/s/N-cLbHHqBKxUz4bqptMSJg" target="_blank" rel="noopener">由浅入深掌握Future</a></li><li><a href="https://mp.weixin.qq.com/s/Z0hjAKuiRZI2JOd-CUPETQ" target="_blank" rel="noopener">由浅入深掌握ExecutorService</a></li><li><a href="https://mp.weixin.qq.com/s/my64BSuMnJzX8bfhIblwPQ" target="_blank" rel="noopener">Fork/Join框架快速上手指南</a></li><li><a href="https://mp.weixin.qq.com/s/L5NUIde7li1sayF6hTqncw" target="_blank" rel="noopener">如何延迟执行一段代码</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><ul><li>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a></li><li>个人博文：<a href="https://www.howardliu.cn/">正则表达式实用指南（二）：转移字符、量词和捕获小能手</a></li><li>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a></li><li>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">正则表达式实用指南（二）：转移字符、量词和捕获小能手</a></li></ul><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      正则表达式是Java变成中一把利器，常出现在文本检查、替换等逻辑中。本文中，我们将深入探讨 Java 正则表达式 API，并研究如何在 Java 编程语言中运用正则表达式。在正则表达式的领域中，存在多种不同的“风格”可供选择，例如 grep、Perl、Python、PHP、awk 等等。这就意味着，在一种编程语言中有效的正则表达式，在另一种语言中可能无法正常工作。Java 中的正则表达式语法与 Perl 中的最为相似。
    
    </summary>
    
    
      <category term="regular" scheme="https://www.howardliu.cn/categories/regular/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="regular" scheme="https://www.howardliu.cn/tags/regular/"/>
    
  </entry>
  
  <entry>
    <title>正则表达式实用指南（一）：元字符及关系运算</title>
    <link href="https://www.howardliu.cn/regular-expressions-java-1/"/>
    <id>https://www.howardliu.cn/regular-expressions-java-1/</id>
    <published>2025-01-12T13:24:34.000Z</published>
    <updated>2025-01-12T13:24:34.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/20250112211712351.jpg" alt="正则表达式实用指南（一）：元字符及关系运算"></p><blockquote><p> 本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>你好，我是看山。</p><p>正则表达式是Java变成中一把利器，常出现在文本检查、替换等逻辑中。本文中，我们将深入探讨 Java 正则表达式 API，并研究如何在 Java 编程语言中运用正则表达式。</p><a id="more"></a><p>在正则表达式的领域中，存在多种不同的“风格”可供选择，例如 grep、Perl、Python、PHP、awk 等等。这就意味着，在一种编程语言中有效的正则表达式，在另一种语言中可能无法正常工作。Java 中的正则表达式语法与 Perl 中的最为相似。</p><h2 id="一、简介"><a href="#一、简介" class="headerlink" title="一、简介"></a>一、简介</h2><p>在 Java 中使用正则表达式无需进行特殊的设置，JDK 包含了一个专门用于正则操作的包<code>java.util.regex</code>，我们只需在代码中导入该包即可。此外，<code>java.lang.String</code>类也内置了对正则表达式的支持，这在日常编码中经常会用到。</p><p><code>java.util.regex</code>包主要由三个类组成：<code>Pattern</code>、<code>Matcher</code>和<code>PatternSyntaxException</code>。</p><ul><li><code>Pattern</code>是经过编译的正则表达式。<code>Pattern</code>类没有提供公共的构造函数，要创建一个<code>Pattern</code>对象，必须先调用其公共的静态<code>compile</code>方法，该方法会返回一个<code>Pattern</code>对象，其第一个参数即为要编译的正则表达式。</li><li><code>Matcher</code>用于解释模式，并针对输入字符串执行匹配操作。它同样没有公共构造函数，我们通过在<code>Pattern</code>对象上调用<code>matcher</code>方法，并传入要检查匹配的文本，从而获得一个<code>Matcher</code>对象。</li><li><code>PatternSyntaxException</code>是一个非受检异常，用于指示正则表达式模式中的语法错误。</li></ul><p>我们将详细探究这些类，但在此之前，我们需要先了解如何在 Java 中构建正则表达式。如果我们已经熟悉其他环境中的正则表达式，可能会发现一些细微的差异，但总体来说区别不大。</p><h2 id="二、简单示例"><a href="#二、简单示例" class="headerlink" title="二、简单示例"></a>二、简单示例</h2><p>让我们从正则表达式的一个最简单用例开始。当将正则表达式应用于字符串时，它可能匹配零次或多次，<code>java.util.regex</code>API支持的最基本的模式匹配形式是字符串字面量的匹配。</p><p>例如，如果正则表达式为“foo”，输入字符串也是“foo”，那么匹配将成功，因为这两个字符串完全相同：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;foo&quot;);Matcher matcher = pattern.matcher(&quot;foo&quot;);assertTrue(matcher.find());</code></pre><p>我们首先通过调用<code>Pattern</code>类的<code>compile</code>方法，并传入所需的正则表达式创建一个<code>Pattern</code>对象。然后，通过调用<code>matcher</code>方法，并传入要检查匹配的文本，创建一个 Matcher 对象。最后，调用<code>find</code>方法。<code>find</code>方法会在输入文本中不断前进，每次找到匹配项时返回<code>true</code>，因此我们也可以用它来计算匹配的次数：</p><pre><code class="java">Pattern pattern = Pattern.compile(&quot;foo&quot;);Matcher matcher = pattern.matcher(&quot;foofoo&quot;);int matches = 0;while (matcher.find()) {    matches++;}assertEquals(matches, 2);</code></pre><p>后续我们会重复使用这个逻辑，可以将查找匹配次数的逻辑抽象到一个名为<code>runTest</code>的方法：</p><pre><code class="java">public static int runTest(String regex, String text) {    Pattern pattern = Pattern.compile(regex);    Matcher matcher = pattern.matcher(text);    int matches = 0;    while (matcher.find()) {        matches++;    }    return matches;}</code></pre><p>当匹配次数为<code>0</code>时，表示待匹配文本中没有命中正则表达式。</p><h2 id="二、元字符"><a href="#二、元字符" class="headerlink" title="二、元字符"></a>二、元字符</h2><p>元字符会影响模式的匹配方式，从某种程度上说，它们为搜索模式增添了逻辑。Java API 支持多种元字符，其中最直接的是点“.”，它可以匹配任何字符：</p><pre><code class="java">int matches = runTest(&quot;.&quot;, &quot;foo&quot;);assertTrue(matches &gt; 0);</code></pre><p>考虑前面的例子，正则表达式“foo”可以匹配文本“foo”以及“foofoo”两次。如果在正则表达式中使用点元字符，在第二种情况下就不会得到两次匹配：</p><pre><code class="java">int matches = runTest(&quot;foo.&quot;, &quot;foofoo&quot;);assertEquals(matches, 1);</code></pre><p>注意正则表达式中“foo”后面的点。匹配器会匹配所有前面是“foo”的文本，因为最后的点表示后面可以是任何字符。所以在找到第一个“foo”后，其余部分被视为任意字符，这就是为什么只有一次匹配。</p><p>API 还支持其他一些元字符，如<code>&lt;([{\^-=$!|]})?*+.</code>，我们将在本文中进一步探讨它们。</p><h2 id="三、字符类"><a href="#三、字符类" class="headerlink" title="三、字符类"></a>三、字符类</h2><p>在字符类下，大约有六种构造。</p><h3 id="（一）或类"><a href="#（一）或类" class="headerlink" title="（一）或类"></a>（一）或类</h3><p>我们通过“[abc]”来构造或类，它可以匹配集合中的任意一个元素：</p><pre><code class="java">int matches = runTest(&quot;[abc]&quot;, &quot;b&quot;);assertEquals(matches, 1);</code></pre><p>如果它们都出现在文本中，它会分别匹配每个元素，而不考虑顺序：</p><pre><code class="java">int matches = runTest(&quot;[abc]&quot;, &quot;cab&quot;);assertEquals(matches, 3);</code></pre><p>它们也可以作为字符串的一部分交替出现。在下面的例子中，当我们通过将集合中的每个元素与第一个字母交替来创建不同的单词时，它们都会被匹配：</p><pre><code class="java">int matches = runTest(&quot;[bcr]at&quot;, &quot;bat cat rat&quot;);assertEquals(matches, 3);</code></pre><h3 id="（二）非或类"><a href="#（二）非或类" class="headerlink" title="（二）非或类"></a>（二）非或类</h3><p>通过在集合的第一个元素前添加脱字符号“^”，可以否定上述集合：</p><pre><code class="java">int matches = runTest(&quot;[^abc]&quot;, &quot;g&quot;);assertTrue(matches &gt; 0);</code></pre><p>再看另一个例子：</p><pre><code class="java">int matches = runTest(&quot;[^bcr]at&quot;, &quot;sat mat eat&quot;);assertTrue(matches &gt; 0);</code></pre><h3 id="（三）范围类"><a href="#（三）范围类" class="headerlink" title="（三）范围类"></a>（三）范围类</h3><p>我们可以使用连字符“-”定义一个类，指定匹配文本应所属的范围。同样，我们也可以否定一个范围。</p><p>匹配大写字母：</p><pre><code class="java">int matches = runTest(&quot;[A-Z]&quot;, &quot;Two Uppercase alphabets 34 overall&quot;);assertEquals(matches, 2);</code></pre><p>匹配小写字母：</p><pre><code class="java">int matches = runTest(&quot;[a-z]&quot;, &quot;Two Uppercase alphabets 34 overall&quot;);assertEquals(matches, 26);</code></pre><p>匹配大写和小写字母：</p><pre><code class="java">int matches = runTest(&quot;[a-zA-Z]&quot;, &quot;Two Uppercase alphabets 34 overall&quot;);assertEquals(matches, 28);</code></pre><p>匹配给定范围的数字：</p><pre><code class="java">int matches = runTest(&quot;[1-5]&quot;, &quot;Two Uppercase alphabets 34 overall&quot;);assertEquals(matches, 2);</code></pre><p>匹配另一个范围的数字：</p><pre><code class="java">int matches = runTest(&quot;3[0-5]&quot;, &quot;Two Uppercase alphabets 34 overall&quot;);assertEquals(matches, 1);</code></pre><h3 id="（四）并集类"><a href="#（四）并集类" class="headerlink" title="（四）并集类"></a>（四）并集类</h3><p>并集字符类是由两个或多个字符类组合而成的：</p><pre><code class="java">int matches = runTest(&quot;[1-3[7-9]]&quot;, &quot;123456789&quot;);assertEquals(matches, 6);</code></pre><p>上述测试只会匹配九个整数中的六个，因为并集跳过了 4、5 和 6。</p><h3 id="（五）交集类"><a href="#（五）交集类" class="headerlink" title="（五）交集类"></a>（五）交集类</h3><p>与并集类类似，交集类是从两个或多个集合中选取共同元素得到的。要应用交集，我们使用“&amp;&amp;”：</p><pre><code class="java">int matches = runTest(&quot;[1-6&amp;&amp;[3-9]]&quot;, &quot;123456789&quot;);assertEquals(matches, 4);</code></pre><p>我们会得到四个匹配，因为两个集合的交集只有四个元素。</p><h3 id="（六）差集类"><a href="#（六）差集类" class="headerlink" title="（六）差集类"></a>（六）差集类</h3><p>我们可以使用减法来否定一个或多个字符类。例如，我们可以匹配一组奇数：</p><pre><code class="java">int matches = runTest(&quot;[0-9&amp;&amp;[^2468]]&quot;, &quot;123456789&quot;);assertEquals(matches, 5);</code></pre><p>只有 1、3、5、7、9 会被匹配。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>正则表达式是一个开发利器，用的好的话，会大大提升我们的开发效率。本文介绍了元字符和关系运算，后续慢慢展开，把正则表达式的各种功能展示出来。</p><p>文中代码在公众号「看山的小屋」，回复“java”可获得。我的个人微信号是：hellokanshan ，如果有需要可以添加。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=3757675060425670656#wechat_redirect" target="_blank" rel="noopener">并发编程</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=3809802644269318145#wechat_redirect" target="_blank" rel="noopener">MapStruct实用手册</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1750195552933199874#wechat_redirect" target="_blank" rel="noopener">一个架构师的素养修炼</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/4lOmXnKMHf3YUkYWfK_PYg" target="_blank" rel="noopener">原来OpenFeign功能这么强大</a></li><li><a href="https://mp.weixin.qq.com/s/iVo-Cr8shxjmh9UK_rWQMw" target="_blank" rel="noopener">原来OpenFeign功能这么强大（第二弹）</a></li><li><a href="https://mp.weixin.qq.com/s/_032A7UPP4SgqrpYolSyiQ" target="_blank" rel="noopener">使用OpenFeign的5个步骤和7个高级功能</a></li><li><a href="https://mp.weixin.qq.com/s/Ahsz70-30IYO-9hQ0VDefw" target="_blank" rel="noopener">由浅入深掌握CompletableFuture的七种用法</a></li><li><a href="https://mp.weixin.qq.com/s/mAthOXL3eZNUiuV7SPp7Cw" target="_blank" rel="noopener">全新的HttpClient，现代高效的网络通信利器</a></li><li><a href="https://mp.weixin.qq.com/s/b2WKGF9Z0gUkYYRP7PdybA" target="_blank" rel="noopener">Java 中反射、内省的性能差距居然如此</a></li><li><a href="https://mp.weixin.qq.com/s/vXJxyTm2BWZtUV8Kiwp3NQ" target="_blank" rel="noopener">比反射更好用的内省</a></li><li><a href="https://mp.weixin.qq.com/s/ak_t3qQ7OPI116xHL2RO1w" target="_blank" rel="noopener">异常处理的9条建议</a></li><li><a href="https://mp.weixin.qq.com/s/nkS8bPfyQ1L9knPrpGrxbA" target="_blank" rel="noopener">Java原生支持Lombok了</a></li><li><a href="https://mp.weixin.qq.com/s/ZCm71il_n5FG8gAHSiKCzQ" target="_blank" rel="noopener">MapStruct教程</a></li><li><a href="https://mp.weixin.qq.com/s/KXTzTcxvf3pqXzEe2tdYOg" target="_blank" rel="noopener">手写重试器</a></li><li><a href="https://mp.weixin.qq.com/s/ETg09gegy4a5i_fdgUUx7w" target="_blank" rel="noopener">等待线程完成的三种常用方式</a></li><li><a href="https://mp.weixin.qq.com/s/F3Pv1B5-2XErTxK6qqkoTA" target="_blank" rel="noopener">如何在线程完成时执行回调</a></li><li><a href="https://mp.weixin.qq.com/s/N-cLbHHqBKxUz4bqptMSJg" target="_blank" rel="noopener">由浅入深掌握Future</a></li><li><a href="https://mp.weixin.qq.com/s/Z0hjAKuiRZI2JOd-CUPETQ" target="_blank" rel="noopener">由浅入深掌握ExecutorService</a></li><li><a href="https://mp.weixin.qq.com/s/my64BSuMnJzX8bfhIblwPQ" target="_blank" rel="noopener">Fork/Join框架快速上手指南</a></li><li><a href="https://mp.weixin.qq.com/s/L5NUIde7li1sayF6hTqncw" target="_blank" rel="noopener">如何延迟执行一段代码</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><ul><li>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a></li><li>个人博文：<a href="https://www.howardliu.cn/">正则表达式实用指南（一）：元字符及关系运算</a></li><li>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a></li><li>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">正则表达式实用指南（一）：元字符及关系运算</a></li></ul><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      正则表达式是Java变成中一把利器，常出现在文本检查、替换等逻辑中。本文中，我们将深入探讨 Java 正则表达式 API，并研究如何在 Java 编程语言中运用正则表达式。在正则表达式的领域中，存在多种不同的“风格”可供选择，例如 grep、Perl、Python、PHP、awk 等等。这就意味着，在一种编程语言中有效的正则表达式，在另一种语言中可能无法正常工作。Java 中的正则表达式语法与 Perl 中的最为相似。
    
    </summary>
    
    
      <category term="regular" scheme="https://www.howardliu.cn/categories/regular/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="regular" scheme="https://www.howardliu.cn/tags/regular/"/>
    
  </entry>
  
  <entry>
    <title>从Java8到Java23值得期待的x个特性（6）：一些可能不会用到的特性</title>
    <link href="https://www.howardliu.cn/java/java-features-8-23-6/"/>
    <id>https://www.howardliu.cn/java/java-features-8-23-6/</id>
    <published>2024-09-22T00:20:00.000Z</published>
    <updated>2024-12-06T11:36:03.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409161332708.jpg" alt="从Java8到Java23值得期待的x个特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。</p><p><img src="https://static.howardliu.cn/202409111451409.png" alt="Java版本分布"></p><blockquote><p>数据来源<a href="https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-2024-05-29.pdf" target="_blank" rel="noopener">2024 State of the Java Ecosystem</a></p></blockquote><h2 id="一些可能不会用到的特性"><a href="#一些可能不会用到的特性" class="headerlink" title="一些可能不会用到的特性"></a>一些可能不会用到的特性</h2><h3 id="web服务器jwebserver（Java-18）"><a href="#web服务器jwebserver（Java-18）" class="headerlink" title="web服务器jwebserver（Java 18）"></a>web服务器jwebserver（Java 18）</h3><p>为开发者提供一个轻量级、简单易用的 HTTP 服务器，通过命令行工具<code>jwebserver</code>可以启动一个最小化的静态 Web 服务器，这个服务器仅支持静态资源的访问，不支持 CGI（Common Gateway Interface）或类似 servlet 的功能，主要用于原型制作、测试和开发环境中的静态文件托管与共享。</p><p>它的应用场景包括：</p><ul><li>原型开发：由于其简单易用的特性，jwebserver 可以作为快速原型开发工具，帮助开发者在短时间内搭建起一个可以访问静态资源的 Web 服务。这对于需要验证某个功能或概念的初期阶段非常有用。</li><li>快速部署：对于一些小规模的应用或者临时性的项目，使用 jwebserver 可以快速启动并运行一个简单的 Web 服务，而无需复杂的配置和环境搭建。这使得开发者能够迅速将想法转化为实际的可访问服务。</li><li>学习与教育：jwebserver 提供了一个直观的平台，让初学者可以轻松上手 Java Web 开发。通过简单的命令行操作，用户可以快速理解 Web 服务器的工作原理及其基本配置。</li><li>测试与调试：在进行 Web 应用的测试和调试时，jwebserver 可以作为一个独立的工具来提供静态文件的访问服务，从而方便开发者对应用进行测试和调试。</li><li>本地开发环境：在本地开发环境中，jwebserver 可以替代传统的 Web 服务器如 Apache Tomcat 或 Nginx，为开发者提供一个轻量级的选择，以减少系统资源的占用。</li></ul><p>我们可以简单试一下，在当前目录编写index.html：</p><pre><code class="HTML">&lt;html&gt;&lt;head&gt;    &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot; /&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;欢迎参观&lt;/h2&gt;    &lt;h3&gt;&lt;a href=&quot;https://www.howardliu.cn/&quot;&gt;看山的小屋 howardliu.cn&lt;/a&gt;&lt;/h3&gt;    &lt;p&gt;        一起&lt;strong&gt;开心&lt;/strong&gt;学技术    &lt;/p&gt;    &lt;p&gt;        让我们一起&lt;strong&gt;扬帆起航&lt;/strong&gt;    &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</code></pre><p>运行<code>jwebserver</code>命令：</p><pre><code class="Bash">$ ./bin/jwebserverBinding to loopback by default. For all interfaces use &quot;-b 0.0.0.0&quot; or &quot;-b ::&quot;.Serving /Users/liuxinghao/Library/Java/JavaVirtualMachines/temurin-18.0.2.1/Contents/Home and subdirectories on 127.0.0.1 port 8000URL http://127.0.0.1:8000/</code></pre><p>打开<code>[http://127.0.0.1:8000/](http://127.0.0.1:8000/)</code>就可以直接看到index.html的效果：</p><p><img src="https://static.howardliu.cn/202409161340780.png" alt="公众号：看山的小屋"></p><p><code>jwebserver</code>还支持指定地址和端口等参数，具体使用可以通过命令查看：</p><pre><code class="Bash">$ ./bin/jwebserver -hUsage: jwebserver [-b bind address] [-p port] [-d directory]                  [-o none|info|verbose] [-h to show options]                  [-version to show version information]Options:-b, --bind-address    - Address to bind to. Default: 127.0.0.1 (loopback).                        For all interfaces use &quot;-b 0.0.0.0&quot; or &quot;-b ::&quot;.-d, --directory       - Directory to serve. Default: current directory.-o, --output          - Output format. none|info|verbose. Default: info.-p, --port            - Port to listen on. Default: 8000.-h, -?, --help        - Prints this help message and exits.-version, --version   - Prints version information and exits.</code></pre><h3 id="隐式声明的类和实例方法（预览特性）"><a href="#隐式声明的类和实例方法（预览特性）" class="headerlink" title="隐式声明的类和实例方法（预览特性）"></a>隐式声明的类和实例方法（预览特性）</h3><p>隐式声明的类和实例方法的目标是简化 Java 语言，使得学生和初学者可以更容易地编写他们的第一个程序，而无需理解为大型程序设计的复杂语言特性。</p><p>无论学习哪门语言，第一课一定是打印<code>Hello, World!</code>，Java中的写法是：</p><pre><code class="Java">public class HelloWorld {    public static void main(String[] args) {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>如果是第一次接触，一定会有很多疑问，<code>public</code>干啥的，<code>main</code>方法的约定参数<code>args</code>是什么鬼？然后老师就说，这就是模板，照着抄就行，不这样写不运行。</p><p>现在可以简化为：</p><pre><code class="Java">class HelloWorld {    void main() {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>我们还可以这样写：</p><pre><code class="Java">String greeting() { return &quot;Hello, World!&quot;; }void main() {    System.out.println(greeting());}</code></pre><p><code>main</code>方法直接简化为名字和括号，甚至连类也不需要显性定义了。虽然看起来没啥用，但是在JShell中使用，就比较友好了。</p><p>本次预览新增了三个IO操作方法：</p><pre><code class="Java">public static void println(Object obj);public static void print(Object obj);public static String readln(String prompt);</code></pre><p>想要快速实现控制台操作，可以这样写了：</p><pre><code class="Java">void main() {    String name = readln(&quot;请输入姓名: &quot;);    print(&quot;很高兴见到你, &quot;);    println(name);}</code></pre><p>作为一个老程序猿，也不得不哇塞一下。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>想要了解各版本的详细特性，可以从<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中查看。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><ul><li>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a></li><li>个人博文：<a href="https://www.howardliu.cn/">从Java8到Java23值得期待的x个特性（6）：一些可能不会用到的特性</a></li><li>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a></li><li>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">从Java8到Java23值得期待的x个特性（6）：一些可能不会用到的特性</a></li></ul><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      一些可能不会用到的特性。从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java10" scheme="https://www.howardliu.cn/tags/Java10/"/>
    
      <category term="Java11" scheme="https://www.howardliu.cn/tags/Java11/"/>
    
      <category term="Java12" scheme="https://www.howardliu.cn/tags/Java12/"/>
    
      <category term="Java14" scheme="https://www.howardliu.cn/tags/Java14/"/>
    
      <category term="Java15" scheme="https://www.howardliu.cn/tags/Java15/"/>
    
      <category term="Java13" scheme="https://www.howardliu.cn/tags/Java13/"/>
    
      <category term="Java16" scheme="https://www.howardliu.cn/tags/Java16/"/>
    
      <category term="Java17" scheme="https://www.howardliu.cn/tags/Java17/"/>
    
      <category term="Java18" scheme="https://www.howardliu.cn/tags/Java18/"/>
    
      <category term="Java19" scheme="https://www.howardliu.cn/tags/Java19/"/>
    
      <category term="Java20" scheme="https://www.howardliu.cn/tags/Java20/"/>
    
      <category term="Java21" scheme="https://www.howardliu.cn/tags/Java21/"/>
    
      <category term="Java8" scheme="https://www.howardliu.cn/tags/Java8/"/>
    
      <category term="Java22" scheme="https://www.howardliu.cn/tags/Java22/"/>
    
      <category term="Java9" scheme="https://www.howardliu.cn/tags/Java9/"/>
    
      <category term="Java23" scheme="https://www.howardliu.cn/tags/Java23/"/>
    
  </entry>
  
  <entry>
    <title>从Java8到Java23值得期待的x个特性（5）：增强小特性</title>
    <link href="https://www.howardliu.cn/java/java-features-8-23-5/"/>
    <id>https://www.howardliu.cn/java/java-features-8-23-5/</id>
    <published>2024-09-21T00:20:00.000Z</published>
    <updated>2024-12-06T11:36:03.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409161331298.jpg" alt="从Java8到Java23值得期待的x个特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。</p><p><img src="https://static.howardliu.cn/202409111451409.png" alt="Java版本分布"></p><blockquote><p>数据来源<a href="https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-2024-05-29.pdf" target="_blank" rel="noopener">2024 State of the Java Ecosystem</a></p></blockquote><h2 id="一些小特性"><a href="#一些小特性" class="headerlink" title="一些小特性"></a>一些小特性</h2><h3 id="增强-String"><a href="#增强-String" class="headerlink" title="增强 String"></a>增强 String</h3><p>String中新增的方法：repeat、strip、stripLeading、stripTrailing、isBlank、lines、indent 和 transform。</p><p>这些方法还是挺有用的，以前我们可能需要借助第三方类库（比如 Apache 出品的 commons-lang）中的工具类，现在可以直接使用嫡亲方法了。</p><h4 id="repeat"><a href="#repeat" class="headerlink" title="repeat"></a>repeat</h4><p>repeat是实例方法，顾名思义，这个方法是返回给定字符串的重复值的，参数是int类型，传参的时候需要注意：</p><ul><li>如果重复次数小于 0 会抛出IllegalArgumentException异常；</li><li>如果重复次数为 0 或者字符串本身是空字符串，将返回空字符串；</li><li>如果重复次数为 1 直接返回本身；</li><li>如果字符串重复指定次数后，长度超过Integer.MAX_VALUE<code>，会抛出</code>OutOfMemoryError`错误。</li></ul><p>用法很简单：</p><pre><code class="Java">@Testvoid testRepeat() {    String output = &quot;foo &quot;.repeat(2) + &quot;bar&quot;;    assertEquals(&quot;foo foo bar&quot;, output);}</code></pre><p>小而美的一个工具方法。</p><h4 id="strip、stripLeading、stripTrailing"><a href="#strip、stripLeading、stripTrailing" class="headerlink" title="strip、stripLeading、stripTrailing"></a>strip、stripLeading、stripTrailing</h4><p><code>strip</code>方法算是<code>trim</code>方法的增强版，<code>trim</code>方法可以删除字符串两侧的空白字符（空格、tab 键、换行符），但是对于<code>Unicode</code>的空白字符无能为力，<code>strip</code>补足这一短板。</p><p>用起来是这样的：</p><pre><code class="Java">@Testvoid testTrip() {    final String output = &quot;      hello   \u2005&quot;.strip();    assertEquals(&quot;hello&quot;, output);    final String trimOutput = &quot;      hello   \u2005&quot;.trim();    assertEquals(&quot;hello   \u2005&quot;, trimOutput);}</code></pre><p>对比一下可以看到，<code>trim</code>方法的清理功能稍弱。</p><p><code>stripLeading</code>和<code>stripTrailing</code>与<code>strip</code>类似，区别是一个清理头，一个清理尾。用法如下：</p><pre><code class="Java">@Testvoid testTripLeading() {    final String output = &quot;      hello   \u2005&quot;.stripLeading();    assertEquals(&quot;hello   \u2005&quot;, output);}@Testvoid testTripTrailing() {    final String output = &quot;      hello   \u2005&quot;.stripTrailing();    assertEquals(&quot;      hello&quot;, output);}</code></pre><h4 id="isBlank"><a href="#isBlank" class="headerlink" title="isBlank"></a>isBlank</h4><p>这个方法是用于判断字符串是否都是空白字符，除了空格、tab 键、换行符，也包括<code>Unicode</code>的空白字符。</p><p>用法很简单：</p><pre><code class="Java">@Testvoid testIsBlank() {    assertTrue(&quot;    \u2005&quot;.isBlank());}</code></pre><h4 id="lines"><a href="#lines" class="headerlink" title="lines"></a>lines</h4><p>最后这个方法是将字符串转化为字符串<code>Stream</code>类型，字符串分隔依据是换行符：<code>\n</code>、<code>\r</code>、<code>\r\n</code>，用法如下：</p><pre><code class="Java">@Testvoid testLines() {    final String multiline = &quot;This isa multilinestring.&quot;;    final String output = multiline.lines()            .filter(Predicate.not(String::isBlank))            .collect(Collectors.joining(&quot; &quot;));    assertEquals(&quot;This is a multiline string.&quot;, output);}</code></pre><h4 id="indent"><a href="#indent" class="headerlink" title="indent"></a>indent</h4><p><code>indent</code>方法是对字符串每行（使用<code>\r</code>或<code>\n</code>分隔）数据缩进指定空白字符，参数是 int 类型。</p><p>如果参数大于 0，就缩进指定数量的空格；如果参数小于 0，就将左侧的空字符删除指定数量，即右移。</p><p>我们看下源码：</p><pre><code class="Java">public String indent(int n) {    if (isEmpty()) {        return &quot;&quot;;    }    Stream&lt;String&gt; stream = lines();    if (n &gt; 0) {        final String spaces = &quot; &quot;.repeat(n);        stream = stream.map(s -&gt; spaces + s);    } else if (n == Integer.MIN_VALUE) {        stream = stream.map(s -&gt; s.stripLeading());    } else if (n &lt; 0) {        stream = stream.map(s -&gt; s.substring(Math.min(-n, s.indexOfNonWhitespace())));    }    return stream.collect(Collectors.joining(&quot;&quot;, &quot;&quot;, &quot;&quot;));}</code></pre><p><code>indent</code>最后会将多行数据通过<code>Collectors.joining(&quot;\n&quot;, &quot;&quot;, &quot;\n&quot;)</code>方法拼接，结果会有两点需要注意：</p><ul><li><code>\r</code>会被替换成<code>\n</code>；</li><li>如果原字符串是多行数据，最后一行的结尾没有<code>\n</code>，最后会补上一个<code>\n</code>，即多了一个空行。</li></ul><p>我们看下测试代码：</p><pre><code class="Java">@Testvoid testIndent() {    final String text = &quot;             你好，我是看山。 \u0020\u2005Java12 的 新特性。 欢迎三连+关注哟&quot;;    assertEquals(&quot;                 你好，我是看山。     \u0020\u2005Java12 的 新特性。    欢迎三连+关注哟、n&quot;, text.indent(4));    assertEquals(&quot;     你好，我是看山。\u2005Java12 的 新特性。 欢迎三连+关注哟、n&quot;, text.indent(-2));    final String text2 = &quot;山水有相逢&quot;;    assertEquals(&quot;山水有相逢&quot;, text2);}</code></pre><h4 id="transform"><a href="#transform" class="headerlink" title="transform"></a>transform</h4><p>我们再来看看<code>transform</code>方法，源码一目了然：</p><pre><code class="Java">public &lt;R&gt; R transform(Function&lt;? super String, ? extends R&gt; f) {    return f.apply(this);}</code></pre><p>通过传入的<code>Function</code>对当前字符串进行转换，转换结果由<code>Function</code>决定。比如，我们要对字符串反转：</p><pre><code class="Java">@Testvoid testTransform() {    final String text = &quot;看山是山&quot;;    final String reverseText = text.transform(s -&gt; new StringBuilder(s).reverse().toString());    assertEquals(&quot;山是山看&quot;, reverseText);}</code></pre><h3 id="增强文件读写（Java-11）"><a href="#增强文件读写（Java-11）" class="headerlink" title="增强文件读写（Java 11）"></a>增强文件读写（Java 11）</h3><p>本次更新在<code>Files</code>中增加了两个方法：<code>readString</code>和<code>writeString</code>。<code>writeString</code>作用是将指定字符串写入文件，<code>readString</code>作用是从文件中读出内容到字符串。是一个对<code>Files</code>工具类的增强，封装了对输出流、字节等内容的操作。</p><p>用法比较简单：</p><pre><code class="Java">@Testvoid testReadWriteString() throws IOException {    final Path tmpPath = Path.of(&quot;./&quot;);    final Path tempFile = Files.createTempFile(tmpPath, &quot;demo&quot;, &quot;.txt&quot;);    final Path filePath = Files.writeString(tempFile, &quot;看山 howardliu.cn 公众号：看山的小屋&quot;);    assertEquals(tempFile, filePath);    final String fileContent = Files.readString(filePath);    assertEquals(&quot;看山 howardliu.cn 公众号：看山的小屋&quot;, fileContent);    Files.deleteIfExists(filePath);}</code></pre><p><code>readString</code>和<code>writeString</code>还可以指定字符集，不指定默认使用<code>StandardCharsets.UTF\_8</code>字符集，可以应对大部分场景了。</p><h3 id="增强函数-Predicate（Java-11）"><a href="#增强函数-Predicate（Java-11）" class="headerlink" title="增强函数 Predicate（Java 11）"></a>增强函数 Predicate（Java 11）</h3><p>这个也是方法增强，在以前，我们在<code>Stream</code>中的<code>filter</code>方法判断否的时候，一般需要<code>!</code>运算，比如我们想要找到字符串列表中的数字，可以这样写：</p><pre><code class="Java">final List&lt;String&gt; list = Arrays.asList(&quot;1&quot;, &quot;a&quot;);final List&lt;String&gt; nums = list.stream()        .filter(NumberUtils::isDigits)        .collect(Collectors.toList());Assertions.assertEquals(1, nums.size());Assertions.assertTrue(nums.contains(&quot;1&quot;));</code></pre><p>想要找到非数字的，<code>filter</code>方法写的就会用到<code>!</code>非操作：</p><pre><code class="Java">final List&lt;String&gt; notNums = list.stream()        .filter(x -&gt; !NumberUtils.isDigits(x))        .collect(Collectors.toList());Assertions.assertEquals(1, notNums.size());Assertions.assertTrue(notNums.contains(&quot;a&quot;));</code></pre><p><code>Predicate</code>增加<code>not</code>方法，可以更加简单的实现非操作：</p><pre><code class="Java">final List&lt;String&gt; notNums2 = list.stream()        .filter(Predicate.not(NumberUtils::isDigits))        .collect(Collectors.toList());Assertions.assertEquals(1, notNums2.size());Assertions.assertTrue(notNums2.contains(&quot;a&quot;));</code></pre><p>有些教程还会推崇静态引入，比如在头部使用<code>import static java.util.function.Predicate.not</code>，这样在函数式编程时，可以写更少的代码，语义更强，比如：</p><pre><code class="Java">final List&lt;String&gt; notNums2 = list.stream()        .filter(not(NumberUtils::isDigits))        .collect(toList());</code></pre><h3 id="未命名模式和变量（Java-22）"><a href="#未命名模式和变量（Java-22）" class="headerlink" title="未命名模式和变量（Java 22）"></a>未命名模式和变量（Java 22）</h3><p>该特性使用下划线字符 <code>\_</code> 来表示未命名的模式和变量，从而简化代码并提高代码可读性和可维护性。</p><p>比如：</p><pre><code class="Java">public static void main(String[] args) {    var _ = new Point(1, 2);}record Point(int x, int y) {}</code></pre><p>这个可以用在任何定义变量的地方，比如：</p><ul><li><code>... instanceof Point(\_, int y)</code></li><li><code>r instanceof Point \_</code></li><li><code>switch …… case Box(\_)</code></li><li><code>for (Order \_ : orders)</code></li><li><code>for (int i = 0, \_ = sideEffect(); i &lt; 10; i++)</code></li><li><code>try { ... } catch (Exception \_) { ... } catch (Throwable \_) { ... }</code></li></ul><p>只要是这个不准备用，可以一律使用<code>\_</code>代替。</p><h3 id="Markdown格式文档注释（Java-23）"><a href="#Markdown格式文档注释（Java-23）" class="headerlink" title="Markdown格式文档注释（Java 23）"></a>Markdown格式文档注释（Java 23）</h3><p>Markdown是一种轻量级的标记语言，可用于在纯文本文档中添加格式化元素，具体语法可以参考<a href="https://www.markdownguide.org/basic-syntax/" target="_blank" rel="noopener">Markdown Guide</a>。本文就是使用Markdown语法编写的。</p><p>在Java注释中引入Markdown，目标是使API文档注释以源代码形式更易于编写和阅读。主要收益包括：</p><ul><li>提高文档编写的效率：Markdown语法相比HTML更为简洁，开发者可以更快地编写和修改文档注释。</li><li>增强文档的可读性：Markdown格式的文档在源代码中更易于阅读，有助于开发者快速理解API的用途和行为。</li><li>促进文档的一致性：通过支持Markdown，可以确保文档风格的一致性，减少因格式问题导致的文档混乱。</li><li>简化文档维护：Markdown格式的文档注释更易于维护和更新，特别是在多人协作的项目中，可以减少因文档格式问题导致的沟通成本。</li></ul><p>具体使用方式是在注释前面增加<code>///</code>，比如<code>java.lang.Object.hashCode</code>的注释：</p><pre><code class="Java">/** * Returns a hash code value for the object. This method is * supported for the benefit of hash tables such as those provided by * {@link java.util.HashMap}. * &lt;p&gt; * The general contract of {@code hashCode} is: * &lt;ul&gt; * &lt;li&gt;Whenever it is invoked on the same object more than once during *     an execution of a Java application, the {@code hashCode} method *     must consistently return the same integer, provided no information *     used in {@code equals} comparisons on the object is modified. *     This integer need not remain consistent from one execution of an *     application to another execution of the same application. * &lt;li&gt;If two objects are equal according to the {@link *     #equals(Object) equals} method, then calling the {@code *     hashCode} method on each of the two objects must produce the *     same integer result. * &lt;li&gt;It is &lt;em&gt;not&lt;/em&gt; required that if two objects are unequal *     according to the {@link #equals(Object) equals} method, then *     calling the {@code hashCode} method on each of the two objects *     must produce distinct integer results.  However, the programmer *     should be aware that producing distinct integer results for *     unequal objects may improve the performance of hash tables. * &lt;/ul&gt; * * @implSpec * As far as is reasonably practical, the {@code hashCode} method defined * by class {@code Object} returns distinct integers for distinct objects. * * @return  a hash code value for this object. * @see     java.lang.Object#equals(java.lang.Object) * @see     java.lang.System#identityHashCode */</code></pre><p>如果使用JEP 467的Markdown方式：</p><pre><code class="Java">/// Returns a hash code value for the object. This method is/// supported for the benefit of hash tables such as those provided by/// [java.util.HashMap].////// The general contract of `hashCode` is://////   - Whenever it is invoked on the same object more than once during///     an execution of a Java application, the `hashCode` method///     must consistently return the same integer, provided no information///     used in `equals` comparisons on the object is modified.///     This integer need not remain consistent from one execution of an///     application to another execution of the same application.///   - If two objects are equal according to the///     [equals][#equals(Object)] method, then calling the///     `hashCode` method on each of the two objects must produce the///     same integer result.///   - It is _not_ required that if two objects are unequal///     according to the [equals][#equals(Object)] method, then///     calling the `hashCode` method on each of the two objects///     must produce distinct integer results.  However, the programmer///     should be aware that producing distinct integer results for///     unequal objects may improve the performance of hash tables.////// @implSpec/// As far as is reasonably practical, the `hashCode` method defined/// by class `Object` returns distinct integers for distinct objects.////// @return  a hash code value for this object./// @see     java.lang.Object#equals(java.lang.Object)/// @see     java.lang.System#identityHashCode</code></pre><p>简单两种写法的差异，相同注释，Markdown的写法更加简洁：</p><p><img src="https://static.howardliu.cn/202409161338899.png" alt="Object hashcode注释差异"></p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>想要了解各版本的详细特性，可以从<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中查看。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><ul><li>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a></li><li>个人博文：<a href="https://www.howardliu.cn/">从Java8到Java23值得期待的x个特性（5）：增强小特性</a></li><li>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a></li><li>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">从Java8到Java23值得期待的x个特性（5）：增强小特性</a></li></ul><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      增强小特性。从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java10" scheme="https://www.howardliu.cn/tags/Java10/"/>
    
      <category term="Java11" scheme="https://www.howardliu.cn/tags/Java11/"/>
    
      <category term="Java12" scheme="https://www.howardliu.cn/tags/Java12/"/>
    
      <category term="Java14" scheme="https://www.howardliu.cn/tags/Java14/"/>
    
      <category term="Java15" scheme="https://www.howardliu.cn/tags/Java15/"/>
    
      <category term="Java13" scheme="https://www.howardliu.cn/tags/Java13/"/>
    
      <category term="Java16" scheme="https://www.howardliu.cn/tags/Java16/"/>
    
      <category term="Java17" scheme="https://www.howardliu.cn/tags/Java17/"/>
    
      <category term="Java18" scheme="https://www.howardliu.cn/tags/Java18/"/>
    
      <category term="Java19" scheme="https://www.howardliu.cn/tags/Java19/"/>
    
      <category term="Java20" scheme="https://www.howardliu.cn/tags/Java20/"/>
    
      <category term="Java21" scheme="https://www.howardliu.cn/tags/Java21/"/>
    
      <category term="Java8" scheme="https://www.howardliu.cn/tags/Java8/"/>
    
      <category term="Java22" scheme="https://www.howardliu.cn/tags/Java22/"/>
    
      <category term="Java9" scheme="https://www.howardliu.cn/tags/Java9/"/>
    
      <category term="Java23" scheme="https://www.howardliu.cn/tags/Java23/"/>
    
  </entry>
  
  <entry>
    <title>从Java8到Java23值得期待的x个特性（4）：垃圾收集器</title>
    <link href="https://www.howardliu.cn/java/java-features-8-23-4/"/>
    <id>https://www.howardliu.cn/java/java-features-8-23-4/</id>
    <published>2024-09-20T00:20:00.000Z</published>
    <updated>2024-12-06T11:36:03.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409161331364.jpg" alt="从Java8到Java23值得期待的x个特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。</p><p><img src="https://static.howardliu.cn/202409111451409.png" alt="Java版本分布"></p><blockquote><p>数据来源<a href="https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-2024-05-29.pdf" target="_blank" rel="noopener">2024 State of the Java Ecosystem</a></p></blockquote><h2 id="垃圾收集器"><a href="#垃圾收集器" class="headerlink" title="垃圾收集器"></a>垃圾收集器</h2><h3 id="G1（Java-9）"><a href="#G1（Java-9）" class="headerlink" title="G1（Java 9）"></a>G1（Java 9）</h3><p>G1垃圾收集器（Garbage-First Garbage Collector，简称G1 GC）是一种专为服务器端应用设计的垃圾收集器，特别适用于具有多核处理器和大内存的机器。它在Java 9中成为默认的垃圾收集器。</p><ol><li>分区机制：G1将整个堆空间划分为若干个大小相等的独立区域（Region），每个Region的大小根据堆的实际大小而定，通常控制在1MB到32MB之间。这种分区的思想弱化了传统的分代概念，使得垃圾收集更加灵活和高效。</li><li>垃圾回收策略：G1采用混合垃圾回收策略（Mix GC），不仅回收新生代中的所有Region，还会回收部分老年代中的Region。这种策略的目标是在保证停顿时间不超过预期的情况下，尽可能地回收更多的垃圾对象。</li><li>并行与并发：G1能够充分利用CPU、多核环境下的硬件优势，使用多个CPU核心来缩短Stop-The-World停顿时间。部分其他收集器需要停顿Java线程执行的GC动作，但G1可以通过并发的方式让Java程序继续运行。</li><li>高吞吐量与低延迟：G1的主要目标是在满足高吞吐量的同时，尽可能减少GC停顿时间。通过跟踪各个Region中的垃圾堆积情况，每次根据设置的垃圾回收时间，优先处理优先级最高的区域，避免一次性清理整个新生代或整个老年代的垃圾。</li></ol><p>G1垃圾收集器非常适合大尺寸堆内存的应用场景，特别是在多处理器环境下。它的设计目标是能够在有限的时间内获取尽可能高的收集效率，并且避免内存碎片。对于那些对GC停顿时间敏感的应用，如实时系统和大数据处理系统，G1是一个非常合适的选择。</p><h3 id="ZGC（Java-15）"><a href="#ZGC（Java-15）" class="headerlink" title="ZGC（Java 15）"></a>ZGC（Java 15）</h3><p>ZGC 是一个可伸缩、低延迟的垃圾收集器，使用-XX:+UseZGC 命令开启。</p><p>名称中的“Z”并没有特定的含义，主要是一个名称，灵感来源于Oracle的ZFS文件系统。ZFS在设计上具有革命性，因此ZGC的命名也向其致敬。</p><p>在Java15发布时，提供的是不分代收集器，所有对象都放在一起，期望STW最大10ms。</p><p>在Java21的时候，基于「大部分对象朝生夕死」的分代假说，ZGC提供了分代版本，将内存划分为年轻代和老年代，并为这两种代分别维护不同的垃圾收集策略，期望STW最大是1ms。</p><p>我们看下<a href="https://kstefanj.github.io/2023/11/07/hazelcast-on-generational-zgc.html" target="_blank" rel="noopener">Hazelcast Jet on Generational ZGC</a>中给出的测评效果：</p><p><img src="https://static.howardliu.cn/202409161336772.png" alt="JEP 439: 分代ZGC（Generational ZGC）"></p><p>从上图可以看到，非分代 ZGC 在低负载下表现非常好，但随着分配压力的增加，延迟也会增加。使用分代 ZGC 后，即使在高负载下，延迟也非常低，而且延迟效果优于G1。</p><h3 id="Shenandoah（Java-15）"><a href="#Shenandoah（Java-15）" class="headerlink" title="Shenandoah（Java 15）"></a>Shenandoah（Java 15）</h3><p>Shenandoah（读音：谢南多厄），作为一个低停顿的垃圾收集器。</p><p>Shenandoah 垃圾收集器是 RedHat 在 2014 年宣布进行的垃圾收集器研究项目，其工作原理是通过与 Java 应用执行线程同时运行来降低停顿时间。</p><p>简单的说就是，Shenandoah 工作时与应用程序线程并发，通过交换 CPU 并发周期和空间以改善停顿时间，使得垃圾回收器执行线程能够在 Java 线程运行时进行堆压缩，并且标记和整理能够同时进行，因此避免了在大多数 JVM 垃圾收集器中所遇到的问题。</p><p><img src="https://static.howardliu.cn/202409161336975.png" alt="Shenandoah GC"></p><p>Shenandoah 垃圾回收器的暂停时间与堆大小无关，这意味着无论将堆设置为 200MB 还是 200GB，都将拥有一致的系统暂停时间，不过实际使用性能将取决于实际工作堆的大小和工作负载。</p><p><a href="https://wiki.openjdk.java.net/display/shenandoah" target="_blank" rel="noopener">https://wiki.openjdk.java.net/display/shenandoah</a></p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>想要了解各版本的详细特性，可以从<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中查看。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><ul><li>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a></li><li>个人博文：<a href="https://www.howardliu.cn/">从Java8到Java23值得期待的x个特性（4）：垃圾收集器</a></li><li>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a></li><li>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">从Java8到Java23值得期待的x个特性（4）：垃圾收集器</a></li></ul><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      垃圾收集器。从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java10" scheme="https://www.howardliu.cn/tags/Java10/"/>
    
      <category term="Java11" scheme="https://www.howardliu.cn/tags/Java11/"/>
    
      <category term="Java12" scheme="https://www.howardliu.cn/tags/Java12/"/>
    
      <category term="Java14" scheme="https://www.howardliu.cn/tags/Java14/"/>
    
      <category term="Java15" scheme="https://www.howardliu.cn/tags/Java15/"/>
    
      <category term="Java13" scheme="https://www.howardliu.cn/tags/Java13/"/>
    
      <category term="Java16" scheme="https://www.howardliu.cn/tags/Java16/"/>
    
      <category term="Java17" scheme="https://www.howardliu.cn/tags/Java17/"/>
    
      <category term="Java18" scheme="https://www.howardliu.cn/tags/Java18/"/>
    
      <category term="Java19" scheme="https://www.howardliu.cn/tags/Java19/"/>
    
      <category term="Java20" scheme="https://www.howardliu.cn/tags/Java20/"/>
    
      <category term="Java21" scheme="https://www.howardliu.cn/tags/Java21/"/>
    
      <category term="Java8" scheme="https://www.howardliu.cn/tags/Java8/"/>
    
      <category term="Java22" scheme="https://www.howardliu.cn/tags/Java22/"/>
    
      <category term="Java9" scheme="https://www.howardliu.cn/tags/Java9/"/>
    
      <category term="Java23" scheme="https://www.howardliu.cn/tags/Java23/"/>
    
  </entry>
  
  <entry>
    <title>从Java8到Java23值得期待的x个特性</title>
    <link href="https://www.howardliu.cn/java/java-features-8-23/"/>
    <id>https://www.howardliu.cn/java/java-features-8-23/</id>
    <published>2024-09-19T00:23:00.000Z</published>
    <updated>2024-12-06T11:36:03.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409161115516.jpg" alt="从Java8到Java23值得期待的x个特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。</p><p><img src="https://static.howardliu.cn/202409111451409.png" alt="Java版本分布"></p><blockquote><p>数据来源<a href="https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-2024-05-29.pdf" target="_blank" rel="noopener">2024 State of the Java Ecosystem</a></p></blockquote><h2 id="有意思的特性"><a href="#有意思的特性" class="headerlink" title="有意思的特性"></a>有意思的特性</h2><blockquote><p>可以从 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中查看各个版本的特性。</p></blockquote><h3 id="大家都很熟悉的特性"><a href="#大家都很熟悉的特性" class="headerlink" title="大家都很熟悉的特性"></a>大家都很熟悉的特性</h3><p>Java8中的Lambda表达式、Stream流、Optional是大版本特性，Java8是2014年发布，已有十年历史了。大家应该分厂熟悉了，这里不在赘述。推荐两篇文章：</p><ul><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li></ul><h3 id="补全技能树"><a href="#补全技能树" class="headerlink" title="补全技能树"></a>补全技能树</h3><h4 id="Record-类型（Java-16）"><a href="#Record-类型（Java-16）" class="headerlink" title="Record 类型（Java 16）"></a>Record 类型（Java 16）</h4><p>Java新增了一个关键字<code>record</code>，它是定义不可变数据类型封装类的关键字，主要用在特定领域类上。</p><p>我们都知道，在Java开发中，我们需要定义POJO作为数据存储对象，根据规范，POJO中除了属性是个性化的，其他的比如<code>getter</code>、<code>setter</code>、<code>equals</code>、<code>hashCode</code>、<code>toString</code>都是模板化的写法，所以为了简便，很多类似Lombok的组件提供Java类编译时增强，通过在类上定义<code>@Data</code>注解自动添加这些模板化方法。在Java14中，我们可以直接使用<code>record</code>解决这个问题。</p><p>比如，我们定义一个<code>Person</code>类：</p><pre><code class="Java">public record Person(String name, String address) {}</code></pre><p>我们转换为之前的定义会是一坨下面这种代码：</p><pre><code class="Java">public final class PersonBefore {    private final String name;    private final String address;    public PersonBefore(String name, String address) {        this.name = name;        this.address = address;    }    public String name() {        return name;    }    public String address() {        return address;    }    @Override    public boolean equals(Object o) {        if (this == o) {            return true;        }        if (o == null || getClass() != o.getClass()) {            return false;        }        PersonBefore14 that = (PersonBefore14) o;        return Objects.equals(name, that.name) &amp;&amp; Objects.equals(address, that.address);    }    @Override    public int hashCode() {        return Objects.hash(name, address);    }    @Override    public String toString() {        return &quot;PersonBefore{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &quot;, address=&#39;&quot; + address + &#39;\&#39;&#39; +                &#39;}&#39;;    }}</code></pre><p>我们可以发现Record类有如下特征：</p><ol><li>一个构造方法</li><li>getter方法名与属性名相同</li><li>有<code>equals()</code>、<code>hashCode()</code>方法</li><li>有<code>toString()</code>方法</li><li>类对象和属性被<code>final</code>修饰，所以构造函数是包含所有属性的，而且没有setter方法</li></ol><p>在<code>Class</code>类中也新增了对应的处理方法：</p><ul><li><code>getRecordComponents()</code>：返回一组<code>java.lang.reflect.RecordComponent</code>对象组成的数组，该数组的元素与<code>Record</code>类中的组件相对应，其顺序与在记录声明中出现的顺序相同，可以从该数组中的每个<code>RecordComponent</code>中提取到组件信息，包括其名称、类型、泛型类型、注释及其访问方法。</li><li><code>isRecord()</code>：返回所在类是否是 Record 类型，如果是，则返回 true。</li></ul><p>看起来，Record类和Enum很像，都是一定的模板类，通过语法糖定义，在Java编译过程中，将其编译成特定的格式，功能很好，但如果没有习惯使用，可能会觉得限制太多。 </p><h4 id="密封类和接口（Java-17）"><a href="#密封类和接口（Java-17）" class="headerlink" title="密封类和接口（Java 17）"></a>密封类和接口（Java 17）</h4><p>目前，Java 没有提供对继承的细粒度控制，只有 public、protected、private、包内控制四种非常粗粒度的控制方式。</p><p>为此，密封类的目标是允许单个类声明哪些类型可以用作其子类型。这也适用于接口，并确定哪些类型可以实现它们。该功能特性新增了<code>sealed</code>和<code>non-sealed</code>修饰符和<code>permits</code>关键字。</p><p>我们可以做如下定义：</p><pre><code class="Java">public sealed class Person permits Student, Worker, Teacher {}public sealed class Student extends Person        permits Pupil, JuniorSchoolStudent, HighSchoolStudent, CollegeStudent, GraduateStudent {}public final class Pupil extends Student {}public non-sealed class Worker extends Person {}public class OtherClass extends Worker {}public final class Teacher extends Person {}</code></pre><p>我们可以先定义一个<code>sealed</code>修饰的类<code>Person</code>，使用<code>permits</code>指定被继承的子类，这些子类必须是使用<code>final</code>或<code>sealed</code>或<code>non-sealed</code>修饰的类。其中<code>Student</code>是使用<code>sealed</code>修饰，所以也需要使用<code>permits</code>指定被继承的子类。<code>Worker</code>类使用<code>non-sealed</code>修饰，成为普通类，其他类都可以继承它。<code>Teacher</code>使用<code>final</code>修饰，不可再被继承。</p><p>从类图上看没有太多区别：</p><p><img src="https://static.howardliu.cn/202409161142744.png" alt="密封类和接口"></p><p>但是从功能特性上，起到了很好的约束作用，我们可以放心大胆的定义可以公开使用，但又不想被非特定类继承的类了。</p><h4 id="全新的-HTTP-客户端（Java-11）"><a href="#全新的-HTTP-客户端（Java-11）" class="headerlink" title="全新的 HTTP 客户端（Java 11）"></a>全新的 HTTP 客户端（Java 11）</h4><p>老版 HTTP 客户端存在很多问题，大家开发的时候基本上都是使用第三方 HTTP 库，比如 Apache HttpClient、Netty、Jetty 等。</p><p>新版 HTTP 客户端的目标很多，毕竟这么多珠玉在前，如果还是做成一坨，指定是要被笑死的。所以新版 HTTP 客户端列出了 16 个目标，包括简单易用、打印关键信息、WebSocket、HTTP/2、HTTPS/TLS、良好的性能、非阻塞 API 等等。</p><pre><code class="Java">@Testvoid testHttpClient() throws IOException, InterruptedException {    final HttpClient httpClient = HttpClient.newBuilder()            .version(HttpClient.Version.HTTP_2)            .connectTimeout(Duration.ofSeconds(20))            .build();    final HttpRequest httpRequest = HttpRequest.newBuilder()            .GET()            .uri(URI.create(&quot;https://www.howardliu.cn/robots.txt&quot;))            .build();    final HttpResponse&lt;String&gt; httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString());    final String responseBody = httpResponse.body();    assertTrue(responseBody.contains(&quot;Allow&quot;));}</code></pre><h4 id="有序集合（Java-21）"><a href="#有序集合（Java-21）" class="headerlink" title="有序集合（Java 21）"></a>有序集合（Java 21）</h4><p>有序集合是Java21版本中引入的一个新特性，旨在为Java集合框架添加对有序集合的支持。</p><p>有序集合是一种具有定义好的元素访问顺序的集合类型，它允许以一致的方式访问和处理集合中的元素，无论是从第一个元素到最后一个元素，还是从最后一个元素到第一个元素。</p><p>在 Java 中，集合类库非常重要且使用频率非常高，但是缺乏一种能够表示具有定义好的元素访问顺序的集合类型。</p><p>例如，<code>List</code>和<code>Deque</code>都定义了元素的访问顺序，但它们的共同父接口<code>Collection</code>却没有。同样，Set不定义元素的访问顺序，其子类型如<code>HashSet</code>也没有定义，但子类型如<code>SortedSet</code>和<code>LinkedHashSet</code>则有定义。因此，支持访问顺序的功能散布在整个类型层次结构中，使得在API中表达某些有用的概念变得困难。<code>Collection</code>太通用，将此类约束留给文档规范，可能导致难以调试的错误。</p><p>而且，虽然某些集合有顺序操作方法，但是却不尽相同，比如</p><table><thead><tr><th><br></th><th>First element</th><th>Last element</th></tr></thead><tbody><tr><td>List</td><td>list.get(0)</td><td>list.get(list.size() - 1)</td></tr><tr><td>Deque</td><td>deque.getFirst()</td><td>deque.getLast()</td></tr><tr><td>SortedSet</td><td>sortedSet.first()</td><td>sortedSet.last()</td></tr><tr><td>LinkedHashSet</td><td>linkedHashSet.iterator().next()</td><td>缺失</td></tr></tbody></table><p>提供了有序集合<code>SequencedCollection</code>、<code>SequencedSet</code>、<code>SequencedMap</code>：</p><pre><code class="Java">interface SequencedCollection&lt;E&gt; extends Collection&lt;E&gt; {    // new method    SequencedCollection&lt;E&gt; reversed();    // methods promoted from Deque    void addFirst(E);    void addLast(E);    E getFirst();    E getLast();    E removeFirst();    E removeLast();}interface SequencedSet&lt;E&gt; extends Set&lt;E&gt;, SequencedCollection&lt;E&gt; {    SequencedSet&lt;E&gt; reversed();    // covariant override}interface SequencedMap&lt;K,V&gt; extends Map&lt;K,V&gt; {    // new methods    SequencedMap&lt;K,V&gt; reversed();    SequencedSet&lt;K&gt; sequencedKeySet();    SequencedCollection&lt;V&gt; sequencedValues();    SequencedSet&lt;Entry&lt;K,V&gt;&gt; sequencedEntrySet();    V putFirst(K, V);    V putLast(K, V);    // methods promoted from NavigableMap    Entry&lt;K, V&gt; firstEntry();    Entry&lt;K, V&gt; lastEntry();    Entry&lt;K, V&gt; pollFirstEntry();    Entry&lt;K, V&gt; pollLastEntry();}</code></pre><p><code>SequencedCollection</code>的<code>reversed()</code>方法提供了一个原始集合的反向视图。任何对原始集合的修改都会在视图中可见。如果允许，视图中的修改会写回到原始集合。</p><p><img src="https://static.howardliu.cn/202409161143487.png" alt="JEP 431: 有序集合（Sequenced Collections）"></p><p>我们看一个例子，假设我们有一个LinkedHashSet，现在我们想要获取它的反向视图并以反向顺序遍历它：</p><pre><code class="Java">LinkedHashSet&lt;Integer&gt; linkedHashSet = new LinkedHashSet&lt;&gt;(Arrays.asList(3, 2, 1));// 获取反向视图SequencedCollection&lt;Integer&gt; reversed = linkedHashSet.reversed();// 反向遍历System.out.println(&quot;原始数据：&quot; + linkedHashSet);System.out.println(&quot;反转数据：&quot; + reversed);// 运行结果：// 原始数据：[3, 2, 1]// 反转数据：[1, 2, 3]</code></pre><p>这些方法都是便捷方法，内部数据结构没有变化，其实本质也是原来的用法。比如<code>ArrayList</code>中的<code>getFirst</code>和<code>getLast</code>方法：</p><pre><code class="java">/** * {@inheritDoc} * * @throws NoSuchElementException {@inheritDoc} * @since 21 */public E getFirst() {    if (size == 0) {        throw new NoSuchElementException();    } else {        return elementData(0);    }}/** * {@inheritDoc} * * @throws NoSuchElementException {@inheritDoc} * @since 21 */public E getLast() {    int last = size - 1;    if (last &lt; 0) {        throw new NoSuchElementException();    } else {        return elementData(last);    }}</code></pre><h2 id="提升幸福感的特性"><a href="#提升幸福感的特性" class="headerlink" title="提升幸福感的特性"></a>提升幸福感的特性</h2><h3 id="局部变量类型推断（Java-10）"><a href="#局部变量类型推断（Java-10）" class="headerlink" title="局部变量类型推断（Java 10）"></a>局部变量类型推断（Java 10）</h3><pre><code class="Java">// 以前的写法Map&lt;String, List&lt;ProcSequenceFlow&gt;&gt; sourceMap = sequenceFlowList.stream()        .collect(Collectors.groupingBy(ProcSequenceFlow::getSourceRef));// 现在的写法var sourceMap2 = sequenceFlowList.stream()        .collect(Collectors.groupingBy(ProcSequenceFlow::getSourceRef));</code></pre><h3 id="Switch表达式（Java-14）"><a href="#Switch表达式（Java-14）" class="headerlink" title="Switch表达式（Java 14）"></a>Switch表达式（Java 14）</h3><p>我们以“判断是否工作日”的例子展示一下，在Java14之前：</p><pre><code class="Java">@Testvoid testSwitch() {    final DayOfWeek day = DayOfWeek.from(LocalDate.now());    String typeOfDay = &quot;&quot;;    switch (day) {        case MONDAY:        case TUESDAY:        case WEDNESDAY:        case THURSDAY:        case FRIDAY:            typeOfDay = &quot;Working Day&quot;;            break;        case SATURDAY:        case SUNDAY:            typeOfDay = &quot;Rest Day&quot;;            break;    }    Assertions.assertFalse(typeOfDay.isEmpty());}</code></pre><p>在Java14中：</p><pre><code class="Java">@Testvoid testSwitchExpression() {    final DayOfWeek day = DayOfWeek.SATURDAY;    final String typeOfDay = switch (day) {        case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -&gt; {            System.out.println(&quot;Working Day: &quot; + day);            yield &quot;Working Day&quot;;        }        case SATURDAY, SUNDAY -&gt; &quot;Day Off&quot;;    };    Assertions.assertEquals(&quot;Day Off&quot;, typeOfDay);}</code></pre><p>注意看，例子中我们使用了两种写法，一种是通过<code>yield</code>关键字表示返回结果，一种是在<code>-&gt;</code>之后直接返回。</p><h3 id="文本块（Java-15）"><a href="#文本块（Java-15）" class="headerlink" title="文本块（Java 15）"></a>文本块（Java 15）</h3><p>直接上代码</p><pre><code class="Java">@Testvoid testTextBlock() {    final String singleLine = &quot;你好，我是看山，公众号「看山的小屋」。这行没有换行，而且我的后面多了一个空格  这次换行了&quot;;    final String textBlockSingleLine = &quot;&quot;&quot;            你好，我是看山，公众号「看山的小屋」。\            这行没有换行，而且我的后面多了一个空格\s            这次换行了&quot;&quot;&quot;;    Assertions.assertEquals(singleLine, textBlockSingleLine);}</code></pre><h3 id="字符串模板（预览）"><a href="#字符串模板（预览）" class="headerlink" title="字符串模板（预览）"></a>字符串模板（预览）</h3><p>字符串模板通过将文本和嵌入式表达式结合在一起，使得Java程序能够以一种更加直观和安全的方式构建字符串。</p><p>与传统的字符串拼接（使用+操作符）、<code>StringBuilder</code>或<code>String.format</code> 等方法相比，字符串模板提供了一种更加清晰和安全的字符串构建方式。</p><p>特别是当字符串需要从用户提供的值构建并传递给其他系统时（例如，构建数据库查询），使用字符串模板可以有效地验证和转换模板及其嵌入表达式的值，从而提高Java程序的安全性。</p><p>让我们通过代码看一下这个特性的魅力：</p><pre><code class="Java">public static void main(String[] args) {    // 拼装变量    String name = &quot;看山&quot;;    String info = STR. &quot;My name is \{ name }&quot; ;    assert info.equals(&quot;My name is 看山&quot;);    // 拼装变量    String firstName = &quot;Howard&quot;;    String lastName = &quot;Liu&quot;;    String fullName = STR. &quot;\{ firstName } \{ lastName }&quot; ;    assert fullName.equals(&quot;Howard Liu&quot;);    String sortName = STR. &quot;\{ lastName }, \{ firstName }&quot; ;    assert sortName.equals(&quot;Liu, Howard&quot;);    // 模板中调用方法    String s2 = STR. &quot;You have a \{ getOfferType() } waiting for you!&quot; ;    assert s2.equals(&quot;You have a gift waiting for you!&quot;);    Request req = new Request(&quot;2017-07-19&quot;, &quot;09:15&quot;, &quot;https://www.howardliu.cn&quot;);    // 模板中引用对象属性    String s3 = STR. &quot;Access at \{ req.date } \{ req.time } from \{ req.address }&quot; ;    assert s3.equals(&quot;Access at 2017-07-19 09:15 from https://www.howardliu.cn&quot;);    LocalTime now = LocalTime.now();    String markTime = DateTimeFormatter            .ofPattern(&quot;HH:mm:ss&quot;)            .format(now);    // 模板中调用方法    String time = STR. &quot;The time is \{            // The java.time.format package is very useful            DateTimeFormatter                    .ofPattern(&quot;HH:mm:ss&quot;)                    .format(now)            } right now&quot; ;    assert time.equals(&quot;The time is &quot; + markTime + &quot; right now&quot;);    // 模板嵌套模板    String[] fruit = {&quot;apples&quot;, &quot;oranges&quot;, &quot;peaches&quot;};    String s4 = STR. &quot;\{ fruit[0] }, \{            STR. &quot;\{ fruit[1] }, \{ fruit[2] }&quot;            }&quot; ;    assert s4.equals(&quot;apples, oranges, peaches&quot;);    // 模板与文本块结合    String title = &quot;My Web Page&quot;;    String text = &quot;Hello, world&quot;;    String html = STR. &quot;&quot;&quot;    &lt;html&gt;      &lt;head&gt;        &lt;title&gt;\{ title }&lt;/title&gt;      &lt;/head&gt;      &lt;body&gt;        &lt;p&gt;\{ text }&lt;/p&gt;      &lt;/body&gt;    &lt;/html&gt;    &quot;&quot;&quot; ;    assert html.equals(&quot;&quot;&quot;            &lt;html&gt;              &lt;head&gt;                &lt;title&gt;My Web Page&lt;/title&gt;              &lt;/head&gt;              &lt;body&gt;                &lt;p&gt;Hello, world&lt;/p&gt;              &lt;/body&gt;            &lt;/html&gt;            &quot;&quot;&quot;);    // 带格式化的字符串模板    record Rectangle(String name, double width, double height) {        double area() {            return width * height;        }    }    Rectangle[] zone = new Rectangle[] {            new Rectangle(&quot;Alfa&quot;, 17.8, 31.4),            new Rectangle(&quot;Bravo&quot;, 9.6, 12.4),            new Rectangle(&quot;Charlie&quot;, 7.1, 11.23),    };    String table = FMT. &quot;&quot;&quot;        Description     Width    Height     Area        %-12s\{ zone[0].name }  %7.2f\{ zone[0].width }  %7.2f\{ zone[0].height }     %7.2f\{ zone[0].area() }        %-12s\{ zone[1].name }  %7.2f\{ zone[1].width }  %7.2f\{ zone[1].height }     %7.2f\{ zone[1].area() }        %-12s\{ zone[2].name }  %7.2f\{ zone[2].width }  %7.2f\{ zone[2].height }     %7.2f\{ zone[2].area() }        \{ &quot; &quot;.repeat(28) } Total %7.2f\{ zone[0].area() + zone[1].area() + zone[2].area() }        &quot;&quot;&quot;;    assert table.equals(&quot;&quot;&quot;            Description     Width    Height     Area            Alfa            17.80    31.40      558.92            Bravo            9.60    12.40      119.04            Charlie          7.10    11.23       79.73                                         Total  757.69            &quot;&quot;&quot;);}public static String getOfferType() {    return &quot;gift&quot;;}record Request(String date, String time, String address) {}</code></pre><p>这个功能当前是第一次预览，在Java22第二次预览，Java23的8.12版本中还没有展示字符串模板的第三次预览（JEP 465: String Templates），还不能确定什么时候可以正式用上。</p><h3 id="模式匹配"><a href="#模式匹配" class="headerlink" title="模式匹配"></a>模式匹配</h3><h4 id="instanceof模式匹配（Java-16）"><a href="#instanceof模式匹配（Java-16）" class="headerlink" title="instanceof模式匹配（Java 16）"></a>instanceof模式匹配（Java 16）</h4><p><code>instanceof</code>主要用来检查对象类型，作为类型强转前的安全检查。</p><p>比如：</p><pre><code class="Java">@Testvoid test() {    final Object obj1 = &quot;Hello, World!&quot;;    int result = 0;    if (obj1 instanceof String) {        String str = (String) obj1;        result = str.length();    } else if (obj1 instanceof Number) {        Number num = (Number) obj1;        result = num.intValue();    }    Assertions.assertEquals(13, result);}</code></pre><p>可以看到，我们每次判断类型之后，需要声明一个判断类型的变量，然后将判断参数强制转换类型，赋值给新声明的变量。</p><p>这种写法显得繁琐且多余。</p><p><code>instanceof</code>改进后：</p><pre><code class="Java">@Testvoid test1() {    final Object obj1 = &quot;Hello, World!&quot;;    int result = 0;    if (obj1 instanceof String str) {        result = str.length();    } else if (obj1 instanceof Number num) {        result = num.intValue();    }    Assertions.assertEquals(13, result);}</code></pre><p>不仅如此，<code>instanceof</code>模式匹配的作用域还可以扩展。在<code>if</code>条件判断中，我们都知道<code>&amp;&amp;</code>与判断是会执行所有的表达式，所以使用<code>instanceof</code>模式匹配定义的局部变量继续判断。</p><p>比如：</p><pre><code class="Java">if (obj1 instanceof String str &amp;&amp; str.length() &gt; 20) {    result = str.length();}</code></pre><p>与原来的写法对比，新的写法代码更加简洁、可读性更高，能够提出很多冗余繁琐的代码，非常实用的一个特性。</p><h4 id="switch模式匹配（Java-21）"><a href="#switch模式匹配（Java-21）" class="headerlink" title="switch模式匹配（Java 21）"></a>switch模式匹配（Java 21）</h4><p>switch模式匹配是一个非常赞的功能，可以在选择器表达式使用基础判断和任意引用类型，包括instanceof操作符。这意味着可以更灵活地使用对象、数组、列表等复杂数据结构作为switch语句的基础，从而简化代码并提高可读性。</p><p>switch模式匹配允许在switch语句中使用模式来测试表达式，每个模式都有特定的动作，从而可以简洁、安全地表达复杂的数据导向查询。</p><p>通过代码看下switch模式匹配的魅力；</p><pre><code class="Java">static String formatValue(Object obj) {    return switch (obj) {        case null -&gt; &quot;null&quot;;        case Integer i -&gt; String.format(&quot;int %d&quot;, i);        case Long l -&gt; String.format(&quot;long %d&quot;, l);        case Double d -&gt; String.format(&quot;double %f&quot;, d);        case String s -&gt; String.format(&quot;String %s&quot;, s);        case Person(String name, String address) -&gt; String.format(&quot;Person %s %s&quot;, name, address);        default -&gt; obj.toString();    };}public record Person(String name, String address) {}public static void main(String[] args) {    System.out.println(formatValue(10));    System.out.println(formatValue(20L));    System.out.println(formatValue(3.14));    System.out.println(formatValue(&quot;Hello&quot;));    System.out.println(formatValue(null));    System.out.println(formatValue(new Person(&quot;Howard&quot;, &quot;Beijing&quot;)));}// 运行结果// int 10// long 20// double 3.140000// String Hello// null// Person Howard Beijing</code></pre><h4 id="Record-模式（Java-21）"><a href="#Record-模式（Java-21）" class="headerlink" title="Record 模式（Java 21）"></a>Record 模式（Java 21）</h4><p>Record是Java16正式发布的基础类型，提供不可变对象的简单实现（其实就是Java Bean，但是省略一堆的getter、setter、hashcode、equals、toString等方法）。</p><p>Record模式，主要是使Record类型可以直接在instanceof和switch模式匹配中使用。</p><p>我们一起看个示例，比如有下面几个基础元素：</p><pre><code class="Java">// 颜色enum Color { RED, GREEN, BLUE}// 点record Point(int x, int y) {}// 带颜色的点record ColoredPoint(Point p, Color color) {}// 正方形record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}</code></pre><p>我们分别通过instanceof模式匹配和switch模式匹配判断输入参数的类型，打印不同的格式：</p><pre><code class="Java">private static void instancePatternsAndPrint(Object o) {    if (o instanceof Square(ColoredPoint upperLeft, ColoredPoint lowerRight)) {        System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);    } else if (o instanceof ColoredPoint(Point(int x, int y), Color color)) {        System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);    } else if (o instanceof Point p) {        System.out.println(&quot;Point类型：&quot; + p);    }}private static void switchPatternsAndPrint(Object o) {    switch (o) {        case Square(ColoredPoint upperLeft, ColoredPoint lowerRight) -&gt; {            System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);        }        case ColoredPoint(Point(int x, int y), Color color) -&gt; {            System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);        }        case Point p -&gt; {            System.out.println(&quot;Point类型：&quot; + p);        }        default -&gt; throw new IllegalStateException(&quot;Unexpected value: &quot; + o);    }}</code></pre><p>我们通过main方法执行下：</p><pre><code class="Java">public static void main(String[] args) {    var p = new Point(1, 2);    var cp1 = new ColoredPoint(p, Color.RED);    var cp2 = new ColoredPoint(p, Color.GREEN);    var square = new Square(cp1, cp2);    instancePatternsAndPrint(square);    instancePatternsAndPrint(cp1);    instancePatternsAndPrint(p);    switchPatternsAndPrint(square);    switchPatternsAndPrint(cp1);    switchPatternsAndPrint(p);}// 结果是：//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]</code></pre><p>是不是很简洁，就像Java8提供的Lambda表达式一样，很丝滑。</p><h3 id="灵活的构造函数主体（预览特性）"><a href="#灵活的构造函数主体（预览特性）" class="headerlink" title="灵活的构造函数主体（预览特性）"></a>灵活的构造函数主体（预览特性）</h3><p>我们都知道，在子类的构造函数中，比如通过<code>super(……)</code>调用父类，在<code>super</code>之前是不允许有其他语句的。</p><p>大部分的时候这种限制都没问题，但是有时候不太灵活。如果想在<code>super</code>之前加上一些子类特有逻辑，比如想统计下子类构造耗时，就得重写一遍父类的实现。</p><p>除了有损灵活性，这种重写的做法也会造成父子类之间的关系变得奇怪。假设父类是SDK中的一个类，SDK升级时在父类构造函数增加了一些逻辑，我们项目中是无法继承这些逻辑的，某次需要升级SDK（比如低版本有安全风险），验证不完整的情况下，就很容易出现bug。</p><p>该特性的目标是提高构造函数的可读性和可预测性，同时保持构造函数调用的自上而下的规则。通过允许在显式调用 <code>super()</code> 或 <code>this()</code> 前初始化字段，从而实现更灵活的构造函数主体。这一变化使得代码更具表现力。</p><p>我们看下示例代码：</p><pre><code class="Java">public class PositiveBigInteger extends BigInteger {    public PositiveBigInteger(long value) {        if (value &lt;= 0) {            throw new IllegalArgumentException(&quot;non-positive value&quot;);        }        super(value);    }}</code></pre><h2 id="永远学不完的多线程"><a href="#永远学不完的多线程" class="headerlink" title="永远学不完的多线程"></a>永远学不完的多线程</h2><h3 id="虚拟线程（Java-21）"><a href="#虚拟线程（Java-21）" class="headerlink" title="虚拟线程（Java 21）"></a>虚拟线程（Java 21）</h3><p>虚拟线程是一种轻量级的线程实现，旨在显著降低编写、维护和观察高吞吐量并发应用程序的难度。它们占用的资源少，不需要被池化，可以创建大量虚拟线程，特别适用于IO密集型任务，因为它们可以高效地调度大量虚拟线程来处理并发请求，从而显著提高程序的吞吐量和响应速度。</p><p>虚拟线程有下面几个特点：</p><ol><li>轻量级：虚拟线程是JVM内部实现的轻量级线程，不需要操作系统内核参与，创建和上下文切换的成本远低于传统的操作系统线程（即平台线程），且占用的内存资源较少。</li><li>减少CPU时间消耗：由于虚拟线程不依赖于操作系统平台线程，因此在进行线程切换时耗费的CPU时间会大大减少，从而提高了程序的执行效率。</li><li>简化多线程编程：虚拟线程通过结构化并发API来简化多线程编程，使得开发者可以更容易地编写、维护和观察高吞吐量并发应用程序。</li><li>适用于大量任务场景：虚拟线程非常适合需要创建和销毁大量线程的任务、需要执行大量计算的任务（如数据处理、科学计算等）以及需要实现任务并行执行以提高程序性能的场景。</li><li>提高系统吞吐量：通过对虚拟线程的介绍和与Go协程的对比，可以看出虚拟线程能够大幅提高系统的整体吞吐量。</li></ol><p>不考虑虚拟线程实现原理，对开发者而言，使用体验上与传统线程几乎没有区别。</p><pre><code class="Java">try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {    IntStream.range(0, 10_000).forEach(i -&gt; {        executor.submit(() -&gt; {            Thread.sleep(Duration.ofSeconds(1));            System.out.println(Thread.currentThread().getName() + &quot;: &quot; + i);            return i;        });    });}Thread.startVirtualThread(() -&gt; {    System.out.println(&quot;Hello from a virtual thread[Thread.startVirtualThread]&quot;);});final ThreadFactory factory = Thread.ofVirtual().factory();factory.newThread(() -&gt; {            System.out.println(&quot;Hello from a virtual thread[ThreadFactory.newThread]&quot;);        })        .start();</code></pre><p>虚拟线程为了降低使用门槛，直接提供了与原生线程类似的方法：</p><ul><li><code>Executors.newVirtualThreadPerTaskExecutor()</code>，可以像普通线程池一样创建虚拟线程。</li><li><code>Thread.startVirtualThread</code>，通过工具方法直接创建并运行虚拟线程。</li><li><code>Thread.ofVirtual().factory().newThread()</code>，另一个工具方法可以创建并运行虚拟线程。<code>Thread</code>还有一个<code>ofPlatform()</code>方法，用来构建普通线程。</li></ul><p>需要注意的是，虚拟线程适用于IO密集场景，而非CPU密集的场景。</p><h3 id="作用域值（预览特性）"><a href="#作用域值（预览特性）" class="headerlink" title="作用域值（预览特性）"></a>作用域值（预览特性）</h3><p>作用域值（Scoped Values），旨在促进在线程内和线程间共享不可变数据。这一特性为现代 Java 应用程序提供了一种更高效和安全的替代传统 ThreadLocal 机制的方法，尤其是在并发编程的背景下。</p><p>作用域值允许开发者定义一个变量，该变量可以在特定的作用域内访问，包括当前线程及其创建的任何子线程。这一机制特别适用于在方法调用链中隐式传递数据，而无需在方法签名中添加额外的参数。</p><p>作用域值的主要特点：</p><ul><li>不可变性：作用域值是不可变的，这意味着一旦设置，其值就不能更改。这种不可变性减少了并发编程中意外副作用的风险。</li><li>作用域生命周期：作用域值的生命周期仅限于 run 方法定义的作用域。一旦执行离开该作用域，作用域值将不再可访问。</li><li>继承性：子线程会自动继承父线程的作用域值，从而允许在线程边界间无缝共享数据。</li></ul><p>在之前，在多线程间传递数据，我们会使用<code>ThreadLocal</code>来保存当前线程变量，用完需要手动清理，如果忘记清理或者使用不规范，可能导致内存泄漏等问题。作用域值通过自动管理生命周期和内存，减少了这种风险。</p><p>我们一起看下作用域值的使用：</p><pre><code class="Java">// 声明一个作用域值用于存储用户名public final static ScopedValue&lt;String&gt; USERNAME = ScopedValue.newInstance();private static final Runnable printUsername = () -&gt;        System.out.println(Thread.currentThread().threadId() + &quot; 用户名是 &quot; + USERNAME.get());public static void main(String[] args) throws Exception {    // 将用户名 &quot;Bob&quot; 绑定到作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Bob&quot;).run(() -&gt; {        printUsername.run();        new Thread(printUsername).start();    });    // 将用户名 &quot;Chris&quot; 绑定到另一个作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Chris&quot;).run(() -&gt; {        printUsername.run();        new Thread(() -&gt; {            new Thread(printUsername).start();            printUsername.run();        }).start();    });    // 检查在任何作用域外 USERNAME 是否被绑定    System.out.println(&quot;用户名是否被绑定: &quot; + USERNAME.isBound());}</code></pre><p>写起来干净利索，而且功能更强。</p><h3 id="结构化并发API（预览特性）"><a href="#结构化并发API（预览特性）" class="headerlink" title="结构化并发API（预览特性）"></a>结构化并发API（预览特性）</h3><p>结构化并发API（Structured Concurrency API）旨在简化多线程编程，通过引入一个API来处理在不同线程中运行的多个任务作为一个单一工作单元，从而简化错误处理和取消操作，提高可靠性，并增强可观测性。这是一个处于孵化阶段的API。</p><p>结构化并发API提供了明确的语法结构来定义子任务的生命周期，并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。</p><p>Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时，如何被传递和处理的过程称为异常传播。</p><p>在结构化并发环境中，异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。</p><p>在Java并发编程中，非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理，这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。</p><pre><code class="Java">try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {    var task1 = scope.fork(() -&gt; {        Thread.sleep(1000);        return &quot;Result from task 1&quot;;    });    var task2 = scope.fork(() -&gt; {        Thread.sleep(2000);        return &quot;Result from task 2&quot;;    });    scope.join();    scope.throwIfFailed(RuntimeException::new);    System.out.println(task1.get());    System.out.println(task2.get());} catch (Exception e) {    e.printStackTrace();}</code></pre><p>在这个例子中，handle()方法使用StructuredTaskScope来并行执行两个子任务：task1和task2。通过使用try-with-resources语句自动管理资源，并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关，提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性：</p><ul><li>错误处理与短路：如果task1或task2子任务中的任何一个失败，另一个如果尚未完成则会被取消。（这由 ShutdownOnFailure 实现的关闭策略来管理；还有其他策略可能）。</li><li>取消传播：如果在运行上面方法的线程在调用 join() 之前或之中被中断，则线程在退出作用域时会自动取消两个子任务。</li><li>清晰性：设置子任务，等待它们完成或被取消，然后决定是成功（并处理已经完成的子任务的结果）还是失败（子任务已经完成，因此没有更多需要清理的）。</li><li>可观察性：线程转储清楚地显示了任务层次结构，其中运行task1或task2的线程被显示为作用域的子任务。</li></ul><p>上面的示例能够很好的解决我们的一个痛点，有两个可并行的任务A和B，A+B才是完整结果，任何一个失败，另外一个也不需要成功，结构化并发API就可以很容易的实现这个逻辑。</p><h2 id="垃圾收集器"><a href="#垃圾收集器" class="headerlink" title="垃圾收集器"></a>垃圾收集器</h2><h3 id="G1（Java-9）"><a href="#G1（Java-9）" class="headerlink" title="G1（Java 9）"></a>G1（Java 9）</h3><p>G1垃圾收集器（Garbage-First Garbage Collector，简称G1 GC）是一种专为服务器端应用设计的垃圾收集器，特别适用于具有多核处理器和大内存的机器。它在Java 9中成为默认的垃圾收集器。</p><ol><li>分区机制：G1将整个堆空间划分为若干个大小相等的独立区域（Region），每个Region的大小根据堆的实际大小而定，通常控制在1MB到32MB之间。这种分区的思想弱化了传统的分代概念，使得垃圾收集更加灵活和高效。</li><li>垃圾回收策略：G1采用混合垃圾回收策略（Mix GC），不仅回收新生代中的所有Region，还会回收部分老年代中的Region。这种策略的目标是在保证停顿时间不超过预期的情况下，尽可能地回收更多的垃圾对象。</li><li>并行与并发：G1能够充分利用CPU、多核环境下的硬件优势，使用多个CPU核心来缩短Stop-The-World停顿时间。部分其他收集器需要停顿Java线程执行的GC动作，但G1可以通过并发的方式让Java程序继续运行。</li><li>高吞吐量与低延迟：G1的主要目标是在满足高吞吐量的同时，尽可能减少GC停顿时间。通过跟踪各个Region中的垃圾堆积情况，每次根据设置的垃圾回收时间，优先处理优先级最高的区域，避免一次性清理整个新生代或整个老年代的垃圾。</li></ol><p>G1垃圾收集器非常适合大尺寸堆内存的应用场景，特别是在多处理器环境下。它的设计目标是能够在有限的时间内获取尽可能高的收集效率，并且避免内存碎片。对于那些对GC停顿时间敏感的应用，如实时系统和大数据处理系统，G1是一个非常合适的选择。</p><h3 id="ZGC（Java-15）"><a href="#ZGC（Java-15）" class="headerlink" title="ZGC（Java 15）"></a>ZGC（Java 15）</h3><p>ZGC 是一个可伸缩、低延迟的垃圾收集器，使用-XX:+UseZGC 命令开启。</p><p>名称中的“Z”并没有特定的含义，主要是一个名称，灵感来源于Oracle的ZFS文件系统。ZFS在设计上具有革命性，因此ZGC的命名也向其致敬。</p><p>在Java15发布时，提供的是不分代收集器，所有对象都放在一起，期望STW最大10ms。</p><p>在Java21的时候，基于「大部分对象朝生夕死」的分代假说，ZGC提供了分代版本，将内存划分为年轻代和老年代，并为这两种代分别维护不同的垃圾收集策略，期望STW最大是1ms。</p><p>我们看下<a href="https://kstefanj.github.io/2023/11/07/hazelcast-on-generational-zgc.html" target="_blank" rel="noopener">Hazelcast Jet on Generational ZGC</a>中给出的测评效果：</p><p><img src="https://static.howardliu.cn/202409161336772.png" alt="JEP 439: 分代ZGC（Generational ZGC）"></p><p>从上图可以看到，非分代 ZGC 在低负载下表现非常好，但随着分配压力的增加，延迟也会增加。使用分代 ZGC 后，即使在高负载下，延迟也非常低，而且延迟效果优于G1。</p><h3 id="Shenandoah（Java-15）"><a href="#Shenandoah（Java-15）" class="headerlink" title="Shenandoah（Java 15）"></a>Shenandoah（Java 15）</h3><p>Shenandoah（读音：谢南多厄），作为一个低停顿的垃圾收集器。</p><p>Shenandoah 垃圾收集器是 RedHat 在 2014 年宣布进行的垃圾收集器研究项目，其工作原理是通过与 Java 应用执行线程同时运行来降低停顿时间。</p><p>简单的说就是，Shenandoah 工作时与应用程序线程并发，通过交换 CPU 并发周期和空间以改善停顿时间，使得垃圾回收器执行线程能够在 Java 线程运行时进行堆压缩，并且标记和整理能够同时进行，因此避免了在大多数 JVM 垃圾收集器中所遇到的问题。</p><p><img src="https://static.howardliu.cn/202409161336975.png" alt="Shenandoah GC"></p><p>Shenandoah 垃圾回收器的暂停时间与堆大小无关，这意味着无论将堆设置为 200MB 还是 200GB，都将拥有一致的系统暂停时间，不过实际使用性能将取决于实际工作堆的大小和工作负载。</p><p><a href="https://wiki.openjdk.java.net/display/shenandoah" target="_blank" rel="noopener">https://wiki.openjdk.java.net/display/shenandoah</a></p><h2 id="一些小特性"><a href="#一些小特性" class="headerlink" title="一些小特性"></a>一些小特性</h2><h3 id="增强-String"><a href="#增强-String" class="headerlink" title="增强 String"></a>增强 String</h3><p>String中新增的方法：repeat、strip、stripLeading、stripTrailing、isBlank、lines、indent 和 transform。</p><p>这些方法还是挺有用的，以前我们可能需要借助第三方类库（比如 Apache 出品的 commons-lang）中的工具类，现在可以直接使用嫡亲方法了。</p><h4 id="repeat"><a href="#repeat" class="headerlink" title="repeat"></a>repeat</h4><p>repeat是实例方法，顾名思义，这个方法是返回给定字符串的重复值的，参数是int类型，传参的时候需要注意：</p><ul><li>如果重复次数小于 0 会抛出IllegalArgumentException异常；</li><li>如果重复次数为 0 或者字符串本身是空字符串，将返回空字符串；</li><li>如果重复次数为 1 直接返回本身；</li><li>如果字符串重复指定次数后，长度超过Integer.MAX_VALUE<code>，会抛出</code>OutOfMemoryError`错误。</li></ul><p>用法很简单：</p><pre><code class="Java">@Testvoid testRepeat() {    String output = &quot;foo &quot;.repeat(2) + &quot;bar&quot;;    assertEquals(&quot;foo foo bar&quot;, output);}</code></pre><p>小而美的一个工具方法。</p><h4 id="strip、stripLeading、stripTrailing"><a href="#strip、stripLeading、stripTrailing" class="headerlink" title="strip、stripLeading、stripTrailing"></a>strip、stripLeading、stripTrailing</h4><p><code>strip</code>方法算是<code>trim</code>方法的增强版，<code>trim</code>方法可以删除字符串两侧的空白字符（空格、tab 键、换行符），但是对于<code>Unicode</code>的空白字符无能为力，<code>strip</code>补足这一短板。</p><p>用起来是这样的：</p><pre><code class="Java">@Testvoid testTrip() {    final String output = &quot;      hello   \u2005&quot;.strip();    assertEquals(&quot;hello&quot;, output);    final String trimOutput = &quot;      hello   \u2005&quot;.trim();    assertEquals(&quot;hello   \u2005&quot;, trimOutput);}</code></pre><p>对比一下可以看到，<code>trim</code>方法的清理功能稍弱。</p><p><code>stripLeading</code>和<code>stripTrailing</code>与<code>strip</code>类似，区别是一个清理头，一个清理尾。用法如下：</p><pre><code class="Java">@Testvoid testTripLeading() {    final String output = &quot;      hello   \u2005&quot;.stripLeading();    assertEquals(&quot;hello   \u2005&quot;, output);}@Testvoid testTripTrailing() {    final String output = &quot;      hello   \u2005&quot;.stripTrailing();    assertEquals(&quot;      hello&quot;, output);}</code></pre><h4 id="isBlank"><a href="#isBlank" class="headerlink" title="isBlank"></a>isBlank</h4><p>这个方法是用于判断字符串是否都是空白字符，除了空格、tab 键、换行符，也包括<code>Unicode</code>的空白字符。</p><p>用法很简单：</p><pre><code class="Java">@Testvoid testIsBlank() {    assertTrue(&quot;    \u2005&quot;.isBlank());}</code></pre><h4 id="lines"><a href="#lines" class="headerlink" title="lines"></a>lines</h4><p>最后这个方法是将字符串转化为字符串<code>Stream</code>类型，字符串分隔依据是换行符：<code>\n</code>、<code>\r</code>、<code>\r\n</code>，用法如下：</p><pre><code class="Java">@Testvoid testLines() {    final String multiline = &quot;This isa multilinestring.&quot;;    final String output = multiline.lines()            .filter(Predicate.not(String::isBlank))            .collect(Collectors.joining(&quot; &quot;));    assertEquals(&quot;This is a multiline string.&quot;, output);}</code></pre><h4 id="indent"><a href="#indent" class="headerlink" title="indent"></a>indent</h4><p><code>indent</code>方法是对字符串每行（使用<code>\r</code>或<code>\n</code>分隔）数据缩进指定空白字符，参数是 int 类型。</p><p>如果参数大于 0，就缩进指定数量的空格；如果参数小于 0，就将左侧的空字符删除指定数量，即右移。</p><p>我们看下源码：</p><pre><code class="Java">public String indent(int n) {    if (isEmpty()) {        return &quot;&quot;;    }    Stream&lt;String&gt; stream = lines();    if (n &gt; 0) {        final String spaces = &quot; &quot;.repeat(n);        stream = stream.map(s -&gt; spaces + s);    } else if (n == Integer.MIN_VALUE) {        stream = stream.map(s -&gt; s.stripLeading());    } else if (n &lt; 0) {        stream = stream.map(s -&gt; s.substring(Math.min(-n, s.indexOfNonWhitespace())));    }    return stream.collect(Collectors.joining(&quot;&quot;, &quot;&quot;, &quot;&quot;));}</code></pre><p><code>indent</code>最后会将多行数据通过<code>Collectors.joining(&quot;\n&quot;, &quot;&quot;, &quot;\n&quot;)</code>方法拼接，结果会有两点需要注意：</p><ul><li><code>\r</code>会被替换成<code>\n</code>；</li><li>如果原字符串是多行数据，最后一行的结尾没有<code>\n</code>，最后会补上一个<code>\n</code>，即多了一个空行。</li></ul><p>我们看下测试代码：</p><pre><code class="Java">@Testvoid testIndent() {    final String text = &quot;             你好，我是看山。 \u0020\u2005Java12 的 新特性。 欢迎三连+关注哟&quot;;    assertEquals(&quot;                 你好，我是看山。     \u0020\u2005Java12 的 新特性。    欢迎三连+关注哟、n&quot;, text.indent(4));    assertEquals(&quot;     你好，我是看山。\u2005Java12 的 新特性。 欢迎三连+关注哟、n&quot;, text.indent(-2));    final String text2 = &quot;山水有相逢&quot;;    assertEquals(&quot;山水有相逢&quot;, text2);}</code></pre><h4 id="transform"><a href="#transform" class="headerlink" title="transform"></a>transform</h4><p>我们再来看看<code>transform</code>方法，源码一目了然：</p><pre><code class="Java">public &lt;R&gt; R transform(Function&lt;? super String, ? extends R&gt; f) {    return f.apply(this);}</code></pre><p>通过传入的<code>Function</code>对当前字符串进行转换，转换结果由<code>Function</code>决定。比如，我们要对字符串反转：</p><pre><code class="Java">@Testvoid testTransform() {    final String text = &quot;看山是山&quot;;    final String reverseText = text.transform(s -&gt; new StringBuilder(s).reverse().toString());    assertEquals(&quot;山是山看&quot;, reverseText);}</code></pre><h3 id="增强文件读写（Java-11）"><a href="#增强文件读写（Java-11）" class="headerlink" title="增强文件读写（Java 11）"></a>增强文件读写（Java 11）</h3><p>本次更新在<code>Files</code>中增加了两个方法：<code>readString</code>和<code>writeString</code>。<code>writeString</code>作用是将指定字符串写入文件，<code>readString</code>作用是从文件中读出内容到字符串。是一个对<code>Files</code>工具类的增强，封装了对输出流、字节等内容的操作。</p><p>用法比较简单：</p><pre><code class="Java">@Testvoid testReadWriteString() throws IOException {    final Path tmpPath = Path.of(&quot;./&quot;);    final Path tempFile = Files.createTempFile(tmpPath, &quot;demo&quot;, &quot;.txt&quot;);    final Path filePath = Files.writeString(tempFile, &quot;看山 howardliu.cn 公众号：看山的小屋&quot;);    assertEquals(tempFile, filePath);    final String fileContent = Files.readString(filePath);    assertEquals(&quot;看山 howardliu.cn 公众号：看山的小屋&quot;, fileContent);    Files.deleteIfExists(filePath);}</code></pre><p><code>readString</code>和<code>writeString</code>还可以指定字符集，不指定默认使用<code>StandardCharsets.UTF\_8</code>字符集，可以应对大部分场景了。</p><h3 id="增强函数-Predicate（Java-11）"><a href="#增强函数-Predicate（Java-11）" class="headerlink" title="增强函数 Predicate（Java 11）"></a>增强函数 Predicate（Java 11）</h3><p>这个也是方法增强，在以前，我们在<code>Stream</code>中的<code>filter</code>方法判断否的时候，一般需要<code>!</code>运算，比如我们想要找到字符串列表中的数字，可以这样写：</p><pre><code class="Java">final List&lt;String&gt; list = Arrays.asList(&quot;1&quot;, &quot;a&quot;);final List&lt;String&gt; nums = list.stream()        .filter(NumberUtils::isDigits)        .collect(Collectors.toList());Assertions.assertEquals(1, nums.size());Assertions.assertTrue(nums.contains(&quot;1&quot;));</code></pre><p>想要找到非数字的，<code>filter</code>方法写的就会用到<code>!</code>非操作：</p><pre><code class="Java">final List&lt;String&gt; notNums = list.stream()        .filter(x -&gt; !NumberUtils.isDigits(x))        .collect(Collectors.toList());Assertions.assertEquals(1, notNums.size());Assertions.assertTrue(notNums.contains(&quot;a&quot;));</code></pre><p><code>Predicate</code>增加<code>not</code>方法，可以更加简单的实现非操作：</p><pre><code class="Java">final List&lt;String&gt; notNums2 = list.stream()        .filter(Predicate.not(NumberUtils::isDigits))        .collect(Collectors.toList());Assertions.assertEquals(1, notNums2.size());Assertions.assertTrue(notNums2.contains(&quot;a&quot;));</code></pre><p>有些教程还会推崇静态引入，比如在头部使用<code>import static java.util.function.Predicate.not</code>，这样在函数式编程时，可以写更少的代码，语义更强，比如：</p><pre><code class="Java">final List&lt;String&gt; notNums2 = list.stream()        .filter(not(NumberUtils::isDigits))        .collect(toList());</code></pre><h3 id="未命名模式和变量（Java-22）"><a href="#未命名模式和变量（Java-22）" class="headerlink" title="未命名模式和变量（Java 22）"></a>未命名模式和变量（Java 22）</h3><p>该特性使用下划线字符 <code>\_</code> 来表示未命名的模式和变量，从而简化代码并提高代码可读性和可维护性。</p><p>比如：</p><pre><code class="Java">public static void main(String[] args) {    var _ = new Point(1, 2);}record Point(int x, int y) {}</code></pre><p>这个可以用在任何定义变量的地方，比如：</p><ul><li><code>... instanceof Point(\_, int y)</code></li><li><code>r instanceof Point \_</code></li><li><code>switch …… case Box(\_)</code></li><li><code>for (Order \_ : orders)</code></li><li><code>for (int i = 0, \_ = sideEffect(); i &lt; 10; i++)</code></li><li><code>try { ... } catch (Exception \_) { ... } catch (Throwable \_) { ... }</code></li></ul><p>只要是这个不准备用，可以一律使用<code>\_</code>代替。</p><h3 id="Markdown格式文档注释（Java-23）"><a href="#Markdown格式文档注释（Java-23）" class="headerlink" title="Markdown格式文档注释（Java 23）"></a>Markdown格式文档注释（Java 23）</h3><p>Markdown是一种轻量级的标记语言，可用于在纯文本文档中添加格式化元素，具体语法可以参考<a href="https://www.markdownguide.org/basic-syntax/" target="_blank" rel="noopener">Markdown Guide</a>。本文就是使用Markdown语法编写的。</p><p>在Java注释中引入Markdown，目标是使API文档注释以源代码形式更易于编写和阅读。主要收益包括：</p><ul><li>提高文档编写的效率：Markdown语法相比HTML更为简洁，开发者可以更快地编写和修改文档注释。</li><li>增强文档的可读性：Markdown格式的文档在源代码中更易于阅读，有助于开发者快速理解API的用途和行为。</li><li>促进文档的一致性：通过支持Markdown，可以确保文档风格的一致性，减少因格式问题导致的文档混乱。</li><li>简化文档维护：Markdown格式的文档注释更易于维护和更新，特别是在多人协作的项目中，可以减少因文档格式问题导致的沟通成本。</li></ul><p>具体使用方式是在注释前面增加<code>///</code>，比如<code>java.lang.Object.hashCode</code>的注释：</p><pre><code class="Java">/** * Returns a hash code value for the object. This method is * supported for the benefit of hash tables such as those provided by * {@link java.util.HashMap}. * &lt;p&gt; * The general contract of {@code hashCode} is: * &lt;ul&gt; * &lt;li&gt;Whenever it is invoked on the same object more than once during *     an execution of a Java application, the {@code hashCode} method *     must consistently return the same integer, provided no information *     used in {@code equals} comparisons on the object is modified. *     This integer need not remain consistent from one execution of an *     application to another execution of the same application. * &lt;li&gt;If two objects are equal according to the {@link *     #equals(Object) equals} method, then calling the {@code *     hashCode} method on each of the two objects must produce the *     same integer result. * &lt;li&gt;It is &lt;em&gt;not&lt;/em&gt; required that if two objects are unequal *     according to the {@link #equals(Object) equals} method, then *     calling the {@code hashCode} method on each of the two objects *     must produce distinct integer results.  However, the programmer *     should be aware that producing distinct integer results for *     unequal objects may improve the performance of hash tables. * &lt;/ul&gt; * * @implSpec * As far as is reasonably practical, the {@code hashCode} method defined * by class {@code Object} returns distinct integers for distinct objects. * * @return  a hash code value for this object. * @see     java.lang.Object#equals(java.lang.Object) * @see     java.lang.System#identityHashCode */</code></pre><p>如果使用JEP 467的Markdown方式：</p><pre><code class="Java">/// Returns a hash code value for the object. This method is/// supported for the benefit of hash tables such as those provided by/// [java.util.HashMap].////// The general contract of `hashCode` is://////   - Whenever it is invoked on the same object more than once during///     an execution of a Java application, the `hashCode` method///     must consistently return the same integer, provided no information///     used in `equals` comparisons on the object is modified.///     This integer need not remain consistent from one execution of an///     application to another execution of the same application.///   - If two objects are equal according to the///     [equals][#equals(Object)] method, then calling the///     `hashCode` method on each of the two objects must produce the///     same integer result.///   - It is _not_ required that if two objects are unequal///     according to the [equals][#equals(Object)] method, then///     calling the `hashCode` method on each of the two objects///     must produce distinct integer results.  However, the programmer///     should be aware that producing distinct integer results for///     unequal objects may improve the performance of hash tables.////// @implSpec/// As far as is reasonably practical, the `hashCode` method defined/// by class `Object` returns distinct integers for distinct objects.////// @return  a hash code value for this object./// @see     java.lang.Object#equals(java.lang.Object)/// @see     java.lang.System#identityHashCode</code></pre><p>简单两种写法的差异，相同注释，Markdown的写法更加简洁：</p><p><img src="https://static.howardliu.cn/202409161338899.png" alt="Object hashcode注释差异"></p><h2 id="一些可能不会用到的特性"><a href="#一些可能不会用到的特性" class="headerlink" title="一些可能不会用到的特性"></a>一些可能不会用到的特性</h2><h3 id="web服务器jwebserver（Java-18）"><a href="#web服务器jwebserver（Java-18）" class="headerlink" title="web服务器jwebserver（Java 18）"></a>web服务器jwebserver（Java 18）</h3><p>为开发者提供一个轻量级、简单易用的 HTTP 服务器，通过命令行工具<code>jwebserver</code>可以启动一个最小化的静态 Web 服务器，这个服务器仅支持静态资源的访问，不支持 CGI（Common Gateway Interface）或类似 servlet 的功能，主要用于原型制作、测试和开发环境中的静态文件托管与共享。</p><p>它的应用场景包括：</p><ul><li>原型开发：由于其简单易用的特性，jwebserver 可以作为快速原型开发工具，帮助开发者在短时间内搭建起一个可以访问静态资源的 Web 服务。这对于需要验证某个功能或概念的初期阶段非常有用。</li><li>快速部署：对于一些小规模的应用或者临时性的项目，使用 jwebserver 可以快速启动并运行一个简单的 Web 服务，而无需复杂的配置和环境搭建。这使得开发者能够迅速将想法转化为实际的可访问服务。</li><li>学习与教育：jwebserver 提供了一个直观的平台，让初学者可以轻松上手 Java Web 开发。通过简单的命令行操作，用户可以快速理解 Web 服务器的工作原理及其基本配置。</li><li>测试与调试：在进行 Web 应用的测试和调试时，jwebserver 可以作为一个独立的工具来提供静态文件的访问服务，从而方便开发者对应用进行测试和调试。</li><li>本地开发环境：在本地开发环境中，jwebserver 可以替代传统的 Web 服务器如 Apache Tomcat 或 Nginx，为开发者提供一个轻量级的选择，以减少系统资源的占用。</li></ul><p>我们可以简单试一下，在当前目录编写index.html：</p><pre><code class="HTML">&lt;html&gt;&lt;head&gt;    &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot; /&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;欢迎参观&lt;/h2&gt;    &lt;h3&gt;&lt;a href=&quot;https://www.howardliu.cn/&quot;&gt;看山的小屋 howardliu.cn&lt;/a&gt;&lt;/h3&gt;    &lt;p&gt;        一起&lt;strong&gt;开心&lt;/strong&gt;学技术    &lt;/p&gt;    &lt;p&gt;        让我们一起&lt;strong&gt;扬帆起航&lt;/strong&gt;    &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</code></pre><p>运行<code>jwebserver</code>命令：</p><pre><code class="Bash">$ ./bin/jwebserverBinding to loopback by default. For all interfaces use &quot;-b 0.0.0.0&quot; or &quot;-b ::&quot;.Serving /Users/liuxinghao/Library/Java/JavaVirtualMachines/temurin-18.0.2.1/Contents/Home and subdirectories on 127.0.0.1 port 8000URL http://127.0.0.1:8000/</code></pre><p>打开<code>[http://127.0.0.1:8000/](http://127.0.0.1:8000/)</code>就可以直接看到index.html的效果：</p><p><img src="https://static.howardliu.cn/202409161340780.png" alt="公众号：看山的小屋"></p><p><code>jwebserver</code>还支持指定地址和端口等参数，具体使用可以通过命令查看：</p><pre><code class="Bash">$ ./bin/jwebserver -hUsage: jwebserver [-b bind address] [-p port] [-d directory]                  [-o none|info|verbose] [-h to show options]                  [-version to show version information]Options:-b, --bind-address    - Address to bind to. Default: 127.0.0.1 (loopback).                        For all interfaces use &quot;-b 0.0.0.0&quot; or &quot;-b ::&quot;.-d, --directory       - Directory to serve. Default: current directory.-o, --output          - Output format. none|info|verbose. Default: info.-p, --port            - Port to listen on. Default: 8000.-h, -?, --help        - Prints this help message and exits.-version, --version   - Prints version information and exits.</code></pre><h3 id="隐式声明的类和实例方法（预览特性）"><a href="#隐式声明的类和实例方法（预览特性）" class="headerlink" title="隐式声明的类和实例方法（预览特性）"></a>隐式声明的类和实例方法（预览特性）</h3><p>隐式声明的类和实例方法的目标是简化 Java 语言，使得学生和初学者可以更容易地编写他们的第一个程序，而无需理解为大型程序设计的复杂语言特性。</p><p>无论学习哪门语言，第一课一定是打印<code>Hello, World!</code>，Java中的写法是：</p><pre><code class="Java">public class HelloWorld {    public static void main(String[] args) {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>如果是第一次接触，一定会有很多疑问，<code>public</code>干啥的，<code>main</code>方法的约定参数<code>args</code>是什么鬼？然后老师就说，这就是模板，照着抄就行，不这样写不运行。</p><p>现在可以简化为：</p><pre><code class="Java">class HelloWorld {    void main() {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>我们还可以这样写：</p><pre><code class="Java">String greeting() { return &quot;Hello, World!&quot;; }void main() {    System.out.println(greeting());}</code></pre><p><code>main</code>方法直接简化为名字和括号，甚至连类也不需要显性定义了。虽然看起来没啥用，但是在JShell中使用，就比较友好了。</p><p>本次预览新增了三个IO操作方法：</p><pre><code class="Java">public static void println(Object obj);public static void print(Object obj);public static String readln(String prompt);</code></pre><p>想要快速实现控制台操作，可以这样写了：</p><pre><code class="Java">void main() {    String name = readln(&quot;请输入姓名: &quot;);    print(&quot;很高兴见到你, &quot;);    println(name);}</code></pre><p>作为一个老程序猿，也不得不哇塞一下。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>想要了解各版本的详细特性，可以从<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中查看。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><ul><li>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a></li><li>个人博文：<a href="https://www.howardliu.cn/">从Java8到Java23值得期待的x个特性</a></li><li>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a></li><li>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">从Java8到Java23值得期待的x个特性</a></li></ul><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      万字长文掌握从Java8到Java23值得期待的x个特性。从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java10" scheme="https://www.howardliu.cn/tags/Java10/"/>
    
      <category term="Java11" scheme="https://www.howardliu.cn/tags/Java11/"/>
    
      <category term="Java12" scheme="https://www.howardliu.cn/tags/Java12/"/>
    
      <category term="Java14" scheme="https://www.howardliu.cn/tags/Java14/"/>
    
      <category term="Java15" scheme="https://www.howardliu.cn/tags/Java15/"/>
    
      <category term="Java13" scheme="https://www.howardliu.cn/tags/Java13/"/>
    
      <category term="Java16" scheme="https://www.howardliu.cn/tags/Java16/"/>
    
      <category term="Java17" scheme="https://www.howardliu.cn/tags/Java17/"/>
    
      <category term="Java18" scheme="https://www.howardliu.cn/tags/Java18/"/>
    
      <category term="Java19" scheme="https://www.howardliu.cn/tags/Java19/"/>
    
      <category term="Java20" scheme="https://www.howardliu.cn/tags/Java20/"/>
    
      <category term="Java21" scheme="https://www.howardliu.cn/tags/Java21/"/>
    
      <category term="Java8" scheme="https://www.howardliu.cn/tags/Java8/"/>
    
      <category term="Java22" scheme="https://www.howardliu.cn/tags/Java22/"/>
    
      <category term="Java9" scheme="https://www.howardliu.cn/tags/Java9/"/>
    
      <category term="Java23" scheme="https://www.howardliu.cn/tags/Java23/"/>
    
  </entry>
  
  <entry>
    <title>从Java8到Java23值得期待的x个特性（3）：永远学不完的多线程</title>
    <link href="https://www.howardliu.cn/java/java-features-8-23-3/"/>
    <id>https://www.howardliu.cn/java/java-features-8-23-3/</id>
    <published>2024-09-19T00:20:00.000Z</published>
    <updated>2024-12-06T11:36:03.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409161331954.jpg" alt="从Java8到Java23值得期待的x个特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。</p><p><img src="https://static.howardliu.cn/202409111451409.png" alt="Java版本分布"></p><blockquote><p>数据来源<a href="https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-2024-05-29.pdf" target="_blank" rel="noopener">2024 State of the Java Ecosystem</a></p></blockquote><h2 id="永远学不完的多线程"><a href="#永远学不完的多线程" class="headerlink" title="永远学不完的多线程"></a>永远学不完的多线程</h2><h3 id="虚拟线程（Java-21）"><a href="#虚拟线程（Java-21）" class="headerlink" title="虚拟线程（Java 21）"></a>虚拟线程（Java 21）</h3><p>虚拟线程是一种轻量级的线程实现，旨在显著降低编写、维护和观察高吞吐量并发应用程序的难度。它们占用的资源少，不需要被池化，可以创建大量虚拟线程，特别适用于IO密集型任务，因为它们可以高效地调度大量虚拟线程来处理并发请求，从而显著提高程序的吞吐量和响应速度。</p><p>虚拟线程有下面几个特点：</p><ol><li>轻量级：虚拟线程是JVM内部实现的轻量级线程，不需要操作系统内核参与，创建和上下文切换的成本远低于传统的操作系统线程（即平台线程），且占用的内存资源较少。</li><li>减少CPU时间消耗：由于虚拟线程不依赖于操作系统平台线程，因此在进行线程切换时耗费的CPU时间会大大减少，从而提高了程序的执行效率。</li><li>简化多线程编程：虚拟线程通过结构化并发API来简化多线程编程，使得开发者可以更容易地编写、维护和观察高吞吐量并发应用程序。</li><li>适用于大量任务场景：虚拟线程非常适合需要创建和销毁大量线程的任务、需要执行大量计算的任务（如数据处理、科学计算等）以及需要实现任务并行执行以提高程序性能的场景。</li><li>提高系统吞吐量：通过对虚拟线程的介绍和与Go协程的对比，可以看出虚拟线程能够大幅提高系统的整体吞吐量。</li></ol><p>不考虑虚拟线程实现原理，对开发者而言，使用体验上与传统线程几乎没有区别。</p><pre><code class="Java">try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {    IntStream.range(0, 10_000).forEach(i -&gt; {        executor.submit(() -&gt; {            Thread.sleep(Duration.ofSeconds(1));            System.out.println(Thread.currentThread().getName() + &quot;: &quot; + i);            return i;        });    });}Thread.startVirtualThread(() -&gt; {    System.out.println(&quot;Hello from a virtual thread[Thread.startVirtualThread]&quot;);});final ThreadFactory factory = Thread.ofVirtual().factory();factory.newThread(() -&gt; {            System.out.println(&quot;Hello from a virtual thread[ThreadFactory.newThread]&quot;);        })        .start();</code></pre><p>虚拟线程为了降低使用门槛，直接提供了与原生线程类似的方法：</p><ul><li><code>Executors.newVirtualThreadPerTaskExecutor()</code>，可以像普通线程池一样创建虚拟线程。</li><li><code>Thread.startVirtualThread</code>，通过工具方法直接创建并运行虚拟线程。</li><li><code>Thread.ofVirtual().factory().newThread()</code>，另一个工具方法可以创建并运行虚拟线程。<code>Thread</code>还有一个<code>ofPlatform()</code>方法，用来构建普通线程。</li></ul><p>需要注意的是，虚拟线程适用于IO密集场景，而非CPU密集的场景。</p><h3 id="作用域值（预览特性）"><a href="#作用域值（预览特性）" class="headerlink" title="作用域值（预览特性）"></a>作用域值（预览特性）</h3><p>作用域值（Scoped Values），旨在促进在线程内和线程间共享不可变数据。这一特性为现代 Java 应用程序提供了一种更高效和安全的替代传统 ThreadLocal 机制的方法，尤其是在并发编程的背景下。</p><p>作用域值允许开发者定义一个变量，该变量可以在特定的作用域内访问，包括当前线程及其创建的任何子线程。这一机制特别适用于在方法调用链中隐式传递数据，而无需在方法签名中添加额外的参数。</p><p>作用域值的主要特点：</p><ul><li>不可变性：作用域值是不可变的，这意味着一旦设置，其值就不能更改。这种不可变性减少了并发编程中意外副作用的风险。</li><li>作用域生命周期：作用域值的生命周期仅限于 run 方法定义的作用域。一旦执行离开该作用域，作用域值将不再可访问。</li><li>继承性：子线程会自动继承父线程的作用域值，从而允许在线程边界间无缝共享数据。</li></ul><p>在之前，在多线程间传递数据，我们会使用<code>ThreadLocal</code>来保存当前线程变量，用完需要手动清理，如果忘记清理或者使用不规范，可能导致内存泄漏等问题。作用域值通过自动管理生命周期和内存，减少了这种风险。</p><p>我们一起看下作用域值的使用：</p><pre><code class="Java">// 声明一个作用域值用于存储用户名public final static ScopedValue&lt;String&gt; USERNAME = ScopedValue.newInstance();private static final Runnable printUsername = () -&gt;        System.out.println(Thread.currentThread().threadId() + &quot; 用户名是 &quot; + USERNAME.get());public static void main(String[] args) throws Exception {    // 将用户名 &quot;Bob&quot; 绑定到作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Bob&quot;).run(() -&gt; {        printUsername.run();        new Thread(printUsername).start();    });    // 将用户名 &quot;Chris&quot; 绑定到另一个作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Chris&quot;).run(() -&gt; {        printUsername.run();        new Thread(() -&gt; {            new Thread(printUsername).start();            printUsername.run();        }).start();    });    // 检查在任何作用域外 USERNAME 是否被绑定    System.out.println(&quot;用户名是否被绑定: &quot; + USERNAME.isBound());}</code></pre><p>写起来干净利索，而且功能更强。</p><h3 id="结构化并发API（预览特性）"><a href="#结构化并发API（预览特性）" class="headerlink" title="结构化并发API（预览特性）"></a>结构化并发API（预览特性）</h3><p>结构化并发API（Structured Concurrency API）旨在简化多线程编程，通过引入一个API来处理在不同线程中运行的多个任务作为一个单一工作单元，从而简化错误处理和取消操作，提高可靠性，并增强可观测性。这是一个处于孵化阶段的API。</p><p>结构化并发API提供了明确的语法结构来定义子任务的生命周期，并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。</p><p>Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时，如何被传递和处理的过程称为异常传播。</p><p>在结构化并发环境中，异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。</p><p>在Java并发编程中，非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理，这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。</p><pre><code class="Java">try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {    var task1 = scope.fork(() -&gt; {        Thread.sleep(1000);        return &quot;Result from task 1&quot;;    });    var task2 = scope.fork(() -&gt; {        Thread.sleep(2000);        return &quot;Result from task 2&quot;;    });    scope.join();    scope.throwIfFailed(RuntimeException::new);    System.out.println(task1.get());    System.out.println(task2.get());} catch (Exception e) {    e.printStackTrace();}</code></pre><p>在这个例子中，handle()方法使用StructuredTaskScope来并行执行两个子任务：task1和task2。通过使用try-with-resources语句自动管理资源，并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关，提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性：</p><ul><li>错误处理与短路：如果task1或task2子任务中的任何一个失败，另一个如果尚未完成则会被取消。（这由 ShutdownOnFailure 实现的关闭策略来管理；还有其他策略可能）。</li><li>取消传播：如果在运行上面方法的线程在调用 join() 之前或之中被中断，则线程在退出作用域时会自动取消两个子任务。</li><li>清晰性：设置子任务，等待它们完成或被取消，然后决定是成功（并处理已经完成的子任务的结果）还是失败（子任务已经完成，因此没有更多需要清理的）。</li><li>可观察性：线程转储清楚地显示了任务层次结构，其中运行task1或task2的线程被显示为作用域的子任务。</li></ul><p>上面的示例能够很好的解决我们的一个痛点，有两个可并行的任务A和B，A+B才是完整结果，任何一个失败，另外一个也不需要成功，结构化并发API就可以很容易的实现这个逻辑。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>想要了解各版本的详细特性，可以从<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中查看。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><ul><li>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a></li><li>个人博文：<a href="https://www.howardliu.cn/">从Java8到Java23值得期待的x个特性（3）：永远学不完的多线程</a></li><li>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a></li><li>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">从Java8到Java23值得期待的x个特性（3）：永远学不完的多线程</a></li></ul><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      永远学不完的多线程。从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java10" scheme="https://www.howardliu.cn/tags/Java10/"/>
    
      <category term="Java11" scheme="https://www.howardliu.cn/tags/Java11/"/>
    
      <category term="Java12" scheme="https://www.howardliu.cn/tags/Java12/"/>
    
      <category term="Java14" scheme="https://www.howardliu.cn/tags/Java14/"/>
    
      <category term="Java15" scheme="https://www.howardliu.cn/tags/Java15/"/>
    
      <category term="Java13" scheme="https://www.howardliu.cn/tags/Java13/"/>
    
      <category term="Java16" scheme="https://www.howardliu.cn/tags/Java16/"/>
    
      <category term="Java17" scheme="https://www.howardliu.cn/tags/Java17/"/>
    
      <category term="Java18" scheme="https://www.howardliu.cn/tags/Java18/"/>
    
      <category term="Java19" scheme="https://www.howardliu.cn/tags/Java19/"/>
    
      <category term="Java20" scheme="https://www.howardliu.cn/tags/Java20/"/>
    
      <category term="Java21" scheme="https://www.howardliu.cn/tags/Java21/"/>
    
      <category term="Java8" scheme="https://www.howardliu.cn/tags/Java8/"/>
    
      <category term="Java22" scheme="https://www.howardliu.cn/tags/Java22/"/>
    
      <category term="Java9" scheme="https://www.howardliu.cn/tags/Java9/"/>
    
      <category term="Java23" scheme="https://www.howardliu.cn/tags/Java23/"/>
    
  </entry>
  
  <entry>
    <title>从Java8到Java23值得期待的x个特性（2）：提升幸福感的特性</title>
    <link href="https://www.howardliu.cn/java/java-features-8-23-2/"/>
    <id>https://www.howardliu.cn/java/java-features-8-23-2/</id>
    <published>2024-09-18T00:20:00.000Z</published>
    <updated>2024-12-05T09:14:01.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409161331867.jpg" alt="从Java8到Java23值得期待的x个特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。</p><p><img src="https://static.howardliu.cn/202409111451409.png" alt="Java版本分布"></p><blockquote><p>数据来源<a href="https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-2024-05-29.pdf" target="_blank" rel="noopener">2024 State of the Java Ecosystem</a></p></blockquote><a id="more"></a><h2 id="提升幸福感的特性"><a href="#提升幸福感的特性" class="headerlink" title="提升幸福感的特性"></a>提升幸福感的特性</h2><h3 id="局部变量类型推断（Java-10）"><a href="#局部变量类型推断（Java-10）" class="headerlink" title="局部变量类型推断（Java 10）"></a>局部变量类型推断（Java 10）</h3><pre><code class="Java">// 以前的写法Map&lt;String, List&lt;ProcSequenceFlow&gt;&gt; sourceMap = sequenceFlowList.stream()        .collect(Collectors.groupingBy(ProcSequenceFlow::getSourceRef));// 现在的写法var sourceMap2 = sequenceFlowList.stream()        .collect(Collectors.groupingBy(ProcSequenceFlow::getSourceRef));</code></pre><h3 id="Switch表达式（Java-14）"><a href="#Switch表达式（Java-14）" class="headerlink" title="Switch表达式（Java 14）"></a>Switch表达式（Java 14）</h3><p>我们以“判断是否工作日”的例子展示一下，在Java14之前：</p><pre><code class="Java">@Testvoid testSwitch() {    final DayOfWeek day = DayOfWeek.from(LocalDate.now());    String typeOfDay = &quot;&quot;;    switch (day) {        case MONDAY:        case TUESDAY:        case WEDNESDAY:        case THURSDAY:        case FRIDAY:            typeOfDay = &quot;Working Day&quot;;            break;        case SATURDAY:        case SUNDAY:            typeOfDay = &quot;Rest Day&quot;;            break;    }    Assertions.assertFalse(typeOfDay.isEmpty());}</code></pre><p>在Java14中：</p><pre><code class="Java">@Testvoid testSwitchExpression() {    final DayOfWeek day = DayOfWeek.SATURDAY;    final String typeOfDay = switch (day) {        case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -&gt; {            System.out.println(&quot;Working Day: &quot; + day);            yield &quot;Working Day&quot;;        }        case SATURDAY, SUNDAY -&gt; &quot;Day Off&quot;;    };    Assertions.assertEquals(&quot;Day Off&quot;, typeOfDay);}</code></pre><p>注意看，例子中我们使用了两种写法，一种是通过<code>yield</code>关键字表示返回结果，一种是在<code>-&gt;</code>之后直接返回。</p><h3 id="文本块（Java-15）"><a href="#文本块（Java-15）" class="headerlink" title="文本块（Java 15）"></a>文本块（Java 15）</h3><p>直接上代码</p><pre><code class="Java">@Testvoid testTextBlock() {    final String singleLine = &quot;你好，我是看山，公众号「看山的小屋」。这行没有换行，而且我的后面多了一个空格 \n 这次换行了&quot;;    final String textBlockSingleLine = &quot;&quot;&quot;            你好，我是看山，公众号「看山的小屋」。\            这行没有换行，而且我的后面多了一个空格\s            这次换行了&quot;&quot;&quot;;    Assertions.assertEquals(singleLine, textBlockSingleLine);}</code></pre><h3 id="字符串模板（预览）"><a href="#字符串模板（预览）" class="headerlink" title="字符串模板（预览）"></a>字符串模板（预览）</h3><p>字符串模板通过将文本和嵌入式表达式结合在一起，使得Java程序能够以一种更加直观和安全的方式构建字符串。</p><p>与传统的字符串拼接（使用+操作符）、<code>StringBuilder</code>或<code>String.format</code> 等方法相比，字符串模板提供了一种更加清晰和安全的字符串构建方式。</p><p>特别是当字符串需要从用户提供的值构建并传递给其他系统时（例如，构建数据库查询），使用字符串模板可以有效地验证和转换模板及其嵌入表达式的值，从而提高Java程序的安全性。</p><p>让我们通过代码看一下这个特性的魅力：</p><pre><code class="Java">public static void main(String[] args) {    // 拼装变量    String name = &quot;看山&quot;;    String info = STR. &quot;My name is \{ name }&quot; ;    assert info.equals(&quot;My name is 看山&quot;);    // 拼装变量    String firstName = &quot;Howard&quot;;    String lastName = &quot;Liu&quot;;    String fullName = STR. &quot;\{ firstName } \{ lastName }&quot; ;    assert fullName.equals(&quot;Howard Liu&quot;);    String sortName = STR. &quot;\{ lastName }, \{ firstName }&quot; ;    assert sortName.equals(&quot;Liu, Howard&quot;);    // 模板中调用方法    String s2 = STR. &quot;You have a \{ getOfferType() } waiting for you!&quot; ;    assert s2.equals(&quot;You have a gift waiting for you!&quot;);    Request req = new Request(&quot;2017-07-19&quot;, &quot;09:15&quot;, &quot;https://www.howardliu.cn&quot;);    // 模板中引用对象属性    String s3 = STR. &quot;Access at \{ req.date } \{ req.time } from \{ req.address }&quot; ;    assert s3.equals(&quot;Access at 2017-07-19 09:15 from https://www.howardliu.cn&quot;);    LocalTime now = LocalTime.now();    String markTime = DateTimeFormatter            .ofPattern(&quot;HH:mm:ss&quot;)            .format(now);    // 模板中调用方法    String time = STR. &quot;The time is \{            // The java.time.format package is very useful            DateTimeFormatter                    .ofPattern(&quot;HH:mm:ss&quot;)                    .format(now)            } right now&quot; ;    assert time.equals(&quot;The time is &quot; + markTime + &quot; right now&quot;);    // 模板嵌套模板    String[] fruit = {&quot;apples&quot;, &quot;oranges&quot;, &quot;peaches&quot;};    String s4 = STR. &quot;\{ fruit[0] }, \{            STR. &quot;\{ fruit[1] }, \{ fruit[2] }&quot;            }&quot; ;    assert s4.equals(&quot;apples, oranges, peaches&quot;);    // 模板与文本块结合    String title = &quot;My Web Page&quot;;    String text = &quot;Hello, world&quot;;    String html = STR. &quot;&quot;&quot;    &lt;html&gt;      &lt;head&gt;        &lt;title&gt;\{ title }&lt;/title&gt;      &lt;/head&gt;      &lt;body&gt;        &lt;p&gt;\{ text }&lt;/p&gt;      &lt;/body&gt;    &lt;/html&gt;    &quot;&quot;&quot; ;    assert html.equals(&quot;&quot;&quot;            &lt;html&gt;              &lt;head&gt;                &lt;title&gt;My Web Page&lt;/title&gt;              &lt;/head&gt;              &lt;body&gt;                &lt;p&gt;Hello, world&lt;/p&gt;              &lt;/body&gt;            &lt;/html&gt;            &quot;&quot;&quot;);    // 带格式化的字符串模板    record Rectangle(String name, double width, double height) {        double area() {            return width * height;        }    }    Rectangle[] zone = new Rectangle[] {            new Rectangle(&quot;Alfa&quot;, 17.8, 31.4),            new Rectangle(&quot;Bravo&quot;, 9.6, 12.4),            new Rectangle(&quot;Charlie&quot;, 7.1, 11.23),    };    String table = FMT. &quot;&quot;&quot;        Description     Width    Height     Area        %-12s\{ zone[0].name }  %7.2f\{ zone[0].width }  %7.2f\{ zone[0].height }     %7.2f\{ zone[0].area() }        %-12s\{ zone[1].name }  %7.2f\{ zone[1].width }  %7.2f\{ zone[1].height }     %7.2f\{ zone[1].area() }        %-12s\{ zone[2].name }  %7.2f\{ zone[2].width }  %7.2f\{ zone[2].height }     %7.2f\{ zone[2].area() }        \{ &quot; &quot;.repeat(28) } Total %7.2f\{ zone[0].area() + zone[1].area() + zone[2].area() }        &quot;&quot;&quot;;    assert table.equals(&quot;&quot;&quot;            Description     Width    Height     Area            Alfa            17.80    31.40      558.92            Bravo            9.60    12.40      119.04            Charlie          7.10    11.23       79.73                                         Total  757.69            &quot;&quot;&quot;);}public static String getOfferType() {    return &quot;gift&quot;;}record Request(String date, String time, String address) {}</code></pre><p>这个功能当前是第一次预览，在Java22第二次预览，Java23的8.12版本中还没有展示字符串模板的第三次预览（JEP 465: String Templates），还不能确定什么时候可以正式用上。</p><h3 id="模式匹配"><a href="#模式匹配" class="headerlink" title="模式匹配"></a>模式匹配</h3><h4 id="instanceof模式匹配（Java-16）"><a href="#instanceof模式匹配（Java-16）" class="headerlink" title="instanceof模式匹配（Java 16）"></a>instanceof模式匹配（Java 16）</h4><p><code>instanceof</code>主要用来检查对象类型，作为类型强转前的安全检查。</p><p>比如：</p><pre><code class="Java">@Testvoid test() {    final Object obj1 = &quot;Hello, World!&quot;;    int result = 0;    if (obj1 instanceof String) {        String str = (String) obj1;        result = str.length();    } else if (obj1 instanceof Number) {        Number num = (Number) obj1;        result = num.intValue();    }    Assertions.assertEquals(13, result);}</code></pre><p>可以看到，我们每次判断类型之后，需要声明一个判断类型的变量，然后将判断参数强制转换类型，赋值给新声明的变量。</p><p>这种写法显得繁琐且多余。</p><p><code>instanceof</code>改进后：</p><pre><code class="Java">@Testvoid test1() {    final Object obj1 = &quot;Hello, World!&quot;;    int result = 0;    if (obj1 instanceof String str) {        result = str.length();    } else if (obj1 instanceof Number num) {        result = num.intValue();    }    Assertions.assertEquals(13, result);}</code></pre><p>不仅如此，<code>instanceof</code>模式匹配的作用域还可以扩展。在<code>if</code>条件判断中，我们都知道<code>&amp;&amp;</code>与判断是会执行所有的表达式，所以使用<code>instanceof</code>模式匹配定义的局部变量继续判断。</p><p>比如：</p><pre><code class="Java">if (obj1 instanceof String str &amp;&amp; str.length() &gt; 20) {    result = str.length();}</code></pre><p>与原来的写法对比，新的写法代码更加简洁、可读性更高，能够提出很多冗余繁琐的代码，非常实用的一个特性。</p><h4 id="switch模式匹配（Java-21）"><a href="#switch模式匹配（Java-21）" class="headerlink" title="switch模式匹配（Java 21）"></a>switch模式匹配（Java 21）</h4><p>switch模式匹配是一个非常赞的功能，可以在选择器表达式使用基础判断和任意引用类型，包括instanceof操作符。这意味着可以更灵活地使用对象、数组、列表等复杂数据结构作为switch语句的基础，从而简化代码并提高可读性。</p><p>switch模式匹配允许在switch语句中使用模式来测试表达式，每个模式都有特定的动作，从而可以简洁、安全地表达复杂的数据导向查询。</p><p>通过代码看下switch模式匹配的魅力；</p><pre><code class="Java">static String formatValue(Object obj) {    return switch (obj) {        case null -&gt; &quot;null&quot;;        case Integer i -&gt; String.format(&quot;int %d&quot;, i);        case Long l -&gt; String.format(&quot;long %d&quot;, l);        case Double d -&gt; String.format(&quot;double %f&quot;, d);        case String s -&gt; String.format(&quot;String %s&quot;, s);        case Person(String name, String address) -&gt; String.format(&quot;Person %s %s&quot;, name, address);        default -&gt; obj.toString();    };}public record Person(String name, String address) {}public static void main(String[] args) {    System.out.println(formatValue(10));    System.out.println(formatValue(20L));    System.out.println(formatValue(3.14));    System.out.println(formatValue(&quot;Hello&quot;));    System.out.println(formatValue(null));    System.out.println(formatValue(new Person(&quot;Howard&quot;, &quot;Beijing&quot;)));}// 运行结果// int 10// long 20// double 3.140000// String Hello// null// Person Howard Beijing</code></pre><h4 id="Record-模式（Java-21）"><a href="#Record-模式（Java-21）" class="headerlink" title="Record 模式（Java 21）"></a>Record 模式（Java 21）</h4><p>Record是Java16正式发布的基础类型，提供不可变对象的简单实现（其实就是Java Bean，但是省略一堆的getter、setter、hashcode、equals、toString等方法）。</p><p>Record模式，主要是使Record类型可以直接在instanceof和switch模式匹配中使用。</p><p>我们一起看个示例，比如有下面几个基础元素：</p><pre><code class="Java">// 颜色enum Color { RED, GREEN, BLUE}// 点record Point(int x, int y) {}// 带颜色的点record ColoredPoint(Point p, Color color) {}// 正方形record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}</code></pre><p>我们分别通过instanceof模式匹配和switch模式匹配判断输入参数的类型，打印不同的格式：</p><pre><code class="Java">private static void instancePatternsAndPrint(Object o) {    if (o instanceof Square(ColoredPoint upperLeft, ColoredPoint lowerRight)) {        System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);    } else if (o instanceof ColoredPoint(Point(int x, int y), Color color)) {        System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);    } else if (o instanceof Point p) {        System.out.println(&quot;Point类型：&quot; + p);    }}private static void switchPatternsAndPrint(Object o) {    switch (o) {        case Square(ColoredPoint upperLeft, ColoredPoint lowerRight) -&gt; {            System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);        }        case ColoredPoint(Point(int x, int y), Color color) -&gt; {            System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);        }        case Point p -&gt; {            System.out.println(&quot;Point类型：&quot; + p);        }        default -&gt; throw new IllegalStateException(&quot;Unexpected value: &quot; + o);    }}</code></pre><p>我们通过main方法执行下：</p><pre><code class="Java">public static void main(String[] args) {    var p = new Point(1, 2);    var cp1 = new ColoredPoint(p, Color.RED);    var cp2 = new ColoredPoint(p, Color.GREEN);    var square = new Square(cp1, cp2);    instancePatternsAndPrint(square);    instancePatternsAndPrint(cp1);    instancePatternsAndPrint(p);    switchPatternsAndPrint(square);    switchPatternsAndPrint(cp1);    switchPatternsAndPrint(p);}// 结果是：//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]</code></pre><p>是不是很简洁，就像Java8提供的Lambda表达式一样，很丝滑。</p><h3 id="灵活的构造函数主体（预览特性）"><a href="#灵活的构造函数主体（预览特性）" class="headerlink" title="灵活的构造函数主体（预览特性）"></a>灵活的构造函数主体（预览特性）</h3><p>我们都知道，在子类的构造函数中，比如通过<code>super(……)</code>调用父类，在<code>super</code>之前是不允许有其他语句的。</p><p>大部分的时候这种限制都没问题，但是有时候不太灵活。如果想在<code>super</code>之前加上一些子类特有逻辑，比如想统计下子类构造耗时，就得重写一遍父类的实现。</p><p>除了有损灵活性，这种重写的做法也会造成父子类之间的关系变得奇怪。假设父类是SDK中的一个类，SDK升级时在父类构造函数增加了一些逻辑，我们项目中是无法继承这些逻辑的，某次需要升级SDK（比如低版本有安全风险），验证不完整的情况下，就很容易出现bug。</p><p>该特性的目标是提高构造函数的可读性和可预测性，同时保持构造函数调用的自上而下的规则。通过允许在显式调用 <code>super()</code> 或 <code>this()</code> 前初始化字段，从而实现更灵活的构造函数主体。这一变化使得代码更具表现力。</p><p>我们看下示例代码：</p><pre><code class="Java">public class PositiveBigInteger extends BigInteger {    public PositiveBigInteger(long value) {        if (value &lt;= 0) {            throw new IllegalArgumentException(&quot;non-positive value&quot;);        }        super(value);    }}</code></pre><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>想要了解各版本的详细特性，可以从<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中查看。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/java/java-features-8-23-2">从Java8到Java23值得期待的x个特性（2）：提升幸福感的特性</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">从Java8到Java23值得期待的x个特性（2）：提升幸福感的特性</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      提升幸福感的特性。从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java10" scheme="https://www.howardliu.cn/tags/Java10/"/>
    
      <category term="Java11" scheme="https://www.howardliu.cn/tags/Java11/"/>
    
      <category term="Java12" scheme="https://www.howardliu.cn/tags/Java12/"/>
    
      <category term="Java14" scheme="https://www.howardliu.cn/tags/Java14/"/>
    
      <category term="Java15" scheme="https://www.howardliu.cn/tags/Java15/"/>
    
      <category term="Java13" scheme="https://www.howardliu.cn/tags/Java13/"/>
    
      <category term="Java16" scheme="https://www.howardliu.cn/tags/Java16/"/>
    
      <category term="Java17" scheme="https://www.howardliu.cn/tags/Java17/"/>
    
      <category term="Java18" scheme="https://www.howardliu.cn/tags/Java18/"/>
    
      <category term="Java19" scheme="https://www.howardliu.cn/tags/Java19/"/>
    
      <category term="Java20" scheme="https://www.howardliu.cn/tags/Java20/"/>
    
      <category term="Java21" scheme="https://www.howardliu.cn/tags/Java21/"/>
    
      <category term="Java8" scheme="https://www.howardliu.cn/tags/Java8/"/>
    
      <category term="Java22" scheme="https://www.howardliu.cn/tags/Java22/"/>
    
      <category term="Java9" scheme="https://www.howardliu.cn/tags/Java9/"/>
    
      <category term="Java23" scheme="https://www.howardliu.cn/tags/Java23/"/>
    
  </entry>
  
  <entry>
    <title>从Java8到Java23值得期待的x个特性（1）：有意思的特性</title>
    <link href="https://www.howardliu.cn/java/java-features-8-23-1/"/>
    <id>https://www.howardliu.cn/java/java-features-8-23-1/</id>
    <published>2024-09-17T00:20:00.000Z</published>
    <updated>2024-12-05T09:14:01.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409161115516.jpg" alt="从Java8到Java23值得期待的x个特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。</p><p><img src="https://static.howardliu.cn/202409111451409.png" alt="Java版本分布"></p><blockquote><p>数据来源<a href="https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-2024-05-29.pdf" target="_blank" rel="noopener">2024 State of the Java Ecosystem</a></p></blockquote><a id="more"></a><h2 id="有意思的特性"><a href="#有意思的特性" class="headerlink" title="有意思的特性"></a>有意思的特性</h2><blockquote><p>可以从 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中查看各个版本的特性。</p></blockquote><h3 id="大家都很熟悉的特性"><a href="#大家都很熟悉的特性" class="headerlink" title="大家都很熟悉的特性"></a>大家都很熟悉的特性</h3><p>Java8中的Lambda表达式、Stream流、Optional是大版本特性，Java8是2014年发布，已有十年历史了。大家应该分厂熟悉了，这里不在赘述。推荐两篇文章：</p><ul><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li></ul><h3 id="补全技能树"><a href="#补全技能树" class="headerlink" title="补全技能树"></a>补全技能树</h3><h4 id="Record-类型（Java-16）"><a href="#Record-类型（Java-16）" class="headerlink" title="Record 类型（Java 16）"></a>Record 类型（Java 16）</h4><p>Java新增了一个关键字<code>record</code>，它是定义不可变数据类型封装类的关键字，主要用在特定领域类上。</p><p>我们都知道，在Java开发中，我们需要定义POJO作为数据存储对象，根据规范，POJO中除了属性是个性化的，其他的比如<code>getter</code>、<code>setter</code>、<code>equals</code>、<code>hashCode</code>、<code>toString</code>都是模板化的写法，所以为了简便，很多类似Lombok的组件提供Java类编译时增强，通过在类上定义<code>@Data</code>注解自动添加这些模板化方法。在Java14中，我们可以直接使用<code>record</code>解决这个问题。</p><p>比如，我们定义一个<code>Person</code>类：</p><pre><code class="Java">public record Person(String name, String address) {}</code></pre><p>我们转换为之前的定义会是一坨下面这种代码：</p><pre><code class="Java">public final class PersonBefore {    private final String name;    private final String address;    public PersonBefore(String name, String address) {        this.name = name;        this.address = address;    }    public String name() {        return name;    }    public String address() {        return address;    }    @Override    public boolean equals(Object o) {        if (this == o) {            return true;        }        if (o == null || getClass() != o.getClass()) {            return false;        }        PersonBefore14 that = (PersonBefore14) o;        return Objects.equals(name, that.name) &amp;&amp; Objects.equals(address, that.address);    }    @Override    public int hashCode() {        return Objects.hash(name, address);    }    @Override    public String toString() {        return &quot;PersonBefore{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &quot;, address=&#39;&quot; + address + &#39;\&#39;&#39; +                &#39;}&#39;;    }}</code></pre><p>我们可以发现Record类有如下特征：</p><ol><li>一个构造方法</li><li>getter方法名与属性名相同</li><li>有<code>equals()</code>、<code>hashCode()</code>方法</li><li>有<code>toString()</code>方法</li><li>类对象和属性被<code>final</code>修饰，所以构造函数是包含所有属性的，而且没有setter方法</li></ol><p>在<code>Class</code>类中也新增了对应的处理方法：</p><ul><li><code>getRecordComponents()</code>：返回一组<code>java.lang.reflect.RecordComponent</code>对象组成的数组，该数组的元素与<code>Record</code>类中的组件相对应，其顺序与在记录声明中出现的顺序相同，可以从该数组中的每个<code>RecordComponent</code>中提取到组件信息，包括其名称、类型、泛型类型、注释及其访问方法。</li><li><code>isRecord()</code>：返回所在类是否是 Record 类型，如果是，则返回 true。</li></ul><p>看起来，Record类和Enum很像，都是一定的模板类，通过语法糖定义，在Java编译过程中，将其编译成特定的格式，功能很好，但如果没有习惯使用，可能会觉得限制太多。 </p><h4 id="密封类和接口（Java-17）"><a href="#密封类和接口（Java-17）" class="headerlink" title="密封类和接口（Java 17）"></a>密封类和接口（Java 17）</h4><p>目前，Java 没有提供对继承的细粒度控制，只有 public、protected、private、包内控制四种非常粗粒度的控制方式。</p><p>为此，密封类的目标是允许单个类声明哪些类型可以用作其子类型。这也适用于接口，并确定哪些类型可以实现它们。该功能特性新增了<code>sealed</code>和<code>non-sealed</code>修饰符和<code>permits</code>关键字。</p><p>我们可以做如下定义：</p><pre><code class="Java">public sealed class Person permits Student, Worker, Teacher {}public sealed class Student extends Person        permits Pupil, JuniorSchoolStudent, HighSchoolStudent, CollegeStudent, GraduateStudent {}public final class Pupil extends Student {}public non-sealed class Worker extends Person {}public class OtherClass extends Worker {}public final class Teacher extends Person {}</code></pre><p>我们可以先定义一个<code>sealed</code>修饰的类<code>Person</code>，使用<code>permits</code>指定被继承的子类，这些子类必须是使用<code>final</code>或<code>sealed</code>或<code>non-sealed</code>修饰的类。其中<code>Student</code>是使用<code>sealed</code>修饰，所以也需要使用<code>permits</code>指定被继承的子类。<code>Worker</code>类使用<code>non-sealed</code>修饰，成为普通类，其他类都可以继承它。<code>Teacher</code>使用<code>final</code>修饰，不可再被继承。</p><p>从类图上看没有太多区别：</p><p><img src="https://static.howardliu.cn/202409161142744.png" alt="密封类和接口"></p><p>但是从功能特性上，起到了很好的约束作用，我们可以放心大胆的定义可以公开使用，但又不想被非特定类继承的类了。</p><h4 id="全新的-HTTP-客户端（Java-11）"><a href="#全新的-HTTP-客户端（Java-11）" class="headerlink" title="全新的 HTTP 客户端（Java 11）"></a>全新的 HTTP 客户端（Java 11）</h4><p>老版 HTTP 客户端存在很多问题，大家开发的时候基本上都是使用第三方 HTTP 库，比如 Apache HttpClient、Netty、Jetty 等。</p><p>新版 HTTP 客户端的目标很多，毕竟这么多珠玉在前，如果还是做成一坨，指定是要被笑死的。所以新版 HTTP 客户端列出了 16 个目标，包括简单易用、打印关键信息、WebSocket、HTTP/2、HTTPS/TLS、良好的性能、非阻塞 API 等等。</p><pre><code class="Java">@Testvoid testHttpClient() throws IOException, InterruptedException {    final HttpClient httpClient = HttpClient.newBuilder()            .version(HttpClient.Version.HTTP_2)            .connectTimeout(Duration.ofSeconds(20))            .build();    final HttpRequest httpRequest = HttpRequest.newBuilder()            .GET()            .uri(URI.create(&quot;https://www.howardliu.cn/robots.txt&quot;))            .build();    final HttpResponse&lt;String&gt; httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString());    final String responseBody = httpResponse.body();    assertTrue(responseBody.contains(&quot;Allow&quot;));}</code></pre><h4 id="有序集合（Java-21）"><a href="#有序集合（Java-21）" class="headerlink" title="有序集合（Java 21）"></a>有序集合（Java 21）</h4><p>有序集合是Java21版本中引入的一个新特性，旨在为Java集合框架添加对有序集合的支持。</p><p>有序集合是一种具有定义好的元素访问顺序的集合类型，它允许以一致的方式访问和处理集合中的元素，无论是从第一个元素到最后一个元素，还是从最后一个元素到第一个元素。</p><p>在 Java 中，集合类库非常重要且使用频率非常高，但是缺乏一种能够表示具有定义好的元素访问顺序的集合类型。</p><p>例如，<code>List</code>和<code>Deque</code>都定义了元素的访问顺序，但它们的共同父接口<code>Collection</code>却没有。同样，Set不定义元素的访问顺序，其子类型如<code>HashSet</code>也没有定义，但子类型如<code>SortedSet</code>和<code>LinkedHashSet</code>则有定义。因此，支持访问顺序的功能散布在整个类型层次结构中，使得在API中表达某些有用的概念变得困难。<code>Collection</code>太通用，将此类约束留给文档规范，可能导致难以调试的错误。</p><p>而且，虽然某些集合有顺序操作方法，但是却不尽相同，比如</p><table><thead><tr><th><br></th><th>First element</th><th>Last element</th></tr></thead><tbody><tr><td>List</td><td>list.get(0)</td><td>list.get(list.size() - 1)</td></tr><tr><td>Deque</td><td>deque.getFirst()</td><td>deque.getLast()</td></tr><tr><td>SortedSet</td><td>sortedSet.first()</td><td>sortedSet.last()</td></tr><tr><td>LinkedHashSet</td><td>linkedHashSet.iterator().next()</td><td>缺失</td></tr></tbody></table><p>提供了有序集合<code>SequencedCollection</code>、<code>SequencedSet</code>、<code>SequencedMap</code>：</p><pre><code class="Java">interface SequencedCollection&lt;E&gt; extends Collection&lt;E&gt; {    // new method    SequencedCollection&lt;E&gt; reversed();    // methods promoted from Deque    void addFirst(E);    void addLast(E);    E getFirst();    E getLast();    E removeFirst();    E removeLast();}interface SequencedSet&lt;E&gt; extends Set&lt;E&gt;, SequencedCollection&lt;E&gt; {    SequencedSet&lt;E&gt; reversed();    // covariant override}interface SequencedMap&lt;K,V&gt; extends Map&lt;K,V&gt; {    // new methods    SequencedMap&lt;K,V&gt; reversed();    SequencedSet&lt;K&gt; sequencedKeySet();    SequencedCollection&lt;V&gt; sequencedValues();    SequencedSet&lt;Entry&lt;K,V&gt;&gt; sequencedEntrySet();    V putFirst(K, V);    V putLast(K, V);    // methods promoted from NavigableMap    Entry&lt;K, V&gt; firstEntry();    Entry&lt;K, V&gt; lastEntry();    Entry&lt;K, V&gt; pollFirstEntry();    Entry&lt;K, V&gt; pollLastEntry();}</code></pre><p><code>SequencedCollection</code>的<code>reversed()</code>方法提供了一个原始集合的反向视图。任何对原始集合的修改都会在视图中可见。如果允许，视图中的修改会写回到原始集合。</p><p><img src="https://static.howardliu.cn/202409161143487.png" alt="JEP 431: 有序集合（Sequenced Collections）"></p><p>我们看一个例子，假设我们有一个LinkedHashSet，现在我们想要获取它的反向视图并以反向顺序遍历它：</p><pre><code class="Java">LinkedHashSet&lt;Integer&gt; linkedHashSet = new LinkedHashSet&lt;&gt;(Arrays.asList(3, 2, 1));// 获取反向视图SequencedCollection&lt;Integer&gt; reversed = linkedHashSet.reversed();// 反向遍历System.out.println(&quot;原始数据：&quot; + linkedHashSet);System.out.println(&quot;反转数据：&quot; + reversed);// 运行结果：// 原始数据：[3, 2, 1]// 反转数据：[1, 2, 3]</code></pre><p>这些方法都是便捷方法，内部数据结构没有变化，其实本质也是原来的用法。比如<code>ArrayList</code>中的<code>getFirst</code>和<code>getLast</code>方法：</p><pre><code class="java">/** * {@inheritDoc} * * @throws NoSuchElementException {@inheritDoc} * @since 21 */public E getFirst() {    if (size == 0) {        throw new NoSuchElementException();    } else {        return elementData(0);    }}/** * {@inheritDoc} * * @throws NoSuchElementException {@inheritDoc} * @since 21 */public E getLast() {    int last = size - 1;    if (last &lt; 0) {        throw new NoSuchElementException();    } else {        return elementData(last);    }}</code></pre><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>想要了解各版本的详细特性，可以从<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中查看。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/java/java-features-8-23-1">从Java8到Java23值得期待的x个特性（1）：有意思的特性</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">从Java8到Java23值得期待的x个特性（1）：有意思的特性</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      有意思的特性。从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java10" scheme="https://www.howardliu.cn/tags/Java10/"/>
    
      <category term="Java11" scheme="https://www.howardliu.cn/tags/Java11/"/>
    
      <category term="Java12" scheme="https://www.howardliu.cn/tags/Java12/"/>
    
      <category term="Java14" scheme="https://www.howardliu.cn/tags/Java14/"/>
    
      <category term="Java15" scheme="https://www.howardliu.cn/tags/Java15/"/>
    
      <category term="Java13" scheme="https://www.howardliu.cn/tags/Java13/"/>
    
      <category term="Java16" scheme="https://www.howardliu.cn/tags/Java16/"/>
    
      <category term="Java17" scheme="https://www.howardliu.cn/tags/Java17/"/>
    
      <category term="Java18" scheme="https://www.howardliu.cn/tags/Java18/"/>
    
      <category term="Java19" scheme="https://www.howardliu.cn/tags/Java19/"/>
    
      <category term="Java20" scheme="https://www.howardliu.cn/tags/Java20/"/>
    
      <category term="Java21" scheme="https://www.howardliu.cn/tags/Java21/"/>
    
      <category term="Java8" scheme="https://www.howardliu.cn/tags/Java8/"/>
    
      <category term="Java22" scheme="https://www.howardliu.cn/tags/Java22/"/>
    
      <category term="Java9" scheme="https://www.howardliu.cn/tags/Java9/"/>
    
      <category term="Java23" scheme="https://www.howardliu.cn/tags/Java23/"/>
    
  </entry>
  
  <entry>
    <title>Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java23 的新特性</title>
    <link href="https://www.howardliu.cn/java/java-23-features/"/>
    <id>https://www.howardliu.cn/java/java-23-features/</id>
    <published>2024-09-16T00:20:00.000Z</published>
    <updated>2024-12-05T03:08:06.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409141635767.png" alt="Java23 的新特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。</p><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>Java23 在 2024 年 9 月 17 日发布GA版本，共十二大特性：</p><ul><li>JEP 455: 模式匹配中使用原始类型（Primitive Types in Patterns, instanceof, and switch，预览）</li><li>JEP 466: 类文件API（Class-File API，第二次预览）</li><li>JEP 467: Markdown格式文档注释（Markdown Documentation Comments）</li><li>JEP 469: 向量API（Vector API，第八次孵化）</li><li>JEP 473: 流收集器（Stream Gatherers，第二次预览）</li><li>JEP 471: 标记sun.misc.Unsafe中的内存管理方法为过时（Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal）</li><li>JEP 474: ZGC：默认分代收集模式（ZGC: Generational Mode by Default）</li><li>JEP 476: 模块导入声明（Module Import Declarations，预览）</li><li>JEP 477: 隐式声明的类和实例方法（Implicitly Declared Classes and Instance Main Methods，第三次预览）</li><li>JEP 480: 结构化并发（Structured Concurrency，第三次预览）</li><li>JEP 481: 作用域值（Scoped Values，第三次预览）</li><li>JEP 482: 灵活的构造函数主体（Flexible Constructor Bodies，第二次预览）</li></ul><p>接下来我们一起看看这些特性。</p><h2 id="JEP-467-Markdown格式文档注释（Markdown-Documentation-Comments）"><a href="#JEP-467-Markdown格式文档注释（Markdown-Documentation-Comments）" class="headerlink" title="JEP 467: Markdown格式文档注释（Markdown Documentation Comments）"></a>JEP 467: Markdown格式文档注释（Markdown Documentation Comments）</h2><p>Markdown是一种轻量级的标记语言，可用于在纯文本文档中添加格式化元素，具体语法可以参考<a href="https://www.markdownguide.org/basic-syntax/" target="_blank" rel="noopener">Markdown Guide</a>。本文就是使用Markdown语法编写的。</p><p>在Java注释中引入Markdown，目标是使API文档注释以源代码形式更易于编写和阅读。主要收益包括：</p><ul><li>提高文档编写的效率：Markdown语法相比HTML更为简洁，开发者可以更快地编写和修改文档注释。</li><li>增强文档的可读性：Markdown格式的文档在源代码中更易于阅读，有助于开发者快速理解API的用途和行为。</li><li>促进文档的一致性：通过支持Markdown，可以确保文档风格的一致性，减少因格式问题导致的文档混乱。</li><li>简化文档维护：Markdown格式的文档注释更易于维护和更新，特别是在多人协作的项目中，可以减少因文档格式问题导致的沟通成本。</li></ul><p>具体使用方式是在注释前面增加<code>///</code>，比如<code>java.lang.Object.hashCode</code>的注释：</p><pre><code class="java">/** * Returns a hash code value for the object. This method is * supported for the benefit of hash tables such as those provided by * {@link java.util.HashMap}. * &lt;p&gt; * The general contract of {@code hashCode} is: * &lt;ul&gt; * &lt;li&gt;Whenever it is invoked on the same object more than once during *     an execution of a Java application, the {@code hashCode} method *     must consistently return the same integer, provided no information *     used in {@code equals} comparisons on the object is modified. *     This integer need not remain consistent from one execution of an *     application to another execution of the same application. * &lt;li&gt;If two objects are equal according to the {@link *     #equals(Object) equals} method, then calling the {@code *     hashCode} method on each of the two objects must produce the *     same integer result. * &lt;li&gt;It is &lt;em&gt;not&lt;/em&gt; required that if two objects are unequal *     according to the {@link #equals(Object) equals} method, then *     calling the {@code hashCode} method on each of the two objects *     must produce distinct integer results.  However, the programmer *     should be aware that producing distinct integer results for *     unequal objects may improve the performance of hash tables. * &lt;/ul&gt; * * @implSpec * As far as is reasonably practical, the {@code hashCode} method defined * by class {@code Object} returns distinct integers for distinct objects. * * @return  a hash code value for this object. * @see     java.lang.Object#equals(java.lang.Object) * @see     java.lang.System#identityHashCode */</code></pre><p>如果使用JEP 467的Markdown方式：</p><pre><code class="java">/// Returns a hash code value for the object. This method is/// supported for the benefit of hash tables such as those provided by/// [java.util.HashMap].////// The general contract of `hashCode` is://////   - Whenever it is invoked on the same object more than once during///     an execution of a Java application, the `hashCode` method///     must consistently return the same integer, provided no information///     used in `equals` comparisons on the object is modified.///     This integer need not remain consistent from one execution of an///     application to another execution of the same application.///   - If two objects are equal according to the///     [equals][#equals(Object)] method, then calling the///     `hashCode` method on each of the two objects must produce the///     same integer result.///   - It is _not_ required that if two objects are unequal///     according to the [equals][#equals(Object)] method, then///     calling the `hashCode` method on each of the two objects///     must produce distinct integer results.  However, the programmer///     should be aware that producing distinct integer results for///     unequal objects may improve the performance of hash tables.////// @implSpec/// As far as is reasonably practical, the `hashCode` method defined/// by class `Object` returns distinct integers for distinct objects.////// @return  a hash code value for this object./// @see     java.lang.Object#equals(java.lang.Object)/// @see     java.lang.System#identityHashCode</code></pre><p>简单两种写法的差异，相同注释，Markdown的写法更加简洁：</p><p><img src="https://static.howardliu.cn/Object-hashcode-diff-3.png" alt="Object hashcode注释差异"></p><h2 id="JEP-474-ZGC：默认分代收集模式（ZGC-Generational-Mode-by-Default）"><a href="#JEP-474-ZGC：默认分代收集模式（ZGC-Generational-Mode-by-Default）" class="headerlink" title="JEP 474: ZGC：默认分代收集模式（ZGC: Generational Mode by Default）"></a>JEP 474: ZGC：默认分代收集模式（ZGC: Generational Mode by Default）</h2><p>分代ZGC从Java21正式发布，需要通过命令<code>-XX:+UseZGC -XX:+ZGenerational</code>显式使用分代ZGC。在Java23中，分代ZGC已作为ZGC默认方式，不需要显式指定了。在后续版本中，非分代ZGC将被删除。ZGC发展历程可以查看<a href="https://wiki.openjdk.org/display/zgc/Main" target="_blank" rel="noopener">https://wiki.openjdk.org/display/zgc/Main</a>。</p><p>基于「大部分对象朝生夕死」的分代假说，ZGC提供了分代版本，将内存划分为年轻代和老年代，并为这两种代分别维护不同的垃圾收集策略。</p><p>我们看下<a href="https://kstefanj.github.io/2023/11/07/hazelcast-on-generational-zgc.html" target="_blank" rel="noopener">Hazelcast Jet on Generational ZGC</a>中给出的测评效果：</p><p><img src="https://static.howardliu.cn/p9999-event-latency.png" alt="JEP 439: 分代ZGC（Generational ZGC）"></p><p>从上图可以看到，非分代 ZGC 在低负载下表现非常好，但随着分配压力的增加，延迟也会增加。使用分代 ZGC 后，即使在高负载下，延迟也非常低，而且延迟效果优于G1。</p><p>如果想用回非分代ZGC，需要通过命令<code>-XX:+UseZGC -XX:-ZGenerational</code>切换为非分代ZGC，启动过程会收到JVM的告警：“the ZGenerational option is deprecated is issued.”和“the non-generational mode is deprecated for removal is issued.”。</p><h2 id="预览功能"><a href="#预览功能" class="headerlink" title="预览功能"></a>预览功能</h2><h3 id="JEP-455-模式匹配中使用原始类型（Primitive-Types-in-Patterns-instanceof-and-switch，预览）"><a href="#JEP-455-模式匹配中使用原始类型（Primitive-Types-in-Patterns-instanceof-and-switch，预览）" class="headerlink" title="JEP 455: 模式匹配中使用原始类型（Primitive Types in Patterns, instanceof, and switch，预览）"></a>JEP 455: 模式匹配中使用原始类型（Primitive Types in Patterns, instanceof, and switch，预览）</h3><p>JEP 455 的目标是通过允许在所有模式上下文中使用原始类型来增强模式匹配，并扩展 instanceof 和 switch 以使其适用于所有原始类型。这旨在实现统一的数据探索，无论是原始类型还是引用类型。</p><p>在 Java 中，模式匹配主要针对引用类型，而原始类型在模式匹配中的使用受到限制。这导致了代码的冗余和不一致性。JEP 455 允许在 instanceof 和 switch 语句中直接使用原始类型，而无需进行额外的类型转换。这使得模式匹配更加灵活和强大，提高代码的可读性和一致性。</p><p>我们看下示例代码：</p><pre><code class="java">// 先看个例子switch (x.getStatus()) {    case 0 -&gt; &quot;okay&quot;;    case 1 -&gt; &quot;warning&quot;;    case 2 -&gt; &quot;error&quot;;    default -&gt; &quot;unknown status: &quot; + x.getStatus();}// default语句改造一下switch (x.getStatus()) {    case 0 -&gt; &quot;okay&quot;;    case 1 -&gt; &quot;warning&quot;;    case 2 -&gt; &quot;error&quot;;    case int i -&gt; &quot;unknown status: &quot; + i;}// 还可以增加检查值逻辑switch (x.getYearlyFlights()) {    case 0 -&gt; ...;    case 1 -&gt; ...;    case 2 -&gt; issueDiscount();    case int i when i &gt;= 100 -&gt; issueGoldCard();    case int i -&gt; ... appropriate action when i &gt; 2 &amp;&amp; i &lt; 100 ...}// 在数值精度问题场景中if (i &gt;= -128 &amp;&amp; i &lt;= 127) {    byte b = (byte)i;    ... b ...}// 可以通过instanceof增强直接判断类型if (i instanceof byte b) {    ... b ...}</code></pre><h3 id="JEP-466-类文件API（Class-File-API，第二次预览）"><a href="#JEP-466-类文件API（Class-File-API，第二次预览）" class="headerlink" title="JEP 466: 类文件API（Class-File API，第二次预览）"></a>JEP 466: 类文件API（Class-File API，第二次预览）</h3><p>Java中一直缺少官方的类文件操作API，想要操作class，我们需要借助第三方库，比如javassist、ASM、ByteBuddy等。从2017年开始，Java每半年有一次升级，特性更新频率增加，需要第三方库同步更新，是比较困难的。</p><p>还有一个原因是Java中使用了ASM实现<code>jar</code>、<code>jlink</code>等工具，以及lambda表达式等。这就会出现一个问题，Java版本N依赖了ASM版本M，如果Java N中有类API，ASM M中是不会有的，只有Java N发布后，ASM升级到M+1才会有，Java想要使用ASM M+1，需要升级到Java N+1。是不是很颠，一个官方基础语言，居然要依赖一个依赖这个语言的第三方工具。是可忍孰不可忍。</p><p>于是有了类文件API，目标是提供一个准确、完整、高性能且遵循Java虚拟机规范定义的类文件格式的API，最终也会替换JDK内部的ASM副本。</p><p>因为当前还是预览版，我们先简单看下官方示例：</p><p>如果要实现下面这段：</p><pre><code class="java">void fooBar(boolean z, int x) {    if (z)        foo(x);    else        bar(x);}</code></pre><p>我们在ASM的写法：</p><pre><code class="java">ClassWriter classWriter = ...;MethodVisitor mv = classWriter.visitMethod(0, &quot;fooBar&quot;, &quot;(ZI)V&quot;, null, null);mv.visitCode();mv.visitVarInsn(ILOAD, 1);Label label1 = new Label();mv.visitJumpInsn(IFEQ, label1);mv.visitVarInsn(ALOAD, 0);mv.visitVarInsn(ILOAD, 2);mv.visitMethodInsn(INVOKEVIRTUAL, &quot;Foo&quot;, &quot;foo&quot;, &quot;(I)V&quot;, false);Label label2 = new Label();mv.visitJumpInsn(GOTO, label2);mv.visitLabel(label1);mv.visitVarInsn(ALOAD, 0);mv.visitVarInsn(ILOAD, 2);mv.visitMethodInsn(INVOKEVIRTUAL, &quot;Foo&quot;, &quot;bar&quot;, &quot;(I)V&quot;, false);mv.visitLabel(label2);mv.visitInsn(RETURN);mv.visitEnd();</code></pre><p>在JEP 457中的写法：</p><pre><code class="java">ClassBuilder classBuilder = ...;classBuilder.withMethod(&quot;fooBar&quot;, MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,                        methodBuilder -&gt; methodBuilder.withCode(codeBuilder -&gt; {    Label label1 = codeBuilder.newLabel();    Label label2 = codeBuilder.newLabel();    codeBuilder.iload(1)        .ifeq(label1)        .aload(0)        .iload(2)        .invokevirtual(ClassDesc.of(&quot;Foo&quot;), &quot;foo&quot;, MethodTypeDesc.of(CD_void, CD_int))        .goto_(label2)        .labelBinding(label1)        .aload(0)        .iload(2)        .invokevirtual(ClassDesc.of(&quot;Foo&quot;), &quot;bar&quot;, MethodTypeDesc.of(CD_void, CD_int))        .labelBinding(label2);        .return_();});</code></pre><p>还可以这样写：</p><pre><code class="java">CodeBuilder classBuilder = ...;classBuilder.withMethod(&quot;fooBar&quot;, MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,                        methodBuilder -&gt; methodBuilder.withCode(codeBuilder -&gt; {    codeBuilder.iload(codeBuilder.parameterSlot(0))               .ifThenElse(                   b1 -&gt; b1.aload(codeBuilder.receiverSlot())                           .iload(codeBuilder.parameterSlot(1))                           .invokevirtual(ClassDesc.of(&quot;Foo&quot;), &quot;foo&quot;,                                          MethodTypeDesc.of(CD_void, CD_int)),                   b2 -&gt; b2.aload(codeBuilder.receiverSlot())                           .iload(codeBuilder.parameterSlot(1))                           .invokevirtual(ClassDesc.of(&quot;Foo&quot;), &quot;bar&quot;,                                          MethodTypeDesc.of(CD_void, CD_int))               .return_();});</code></pre><p>写法上比ASM更加优雅。</p><h3 id="JEP-473-流收集器（Stream-Gatherers，第二次预览）"><a href="#JEP-473-流收集器（Stream-Gatherers，第二次预览）" class="headerlink" title="JEP 473: 流收集器（Stream Gatherers，第二次预览）"></a>JEP 473: 流收集器（Stream Gatherers，第二次预览）</h3><p>流收集器旨在增强Java Stream API、以支持自定义中间操作。这一特性允许开发者以更灵活和高效的方式处理数据流，从而提高流管道的表达能力和转换数据的能力。</p><p>流收集器通过引入新的中间操作<code>Stream::gather(Gatherer)</code>，允许开发者定义自定义的转换实体（称为Gatherer），从而对流中的元素进行转换。这些转换可以是一对一、一对多、多对一或多对多的转换方式。此外，流收集器还支持保存以前遇到的元素，以便进行进一步的处理。</p><p>我们通过实例感受下这一特性的魅力：</p><pre><code class="java">public record WindowFixed&lt;TR&gt;(int windowSize) implements Gatherer&lt;TR, ArrayList&lt;TR&gt;, List&lt;TR&gt;&gt; {    public static void main(String[] args) {        var list = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)                .gather(new WindowFixed&lt;&gt;(3))                .toList();        // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]        System.out.println(list);    }    public WindowFixed {        // Validate input        if (windowSize &lt; 1) {            throw new IllegalArgumentException(&quot;window size must be positive&quot;);        }    }    @Override    public Supplier&lt;ArrayList&lt;TR&gt;&gt; initializer() {        // 创建一个 ArrayList 来保存当前打开的窗口        return () -&gt; new ArrayList&lt;&gt;(windowSize);    }    @Override    public Integrator&lt;ArrayList&lt;TR&gt;, TR, List&lt;TR&gt;&gt; integrator() {        // 集成器在每次消费元素时被调用        return Gatherer.Integrator.ofGreedy((window, element, downstream) -&gt; {            // 将元素添加到当前打开的窗口            window.add(element);            // 直到达到所需的窗口大小，            // 返回 true 表示希望继续接收更多元素            if (window.size() &lt; windowSize) {                return true;            }            // 当窗口已满时，通过创建副本关闭窗口            var result = new ArrayList&lt;TR&gt;(window);            // 清空窗口以便开始新的窗口            window.clear();            // 将关闭的窗口发送到下游            return downstream.push(result);        });    }    // 由于此操作本质上是顺序的，因此无法并行化，因此省略了合并器    @Override    public BiConsumer&lt;ArrayList&lt;TR&gt;, Downstream&lt;? super List&lt;TR&gt;&gt;&gt; finisher() {        // 终结器在没有更多元素传递时运行        return (window, downstream) -&gt; {            // 如果下游仍然接受更多元素且当前打开的窗口非空，则将其副本发送到下游            if (!downstream.isRejecting() &amp;&amp; !window.isEmpty()) {                downstream.push(new ArrayList&lt;TR&gt;(window));                window.clear();            }        };    }}</code></pre><p>该特性还是预览版，等正式发布后再细说。</p><h3 id="JEP-476-模块导入声明（Module-Import-Declarations，预览）"><a href="#JEP-476-模块导入声明（Module-Import-Declarations，预览）" class="headerlink" title="JEP 476: 模块导入声明（Module Import Declarations，预览）"></a>JEP 476: 模块导入声明（Module Import Declarations，预览）</h3><p>模块导入声明目标是增强 Java 编程语言，使其能够简洁地导入一个模块导出的所有包。这简化了模块库的重用，但并不要求导入代码本身必须在模块中。</p><p>在 Java 中，开发者经常需要从模块中导入多个包，这会导致代码中出现大量的 import 语句，增加了代码的冗余和复杂性。JEP 476 引入了一种新的导入声明方式，使得开发者可以更简洁地导入一个模块的所有包，从而提高代码的可读性和开发效率。</p><p>比如下面这段代码：</p><pre><code class="java">import java.util.Map;                   // or import java.util.*;import java.util.function.Function;     // or import java.util.function.*;import java.util.stream.Collectors;     // or import java.util.stream.*;import java.util.stream.Stream;         // (can be removed)String[] fruits = new String[] { &quot;apple&quot;, &quot;berry&quot;, &quot;citrus&quot; };Map&lt;String, String&gt; m =    Stream.of(fruits)          .collect(Collectors.toMap(s -&gt; s.toUpperCase().substring(0,1),                                    Function.identity()));</code></pre><p>使用import导入了依赖的类，在JEP 476后，我们可以通过<code>import module java.base;</code>一行代码解决。</p><p>这种方式虽然简洁，但是可能存在问题，比如<code>Date</code>类，在<code>java.util</code>和<code>java.sql</code>中都有<code>Date</code>类，如果有场景需要这两个模块，就需要显式指明<code>Date</code>类的来源：</p><pre><code class="java">import module java.base;      // exports java.util, which has a public Date classimport module java.sql;       // exports java.sql, which has a public Date classimport java.sql.Date;         // resolve the ambiguity of the simple name Date!...Date d = ...                  // Ok!  Date is resolved to java.sql.Date...</code></pre><h3 id="JEP-477-隐式声明的类和实例方法（Implicitly-Declared-Classes-and-Instance-Main-Methods，第三次预览）"><a href="#JEP-477-隐式声明的类和实例方法（Implicitly-Declared-Classes-and-Instance-Main-Methods，第三次预览）" class="headerlink" title="JEP 477: 隐式声明的类和实例方法（Implicitly Declared Classes and Instance Main Methods，第三次预览）"></a>JEP 477: 隐式声明的类和实例方法（Implicitly Declared Classes and Instance Main Methods，第三次预览）</h3><p>隐式声明的类和实例方法的目标是简化 Java 语言，使得学生和初学者可以更容易地编写他们的第一个程序，而无需理解为大型程序设计的复杂语言特性。</p><p>无论学习哪门语言，第一课一定是打印<code>Hello, World!</code>，Java中的写法是：</p><pre><code class="java">public class HelloWorld {    public static void main(String[] args) {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>如果是第一次接触，一定会有很多疑问，<code>public</code>干啥的，<code>main</code>方法的约定参数<code>args</code>是什么鬼？然后老师就说，这就是模板，照着抄就行，不这样写不运行。</p><p>现在可以简化为：</p><pre><code class="java">class HelloWorld {    void main() {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>我们还可以这样写：</p><pre><code class="java">String greeting() { return &quot;Hello, World!&quot;; }void main() {    System.out.println(greeting());}</code></pre><p><code>main</code>方法直接简化为名字和括号，甚至连类也不需要显性定义了。虽然看起来没啥用，但是在JShell中使用，就比较友好了。</p><p>本次预览新增了三个IO操作方法：</p><pre><code class="java">public static void println(Object obj);public static void print(Object obj);public static String readln(String prompt);</code></pre><p>想要快速实现控制台操作，可以这样写了：</p><pre><code class="java">void main() {    String name = readln(&quot;请输入姓名: &quot;);    print(&quot;很高兴见到你, &quot;);    println(name);}</code></pre><p>作为一个老程序猿，也不得不哇塞一下。</p><h3 id="JEP-480-结构化并发（Structured-Concurrency，第三次预览）"><a href="#JEP-480-结构化并发（Structured-Concurrency，第三次预览）" class="headerlink" title="JEP 480: 结构化并发（Structured Concurrency，第三次预览）"></a>JEP 480: 结构化并发（Structured Concurrency，第三次预览）</h3><p>结构化并发API（Structured Concurrency API）旨在简化多线程编程，通过引入一个API来处理在不同线程中运行的多个任务作为一个单一工作单元，从而简化错误处理和取消操作，提高可靠性，并增强可观测性。本次发布是第一次预览。</p><p>结构化并发API提供了明确的语法结构来定义子任务的生命周期，并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。</p><p>Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时，如何被传递和处理的过程称为异常传播。</p><p>在结构化并发环境中，异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。</p><p>在Java并发编程中，非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理，这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。</p><pre><code class="java">try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {    var task1 = scope.fork(() -&gt; {        Thread.sleep(1000);        return &quot;Result from task 1&quot;;    });    var task2 = scope.fork(() -&gt; {        Thread.sleep(2000);        return &quot;Result from task 2&quot;;    });    scope.join();    scope.throwIfFailed(RuntimeException::new);    System.out.println(task1.get());    System.out.println(task2.get());} catch (Exception e) {    e.printStackTrace();}</code></pre><p>在这个例子中，handle()方法使用StructuredTaskScope来并行执行两个子任务：task1和task2。通过使用try-with-resources语句自动管理资源，并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关，提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性：</p><ul><li>错误处理与短路：如果task1或task2子任务中的任何一个失败，另一个如果尚未完成则会被取消。（这由 ShutdownOnFailure 实现的关闭策略来管理；还有其他策略可能）。</li><li>取消传播：如果在运行上面方法的线程在调用 join() 之前或之中被中断，则线程在退出作用域时会自动取消两个子任务。</li><li>清晰性：设置子任务，等待它们完成或被取消，然后决定是成功（并处理已经完成的子任务的结果）还是失败（子任务已经完成，因此没有更多需要清理的）。</li><li>可观察性：线程转储清楚地显示了任务层次结构，其中运行task1或task2的线程被显示为作用域的子任务。</li></ul><p>上面的示例能够很好的解决我们的一个痛点，有两个可并行的任务A和B，A+B才是完整结果，任何一个失败，另外一个也不需要成功，结构化并发API就可以很容易的实现这个逻辑。</p><h3 id="JEP-481-作用域值（Scoped-Values，第三次预览）"><a href="#JEP-481-作用域值（Scoped-Values，第三次预览）" class="headerlink" title="JEP 481: 作用域值（Scoped Values，第三次预览）"></a>JEP 481: 作用域值（Scoped Values，第三次预览）</h3><p>作用域值（Scoped Values）在Java20孵化，在Java21第一次预览，在Java22第二次预览，旨在提供一种安全且高效的方法来共享数据，无需使用方法参数。这一特性允许在不使用方法参数的情况下，将数据安全地共享给方法，优先于线程局部变量，特别是在使用大量虚拟线程时。</p><p>在多线程环境中，作用域值可以在线程内和线程间共享不可变数据，例如从父线程向子线程传递数据，从而解决了在多线程应用中传递数据的问题。此外，作用域值提高了数据的安全性、不变性和封装性，并且在多线程环境中使用事务、安全主体和其他形式的共享上下文的应用程序中表现尤为突出。</p><p>作用域值的主要特点：</p><ul><li>不可变性：作用域值是不可变的，这意味着一旦设置，其值就不能更改。这种不可变性减少了并发编程中意外副作用的风险。</li><li>作用域生命周期：作用域值的生命周期仅限于 run 方法定义的作用域。一旦执行离开该作用域，作用域值将不再可访问。</li><li>继承性：子线程会自动继承父线程的作用域值，从而允许在线程边界间无缝共享数据。</li></ul><p>在这个功能之前，在多线程间传递数据，我们有两种选择：</p><ol><li>方法参数：显示参数传递；缺点是新增参数时修改联动修改一系列方法，如果是框架或SDK层面的，无法做到向下兼容。</li><li><code>ThreadLocal</code>：在<code>ThreadLocal</code>保存当前线程变量。</li></ol><p>使用过<code>ThreadLocal</code>的都清楚，<code>ThreadLocal</code>会有三大问题。</p><ol><li>无约束的可变性：每个线程局部变量都是可变的。任何可以调用线程局部变量的<code>get</code>方法的代码都可以随时调用该变量的<code>set</code>方法。即使线程局部变量中的对象是不可变的，每个字段都被声明为final，情况仍然如此。<code>ThreadLocal</code> API允许这样做，以便支持一个完全通用的通信模型，在该模型中，数据可以在方法之间以任何方向流动。这可能会导致数据流混乱，导致程序难以分辨哪个方法更新共享状态以及以何种顺序进行。</li><li>无界生存期：一旦通过<code>set</code>方法设置了一个线程局部变量的副本，该值就会在该线程的生存期内保留，或者直到该线程中的代码调用<code>remove</code>方法。我们有时候会忘记调用<code>remove</code>，如果使用线程池，在一个任务中设置的线程局部变量的值如果不清除，可能会意外泄漏到无关的任务中，导致危险的安全漏洞（比如人员SSO）。对于依赖于线程局部变量的无约束可变性的程序来说，可能没有明确的点可以保证线程调用<code>remove</code>是安全的，可能会导致内存泄漏，因为每个线程的数据在退出之前都不会被垃圾回收。</li><li>昂贵的继承：当使用大量线程时，线程局部变量的开销可能会更糟糕，因为父线程的线程局部变量可以被子线程继承。（事实上，线程局部变量并不是某个特定线程的本地变量。）当开发人员选择创建一个继承了线程局部变量的子线程时，该子线程必须为之前在父线程中写入的每个线程局部变量分配存储空间。这可能会显著增加内存占用。子线程不能共享父线程使用的存储，因为ThreadLocal API要求更改线程的线程局部变量副本在其他线程中不可见。这也会有另一个隐藏的问题，子线程没有办法向父线程<code>set</code>数据。</li></ol><p>作用域值可以有效解决上面提到的问题，而且写起来更加优雅。</p><p>我们一起看下作用域值的使用：</p><pre><code class="java">// 声明一个作用域值用于存储用户名public final static ScopedValue&lt;String&gt; USERNAME = ScopedValue.newInstance();private static final Runnable printUsername = () -&gt;        System.out.println(Thread.currentThread().threadId() + &quot; 用户名是 &quot; + USERNAME.get());public static void main(String[] args) throws Exception {    // 将用户名 &quot;Bob&quot; 绑定到作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Bob&quot;).run(() -&gt; {        printUsername.run();        new Thread(printUsername).start();    });    // 将用户名 &quot;Chris&quot; 绑定到另一个作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Chris&quot;).run(() -&gt; {        printUsername.run();        new Thread(() -&gt; {            new Thread(printUsername).start();            printUsername.run();        }).start();    });    // 检查在任何作用域外 USERNAME 是否被绑定    System.out.println(&quot;用户名是否被绑定: &quot; + USERNAME.isBound());}</code></pre><p>写起来干净利索，而且功能更强。</p><h3 id="JEP-482-灵活的构造函数主体（Flexible-Constructor-Bodies，第二次预览）"><a href="#JEP-482-灵活的构造函数主体（Flexible-Constructor-Bodies，第二次预览）" class="headerlink" title="JEP 482: 灵活的构造函数主体（Flexible Constructor Bodies，第二次预览）"></a>JEP 482: 灵活的构造函数主体（Flexible Constructor Bodies，第二次预览）</h3><p>我们都知道，在子类的构造函数中，比如通过<code>super(……)</code>调用父类，在<code>super</code>之前是不允许有其他语句的。</p><p>大部分的时候这种限制都没问题，但是有时候不太灵活。如果想在<code>super</code>之前加上一些子类特有逻辑，比如想统计下子类构造耗时，就得重写一遍父类的实现。</p><p>除了有损灵活性，这种重写的做法也会造成父子类之间的关系变得奇怪。假设父类是SDK中的一个类，SDK升级时在父类构造函数增加了一些逻辑，我们项目中是无法继承这些逻辑的，某次需要升级SDK（比如低版本有安全风险），验证不完整的情况下，就很容易出现bug。</p><p>JEP 482 的目标是提高构造函数的可读性和可预测性，同时保持构造函数调用的自上而下的规则。通过允许在显式调用 <code>super()</code> 或 <code>this()</code> 前初始化字段，从而实现更灵活的构造函数主体。这一变化使得代码更具表现力。</p><p>我们看下示例代码：</p><pre><code class="java">public class PositiveBigInteger extends BigInteger {    public PositiveBigInteger(long value) {        if (value &lt;= 0) {            throw new IllegalArgumentException(&quot;non-positive value&quot;);        }        super(value);    }}</code></pre><h2 id="孵化功能"><a href="#孵化功能" class="headerlink" title="孵化功能"></a>孵化功能</h2><h3 id="JEP-469-向量API（Vector-API，第八次孵化）"><a href="#JEP-469-向量API（Vector-API，第八次孵化）" class="headerlink" title="JEP 469: 向量API（Vector API，第八次孵化）"></a>JEP 469: 向量API（Vector API，第八次孵化）</h3><p>向量API的功能是提供一个表达向量计算的API，旨在通过引入向量计算API来提高Java应用程序的性能。这一API允许开发者在支持的CPU架构上可靠地编译为最佳向量指令，从而实现比等效的标量计算更高的性能。这些计算在运行时可靠地编译成支持的CPU架构上的最优向量指令，从而实现比等效标量计算更优的性能。</p><p>下面这个是官方给的示例：</p><pre><code class="java">// 标量计算示例void scalarComputation(float[] a, float[] b, float[] c) {    for (int i = 0; i &lt; a.length ; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}// 使用向量API的向量计算示例static final VectorSpecies&lt;Float&gt; SPECIES = FloatVector.SPECIES_PREFERRED;void vectorComputation(float[] a, float[] b, float[] c) {    int i = 0;    int upperBound = SPECIES.loopBound(a.length);    for (; i &lt; upperBound; i += SPECIES.length()) {        // FloatVector va, vb, vc;        var va = FloatVector.fromArray(SPECIES, a, i);        var vb = FloatVector.fromArray(SPECIES, b, i);        var vc = va.mul(va).add(vb.mul(vb)).neg();        vc.intoArray(c, i);    }    for (; i &lt; a.length; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}</code></pre><p>向量API在Java中的独特优势在于其高效的并行计算能力、丰富的向量化指令集、跨平台的数据并行算法支持以及对机器学习的特别优化。</p><h2 id="弃用或废弃"><a href="#弃用或废弃" class="headerlink" title="弃用或废弃"></a>弃用或废弃</h2><h3 id="JEP-471-标记sun-misc-Unsafe中的内存管理方法为过时（Deprecate-the-Memory-Access-Methods-in-sun-misc-Unsafe-for-Removal）"><a href="#JEP-471-标记sun-misc-Unsafe中的内存管理方法为过时（Deprecate-the-Memory-Access-Methods-in-sun-misc-Unsafe-for-Removal）" class="headerlink" title="JEP 471: 标记sun.misc.Unsafe中的内存管理方法为过时（Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal）"></a>JEP 471: 标记sun.misc.Unsafe中的内存管理方法为过时（Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal）</h3><p>JEP 471 的目标是将 <code>sun.misc.Unsafe</code> 类中的内存访问方法标记为过时，并计划在未来版本中移除这些方法。这些方法包括直接访问 JVM 垃圾回收堆或非堆内存的操作。</p><p><code>sun.misc.Unsafe</code>类自 2002 年引入以来，被广泛用于执行低级操作，如直接内存访问和线程调度。然而，这些方法存在安全隐患，可能导致内存泄漏和数据竞争等问题。随着 Java 平台的发展，出现了更安全的替代 API，如 VarHandle API（JEP 193, JDK 9）和 Foreign Function &amp; Memory API（JEP 454, JDK 22），这些 API 提供了更安全和标准化的内存访问方式。</p><p>这一特性主要收益是：</p><ul><li>提高安全性：移除不安全的内存访问方法，减少潜在的安全风险。</li><li>标准化内存管理：鼓励开发者使用标准 API，提高代码的可维护性和可移植性。</li><li>促进平台发展：为 Java 平台的未来发展铺平道路，支持更多高级特性。</li></ul><p>JEP 471 将 <code>sun.misc.Unsafe</code> 中的内存访问方法标记为过时，并计划在未来版本中移除这些方法。具体步骤包括：</p><ol><li>在 Java23 中标记这些方法为过时。</li><li>在 Java25 或之前版本中发出运行时警告。</li><li>在 Java26 或之后版本中默认抛出异常。</li><li>在后续版本中移除这些方法。</li></ol><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文介绍了 Java23 新增的特性，完整的特性清单可以从 <a href="https://openjdk.org/projects/jdk/23/" target="_blank" rel="noopener">https://openjdk.org/projects/jdk/23/</a> 查看。后续内容会发布在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/java/java-23-features">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java23 的新特性</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java23 的新特性</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      从 2017 年开始，Java 版本更新遵循每六个月发布一次的节奏，LTS版本则每两年发布一次，以快速验证新特性，推动 Java 的发展。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java23" scheme="https://www.howardliu.cn/tags/Java23/"/>
    
  </entry>
  
  <entry>
    <title>Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java22 的新特性</title>
    <link href="https://www.howardliu.cn/java/java-22-features/"/>
    <id>https://www.howardliu.cn/java/java-22-features/</id>
    <published>2024-09-14T00:20:00.000Z</published>
    <updated>2024-12-05T03:08:01.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/202409131947520.jpg" alt="Java22 的新特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>从 2017 年开始，Java 版本更新策略从原来的每两年一个新版本，改为每六个月一个新版本，以快速验证新特性，推动 Java 的发展。让我们跟随 Java 的脚步，配合示例讲解，看一看每个版本的新特性，本期是 Java22 的新特性。</p><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>Java22 在 2024 年 3 月 19 日发布GA版本，共十二大特性：</p><ul><li>JEP 423: G1垃圾回收器的区域固定（Region Pinning for G1）</li><li>JEP 447: super(…)前置语句（Statements before super(…)，预览）</li><li>JEP 454: 外部函数和内存API（Foreign Function &amp; Memory API）</li><li>JEP 456: 未命名变量和模式（Unnamed Variables &amp; Patterns）</li><li>JEP 457: 类文件API（Class-File API，预览）</li><li>JEP 458: 启动多文件源代码程序（Launch Multi-File Source-Code Programs）</li><li>JEP 459: 字符串模板（String Templates，第二次预览）</li><li>JEP 460: 向量API（Vector API，第七次孵化）</li><li>JEP 461: 流收集器（Stream Gatherers，预览）</li><li>JEP 462: 结构化并发（Structured Concurrency，第二次预览）</li><li>JEP 463: 隐式声明的类和实例主方法（Implicitly Declared Classes and Instance Main Methods，第二次预览）</li><li>JEP 464: 作用域值（Scoped Values，第二次预览）</li></ul><p>接下来我们一起看看这些特性。</p><h2 id="JEP-423-G1垃圾回收器的区域固定（Region-Pinning-for-G1）"><a href="#JEP-423-G1垃圾回收器的区域固定（Region-Pinning-for-G1）" class="headerlink" title="JEP 423: G1垃圾回收器的区域固定（Region Pinning for G1）"></a>JEP 423: G1垃圾回收器的区域固定（Region Pinning for G1）</h2><p>JEP 423: G1垃圾回收器的区域固定旨在解决在使用Java本地接口（JNI）时遇到的垃圾回收（GC）延迟问题。</p><p>在使用JNI时，Java线程需要等待GC操作完成，这会导致应用程序的延迟增加。特别是在JNI关键区域，GC操作会被暂停，直到线程离开该区域。这种机制虽然可以确保GC的稳定性，但会显著增加应用程序的延迟。</p><p>JEP 423通过引入区域固定机制来解决上述问题。具体来说，该特性允许在G1垃圾回收器中固定JNI代码使用的内存区域，这样即使在这些区域中存在GC操作，也不会影响到其他区域的垃圾回收。这通过以下方式实现：</p><ol><li>区域计数器：在每个区域中维护一个计数器，用于记录该区域中的临界对象数量。当一个临界对象被获取时，计数器增加；当一个临界对象被释放时，计数器减少。</li><li>区域固定：在进行GC操作时，G1垃圾回收器会固定那些包含临界对象的区域，确保这些区域在GC期间保持不变。这样，即使线程处于JNI关键区域，垃圾回收也可以继续进行，而不会被暂停。</li></ol><p>JEP 423可以带来显著的性能改进：</p><ol><li>减少延迟：通过允许在JNI关键区域期间继续进行垃圾回收，减少了应用程序的延迟。</li><li>提高效率：Java线程无需等待GC操作完成，从而提高了开发人员的工作效率。</li><li>增强可预测性：该特性还增强了垃圾回收的可预测性，特别是在处理大对象时。</li></ol><h2 id="JEP-454-外部函数和内存API（Foreign-Function-amp-Memory-API）"><a href="#JEP-454-外部函数和内存API（Foreign-Function-amp-Memory-API）" class="headerlink" title="JEP 454: 外部函数和内存API（Foreign Function &amp; Memory API）"></a>JEP 454: 外部函数和内存API（Foreign Function &amp; Memory API）</h2><p>FFM API是为了提供一个更安全、更高效的替代JNI（Java Native Interface）的API。JNI虽然允许Java程序调用本地代码，但其使用复杂且容易引入安全问题。FFM API旨在简化这一过程，提高开发者的生产力和体验，同时增强性能、安全性和一致性。在Java22中正式发布。</p><p>FFM API通过有效地调用外部函数（即JVM外部的代码）并安全地访问外部内存（即不受JVM管理的内存），使Java程序能够调用本机库并处理本机数据，而不会出现脆弱性和危险。</p><p>FFM API经历了多轮孵化和预览，从Java17的JEP 412开始，经过Java18的JEP 419和Java19的JEP 424，再到Java20的JEP 434和Java21的JEP 442，最终在Java22中正式发布。这些改进包括：</p><ul><li>API的集中管理：通过Arena接口集中管理本地段的生命周期。</li><li>安全访问外部内存：通过<code>MemoryLayout</code>和<code>VarHandle</code>操作和访问结构化的外部内存。</li><li>调用外部函数：通过<code>Linker</code>、<code>FunctionDescriptor</code>和<code>SymbolLookup</code>调用外部函数。</li></ul><p>FFM API的主要收益包括：</p><ul><li>提高性能：通过直接调用本地函数和操作内存，提高了程序的执行效率。</li><li>增强安全性：通过更严格的内存管理机制，减少了内存泄漏和安全漏洞的风险。</li><li>提升开发体验：简化了与本地代码的交互，使得开发者可以更专注于业务逻辑的实现。</li></ul><p>我们看下官方示例：</p><pre><code class="java">// 1. 在C库路径上查找名为radixsort的外部函数Linker linker = Linker.nativeLinker();SymbolLookup stdlib = linker.defaultLookup();final MemorySegment memorySegment = stdlib.find(&quot;radixsort&quot;).orElseThrow();FunctionDescriptor descriptor = FunctionDescriptor.ofVoid(        ValueLayout.ADDRESS,        ValueLayout.JAVA_INT,        ValueLayout.ADDRESS);MethodHandle radixsort = linker.downcallHandle(memorySegment, descriptor);// 下面的代码将使用这个外部函数对字符串进行排序// 2. 分配栈上内存来存储四个字符串String[] javaStrings = {&quot;mouse&quot;, &quot;cat&quot;, &quot;dog&quot;, &quot;car&quot;};// 3. 使用try-with-resources来管理离堆内存的生命周期try (Arena offHeap = Arena.ofConfined()) {    // 4. 分配一段离堆内存来存储四个指针    MemorySegment pointers = offHeap.allocateArray(ValueLayout.ADDRESS, javaStrings.length);    // 5. 将字符串从栈上内存复制到离堆内存    for (int i = 0; i &lt; javaStrings.length; i++) {        MemorySegment cString = offHeap.allocateUtf8String(javaStrings[i]);        pointers.setAtIndex(ValueLayout.ADDRESS, i, cString);    }    // 6. 通过调用外部函数对离堆数据进行排序    radixsort.invoke(pointers, javaStrings.length, MemorySegment.NULL, &#39;\0&#39;);    // 7. 将排序后的字符串从离堆内存复制回栈上内存    for (int i = 0; i &lt; javaStrings.length; i++) {        MemorySegment cString = pointers.getAtIndex(ValueLayout.ADDRESS, i);        javaStrings[i] = cString.getUtf8String(0);    }} // 8. 所有离堆内存在此处被释放// 验证排序结果assert Arrays.equals(javaStrings, new String[] {&quot;car&quot;, &quot;cat&quot;, &quot;dog&quot;, &quot;mouse&quot;});  // true</code></pre><p>我们都知道，JNI也是可以调用外部代码的，那FFM API相较于JNI的优势在于：</p><ol><li>更安全的内存访问：FFM API 提供了一种更安全和受控的方式来与本地代码交互，避免了JNI中常见的内存泄漏和数据损坏问题。</li><li>直接访问本地内存：FFM API 允许Java程序直接访问本地内存（即Java堆外的内存），这使得数据处理更加高效和灵活。</li><li>跨语言函数调用：FFM API 支持调用Java程序的外部函数，以与外部代码和数据一起操作，而无需依赖JNI的复杂机制。</li><li>更高效的集成：FFM API 使得Java与C、C++等语言编写的库集成更加方便和高效，特别是在数据处理和机器学习等领域。</li><li>减少代码复杂性：FFM API 提供了一种更简洁的API，减少了JNI中复杂的代码编写和维护工作。</li><li>更广泛的适用性：FFM API 不仅适用于简单的函数调用，还可以处理复杂的内存管理任务，如堆外内存的管理。</li><li>提高性能：FFM API 通过高效的调用外部函数和安全地访问外部内存，提高了程序的运行效率。</li></ol><h2 id="JEP-456-未命名变量和模式（Unnamed-Variables-amp-Patterns）"><a href="#JEP-456-未命名变量和模式（Unnamed-Variables-amp-Patterns）" class="headerlink" title="JEP 456: 未命名变量和模式（Unnamed Variables &amp; Patterns）"></a>JEP 456: 未命名变量和模式（Unnamed Variables &amp; Patterns）</h2><p>JEP 456: 未命名变量和模式（Unnamed Variables &amp; Patterns）是一个重要新特性，在Java21中预览，在Java22中发布。旨在提高Java代码的可读性和可维护性。这一特性允许开发者在声明变量或嵌套模式时使用下划线字符（_）来表示未命名的变量或模式，从而简化代码并减少不必要的噪声，提高代码可读性和可维护性。</p><p>比如：</p><pre><code class="java">public static void main(String[] args) {    var _ = new Point(1, 2);}record Point(int x, int y) {}</code></pre><p>这个可以用在任何定义变量的地方，比如：</p><ul><li><code>... instanceof Point(_, int y)</code></li><li><code>r instanceof Point _</code></li><li><code>switch …… case Box(_)</code></li><li><code>for (Order _ : orders)</code></li><li><code>for (int i = 0, _ = sideEffect(); i &lt; 10; i++)</code></li><li><code>try { ... } catch (Exception _) { ... } catch (Throwable _) { ... }</code></li></ul><p>只要是这个不准备用，可以一律使用<code>_</code>代替。</p><h2 id="JEP-458-启动多文件源代码程序（Launch-Multi-File-Source-Code-Programs）"><a href="#JEP-458-启动多文件源代码程序（Launch-Multi-File-Source-Code-Programs）" class="headerlink" title="JEP 458: 启动多文件源代码程序（Launch Multi-File Source-Code Programs）"></a>JEP 458: 启动多文件源代码程序（Launch Multi-File Source-Code Programs）</h2><p>这个功能主要是提升<code>java</code>命令的能力。比如我们手搓了两个类：</p><pre><code class="java">// Prog.javaclass Prog {    public static void main(String[] args) { Helper.run(); }}// Helper.javaclass Helper {    static void run() { System.out.println(&quot;Hello!&quot;); }}</code></pre><p>想要运行<code>Prog</code>的main方法，在JEP 458之前，我们需要先编译<code>Helper</code>，然后通过<code>-classpath</code>指令加载编译后的类，才能运行<code>Prog</code>。但是现在，<code>java</code>帮我们做了，我们直接使用<code>java Prog.java</code>就可以执行了。</p><p>很方便的一个功能，但是似乎好像大概看起来没什么用，但是对于Java生态有深远影响：</p><ol><li>增强Java启动器功能：JEP 458允许Java启动器执行包含一个或多个文件的Java源码应用程序。这一改进使得开发者可以更灵活地启动和运行Java程序，特别是在需要处理多个源文件的复杂项目中，这一特性将大大提升开发效率和便利性。</li><li>促进Java生态系统的持续演进：JEP 458的引入是Java生态系统持续演进的一部分，与Java21的新特性和Quarkus的创新应用相辅相成。这些更新不仅提升了与现代Java生态系统的兼容性，还引入了领域模型验证的改进和依赖于Java17的新特性，为开发者提供了更加稳定、高效的工具集。</li><li>推动Java项目的发展：JEP 458的新特性被归类到四个主要的Java项目中，即Amber、Loom、Panama和Valhalla。这些项目旨在通过精巧的合并，孵化一系列组件，以便最终将其纳入到JDK中。这表明JEP 458不仅是一个独立的特性，而是Java生态系统中更广泛技术演进的一部分，有助于推动整个Java生态系统的创新和发展。</li><li>简化Java开发流程：通过简化启动多文件源码程序的过程，JEP 458有助于简化Java开发流程，使得开发者可以更加专注于业务逻辑的实现，而不是在启动和运行程序上花费过多时间。</li></ol><h2 id="预览功能"><a href="#预览功能" class="headerlink" title="预览功能"></a>预览功能</h2><h3 id="JEP-447-super-…-前置语句（Statements-before-super-…-）"><a href="#JEP-447-super-…-前置语句（Statements-before-super-…-）" class="headerlink" title="JEP 447: super(…)前置语句（Statements before super(…)）"></a>JEP 447: super(…)前置语句（Statements before super(…)）</h3><p>我们都知道，在子类的构造函数中，如果通过<code>super(……)</code>调用父类，在<code>super</code>之前是不允许有其他语句的。</p><p>大部分的时候这种限制都没问题，但是有时候不太灵活。如果想在<code>super</code>之前加上一些子类特有逻辑，比如想统计下子类构造耗时，就得重写一遍父类的实现。</p><p>除了有损灵活性，这种重写的做法也会造成父子类之间的关系变得奇怪。假设父类是SDK中的一个类，SDK升级时在父类构造函数增加了一些逻辑，我们项目中是无法继承这些逻辑的，某次需要升级SDK（比如低版本有安全风险），验证不完整的情况下，就很容易出现bug。</p><p>在 JEP 447 中，允许在构造函数中不引用正在创建的实例的语句出现在显式构造函数调用（如 super()）之前。这一特性旨在为开发者提供更多的灵活性，允许在调用父类构造函数之前执行一些验证或其他处理操作。</p><p>引入这一特性的主要动机是提高构造函数的灵活性和可读性。通过允许在 super() 调用之前执行语句，开发者可以在构造函数中添加额外的逻辑，例如进行一些初始化检查或执行一些特定的处理操作，而不必依赖于 super() 调用之后的代码。</p><p>我们看下示例代码：</p><pre><code class="java">public class PositiveBigInteger extends BigInteger {    public PositiveBigInteger(long value) {        if (value &lt;= 0) {            throw new IllegalArgumentException(&quot;non-positive value&quot;);        }        super(value);    }}</code></pre><h3 id="JEP-457-类文件API（Class-File-API，预览）"><a href="#JEP-457-类文件API（Class-File-API，预览）" class="headerlink" title="JEP 457: 类文件API（Class-File API，预览）"></a>JEP 457: 类文件API（Class-File API，预览）</h3><p>Java中一直缺少官方的类文件操作API，想要操作class，我们需要借助第三方库，比如javassist、ASM、ByteBuddy等。从2017年开始，Java每半年有一次升级，特性更新频率增加，需要第三方库同步更新，是比较困难的。</p><p>还有一个原因是Java中使用了ASM实现<code>jar</code>、<code>jlink</code>等工具，以及lambda表达式等。这就会出现一个问题，Java版本N依赖了ASM版本M，如果Java N中有类API，ASM M中是不会有的，只有Java N发布后，ASM升级到M+1才会有，Java想要使用ASM M+1，需要升级到Java N+1。是不是很颠，一个官方基础语言，居然要依赖一个依赖这个语言的第三方工具。是可忍孰不可忍。</p><p>于是有了JEP 457，目标是提供一个准确、完整、高性能且遵循Java虚拟机规范定义的类文件格式的API，最终也会替换JDK内部的ASM副本。</p><p>因为当前还是预览版，我们先简单看下官方示例：</p><p>如果要实现下面这段：</p><pre><code class="java">void fooBar(boolean z, int x) {    if (z)        foo(x);    else        bar(x);}</code></pre><p>我们在ASM的写法：</p><pre><code class="java">ClassWriter classWriter = ...;MethodVisitor mv = classWriter.visitMethod(0, &quot;fooBar&quot;, &quot;(ZI)V&quot;, null, null);mv.visitCode();mv.visitVarInsn(ILOAD, 1);Label label1 = new Label();mv.visitJumpInsn(IFEQ, label1);mv.visitVarInsn(ALOAD, 0);mv.visitVarInsn(ILOAD, 2);mv.visitMethodInsn(INVOKEVIRTUAL, &quot;Foo&quot;, &quot;foo&quot;, &quot;(I)V&quot;, false);Label label2 = new Label();mv.visitJumpInsn(GOTO, label2);mv.visitLabel(label1);mv.visitVarInsn(ALOAD, 0);mv.visitVarInsn(ILOAD, 2);mv.visitMethodInsn(INVOKEVIRTUAL, &quot;Foo&quot;, &quot;bar&quot;, &quot;(I)V&quot;, false);mv.visitLabel(label2);mv.visitInsn(RETURN);mv.visitEnd();</code></pre><p>在JEP 457中的写法：</p><pre><code class="java">ClassBuilder classBuilder = ...;classBuilder.withMethod(&quot;fooBar&quot;, MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,                        methodBuilder -&gt; methodBuilder.withCode(codeBuilder -&gt; {    Label label1 = codeBuilder.newLabel();    Label label2 = codeBuilder.newLabel();    codeBuilder.iload(1)        .ifeq(label1)        .aload(0)        .iload(2)        .invokevirtual(ClassDesc.of(&quot;Foo&quot;), &quot;foo&quot;, MethodTypeDesc.of(CD_void, CD_int))        .goto_(label2)        .labelBinding(label1)        .aload(0)        .iload(2)        .invokevirtual(ClassDesc.of(&quot;Foo&quot;), &quot;bar&quot;, MethodTypeDesc.of(CD_void, CD_int))        .labelBinding(label2);        .return_();});</code></pre><p>还可以这样写：</p><pre><code class="java">CodeBuilder classBuilder = ...;classBuilder.withMethod(&quot;fooBar&quot;, MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,                        methodBuilder -&gt; methodBuilder.withCode(codeBuilder -&gt; {    codeBuilder.iload(codeBuilder.parameterSlot(0))               .ifThenElse(                   b1 -&gt; b1.aload(codeBuilder.receiverSlot())                           .iload(codeBuilder.parameterSlot(1))                           .invokevirtual(ClassDesc.of(&quot;Foo&quot;), &quot;foo&quot;,                                          MethodTypeDesc.of(CD_void, CD_int)),                   b2 -&gt; b2.aload(codeBuilder.receiverSlot())                           .iload(codeBuilder.parameterSlot(1))                           .invokevirtual(ClassDesc.of(&quot;Foo&quot;), &quot;bar&quot;,                                          MethodTypeDesc.of(CD_void, CD_int))               .return_();});</code></pre><p>写法上比ASM更加优雅。</p><h3 id="JEP-459-字符串模板（String-Templates，第二次预览）"><a href="#JEP-459-字符串模板（String-Templates，第二次预览）" class="headerlink" title="JEP 459: 字符串模板（String Templates，第二次预览）"></a>JEP 459: 字符串模板（String Templates，第二次预览）</h3><p>字符串模板是一个值得期待的功能，旨在增强Java编程语言。该特性通过将文字文本与嵌入式表达式和模板处理器结合，生成专门的结果，从而补充了Java现有的字符串文字和文本块。</p><p>相对Java21中JEP 430，主要的优化改动包括：</p><ul><li>增强的表达式支持：允许在字符串模板中嵌入更复杂的表达式，这些表达式可以在运行时被计算和校验。</li><li>模板处理器：引入了模板处理器的概念，使得字符串模板可以更加灵活地处理不同的数据格式和结构。</li><li>安全性提升：通过在运行时对嵌入的表达式进行校验，提高了代码的安全性。</li></ul><p>字符串模板通过将文本和嵌入式表达式结合在一起，使得Java程序能够以一种更加直观和安全的方式构建字符串。与传统的字符串拼接（使用+操作符）、<code>StringBuilder</code>或<code>String.format</code> 等方法相比，字符串模板提供了一种更加清晰和安全的字符串构建方式。特别是当字符串需要从用户提供的值构建并传递给其他系统时（例如，构建数据库查询），使用字符串模板可以有效地验证和转换模板及其嵌入表达式的值，从而提高Java程序的安全性。</p><p>让我们通过代码看一下这个特性的魅力：</p><pre><code class="java">public static void main(String[] args) {    // 拼装变量    String name = &quot;看山&quot;;    String info = STR. &quot;My name is \{ name }&quot; ;    assert info.equals(&quot;My name is 看山&quot;);    // 拼装变量    String firstName = &quot;Howard&quot;;    String lastName = &quot;Liu&quot;;    String fullName = STR. &quot;\{ firstName } \{ lastName }&quot; ;    assert fullName.equals(&quot;Howard Liu&quot;);    String sortName = STR. &quot;\{ lastName }, \{ firstName }&quot; ;    assert sortName.equals(&quot;Liu, Howard&quot;);    // 模板中调用方法    String s2 = STR. &quot;You have a \{ getOfferType() } waiting for you!&quot; ;    assert s2.equals(&quot;You have a gift waiting for you!&quot;);    Request req = new Request(&quot;2017-07-19&quot;, &quot;09:15&quot;, &quot;https://www.howardliu.cn&quot;);    // 模板中引用对象属性    String s3 = STR. &quot;Access at \{ req.date } \{ req.time } from \{ req.address }&quot; ;    assert s3.equals(&quot;Access at 2017-07-19 09:15 from https://www.howardliu.cn&quot;);    LocalTime now = LocalTime.now();    String markTime = DateTimeFormatter            .ofPattern(&quot;HH:mm:ss&quot;)            .format(now);    // 模板中调用方法    String time = STR. &quot;The time is \{            // The java.time.format package is very useful            DateTimeFormatter                    .ofPattern(&quot;HH:mm:ss&quot;)                    .format(now)            } right now&quot; ;    assert time.equals(&quot;The time is &quot; + markTime + &quot; right now&quot;);    // 模板嵌套模板    String[] fruit = {&quot;apples&quot;, &quot;oranges&quot;, &quot;peaches&quot;};    String s4 = STR. &quot;\{ fruit[0] }, \{            STR. &quot;\{ fruit[1] }, \{ fruit[2] }&quot;            }&quot; ;    assert s4.equals(&quot;apples, oranges, peaches&quot;);    // 模板与文本块结合    String title = &quot;My Web Page&quot;;    String text = &quot;Hello, world&quot;;    String html = STR. &quot;&quot;&quot;    &lt;html&gt;      &lt;head&gt;        &lt;title&gt;\{ title }&lt;/title&gt;      &lt;/head&gt;      &lt;body&gt;        &lt;p&gt;\{ text }&lt;/p&gt;      &lt;/body&gt;    &lt;/html&gt;    &quot;&quot;&quot; ;    assert html.equals(&quot;&quot;&quot;            &lt;html&gt;              &lt;head&gt;                &lt;title&gt;My Web Page&lt;/title&gt;              &lt;/head&gt;              &lt;body&gt;                &lt;p&gt;Hello, world&lt;/p&gt;              &lt;/body&gt;            &lt;/html&gt;            &quot;&quot;&quot;);    // 带格式化的字符串模板    record Rectangle(String name, double width, double height) {        double area() {            return width * height;        }    }    Rectangle[] zone = new Rectangle[] {            new Rectangle(&quot;Alfa&quot;, 17.8, 31.4),            new Rectangle(&quot;Bravo&quot;, 9.6, 12.4),            new Rectangle(&quot;Charlie&quot;, 7.1, 11.23),    };    String table = FMT. &quot;&quot;&quot;        Description     Width    Height     Area        %-12s\{ zone[0].name }  %7.2f\{ zone[0].width }  %7.2f\{ zone[0].height }     %7.2f\{ zone[0].area() }        %-12s\{ zone[1].name }  %7.2f\{ zone[1].width }  %7.2f\{ zone[1].height }     %7.2f\{ zone[1].area() }        %-12s\{ zone[2].name }  %7.2f\{ zone[2].width }  %7.2f\{ zone[2].height }     %7.2f\{ zone[2].area() }        \{ &quot; &quot;.repeat(28) } Total %7.2f\{ zone[0].area() + zone[1].area() + zone[2].area() }        &quot;&quot;&quot;;    assert table.equals(&quot;&quot;&quot;            Description     Width    Height     Area            Alfa            17.80    31.40      558.92            Bravo            9.60    12.40      119.04            Charlie          7.10    11.23       79.73                                         Total  757.69            &quot;&quot;&quot;);}public static String getOfferType() {    return &quot;gift&quot;;}record Request(String date, String time, String address) {}</code></pre><p>这个功能当前是第二次预览，Java23的8.12版本中还没有展示字符串模板的第三次预览（JEP 465: String Templates），还不能确定什么时候可以正式用上。</p><h3 id="JEP-461-流收集器（Stream-Gatherers，预览）"><a href="#JEP-461-流收集器（Stream-Gatherers，预览）" class="headerlink" title="JEP 461: 流收集器（Stream Gatherers，预览）"></a>JEP 461: 流收集器（Stream Gatherers，预览）</h3><p>JEP 461旨在增强Java Stream API、以支持自定义中间操作。这一特性允许开发者以更灵活和高效的方式处理数据流，从而提高流管道的表达能力和转换数据的能力。</p><p>流收集器通过引入新的中间操作<code>Stream::gather(Gatherer)</code>，允许开发者定义自定义的转换实体（称为Gatherer），从而对流中的元素进行转换。这些转换可以是一对一、一对多、多对一或多对多的转换方式。此外，流收集器还支持保存以前遇到的元素，以便进行进一步的处理。</p><p>我们通过实例感受下这一特性的魅力：</p><pre><code class="java">public record WindowFixed&lt;TR&gt;(int windowSize) implements Gatherer&lt;TR, ArrayList&lt;TR&gt;, List&lt;TR&gt;&gt; {    public static void main(String[] args) {        var list = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)                .gather(new WindowFixed&lt;&gt;(3))                .toList();        // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]        System.out.println(list);    }    public WindowFixed {        // Validate input        if (windowSize &lt; 1) {            throw new IllegalArgumentException(&quot;window size must be positive&quot;);        }    }    @Override    public Supplier&lt;ArrayList&lt;TR&gt;&gt; initializer() {        // 创建一个 ArrayList 来保存当前打开的窗口        return () -&gt; new ArrayList&lt;&gt;(windowSize);    }    @Override    public Integrator&lt;ArrayList&lt;TR&gt;, TR, List&lt;TR&gt;&gt; integrator() {        // 集成器在每次消费元素时被调用        return Gatherer.Integrator.ofGreedy((window, element, downstream) -&gt; {            // 将元素添加到当前打开的窗口            window.add(element);            // 直到达到所需的窗口大小，            // 返回 true 表示希望继续接收更多元素            if (window.size() &lt; windowSize) {                return true;            }            // 当窗口已满时，通过创建副本关闭窗口            var result = new ArrayList&lt;TR&gt;(window);            // 清空窗口以便开始新的窗口            window.clear();            // 将关闭的窗口发送到下游            return downstream.push(result);        });    }    // 由于此操作本质上是顺序的，因此无法并行化，因此省略了合并器    @Override    public BiConsumer&lt;ArrayList&lt;TR&gt;, Downstream&lt;? super List&lt;TR&gt;&gt;&gt; finisher() {        // 终结器在没有更多元素传递时运行        return (window, downstream) -&gt; {            // 如果下游仍然接受更多元素且当前打开的窗口非空，则将其副本发送到下游            if (!downstream.isRejecting() &amp;&amp; !window.isEmpty()) {                downstream.push(new ArrayList&lt;TR&gt;(window));                window.clear();            }        };    }}</code></pre><p>该特性还是预览版，等正式发布后再细说。</p><h3 id="JEP-462-结构化并发（Structured-Concurrency，第二次预览）"><a href="#JEP-462-结构化并发（Structured-Concurrency，第二次预览）" class="headerlink" title="JEP 462: 结构化并发（Structured Concurrency，第二次预览）"></a>JEP 462: 结构化并发（Structured Concurrency，第二次预览）</h3><p>结构化并发API（Structured Concurrency API）旨在简化多线程编程，通过引入一个API来处理在不同线程中运行的多个任务作为一个单一工作单元，从而简化错误处理和取消操作，提高可靠性，并增强可观测性。本次发布是第一次预览。</p><p>结构化并发API提供了明确的语法结构来定义子任务的生命周期，并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。</p><p>在JEP 462中，结构化并发API被进一步完善，使其更加易于使用和维护。主要的优化包括：</p><ul><li>工作单元概念：将一组相关任务视为一个工作单元，这样可以简化错误处理和取消操作。</li><li>增强的可观测性：通过API提供了更好的错误处理机制和任务状态跟踪功能，增强了代码的可观察性。</li><li>虚拟线程支持：与Project Loom项目结合，支持在线程内和线程间共享不可变数据，这在使用大量虚拟线程时尤其有用。</li></ul><p>Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时，如何被传递和处理的过程称为异常传播。</p><p>在结构化并发环境中，异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。</p><p>在Java并发编程中，非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理，这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。</p><pre><code class="java">try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {    var task1 = scope.fork(() -&gt; {        Thread.sleep(1000);        return &quot;Result from task 1&quot;;    });    var task2 = scope.fork(() -&gt; {        Thread.sleep(2000);        return &quot;Result from task 2&quot;;    });    scope.join();    scope.throwIfFailed(RuntimeException::new);    System.out.println(task1.get());    System.out.println(task2.get());} catch (Exception e) {    e.printStackTrace();}</code></pre><p>在这个例子中，handle()方法使用StructuredTaskScope来并行执行两个子任务：task1和task2。通过使用try-with-resources语句自动管理资源，并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关，提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性：</p><ul><li>错误处理与短路：如果task1或task2子任务中的任何一个失败，另一个如果尚未完成则会被取消。（这由 ShutdownOnFailure 实现的关闭策略来管理；还有其他策略可能）。</li><li>取消传播：如果在运行上面方法的线程在调用 join() 之前或之中被中断，则线程在退出作用域时会自动取消两个子任务。</li><li>清晰性：设置子任务，等待它们完成或被取消，然后决定是成功（并处理已经完成的子任务的结果）还是失败（子任务已经完成，因此没有更多需要清理的）。</li><li>可观察性：线程转储清楚地显示了任务层次结构，其中运行task1或task2的线程被显示为作用域的子任务。</li></ul><p>上面的示例能够很好的解决我们的一个痛点，有两个可并行的任务A和B，A+B才是完整结果，任何一个失败，另外一个也不需要成功，结构化并发API就可以很容易的实现这个逻辑。</p><h3 id="JEP-463-隐式声明的类和实例主方法（Implicitly-Declared-Classes-and-Instance-Main-Methods，第二次预览）"><a href="#JEP-463-隐式声明的类和实例主方法（Implicitly-Declared-Classes-and-Instance-Main-Methods，第二次预览）" class="headerlink" title="JEP 463: 隐式声明的类和实例主方法（Implicitly Declared Classes and Instance Main Methods，第二次预览）"></a>JEP 463: 隐式声明的类和实例主方法（Implicitly Declared Classes and Instance Main Methods，第二次预览）</h3><p>无论学习哪门语言，第一课一定是打印<code>Hello, World!</code>，Java中的写法是：</p><pre><code class="java">public class HelloWorld {    public static void main(String[] args) {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>如果是第一次接触，一定会有很多疑问，<code>public</code>干啥的，<code>main</code>方法的约定参数<code>args</code>是什么鬼？然后老师就说，这就是模板，照着抄就行，不这样写不运行。</p><p>JEP 445特性后，可以简化为：</p><pre><code class="java">class HelloWorld {    void main() {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>我们还可以这样写：</p><pre><code class="java">String greeting() { return &quot;Hello, World!&quot;; }void main() {    System.out.println(greeting());}</code></pre><p><code>main</code>方法直接简化为名字和括号，甚至连类也不需要显性定义了。虽然看起来没啥用，但是在JShell中使用，就比较友好了。</p><h3 id="JEP-464-作用域值（Scoped-Values，第二次预览）"><a href="#JEP-464-作用域值（Scoped-Values，第二次预览）" class="headerlink" title="JEP 464: 作用域值（Scoped Values，第二次预览）"></a>JEP 464: 作用域值（Scoped Values，第二次预览）</h3><p>作用域值（Scoped Values）在Java20孵化，在Java21第一次预览，在Java22第二次预览，旨在提供一种安全且高效的方法来共享数据，无需使用方法参数。这一特性允许在不使用方法参数的情况下，将数据安全地共享给方法，优先于线程局部变量，特别是在使用大量虚拟线程时。</p><p>在多线程环境中，作用域值可以在线程内和线程间共享不可变数据，例如从父线程向子线程传递数据，从而解决了在多线程应用中传递数据的问题。此外，作用域值提高了数据的安全性、不变性和封装性，并且在多线程环境中使用事务、安全主体和其他形式的共享上下文的应用程序中表现尤为突出。</p><p>作用域值的主要特点：</p><ul><li>不可变性：作用域值是不可变的，这意味着一旦设置，其值就不能更改。这种不可变性减少了并发编程中意外副作用的风险。</li><li>作用域生命周期：作用域值的生命周期仅限于 run 方法定义的作用域。一旦执行离开该作用域，作用域值将不再可访问。</li><li>继承性：子线程会自动继承父线程的作用域值，从而允许在线程边界间无缝共享数据。</li></ul><p>在这个功能之前，在多线程间传递数据，我们有两种选择：</p><ol><li>方法参数：显示参数传递；缺点是新增参数时修改联动修改一系列方法，如果是框架或SDK层面的，无法做到向下兼容。</li><li><code>ThreadLocal</code>：在<code>ThreadLocal</code>保存当前线程变量。</li></ol><p>使用过<code>ThreadLocal</code>的都清楚，<code>ThreadLocal</code>会有三大问题。</p><ol><li>无约束的可变性：每个线程局部变量都是可变的。任何可以调用线程局部变量的<code>get</code>方法的代码都可以随时调用该变量的<code>set</code>方法。即使线程局部变量中的对象是不可变的，每个字段都被声明为final，情况仍然如此。<code>ThreadLocal</code> API允许这样做，以便支持一个完全通用的通信模型，在该模型中，数据可以在方法之间以任何方向流动。这可能会导致数据流混乱，导致程序难以分辨哪个方法更新共享状态以及以何种顺序进行。</li><li>无界生存期：一旦通过<code>set</code>方法设置了一个线程局部变量的副本，该值就会在该线程的生存期内保留，或者直到该线程中的代码调用<code>remove</code>方法。我们有时候会忘记调用<code>remove</code>，如果使用线程池，在一个任务中设置的线程局部变量的值如果不清除，可能会意外泄漏到无关的任务中，导致危险的安全漏洞（比如人员SSO）。对于依赖于线程局部变量的无约束可变性的程序来说，可能没有明确的点可以保证线程调用<code>remove</code>是安全的，可能会导致内存泄漏，因为每个线程的数据在退出之前都不会被垃圾回收。</li><li>昂贵的继承：当使用大量线程时，线程局部变量的开销可能会更糟糕，因为父线程的线程局部变量可以被子线程继承。（事实上，线程局部变量并不是某个特定线程的本地变量。）当开发人员选择创建一个继承了线程局部变量的子线程时，该子线程必须为之前在父线程中写入的每个线程局部变量分配存储空间。这可能会显著增加内存占用。子线程不能共享父线程使用的存储，因为ThreadLocal API要求更改线程的线程局部变量副本在其他线程中不可见。这也会有另一个隐藏的问题，子线程没有办法向父线程<code>set</code>数据。</li></ol><p>作用域值可以有效解决上面提到的问题，而且写起来更加优雅。</p><p>我们一起看下作用域值的使用：</p><pre><code class="java">// 声明一个作用域值用于存储用户名public final static ScopedValue&lt;String&gt; USERNAME = ScopedValue.newInstance();private static final Runnable printUsername = () -&gt;        System.out.println(Thread.currentThread().threadId() + &quot; 用户名是 &quot; + USERNAME.get());public static void main(String[] args) throws Exception {    // 将用户名 &quot;Bob&quot; 绑定到作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Bob&quot;).run(() -&gt; {        printUsername.run();        new Thread(printUsername).start();    });    // 将用户名 &quot;Chris&quot; 绑定到另一个作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Chris&quot;).run(() -&gt; {        printUsername.run();        new Thread(() -&gt; {            new Thread(printUsername).start();            printUsername.run();        }).start();    });    // 检查在任何作用域外 USERNAME 是否被绑定    System.out.println(&quot;用户名是否被绑定: &quot; + USERNAME.isBound());}</code></pre><p>写起来干净利索，而且功能更强。</p><h2 id="孵化功能"><a href="#孵化功能" class="headerlink" title="孵化功能"></a>孵化功能</h2><h3 id="JEP-460-向量API（Vector-API，第七次孵化）"><a href="#JEP-460-向量API（Vector-API，第七次孵化）" class="headerlink" title="JEP 460: 向量API（Vector API，第七次孵化）"></a>JEP 460: 向量API（Vector API，第七次孵化）</h3><p>向量API的功能是提供一个表达向量计算的API，旨在通过引入向量计算API来提高Java应用程序的性能。这一API允许开发者在支持的CPU架构上可靠地编译为最佳向量指令，从而实现比等效的标量计算更高的性能。这些计算在运行时可靠地编译成支持的CPU架构上的最优向量指令，从而实现比等效标量计算更优的性能。</p><p>下面这个是官方给的示例：</p><pre><code class="java">// 标量计算示例void scalarComputation(float[] a, float[] b, float[] c) {    for (int i = 0; i &lt; a.length ; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}// 使用向量API的向量计算示例static final VectorSpecies&lt;Float&gt; SPECIES = FloatVector.SPECIES_PREFERRED;void vectorComputation(float[] a, float[] b, float[] c) {    int i = 0;    int upperBound = SPECIES.loopBound(a.length);    for (; i &lt; upperBound; i += SPECIES.length()) {        // FloatVector va, vb, vc;        var va = FloatVector.fromArray(SPECIES, a, i);        var vb = FloatVector.fromArray(SPECIES, b, i);        var vc = va.mul(va).add(vb.mul(vb)).neg();        vc.intoArray(c, i);    }    for (; i &lt; a.length; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}</code></pre><p>向量API在Java中的独特优势在于其高效的并行计算能力、丰富的向量化指令集、跨平台的数据并行算法支持以及对机器学习的特别优化。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文介绍了 Java22 新增的特性，完整的特性清单可以从 <a href="https://openjdk.org/projects/jdk/22/" target="_blank" rel="noopener">https://openjdk.org/projects/jdk/22/</a> 查看。后续内容会发布在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/java/java-22-features">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java22 的新特性</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java22 的新特性</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      从 2017 年开始，Java 版本更新策略从原来的每两年一个新版本，改为每六个月一个新版本，以快速验证新特性，推动 Java 的发展。让我们跟随 Java 的脚步，配合示例讲解，看一看每个版本的新特性，本期是 Java22 的新特性。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java22" scheme="https://www.howardliu.cn/tags/Java22/"/>
    
  </entry>
  
  <entry>
    <title>Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java21 的新特性</title>
    <link href="https://www.howardliu.cn/java/java-21-features/"/>
    <id>https://www.howardliu.cn/java/java-21-features/</id>
    <published>2024-09-13T00:20:00.000Z</published>
    <updated>2024-12-05T03:07:50.000Z</updated>
    
    <content type="html"><![CDATA[<p>你好，我是看山。</p><a id="more"></a><p><img src="https://static.howardliu.cn/202409122326976.jpg" alt="Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java21 的新特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>从 2017 年开始，Java 版本更新策略从原来的每两年一个新版本，改为每六个月一个新版本，以快速验证新特性，推动 Java 的发展。让我们跟随 Java 的脚步，配合示例讲解，看一看每个版本的新特性，本期是 Java21 的新特性。</p><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>Java21 在 2023 年 9 月 19 日发布GA版本，Java21是长期支持版（LTS，Long-Term Support），共十五大特性：</p><ul><li>JEP 430: 字符串模板（String Templates，预览）</li><li>JEP 431: 有序集合（Sequenced Collections）</li><li>JEP 439: 分代ZGC（Generational ZGC）</li><li>JEP 440: Record模式（Record Patterns）</li><li>JEP 441: switch模式匹配（Pattern Matching for switch）</li><li>JEP 442: 外部函数和内存API（Foreign Function &amp; Memory API，FFM API，第三次预览）</li><li>JEP 443: 未命名模式和变量（Unnamed Patterns and Variables，预览）</li><li>JEP 444: 虚拟线程（Virtual Threads）</li><li>JEP 445: 未命名类和示例main方法（Unnamed Classes and Instance Main Methods，预览）</li><li>JEP 446: 作用域值（Scoped Values，预览）</li><li>JEP 448: 向量API（Vector API，第六次孵化）</li><li>JEP 449: 启用Windows32位 x86支持（Deprecate the Windows 32-bit x86 Port for Removal）</li><li>JEP 451: 准备禁止动态加载代理（Prepare to Disallow the Dynamic Loading of Agents）</li><li>JEP 452: 密钥封装机制 API（Key Encapsulation Mechanism API）</li><li>JEP 453: 结构化并发API（Structured Concurrency，预览）</li></ul><p>接下来我们一起看看这些特性。</p><h2 id="JEP-431-有序集合（Sequenced-Collections）"><a href="#JEP-431-有序集合（Sequenced-Collections）" class="headerlink" title="JEP 431: 有序集合（Sequenced Collections）"></a>JEP 431: 有序集合（Sequenced Collections）</h2><p>JEP 431:有序集合是Java21版本中引入的一个新特性，旨在为Java集合框架添加对有序集合的支持。</p><p>有序集合是一种具有定义好的元素访问顺序的集合类型，它允许以一致的方式访问和处理集合中的元素，无论是从第一个元素到最后一个元素，还是从最后一个元素到第一个元素。</p><p>在 Java 中，集合类库非常重要且使用频率非常高，但是缺乏一种能够表示具有定义好的元素访问顺序的集合类型。</p><p>例如，<code>List</code>和<code>Deque</code>都定义了元素的访问顺序，但它们的共同父接口<code>Collection</code>却没有。同样，Set不定义元素的访问顺序，其子类型如<code>HashSet</code>也没有定义，但子类型如<code>SortedSet</code>和<code>LinkedHashSet</code>则有定义。因此，支持访问顺序的功能散布在整个类型层次结构中，使得在API中表达某些有用的概念变得困难。<code>Collection</code>太通用，将此类约束留给文档规范，可能导致难以调试的错误。</p><p>而且，虽然某些集合有顺序操作方法，但是却不尽相同，比如</p><table><thead><tr><th></th><th>First element</th><th>Last element</th></tr></thead><tbody><tr><td>List</td><td>list.get(0)</td><td>list.get(list.size() - 1)</td></tr><tr><td>Deque</td><td>deque.getFirst()</td><td>deque.getLast()</td></tr><tr><td>SortedSet</td><td>sortedSet.first()</td><td>sortedSet.last()</td></tr><tr><td>LinkedHashSet</td><td>linkedHashSet.iterator().next()</td><td>缺失</td></tr></tbody></table><p>于是在Java21提供了有序集合<code>SequencedCollection</code>、<code>SequencedSet</code>、<code>SequencedMap</code>：</p><pre><code class="java">interface SequencedCollection&lt;E&gt; extends Collection&lt;E&gt; {    // new method    SequencedCollection&lt;E&gt; reversed();    // methods promoted from Deque    void addFirst(E);    void addLast(E);    E getFirst();    E getLast();    E removeFirst();    E removeLast();}interface SequencedSet&lt;E&gt; extends Set&lt;E&gt;, SequencedCollection&lt;E&gt; {    SequencedSet&lt;E&gt; reversed();    // covariant override}interface SequencedMap&lt;K,V&gt; extends Map&lt;K,V&gt; {    // new methods    SequencedMap&lt;K,V&gt; reversed();    SequencedSet&lt;K&gt; sequencedKeySet();    SequencedCollection&lt;V&gt; sequencedValues();    SequencedSet&lt;Entry&lt;K,V&gt;&gt; sequencedEntrySet();    V putFirst(K, V);    V putLast(K, V);    // methods promoted from NavigableMap    Entry&lt;K, V&gt; firstEntry();    Entry&lt;K, V&gt; lastEntry();    Entry&lt;K, V&gt; pollFirstEntry();    Entry&lt;K, V&gt; pollLastEntry();}</code></pre><p><code>SequencedCollection</code>的<code>reversed()</code>方法提供了一个原始集合的反向视图。任何对原始集合的修改都会在视图中可见。如果允许，视图中的修改会写回到原始集合。</p><p><img src="https://static.howardliu.cn/SequencedCollectionDiagram20220216.png" alt="JEP 431: 有序集合（Sequenced Collections）"></p><p>我们看一个例子，假设我们有一个LinkedHashSet，现在我们想要获取它的反向视图并以反向顺序遍历它：</p><pre><code class="java">LinkedHashSet&lt;Integer&gt; linkedHashSet = new LinkedHashSet&lt;&gt;(Arrays.asList(3, 2, 1));// 获取反向视图SequencedCollection&lt;Integer&gt; reversed = linkedHashSet.reversed();// 反向遍历System.out.println(&quot;原始数据：&quot; + linkedHashSet);System.out.println(&quot;反转数据：&quot; + reversed);// 运行结果：// 原始数据：[3, 2, 1]// 反转数据：[1, 2, 3]</code></pre><p>这些方法都是便捷方法，内部数据结构没有变化，其实本质也是原来的用法。比如<code>ArrayList</code>中的<code>getFirst</code>和<code>getLast</code>方法：</p><pre><code class="java">/** * {@inheritDoc} * * @throws NoSuchElementException {@inheritDoc} * @since 21 */public E getFirst() {    if (size == 0) {        throw new NoSuchElementException();    } else {        return elementData(0);    }}/** * {@inheritDoc} * * @throws NoSuchElementException {@inheritDoc} * @since 21 */public E getLast() {    int last = size - 1;    if (last &lt; 0) {        throw new NoSuchElementException();    } else {        return elementData(last);    }}</code></pre><h2 id="JEP-439-分代ZGC（Generational-ZGC）"><a href="#JEP-439-分代ZGC（Generational-ZGC）" class="headerlink" title="JEP 439: 分代ZGC（Generational ZGC）"></a>JEP 439: 分代ZGC（Generational ZGC）</h2><p>ZGC从Java11开始预览，Java15提供生产支持，Java16增加并发线程堆栈处理，发展到Java21提供了分代ZGC，具体发展历程可以查看<a href="https://wiki.openjdk.org/display/zgc/Main" target="_blank" rel="noopener">https://wiki.openjdk.org/display/zgc/Main</a>。</p><p>江湖传说，ZGC（Z Garbage Collector）的Z表示最后一个GC，可见其雄心勃勃。而分代ZGC的出现，就是革自己的命。</p><p>与非分代ZGC相比，在不影响吞吐量的的情况下，带来更多的好处：</p><ul><li>更低的分配停顿风险；</li><li>更低的堆内存开销；</li><li>更低的CPU开销。</li></ul><p>分代ZGC的目标是：</p><ul><li>暂停时间不超过1ms（ZGC的目标是10ms）</li><li>支持从几M字节到几T字节的堆大小；</li><li>尽量少的配置。</li></ul><p>在Java21中，分代ZGC的实现细节主要包括以下几个方面：</p><ul><li>分代设计：基于「大部分对象朝生夕死」的分代假说，分代ZGC将内存划分为年轻代和老年代，并为这两种代分别维护不同的垃圾收集策略。这种设计可以更有效地处理不同生命周期的对象，从而提高整体性能。</li><li>彩色指针结构：分代ZGC使用彩色指针结构来优化内存访问。元数据被放在指针的低阶位，而对象地址被放在高阶位。这种结构可以减少加载屏障中的机器指令数量，从而提高性能。</li><li>写屏障技术：分代ZGC引入了写屏障技术，以确保在对象年龄更新时能够正确地通知垃圾收集器。这有助于减少垃圾收集过程中对应用程序的影响。</li><li>内存管理优化：通过分代收集，分代ZGC可以更频繁地对新生代进行垃圾收集，从而减少分配停顿的风险，降低内存开销，并减少垃圾收集的CPU开销。</li></ul><p>我们可以通过命令<code>-XX:+UseZGC -XX:+ZGenerational</code>使用分代ZGC。</p><p>为了平稳的过渡，在Java21中必须增加<code>-XX:+ZGenerational</code>参数，显性的指明使用分代ZGC，在未来，非分代ZGC将被移除，<code>ZGenerational</code>参数也就作废了。</p><p>我们看下<a href="https://kstefanj.github.io/2023/11/07/hazelcast-on-generational-zgc.html" target="_blank" rel="noopener">Hazelcast Jet on Generational ZGC</a>中给出的测评效果：</p><p><img src="https://static.howardliu.cn/p9999-event-latency.png" alt="JEP 439: 分代ZGC（Generational ZGC）"></p><p>从上图可以看到，非分代 ZGC 在低负载下表现非常好，但随着分配压力的增加，延迟也会增加。使用分代 ZGC 后，即使在高负载下，延迟也非常低，而且延迟效果优于G1。</p><h2 id="JEP-440-Record模式（Record-Patterns）"><a href="#JEP-440-Record模式（Record-Patterns）" class="headerlink" title="JEP 440: Record模式（Record Patterns）"></a>JEP 440: Record模式（Record Patterns）</h2><p>Record类型提供不可变对象的简单实现（其实就是Java Bean，但是省略一堆的getter、setter、hashcode、equals、toString等方法），Java16开始一直在演化增强（参见<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a>）。</p><p>Record模式归属于Amber项目的一部分，Amber项目旨在通过小而美的方式，增强Java语言特性。本次的Record模式，主要是使Record类型可以直接在instanceof和switch模式匹配中使用。</p><p>Record模式最初作为预览功能在JEP 405中提出，并在Java19中交付。JEP 432提出了第二次预览，基于持续的经验和反馈进行了进一步的完善。终于在JEP 440正式转正。</p><p>我们一起看个示例，比如有下面几个基础元素：</p><pre><code class="java">// 颜色enum Color { RED, GREEN, BLUE}// 点record Point(int x, int y) {}// 带颜色的点record ColoredPoint(Point p, Color color) {}// 正方形record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}</code></pre><p>我们分别通过instanceof模式匹配和switch模式匹配判断输入参数的类型，打印不同的格式：</p><pre><code class="java">private static void instancePatternsAndPrint(Object o) {    if (o instanceof Square(ColoredPoint upperLeft, ColoredPoint lowerRight)) {        System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);    } else if (o instanceof ColoredPoint(Point(int x, int y), Color color)) {        System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);    } else if (o instanceof Point p) {        System.out.println(&quot;Point类型：&quot; + p);    }}private static void switchPatternsAndPrint(Object o) {    switch (o) {        case Square(ColoredPoint upperLeft, ColoredPoint lowerRight) -&gt; {            System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);        }        case ColoredPoint(Point(int x, int y), Color color) -&gt; {            System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);        }        case Point p -&gt; {            System.out.println(&quot;Point类型：&quot; + p);        }        default -&gt; throw new IllegalStateException(&quot;Unexpected value: &quot; + o);    }}</code></pre><p>我们通过main方法执行下：</p><pre><code class="java">public static void main(String[] args) {    var p = new Point(1, 2);    var cp1 = new ColoredPoint(p, Color.RED);    var cp2 = new ColoredPoint(p, Color.GREEN);    var square = new Square(cp1, cp2);    instancePatternsAndPrint(square);    instancePatternsAndPrint(cp1);    instancePatternsAndPrint(p);    switchPatternsAndPrint(square);    switchPatternsAndPrint(cp1);    switchPatternsAndPrint(p);}// 结果是：//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]</code></pre><h2 id="JEP-441-switch模式匹配（Pattern-Matching-for-switch）"><a href="#JEP-441-switch模式匹配（Pattern-Matching-for-switch）" class="headerlink" title="JEP 441: switch模式匹配（Pattern Matching for switch）"></a>JEP 441: switch模式匹配（Pattern Matching for switch）</h2><p>switch模式经过四次预览，终于转正。</p><p>switch模式匹配是一个非常赞的功能，可以在选择器表达式使用基础判断和任意引用类型，包括instanceof操作符。这意味着可以更灵活地使用对象、数组、列表等复杂数据结构作为switch语句的基础，从而简化代码并提高可读性。</p><p>switch模式匹配允许在switch语句中使用模式来测试表达式，每个模式都有特定的动作，从而可以简洁、安全地表达复杂的数据导向查询。</p><p>通过代码看下switch模式匹配的魅力；</p><pre><code class="java">static String formatValue(Object obj) {    return switch (obj) {        case null -&gt; &quot;null&quot;;        case Integer i -&gt; String.format(&quot;int %d&quot;, i);        case Long l -&gt; String.format(&quot;long %d&quot;, l);        case Double d -&gt; String.format(&quot;double %f&quot;, d);        case String s -&gt; String.format(&quot;String %s&quot;, s);        case Person(String name, String address) -&gt; String.format(&quot;Person %s %s&quot;, name, address);        default -&gt; obj.toString();    };}public record Person(String name, String address) {}public static void main(String[] args) {    System.out.println(formatValue(10));    System.out.println(formatValue(20L));    System.out.println(formatValue(3.14));    System.out.println(formatValue(&quot;Hello&quot;));    System.out.println(formatValue(null));    System.out.println(formatValue(new Person(&quot;Howard&quot;, &quot;Beijing&quot;)));}// 运行结果// int 10// long 20// double 3.140000// String Hello// null// Person Howard Beijing</code></pre><h2 id="JEP-444-虚拟线程（Virtual-Threads）"><a href="#JEP-444-虚拟线程（Virtual-Threads）" class="headerlink" title="JEP 444: 虚拟线程（Virtual Threads）"></a>JEP 444: 虚拟线程（Virtual Threads）</h2><p>虚拟线程是一种轻量级的线程实现，旨在显著降低编写、维护和观察高吞吐量并发应用程序的难度。它们占用的资源少，不需要被池化，可以创建大量虚拟线程，特别适用于IO密集型任务，因为它们可以高效地调度大量虚拟线程来处理并发请求，从而显著提高程序的吞吐量和响应速度。</p><p>虚拟线程有下面几个特点：</p><ol><li>轻量级：虚拟线程是JVM内部实现的轻量级线程，不需要操作系统内核参与，创建和上下文切换的成本远低于传统的操作系统线程（即平台线程），且占用的内存资源较少。</li><li>减少CPU时间消耗：由于虚拟线程不依赖于操作系统平台线程，因此在进行线程切换时耗费的CPU时间会大大减少，从而提高了程序的执行效率。</li><li>简化多线程编程：虚拟线程通过结构化并发API来简化多线程编程，使得开发者可以更容易地编写、维护和观察高吞吐量并发应用程序。</li><li>适用于大量任务场景：虚拟线程非常适合需要创建和销毁大量线程的任务、需要执行大量计算的任务（如数据处理、科学计算等）以及需要实现任务并行执行以提高程序性能的场景。</li><li>提高系统吞吐量：通过对虚拟线程的介绍和与Go协程的对比，可以看出虚拟线程能够大幅提高系统的整体吞吐量。</li></ol><p>不考虑虚拟线程实现原理，对开发者而言，使用体验上与传统线程几乎没有区别。我们一起试用下。</p><pre><code class="java">try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {    IntStream.range(0, 10_000).forEach(i -&gt; {        executor.submit(() -&gt; {            Thread.sleep(Duration.ofSeconds(1));            System.out.println(Thread.currentThread().getName() + &quot;: &quot; + i);            return i;        });    });}Thread.startVirtualThread(() -&gt; {    System.out.println(&quot;Hello from a virtual thread[Thread.startVirtualThread]&quot;);});final ThreadFactory factory = Thread.ofVirtual().factory();factory.newThread(() -&gt; {            System.out.println(&quot;Hello from a virtual thread[ThreadFactory.newThread]&quot;);        })        .start();</code></pre><p>虚拟线程为了降低使用门槛，直接提供了与原生线程类似的方法：</p><ul><li><code>Executors.newVirtualThreadPerTaskExecutor()</code>，可以像普通线程池一样创建虚拟线程。</li><li><code>Thread.startVirtualThread</code>，通过工具方法直接创建并运行虚拟线程。</li><li><code>Thread.ofVirtual().factory().newThread()</code>，另一个工具方法可以创建并运行虚拟线程。<code>Thread</code>还有一个<code>ofPlatform()</code>方法，用来构建普通线程。</li></ul><p>通过本地简单测试（在「公众号：看山的小屋」回复”java”获取源码），1w个模拟线程运行时，性能方面虚拟线程 &gt; 线程池。</p><p>需要注意的是，虚拟线程适用于IO密集场景，而非CPU密集的场景。</p><h2 id="JEP-452-密钥封装机制-API（Key-Encapsulation-Mechanism-API-KEM-API）"><a href="#JEP-452-密钥封装机制-API（Key-Encapsulation-Mechanism-API-KEM-API）" class="headerlink" title="JEP 452: 密钥封装机制 API（Key Encapsulation Mechanism API, KEM API）"></a>JEP 452: 密钥封装机制 API（Key Encapsulation Mechanism API, KEM API）</h2><p>KEM是一种现代加密技术，它通过使用非对称或公钥密码学来保护对称密钥。与传统的加密方法不同，KEM使用公钥的属性来派生一个相关的对称密钥，这个过程不需要填充，因此可以更容易地证明其安全性。</p><p>KEM的工作流程</p><ol><li>密钥对生成函数：返回包含公钥和私钥的一对密钥。</li><li>密钥封装函数：由发送方调用，接受接收方的公钥和加密选项；返回一个秘密密钥K和一个密钥封装消息（在ISO 18033-2中称为ciphertext）。发送方将密钥封装消息发送给接收方。</li><li>密钥解封函数：由接收方调用，接受接收方的私钥和收到的密钥封装消息；返回秘密密钥K。</li></ol><p>据说这种算法是可以解决量子计算威胁，若有有安全需求，可以深入研究下。</p><h2 id="预览功能"><a href="#预览功能" class="headerlink" title="预览功能"></a>预览功能</h2><h3 id="JEP-430-字符串模板（String-Templates，预览）"><a href="#JEP-430-字符串模板（String-Templates，预览）" class="headerlink" title="JEP 430: 字符串模板（String Templates，预览）"></a>JEP 430: 字符串模板（String Templates，预览）</h3><p>字符串模板是一个值得期待的功能，Java21中提供了预览版。</p><p>字符串模板通过将文本和嵌入式表达式结合在一起，使得Java程序能够以一种更加直观和安全的方式构建字符串。与传统的字符串拼接（使用+操作符）、<code>StringBuilder</code>或<code>String.format</code> 等方法相比，字符串模板提供了一种更加清晰和安全的字符串构建方式。特别是当字符串需要从用户提供的值构建并传递给其他系统时（例如，构建数据库查询），使用字符串模板可以有效地验证和转换模板及其嵌入表达式的值，从而提高Java程序的安全性。</p><p>让我们通过代码看一下这个特性的魅力：</p><pre><code class="java">public static void main(String[] args) {    // 拼装变量    String name = &quot;看山&quot;;    String info = STR. &quot;My name is \{ name }&quot; ;    assert info.equals(&quot;My name is 看山&quot;);    // 拼装变量    String firstName = &quot;Howard&quot;;    String lastName = &quot;Liu&quot;;    String fullName = STR. &quot;\{ firstName } \{ lastName }&quot; ;    assert fullName.equals(&quot;Howard Liu&quot;);    String sortName = STR. &quot;\{ lastName }, \{ firstName }&quot; ;    assert sortName.equals(&quot;Liu, Howard&quot;);    // 模板中调用方法    String s2 = STR. &quot;You have a \{ getOfferType() } waiting for you!&quot; ;    assert s2.equals(&quot;You have a gift waiting for you!&quot;);    Request req = new Request(&quot;2017-07-19&quot;, &quot;09:15&quot;, &quot;https://www.howardliu.cn&quot;);    // 模板中引用对象属性    String s3 = STR. &quot;Access at \{ req.date } \{ req.time } from \{ req.address }&quot; ;    assert s3.equals(&quot;Access at 2017-07-19 09:15 from https://www.howardliu.cn&quot;);    LocalTime now = LocalTime.now();    String markTime = DateTimeFormatter            .ofPattern(&quot;HH:mm:ss&quot;)            .format(now);    // 模板中调用方法    String time = STR. &quot;The time is \{            // The java.time.format package is very useful            DateTimeFormatter                    .ofPattern(&quot;HH:mm:ss&quot;)                    .format(now)            } right now&quot; ;    assert time.equals(&quot;The time is &quot; + markTime + &quot; right now&quot;);    // 模板嵌套模板    String[] fruit = {&quot;apples&quot;, &quot;oranges&quot;, &quot;peaches&quot;};    String s4 = STR. &quot;\{ fruit[0] }, \{            STR. &quot;\{ fruit[1] }, \{ fruit[2] }&quot;            }&quot; ;    assert s4.equals(&quot;apples, oranges, peaches&quot;);    // 模板与文本块结合    String title = &quot;My Web Page&quot;;    String text = &quot;Hello, world&quot;;    String html = STR. &quot;&quot;&quot;    &lt;html&gt;      &lt;head&gt;        &lt;title&gt;\{ title }&lt;/title&gt;      &lt;/head&gt;      &lt;body&gt;        &lt;p&gt;\{ text }&lt;/p&gt;      &lt;/body&gt;    &lt;/html&gt;    &quot;&quot;&quot; ;    assert html.equals(&quot;&quot;&quot;            &lt;html&gt;              &lt;head&gt;                &lt;title&gt;My Web Page&lt;/title&gt;              &lt;/head&gt;              &lt;body&gt;                &lt;p&gt;Hello, world&lt;/p&gt;              &lt;/body&gt;            &lt;/html&gt;            &quot;&quot;&quot;);    // 带格式化的字符串模板    record Rectangle(String name, double width, double height) {        double area() {            return width * height;        }    }    Rectangle[] zone = new Rectangle[] {            new Rectangle(&quot;Alfa&quot;, 17.8, 31.4),            new Rectangle(&quot;Bravo&quot;, 9.6, 12.4),            new Rectangle(&quot;Charlie&quot;, 7.1, 11.23),    };    String table = FMT. &quot;&quot;&quot;        Description     Width    Height     Area        %-12s\{ zone[0].name }  %7.2f\{ zone[0].width }  %7.2f\{ zone[0].height }     %7.2f\{ zone[0].area() }        %-12s\{ zone[1].name }  %7.2f\{ zone[1].width }  %7.2f\{ zone[1].height }     %7.2f\{ zone[1].area() }        %-12s\{ zone[2].name }  %7.2f\{ zone[2].width }  %7.2f\{ zone[2].height }     %7.2f\{ zone[2].area() }        \{ &quot; &quot;.repeat(28) } Total %7.2f\{ zone[0].area() + zone[1].area() + zone[2].area() }        &quot;&quot;&quot;;    assert table.equals(&quot;&quot;&quot;            Description     Width    Height     Area            Alfa            17.80    31.40      558.92            Bravo            9.60    12.40      119.04            Charlie          7.10    11.23       79.73                                         Total  757.69            &quot;&quot;&quot;);}public static String getOfferType() {    return &quot;gift&quot;;}record Request(String date, String time, String address) {}</code></pre><p>这个功能当前是第一次预览，在Java22第二次预览，Java23的8.12版本中还没有展示字符串模板的第三次预览（JEP 465: String Templates），还不能确定什么时候可以正式用上。</p><h3 id="JEP-442-外部函数和内存API（Foreign-Function-amp-Memory-API，FFM-API，第三次预览）"><a href="#JEP-442-外部函数和内存API（Foreign-Function-amp-Memory-API，FFM-API，第三次预览）" class="headerlink" title="JEP 442: 外部函数和内存API（Foreign Function &amp; Memory API，FFM API，第三次预览）"></a>JEP 442: 外部函数和内存API（Foreign Function &amp; Memory API，FFM API，第三次预览）</h3><p>FFM API经历过多次的预览及改进，从JEP 412（第一轮孵化）开始，逐步发展到JEP 442（第三次预览）。相较于前两次的改进主要体现在以下几个方面：</p><ol><li>管理本地内存段的生命周期：在JEP 442中，通过新的Arena API集中管理本地内存段的生命周期，这使得内存管理更加高效和安全。</li><li>增强布局路径：引入了一个新的元素来解引用地址布局，这使得内存布局更加灵活和强大。</li><li>优化短暂使用期函数调用：提供了一个链接器选项，优化了对短暂使用期函数（例如clock_gettime）的调用，这些函数不会调用到Java代码。</li><li>本地链接器实现：引入了一个基于libffi的本地链接器实现，以便于移植。</li><li>移除VaList：在JEP 442中移除了VaList，这是对API的一次简化。</li></ol><p>我们看下官方示例：</p><pre><code class="java">// 1. 在C库路径上查找名为radixsort的外部函数Linker linker = Linker.nativeLinker();SymbolLookup stdlib = linker.defaultLookup();final MemorySegment memorySegment = stdlib.find(&quot;radixsort&quot;).orElseThrow();FunctionDescriptor descriptor = FunctionDescriptor.ofVoid(        ValueLayout.ADDRESS,        ValueLayout.JAVA_INT,        ValueLayout.ADDRESS);MethodHandle radixsort = linker.downcallHandle(memorySegment, descriptor);// 下面的代码将使用这个外部函数对字符串进行排序// 2. 分配栈上内存来存储四个字符串String[] javaStrings = {&quot;mouse&quot;, &quot;cat&quot;, &quot;dog&quot;, &quot;car&quot;};// 3. 使用try-with-resources来管理离堆内存的生命周期try (Arena offHeap = Arena.ofConfined()) {    // 4. 分配一段离堆内存来存储四个指针    MemorySegment pointers = offHeap.allocateArray(ValueLayout.ADDRESS, javaStrings.length);    // 5. 将字符串从栈上内存复制到离堆内存    for (int i = 0; i &lt; javaStrings.length; i++) {        MemorySegment cString = offHeap.allocateUtf8String(javaStrings[i]);        pointers.setAtIndex(ValueLayout.ADDRESS, i, cString);    }    // 6. 通过调用外部函数对离堆数据进行排序    radixsort.invoke(pointers, javaStrings.length, MemorySegment.NULL, &#39;\0&#39;);    // 7. 将排序后的字符串从离堆内存复制回栈上内存    for (int i = 0; i &lt; javaStrings.length; i++) {        MemorySegment cString = pointers.getAtIndex(ValueLayout.ADDRESS, i);        javaStrings[i] = cString.getUtf8String(0);    }} // 8. 所有离堆内存在此处被释放// 验证排序结果assert Arrays.equals(javaStrings, new String[] {&quot;car&quot;, &quot;cat&quot;, &quot;dog&quot;, &quot;mouse&quot;});  // true</code></pre><p>我们都知道，JNI也是可以调用外部代码的，那FFM API相较于JNI的优势在于：</p><ol><li>更安全的内存访问：FFM API 提供了一种更安全和受控的方式来与本地代码交互，避免了JNI中常见的内存泄漏和数据损坏问题。</li><li>直接访问本地内存：FFM API 允许Java程序直接访问本地内存（即Java堆外的内存），这使得数据处理更加高效和灵活。</li><li>跨语言函数调用：FFM API 支持调用Java程序的外部函数，以与外部代码和数据一起操作，而无需依赖JNI的复杂机制。</li><li>更高效的集成：FFM API 使得Java与C、C++等语言编写的库集成更加方便和高效，特别是在数据处理和机器学习等领域。</li><li>减少代码复杂性：FFM API 提供了一种更简洁的API，减少了JNI中复杂的代码编写和维护工作。</li><li>更广泛的适用性：FFM API 不仅适用于简单的函数调用，还可以处理复杂的内存管理任务，如堆外内存的管理。</li><li>提高性能：FFM API 通过高效地调用外部函数和安全地访问外部内存，提高了程序的运行效率。</li></ol><p>这个功能将在下一个版本 Java22 正式发布。</p><h3 id="JEP-443-未命名模式和变量（Unnamed-Patterns-and-Variables，预览）"><a href="#JEP-443-未命名模式和变量（Unnamed-Patterns-and-Variables，预览）" class="headerlink" title="JEP 443: 未命名模式和变量（Unnamed Patterns and Variables，预览）"></a>JEP 443: 未命名模式和变量（Unnamed Patterns and Variables，预览）</h3><p>该特性使用下划线字符 <code>_</code> 来表示未命名的模式和变量，从而简化代码并提高代码可读性和可维护性。</p><p>比如：</p><pre><code class="java">public static void main(String[] args) {    var _ = new Point(1, 2);}record Point(int x, int y) {}</code></pre><p>这个可以用在任何定义变量的地方，比如：</p><ul><li><code>... instanceof Point(_, int y)</code></li><li><code>r instanceof Point _</code></li><li><code>switch …… case Box(_)</code></li><li><code>for (Order _ : orders)</code></li><li><code>for (int i = 0, _ = sideEffect(); i &lt; 10; i++)</code></li><li><code>try { ... } catch (Exception _) { ... } catch (Throwable _) { ... }</code></li></ul><p>只要是这个不准备用，可以一律使用<code>_</code>代替。</p><h3 id="JEP-445-未命名类和示例main方法（Unnamed-Classes-and-Instance-Main-Methods，预览）"><a href="#JEP-445-未命名类和示例main方法（Unnamed-Classes-and-Instance-Main-Methods，预览）" class="headerlink" title="JEP 445: 未命名类和示例main方法（Unnamed Classes and Instance Main Methods，预览）"></a>JEP 445: 未命名类和示例main方法（Unnamed Classes and Instance Main Methods，预览）</h3><p>无论学习哪门语言，第一课一定是打印<code>Hello, World!</code>，Java中的写法是：</p><pre><code class="java">public class HelloWorld {    public static void main(String[] args) {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>如果是第一次接触，一定会有很多疑问，<code>public</code>干啥的，<code>main</code>方法的约定参数<code>args</code>是什么鬼？然后老师就说，这就是模板，照着抄就行，不这样写不运行。</p><p>JEP 445特性后，可以简化为：</p><pre><code class="java">class HelloWorld {    void main() {        System.out.println (&quot;Hello, World!&quot;);    }}</code></pre><p>我们还可以这样写：</p><pre><code class="java">String greeting() { return &quot;Hello, World!&quot;; }void main() {    System.out.println(greeting());}</code></pre><p>虽然看起来没啥用，但是在JShell中使用，就比较友好了。</p><h3 id="JEP-446-作用域值（Scoped-Values，预览）"><a href="#JEP-446-作用域值（Scoped-Values，预览）" class="headerlink" title="JEP 446: 作用域值（Scoped Values，预览）"></a>JEP 446: 作用域值（Scoped Values，预览）</h3><p>作用域值（Scoped Values）在Java20孵化，在Java21预览，旨在提供一种安全且高效的方法来共享数据，无需使用方法参数。这一特性允许在不使用方法参数的情况下，将数据安全地共享给方法，优先于线程局部变量，特别是在使用大量虚拟线程时。</p><p>在多线程环境中，作用域值可以在线程内和线程间共享不可变数据，例如从父线程向子线程传递数据，从而解决了在多线程应用中传递数据的问题。此外，作用域值提高了数据的安全性、不变性和封装性，并且在多线程环境中使用事务、安全主体和其他形式的共享上下文的应用程序中表现尤为突出。</p><p>作用域值的主要特点：</p><ul><li>不可变性：作用域值是不可变的，这意味着一旦设置，其值就不能更改。这种不可变性减少了并发编程中意外副作用的风险。</li><li>作用域生命周期：作用域值的生命周期仅限于 run 方法定义的作用域。一旦执行离开该作用域，作用域值将不再可访问。</li><li>继承性：子线程会自动继承父线程的作用域值，从而允许在线程边界间无缝共享数据。</li></ul><p>在这个功能之前，在多线程间传递数据，我们有两种选择：</p><ol><li>方法参数：显示参数传递；缺点是新增参数时修改联动修改一系列方法，如果是框架或SDK层面的，无法做到向下兼容。</li><li><code>ThreadLocal</code>：在<code>ThreadLocal</code>保存当前线程变量。</li></ol><p>使用过<code>ThreadLocal</code>的都清楚，<code>ThreadLocal</code>会有三大问题。</p><ol><li>无约束的可变性：每个线程局部变量都是可变的。任何可以调用线程局部变量的<code>get</code>方法的代码都可以随时调用该变量的<code>set</code>方法。即使线程局部变量中的对象是不可变的，每个字段都被声明为final，情况仍然如此。<code>ThreadLocal</code> API允许这样做，以便支持一个完全通用的通信模型，在该模型中，数据可以在方法之间以任何方向流动。这可能会导致数据流混乱，导致程序难以分辨哪个方法更新共享状态以及以何种顺序进行。</li><li>无界生存期：一旦通过<code>set</code>方法设置了一个线程局部变量的副本，该值就会在该线程的生存期内保留，或者直到该线程中的代码调用<code>remove</code>方法。我们有时候会忘记调用<code>remove</code>，如果使用线程池，在一个任务中设置的线程局部变量的值如果不清除，可能会意外泄漏到无关的任务中，导致危险的安全漏洞（比如人员SSO）。对于依赖于线程局部变量的无约束可变性的程序来说，可能没有明确的点可以保证线程调用<code>remove</code>是安全的，可能会导致内存泄漏，因为每个线程的数据在退出之前都不会被垃圾回收。</li><li>昂贵的继承：当使用大量线程时，线程局部变量的开销可能会更糟糕，因为父线程的线程局部变量可以被子线程继承。（事实上，线程局部变量并不是某个特定线程的本地变量。）当开发人员选择创建一个继承了线程局部变量的子线程时，该子线程必须为之前在父线程中写入的每个线程局部变量分配存储空间。这可能会显著增加内存占用。子线程不能共享父线程使用的存储，因为ThreadLocal API要求更改线程的线程局部变量副本在其他线程中不可见。这也会有另一个隐藏的问题，子线程没有办法向父线程<code>set</code>数据。</li></ol><p>作用域值可以有效解决上面提到的问题，而且写起来更加优雅。</p><p>我们一起看下作用域值的使用：</p><pre><code class="java">// 声明一个作用域值用于存储用户名public final static ScopedValue&lt;String&gt; USERNAME = ScopedValue.newInstance();private static final Runnable printUsername = () -&gt;        System.out.println(Thread.currentThread().threadId() + &quot; 用户名是 &quot; + USERNAME.get());public static void main(String[] args) throws Exception {    // 将用户名 &quot;Bob&quot; 绑定到作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Bob&quot;).run(() -&gt; {        printUsername.run();        new Thread(printUsername).start();    });    // 将用户名 &quot;Chris&quot; 绑定到另一个作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Chris&quot;).run(() -&gt; {        printUsername.run();        new Thread(() -&gt; {            new Thread(printUsername).start();            printUsername.run();        }).start();    });    // 检查在任何作用域外 USERNAME 是否被绑定    System.out.println(&quot;用户名是否被绑定: &quot; + USERNAME.isBound());}</code></pre><p>写起来干净利索，而且功能更强。</p><h3 id="JEP-453-结构化并发API（Structured-Concurrency，预览）"><a href="#JEP-453-结构化并发API（Structured-Concurrency，预览）" class="headerlink" title="JEP 453: 结构化并发API（Structured Concurrency，预览）"></a>JEP 453: 结构化并发API（Structured Concurrency，预览）</h3><p>JEP 453: 结构化并发API（Structured Concurrency API）旨在简化多线程编程，通过引入一个API来处理在不同线程中运行的多个任务作为一个单一工作单元，从而简化错误处理和取消操作，提高可靠性，并增强可观测性。本次发布是第一次预览。</p><p>结构化并发API提供了明确的语法结构来定义子任务的生命周期，并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。</p><p>Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时，如何被传递和处理的过程称为异常传播。</p><p>在结构化并发环境中，异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。</p><p>在Java并发编程中，非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理，这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。</p><pre><code class="java">try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {    var task1 = scope.fork(() -&gt; {        Thread.sleep(1000);        return &quot;Result from task 1&quot;;    });    var task2 = scope.fork(() -&gt; {        Thread.sleep(2000);        return &quot;Result from task 2&quot;;    });    scope.join();    scope.throwIfFailed(RuntimeException::new);    System.out.println(task1.get());    System.out.println(task2.get());} catch (Exception e) {    e.printStackTrace();}</code></pre><p>在这个例子中，handle()方法使用StructuredTaskScope来并行执行两个子任务：task1和task2。通过使用try-with-resources语句自动管理资源，并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关，提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性：</p><ul><li>错误处理与短路：如果task1或task2子任务中的任何一个失败，另一个如果尚未完成则会被取消。（这由 ShutdownOnFailure 实现的关闭策略来管理；还有其他策略可能）。</li><li>取消传播：如果在运行上面方法的线程在调用 join() 之前或之中被中断，则线程在退出作用域时会自动取消两个子任务。</li><li>清晰性：设置子任务，等待它们完成或被取消，然后决定是成功（并处理已经完成的子任务的结果）还是失败（子任务已经完成，因此没有更多需要清理的）。</li><li>可观察性：线程转储清楚地显示了任务层次结构，其中运行task1或task2的线程被显示为作用域的子任务。</li></ul><p>上面的示例能够很好的解决我们的一个痛点，有两个可并行的任务A和B，A+B才是完整结果，任何一个失败，另外一个也不需要成功，结构化并发API就可以很容易的实现这个逻辑。</p><h2 id="孵化功能"><a href="#孵化功能" class="headerlink" title="孵化功能"></a>孵化功能</h2><h3 id="JEP-448-向量API（Vector-API，第六次孵化）"><a href="#JEP-448-向量API（Vector-API，第六次孵化）" class="headerlink" title="JEP 448: 向量API（Vector API，第六次孵化）"></a>JEP 448: 向量API（Vector API，第六次孵化）</h3><p>向量API的功能是提供一个表达向量计算的API，旨在通过引入向量计算API来提高Java应用程序的性能。这一API允许开发者在支持的CPU架构上可靠地编译为最佳向量指令，从而实现比等效的标量计算更高的性能。这些计算在运行时可靠地编译成支持的CPU架构上的最优向量指令，从而实现比等效标量计算更优的性能。</p><p>下面这个是官方给的示例：</p><pre><code class="java">// 标量计算示例void scalarComputation(float[] a, float[] b, float[] c) {    for (int i = 0; i &lt; a.length ; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}// 使用向量API的向量计算示例static final VectorSpecies&lt;Float&gt; SPECIES = FloatVector.SPECIES_PREFERRED;void vectorComputation(float[] a, float[] b, float[] c) {    int i = 0;    int upperBound = SPECIES.loopBound(a.length);    for (; i &lt; upperBound; i += SPECIES.length()) {        // FloatVector va, vb, vc;        var va = FloatVector.fromArray(SPECIES, a, i);        var vb = FloatVector.fromArray(SPECIES, b, i);        var vc = va.mul(va).add(vb.mul(vb)).neg();        vc.intoArray(c, i);    }    for (; i &lt; a.length; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}</code></pre><p>向量API在Java中的独特优势在于其高效的并行计算能力、丰富的向量化指令集、跨平台的数据并行算法支持以及对机器学习的特别优化。</p><h2 id="弃用"><a href="#弃用" class="headerlink" title="弃用"></a>弃用</h2><h3 id="JEP-449-启用Windows32位-x86支持（Deprecate-the-Windows-32-bit-x86-Port-for-Removal）"><a href="#JEP-449-启用Windows32位-x86支持（Deprecate-the-Windows-32-bit-x86-Port-for-Removal）" class="headerlink" title="JEP 449: 启用Windows32位 x86支持（Deprecate the Windows 32-bit x86 Port for Removal）"></a>JEP 449: 启用Windows32位 x86支持（Deprecate the Windows 32-bit x86 Port for Removal）</h3><p>旨在弃用并最终移除Windows 32位x86平台上的Java支持，原因是该平台已经逐渐被淘汰、性能限制和安全问题等。主要影响对象是OpenJDK的开发者和Windows 32位x86平台上的Java用户。</p><h3 id="JEP-451-准备禁止动态加载代理（Prepare-to-Disallow-the-Dynamic-Loading-of-Agents）"><a href="#JEP-451-准备禁止动态加载代理（Prepare-to-Disallow-the-Dynamic-Loading-of-Agents）" class="headerlink" title="JEP 451: 准备禁止动态加载代理（Prepare to Disallow the Dynamic Loading of Agents）"></a>JEP 451: 准备禁止动态加载代理（Prepare to Disallow the Dynamic Loading of Agents）</h3><p>该特性主要目的是通过在运行中的JVM中动态加载代理时发出警告，来帮助用户为将来的版本做好准备。这些警告旨在提高JVM的默认完整性，因为未来的版本将默认禁止动态加载代理。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文介绍了 Java21 新增的特性，完整的特性清单可以从 <a href="https://openjdk.org/projects/jdk/21/" target="_blank" rel="noopener">https://openjdk.org/projects/jdk/21/</a> 查看。后续内容会发布在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/java/java-21-features">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java21 的新特性</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java21 的新特性</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      Java21 在 2023 年 9 月 19 日发布GA版本，Java21是长期支持版（LTS，Long-Term Support），共十五大特性。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java21" scheme="https://www.howardliu.cn/tags/Java21/"/>
    
  </entry>
  
  <entry>
    <title>Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java20 的新特性</title>
    <link href="https://www.howardliu.cn/java/java-20-features/"/>
    <id>https://www.howardliu.cn/java/java-20-features/</id>
    <published>2024-09-12T01:00:00.000Z</published>
    <updated>2024-12-05T03:07:46.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/woman-8388428_1920.jpg" alt="Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java20 的新特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>从 2017 年开始，Java 版本更新策略从原来的每两年一个新版本，改为每六个月一个新版本，以快速验证新特性，推动 Java 的发展。让我们跟随 Java 的脚步，配合示例讲解，看一看每个版本的新特性，本期是 Java20 的新特性。</p><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>Java20 在 2023 年 3 月 21 日发布GA版本，共七大特性，这些新特性处于预览或孵化阶段，旨在简化并发编程、提高性能和增强语言表达能力。这七大特性分别是：</p><ul><li>JEP 429: 作用域值（Scoped Values，孵化），来自Amber项目，允许在线程内和线程间共享不可变数据，优于传统的线程局部变量。它主要用于在多线程应用中安全地传递和访问数据，例如从父线程向子线程传递数据。</li><li>JEP 432: Record模式（Record Patterns，第二次预览），来自Amber项目，用于简化数据结构的模式匹配。</li><li>JEP 433: Switch模式匹配（Pattern Matching for switch，第四次预览），来自Amber项目，用于改进switch语句中的模式匹配。</li><li>JEP 434: 外部函数和内存API（Foreign Function &amp; Memory API，FFM API，第二次预览），来自Loom项目，旨在提供更高效的内存管理和外部函数调用能力。</li><li>JEP 436: 虚拟线程（Virtual Threads，第二次预览），来自Loom项目，通过引入轻量级虚拟线程来简化高吞吐量并发应用程序的开发和维护。</li><li>JEP 437: 结构化并发API (Structured Concurrency，第二次孵化)，来自Loom项目，旨在简化多线程编程，提高应用程序的可靠性和可观察性。</li><li>JEP 438: 向量API（Vector API，第五次孵化），用于表达向量计算，提高了性能和代码可读性。</li></ul><p>接下来我们一起看看这些特性。</p><h2 id="预览功能"><a href="#预览功能" class="headerlink" title="预览功能"></a>预览功能</h2><h3 id="JEP-432-Record模式（第二次预览）"><a href="#JEP-432-Record模式（第二次预览）" class="headerlink" title="JEP 432: Record模式（第二次预览）"></a>JEP 432: Record模式（第二次预览）</h3><p>Record类型提供不可变对象的简单实现（其实就是Java Bean，但是省略一堆的getter、setter、hashcode、equals、toString等方法），Java16开始一直在演化增强（参见<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a>）。</p><p>Record模式归属于Amber项目的一部分，Amber项目旨在通过小而美的方式，增强Java语言特性。本次的Record模式，主要是使Record类型可以直接在instanceof和switch模式匹配中使用。</p><p>Record模式最初作为预览功能在JEP 405中提出，并在Java19中交付。JEP 432提出了第二次预览，基于持续的经验和反馈进行了进一步的完善。主要变化包括：</p><ul><li>支持推断泛型记录模式的类型参数，</li><li>允许记录模式出现在增强for循环的头部，</li><li>移除了对命名记录模式的支持。</li></ul><p>我们一起看个示例，比如有下面几个基础元素：</p><pre><code class="java">// 颜色enum Color { RED, GREEN, BLUE}// 点record Point(int x, int y) {}// 带颜色的点record ColoredPoint(Point p, Color color) {}// 正方形record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}</code></pre><p>我们分别通过instanceof模式匹配和switch模式匹配判断输入参数的类型，打印不同的格式：</p><pre><code class="java">private static void instancePatternsAndPrint(Object o) {    if (o instanceof Square(ColoredPoint upperLeft, ColoredPoint lowerRight)) {        System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);    } else if (o instanceof ColoredPoint(Point(int x, int y), Color color)) {        System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);    } else if (o instanceof Point p) {        System.out.println(&quot;Point类型：&quot; + p);    }}private static void switchPatternsAndPrint(Object o) {    switch (o) {        case Square(ColoredPoint upperLeft, ColoredPoint lowerRight) -&gt; {            System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);        }        case ColoredPoint(Point(int x, int y), Color color) -&gt; {            System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);        }        case Point p -&gt; {            System.out.println(&quot;Point类型：&quot; + p);        }        default -&gt; throw new IllegalStateException(&quot;Unexpected value: &quot; + o);    }}</code></pre><p>我们通过main方法执行下：</p><pre><code class="java">public static void main(String[] args) {    var p = new Point(1, 2);    var cp1 = new ColoredPoint(p, Color.RED);    var cp2 = new ColoredPoint(p, Color.GREEN);    var square = new Square(cp1, cp2);    instancePatternsAndPrint(square);    instancePatternsAndPrint(cp1);    instancePatternsAndPrint(p);    switchPatternsAndPrint(square);    switchPatternsAndPrint(cp1);    switchPatternsAndPrint(p);}// 结果是：//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]</code></pre><h3 id="JEP-433-switch模式匹配（第四次预览）"><a href="#JEP-433-switch模式匹配（第四次预览）" class="headerlink" title="JEP 433: switch模式匹配（第四次预览）"></a>JEP 433: switch模式匹配（第四次预览）</h3><p>在Java20之前，switch模式匹配的选择器表达式只能是基本数据类型或字符串。在Java20中，这一限制被取消，允许选择器表达式为任何引用类型，包括instanceof操作符。这意味着可以更灵活地使用对象、数组、列表等复杂数据结构作为switch语句的基础，从而简化代码并提高可读性。</p><p>switch模式匹配允许在switch语句中使用模式来测试表达式，每个模式都有特定的动作，从而可以简洁、安全地表达复杂的数据导向查询。</p><p>主要功能包括：</p><ul><li>增强表达性和适用性：允许在case标签中出现模式，扩展了switch表达式和语句的表达能力和适用范围。</li><li>放宽对null的敌意：当需要时，可以放宽switch对null的敌意。</li><li>提高安全性：要求模式switch语句覆盖所有可能的输入值，增加了switch语句的安全性。</li><li>保持兼容性：确保所有现有的switch表达式和语句继续无变化地编译并以相同的语义执行。</li></ul><p>通过代码看下switch模式匹配的魅力；</p><pre><code class="java">static String formatValue(Object obj) {    return switch (obj) {        case null -&gt; &quot;null&quot;;        case Integer i -&gt; String.format(&quot;int %d&quot;, i);        case Long l -&gt; String.format(&quot;long %d&quot;, l);        case Double d -&gt; String.format(&quot;double %f&quot;, d);        case String s -&gt; String.format(&quot;String %s&quot;, s);        case Person(String name, String address) -&gt; String.format(&quot;Person %s %s&quot;, name, address);        default -&gt; obj.toString();    };}public record Person(String name, String address) {}public static void main(String[] args) {    System.out.println(formatValue(10));    System.out.println(formatValue(20L));    System.out.println(formatValue(3.14));    System.out.println(formatValue(&quot;Hello&quot;));    System.out.println(formatValue(null));    System.out.println(formatValue(new Person(&quot;Howard&quot;, &quot;Beijing&quot;)));}// 运行结果// int 10// long 20// double 3.140000// String Hello// null// Person Howard Beijing</code></pre><h3 id="JEP-434-外部函数和内存API（第二次预览）"><a href="#JEP-434-外部函数和内存API（第二次预览）" class="headerlink" title="JEP 434: 外部函数和内存API（第二次预览）"></a>JEP 434: 外部函数和内存API（第二次预览）</h3><p>外部函数和内存API（Foreign Function &amp; Memory API，简称FFM API）旨在提供一种机制，使Java程序能够与Java运行时之外的代码和数据进行互操作。通过高效地调用外部函数（即JVM之外的代码）以及安全地访问外部内存（即非JVM管理的内存），该API使得Java程序能够调用本地库并处理本地数据，而无需使用JNI带来的脆弱性和危险。</p><p>简单说，FFM API是为了替换JNI的函数，在Java20中是第二次预览版，最终会在Java22中转正。</p><p>FFM API的主要功能包括：</p><ul><li>易用性：用一个更优越、完全Java开发模型替代了Java原生接口（JNI）。</li><li>性能：提供与现有API（如JNI和sun.misc.Unsafe ）相当甚至更好的性能。</li><li>通用性：支持操作不同类型的外部内存（例如，本地内存、持久内存和托管堆内存），并计划随着时间的推移适应其他平台（例如，32位x86）和用C语言之外的语言（例如C++、Fortran）编写的外部函数。</li><li>安全性：允许程序在默认情况下对外部内存执行不安全的操作，但会向用户发出警告。</li></ul><p>我们来看一个官方给的示例：如何获取C库函数<code>radixsort</code>的方法句柄，并使用它来对四个字符串进行排序。</p><pre><code class="java">// 1. 找到C库路径上的外部函数Linker linker = Linker.nativeLinker();SymbolLookup stdlib = linker.defaultLookup();MethodHandle radixSort = linker.downcallHandle(stdlib.lookup (&quot;radixsort&quot;), ...);// 2. 在堆上分配内存存储四个字符串String[] javaStrings = { &quot;mouse&quot;, &quot;cat&quot;, &quot;dog&quot;, &quot;car&quot; };// 3. 在非堆上分配内存存储四个指针SegmentAllocator allocator = SegmentAllocator.implicitAllocator();MemorySegment offHeap = allocator.allocateArray(ValueLayout.ADDRESS, javaStrings.length );// 4. 将字符串从堆上复制到非堆上for (int i = 0; i &lt; javaStrings.length ; i++) {    // 分配一个字符串到非堆上，然后存储其指针    MemorySegment cString = allocator.allocateUtf8String(javaStrings[i]);    offHeap.setAtIndex(ValueLayout.ADDRESS, i, cString);}// 5. 调用外部函数对非堆上的数据进行排序radixSort.invoke (offHeap, javaStrings.length , MemoryAddress.NULL, &#39;\0&#39;);// 6. 将（重新排序的）字符串从非堆上复制回堆上for (int i = 0; i &lt; javaStrings.length ; i++) {    MemoryAddress cStringPtr = offHeap.getAtIndex(ValueLayout.ADDRESS, i);    javaStrings[i] = cStringPtr.getUtf8String(0);}assert Arrays.equals (javaStrings, new String[] {&quot;car&quot;, &quot;cat&quot;, &quot;dog&quot;, &quot;mouse&quot;}); // true</code></pre><blockquote><p>友情提示：官方的代码仅是示意，很多API还在调整，只有等到正式发行时才能暂时确定下来。</p></blockquote><h3 id="JEP-436-虚拟线程（第二次预览）"><a href="#JEP-436-虚拟线程（第二次预览）" class="headerlink" title="JEP 436: 虚拟线程（第二次预览）"></a>JEP 436: 虚拟线程（第二次预览）</h3><p>虚拟线程是一种轻量级的线程实现，旨在显著降低编写、维护和观察高吞吐量并发应用程序的难度。它们占用的资源少，不需要被池化，可以创建大量虚拟线程，特别适用于IO密集型任务，因为它们可以高效地调度大量虚拟线程来处理并发请求，从而显著提高程序的吞吐量和响应速度。</p><p>相比于传统现成，虚拟线程性能显著提升。</p><p>虚拟线程在Java19中第一次预览，在Java20第二次预览，本次预览没有引入新的API，做了一些最终化处理，为Java21正式发布做准备。</p><p>虚拟线程有下面几个特点：</p><ol><li>轻量级：虚拟线程是JVM内部实现的轻量级线程，不需要操作系统内核参与，创建和上下文切换的成本远低于传统的操作系统线程（即平台线程），且占用的内存资源较少。</li><li>减少CPU时间消耗：由于虚拟线程不依赖于操作系统平台线程，因此在进行线程切换时耗费的CPU时间会大大减少，从而提高了程序的执行效率。</li><li>简化多线程编程：虚拟线程通过结构化并发API来简化多线程编程，使得开发者可以更容易地编写、维护和观察高吞吐量并发应用程序。</li><li>适用于大量任务场景：虚拟线程非常适合需要创建和销毁大量线程的任务、需要执行大量计算的任务（如数据处理、科学计算等）以及需要实现任务并行执行以提高程序性能的场景。</li><li>提高系统吞吐量：通过对虚拟线程的介绍和与Go协程的对比，可以看出虚拟线程能够大幅提高系统的整体吞吐量。</li></ol><p>虽然虚拟线程是预览功能，不妨碍我们一起试用下。</p><pre><code class="java">try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {    IntStream.range(0, 10_000).forEach(i -&gt; {        executor.submit(() -&gt; {            Thread.sleep(Duration.ofSeconds(1));            System.out.println(Thread.currentThread().getName() + &quot;: &quot; + i);            return i;        });    });}Thread.startVirtualThread(() -&gt; {    System.out.println(&quot;Hello from a virtual thread[Thread.startVirtualThread]&quot;);});final ThreadFactory factory = Thread.ofVirtual().factory();factory.newThread(() -&gt; {            System.out.println(&quot;Hello from a virtual thread[ThreadFactory.newThread]&quot;);        })        .start();</code></pre><p>虚拟线程为了降低使用门槛，直接提供了与原生线程类似的方法：</p><ul><li><code>Executors.newVirtualThreadPerTaskExecutor()</code>，可以像普通线程池一样创建虚拟线程。</li><li><code>Thread.startVirtualThread</code>，通过工具方法直接创建并运行虚拟线程。</li><li><code>Thread.ofVirtual().factory().newThread()</code>，另一个工具方法可以创建并运行虚拟线程。<code>Thread</code>还有一个<code>ofPlatform()</code>方法，用来构建普通线程。</li></ul><p>通过本地简单测试（在「公众号：看山的小屋」回复”java”获取源码），1w个模拟线程运行时，性能方面虚拟线程 &gt; 线程池。</p><h2 id="孵化功能"><a href="#孵化功能" class="headerlink" title="孵化功能"></a>孵化功能</h2><h3 id="JEP-429-作用域值（孵化）"><a href="#JEP-429-作用域值（孵化）" class="headerlink" title="JEP 429: 作用域值（孵化）"></a>JEP 429: 作用域值（孵化）</h3><p>JEP 429: 作用域值（Scoped Values），旨在促进在线程内和线程间共享不可变数据。这一特性为现代 Java 应用程序提供了一种更高效和安全的替代传统 ThreadLocal 机制的方法，尤其是在并发编程的背景下。</p><p>作用域值允许开发者定义一个变量，该变量可以在特定的作用域内访问，包括当前线程及其创建的任何子线程。这一机制特别适用于在方法调用链中隐式传递数据，而无需在方法签名中添加额外的参数。</p><p>作用域值的主要特点：</p><ul><li>不可变性：作用域值是不可变的，这意味着一旦设置，其值就不能更改。这种不可变性减少了并发编程中意外副作用的风险。</li><li>作用域生命周期：作用域值的生命周期仅限于 run 方法定义的作用域。一旦执行离开该作用域，作用域值将不再可访问。</li><li>继承性：子线程会自动继承父线程的作用域值，从而允许在线程边界间无缝共享数据。</li></ul><p>在之前，在多线程间传递数据，我们会使用<code>ThreadLocal</code>来保存当前线程变量，用完需要手动清理，如果忘记清理或者使用不规范，可能导致内存泄漏等问题。作用域值通过自动管理生命周期和内存，减少了这种风险。</p><p>我们一起看下作用域值的使用：</p><pre><code class="java">// 声明一个作用域值用于存储用户名public final static ScopedValue&lt;String&gt; USERNAME = ScopedValue.newInstance();private static final Runnable printUsername = () -&gt;        System.out.println(Thread.currentThread().threadId() + &quot; 用户名是 &quot; + USERNAME.get());public static void main(String[] args) throws Exception {    // 将用户名 &quot;Bob&quot; 绑定到作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Bob&quot;).run(() -&gt; {        printUsername.run();        new Thread(printUsername).start();    });    // 将用户名 &quot;Chris&quot; 绑定到另一个作用域并执行 Runnable    ScopedValue.where(USERNAME, &quot;Chris&quot;).run(() -&gt; {        printUsername.run();        new Thread(() -&gt; {            new Thread(printUsername).start();            printUsername.run();        }).start();    });    // 检查在任何作用域外 USERNAME 是否被绑定    System.out.println(&quot;用户名是否被绑定: &quot; + USERNAME.isBound());}</code></pre><p>写起来干净利索，而且功能更强。</p><h3 id="JEP-437-结构化并发API-第二次孵化"><a href="#JEP-437-结构化并发API-第二次孵化" class="headerlink" title="JEP 437: 结构化并发API (第二次孵化)"></a>JEP 437: 结构化并发API (第二次孵化)</h3><p>JEP 437: 结构化并发API（Structured Concurrency API）旨在简化多线程编程，通过引入一个API来处理在不同线程中运行的多个任务作为一个单一工作单元，从而简化错误处理和取消操作，提高可靠性，并增强可观测性。这是一个处于孵化阶段的API。</p><p>结构化并发API提供了明确的语法结构来定义子任务的生命周期，并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。</p><p>Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时，如何被传递和处理的过程称为异常传播。</p><p>在结构化并发环境中，异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。</p><p>在Java并发编程中，非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理，这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。</p><pre><code class="java">try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {    var task1 = scope.fork(() -&gt; {        Thread.sleep(1000);        return &quot;Result from task 1&quot;;    });    var task2 = scope.fork(() -&gt; {        Thread.sleep(2000);        return &quot;Result from task 2&quot;;    });    scope.join();    scope.throwIfFailed(RuntimeException::new);    System.out.println(task1.get());    System.out.println(task2.get());} catch (Exception e) {    e.printStackTrace();}</code></pre><p>在这个例子中，handle()方法使用StructuredTaskScope来并行执行两个子任务：task1和task2。通过使用try-with-resources语句自动管理资源，并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关，提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性：</p><ul><li>错误处理与短路：如果task1或task2子任务中的任何一个失败，另一个如果尚未完成则会被取消。（这由 ShutdownOnFailure 实现的关闭策略来管理；还有其他策略可能）。</li><li>取消传播：如果在运行上面方法的线程在调用 join() 之前或之中被中断，则线程在退出作用域时会自动取消两个子任务。</li><li>清晰性：设置子任务，等待它们完成或被取消，然后决定是成功（并处理已经完成的子任务的结果）还是失败（子任务已经完成，因此没有更多需要清理的）。</li><li>可观察性：线程转储清楚地显示了任务层次结构，其中运行task1或task2的线程被显示为作用域的子任务。</li></ul><p>上面的示例能够很好的解决我们的一个痛点，有两个可并行的任务A和B，A+B才是完整结果，任何一个失败，另外一个也不需要成功，结构化并发API就可以很容易的实现这个逻辑。</p><h3 id="JEP-438-向量API（第五次孵化）"><a href="#JEP-438-向量API（第五次孵化）" class="headerlink" title="JEP 438: 向量API（第五次孵化）"></a>JEP 438: 向量API（第五次孵化）</h3><p>向量API的功能是提供一个表达向量计算的API，这些计算在运行时可靠地编译成支持的CPU架构上的最优向量指令，从而实现比等效标量计算更优的性能。</p><p>下面这个是官方给的示例：</p><pre><code class="java">// 标量计算示例void scalarComputation(float[] a, float[] b, float[] c) {    for (int i = 0; i &lt; a.length ; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}// 使用向量API的向量计算示例static final VectorSpecies&lt;Float&gt; SPECIES = FloatVector.SPECIES_PREFERRED;void vectorComputation(float[] a, float[] b, float[] c) {    int i = 0;    int upperBound = SPECIES.loopBound(a.length);    for (; i &lt; upperBound; i += SPECIES.length()) {        // FloatVector va, vb, vc;        var va = FloatVector.fromArray(SPECIES, a, i);        var vb = FloatVector.fromArray(SPECIES, b, i);        var vc = va.mul(va).add(vb.mul(vb)).neg();        vc.intoArray(c, i);    }    for (; i &lt; a.length; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}</code></pre><p>向量API在Java中的独特优势在于其高效的并行计算能力、丰富的向量化指令集、跨平台的数据并行算法支持以及对机器学习的特别优化。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文介绍了 Java20 新增的特性，完整的特性清单可以从 <a href="https://openjdk.org/projects/jdk/20/" target="_blank" rel="noopener">https://openjdk.org/projects/jdk/20/</a> 查看。后续内容会发布在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/java/java-20-features">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java20 的新特性</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java20 的新特性</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      Java20 在 2023 年 3 月 21 日发布GA版本，共七大特性，这些新特性处于预览或孵化阶段，旨在简化并发编程、提高性能和增强语言表达能力。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java20" scheme="https://www.howardliu.cn/tags/Java20/"/>
    
  </entry>
  
  <entry>
    <title>Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java19 的新特性</title>
    <link href="https://www.howardliu.cn/java/java-19-features/"/>
    <id>https://www.howardliu.cn/java/java-19-features/</id>
    <published>2024-09-11T00:20:00.000Z</published>
    <updated>2024-12-05T03:03:18.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/bird-8936789_1920.jpg" alt="Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java19 的新特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>从 2017 年开始，Java 版本更新策略从原来的每两年一个新版本，改为每六个月一个新版本，以快速验证新特性，推动 Java 的发展。让我们跟随 Java 的脚步，配合示例讲解，看一看每个版本的新特性，本期是 Java19 的新特性。</p><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>Java19 在 2022 年 9 月 20 日发布GA版本，共七大特性，这些新特性大多处于预览或孵化阶段，旨在简化并发编程、提高性能和增强语言表达能力。这七大特性分别是：</p><ul><li>JEP 405: Record 模式（预览）</li><li>JEP 422: Linux/RISC-V移植</li><li>JEP 424: 外部函数和内存API（预览）</li><li>JEP 425: 虚拟线程（预览）</li><li>JEP 426: 向量API（第四次孵化）</li><li>JEP 427: Switch模式匹配（第三次预览）</li><li>JEP 428: 结构化并发API (孵化)</li></ul><p>接下来我们一起看看这些特性。</p><h2 id="JEP-422-Linux-RISC-V-移植"><a href="#JEP-422-Linux-RISC-V-移植" class="headerlink" title="JEP 422: Linux/RISC-V 移植"></a>JEP 422: Linux/RISC-V 移植</h2><p>JEP 422 是将 Java 开发环境（JDK）移植到基于 RISC-V 指令集架构的 Linux 系统上。</p><p>RISC-V 是一种开源且无版税的指令集架构，最初由加州大学伯克利分校设计，并由 RISC-V 国际基金会共同开发和维护。</p><p>该移植项目的目标包括以下几个方面：</p><ul><li>支持多种子系统：移植版本将支持模板解释器、C1 和 C2 JIT 编译器以及所有当前的主要垃圾回收器，如 ZGC 和 Shenandoah。</li><li>硬件配置支持：目前仅支持 RV64GV 配置，这是一种通用的 64 位 ISA，包含向量指令。未来可能会考虑支持其他 RISC-V 配置，例如 RV32G 的一般用途 32 位配置。</li><li>集成到 JDK 主线代码库中：重点在于将移植的内容集成到 JDK 的主仓库中，确保其能够在 RISC-V 架构上正常运行。</li></ul><p>此外，RISC-V 架构具有模块化设计，采用精简、可靠且支持多平台的优点，这使得它在嵌入式系统和高性能计算领域有广泛的应用前景。</p><p>通过此次移植，Java 将能够更好地适应这些新兴的硬件平台，从而扩展其生态系统并提高其在不同硬件架构上的兼容性和可用性，增强 Java 在嵌入式系统和高性能计算等领域的应用能力.</p><h2 id="预览功能"><a href="#预览功能" class="headerlink" title="预览功能"></a>预览功能</h2><h3 id="JEP-405-Record-模式（预览）"><a href="#JEP-405-Record-模式（预览）" class="headerlink" title="JEP 405: Record 模式（预览）"></a>JEP 405: Record 模式（预览）</h3><p>Record是Java16正式发布的基础类型，提供不可变对象的简单实现（其实就是Java Bean，但是省略一堆的getter、setter、hashcode、equals、toString等方法）（参见<a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a>）。</p><p>JEP 405: Record模式归属于Amber项目的一部分，Amber项目旨在通过小而美的方式，增强Java语言特性。本次的Record模式，主要是使Record类型可以直接在instanceof和switch模式匹配中使用。主要体现在4个方面：</p><ol><li>解构记录值：Record模式允许用户对Record的值进行解构，这意味着可以将一个Record类型的实例分解成其各个组成元素。</li><li>嵌套Record模式和instanceof模式：Record模式可以与instanceof模式匹配嵌套使用，从而创建强大、声明性和可组合的数据导航和处理形式。这种嵌套使得数据查询和处理更加灵活和复杂。</li><li>语法和代码生成的变化：为了支持Record模式，Java19需要从语法到代码生成以及DOM模型进行大量的修改。这表明JEP 405不仅在功能上带来了新的特性，还在语言内部结构上进行了深入的调整。</li><li>目标和用途：JEP 405的目标是扩展模式匹配的能力，实现更复杂的数据查询和处理。通过引入Record模式，开发者可以以一种更加声明性和可组合的方式进行数据导航和处理。</li></ol><p>我们一起看个示例，比如有下面几个基础元素：</p><pre><code class="java">// 颜色enum Color { RED, GREEN, BLUE}// 点record Point(int x, int y) {}// 带颜色的点record ColoredPoint(Point p, Color color) {}// 正方形record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}</code></pre><p>我们分别通过instanceof模式匹配和switch模式匹配判断输入参数的类型，打印不同的格式：</p><pre><code class="java">private static void instancePatternsAndPrint(Object o) {    if (o instanceof Square(ColoredPoint upperLeft, ColoredPoint lowerRight)) {        System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);    } else if (o instanceof ColoredPoint(Point(int x, int y), Color color)) {        System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);    } else if (o instanceof Point p) {        System.out.println(&quot;Point类型：&quot; + p);    }}private static void switchPatternsAndPrint(Object o) {    switch (o) {        case Square(ColoredPoint upperLeft, ColoredPoint lowerRight) -&gt; {            System.out.println(&quot;Square类型：&quot; + upperLeft + &quot; &quot; + lowerRight);        }        case ColoredPoint(Point(int x, int y), Color color) -&gt; {            System.out.println(&quot;ColoredPoint类型：&quot; + x + &quot; &quot; + y + &quot; &quot; + color);        }        case Point p -&gt; {            System.out.println(&quot;Point类型：&quot; + p);        }        default -&gt; throw new IllegalStateException(&quot;Unexpected value: &quot; + o);    }}</code></pre><p>我们通过main方法执行下：</p><pre><code class="java">public static void main(String[] args) {    var p = new Point(1, 2);    var cp1 = new ColoredPoint(p, Color.RED);    var cp2 = new ColoredPoint(p, Color.GREEN);    var square = new Square(cp1, cp2);    instancePatternsAndPrint(square);    instancePatternsAndPrint(cp1);    instancePatternsAndPrint(p);    switchPatternsAndPrint(square);    switchPatternsAndPrint(cp1);    switchPatternsAndPrint(p);}// 结果是：//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]//// Square类型：ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]// ColoredPoint类型：1 2 RED// Point类型：Point[x=1, y=2]</code></pre><p>是不是很简洁，就像Java8提供的Lambda表达式一样，很丝滑。</p><h3 id="JEP-424-外部函数和内存API（预览）"><a href="#JEP-424-外部函数和内存API（预览）" class="headerlink" title="JEP 424: 外部函数和内存API（预览）"></a>JEP 424: 外部函数和内存API（预览）</h3><p>JEP 424: 外部函数和内存API（Foreign Function &amp; Memory API，简称FFM API）旨在提供一种机制，使Java程序能够与Java运行时之外的代码和数据进行互操作。通过高效地调用外部函数（即JVM之外的代码）以及安全地访问外部内存（即非JVM管理的内存），该API使得Java程序能够调用本地库并处理本地数据，而无需使用JNI带来的脆弱性和危险。</p><p>简单说，FFM API是为了替换JNI的函数，在Java19中是第一次预览版，最终会在Java22中转正。</p><p>FFM API的主要功能包括：</p><ul><li>易用性：用一个更优越、完全Java开发模型替代了Java原生接口（JNI）。</li><li>性能：提供与现有API（如JNI和sun.misc.Unsafe ）相当甚至更好的性能。</li><li>通用性：支持操作不同类型的外部内存（例如，本地内存、持久内存和托管堆内存），并计划随着时间的推移适应其他平台（例如，32位x86）和用C语言之外的语言（例如C++、Fortran）编写的外部函数。</li><li>安全性：允许程序在默认情况下对外部内存执行不安全的操作，但会向用户发出警告。</li></ul><p>我们来看一个官方给的示例：如何获取C库函数<code>radixsort</code>的方法句柄，并使用它来对四个字符串进行排序。</p><pre><code class="java">// 1. 找到C库路径上的外部函数Linker linker = Linker.nativeLinker();SymbolLookup stdlib = linker.defaultLookup();MethodHandle radixSort = linker.downcallHandle(stdlib.lookup (&quot;radixsort&quot;), ...);// 2. 在堆上分配内存存储四个字符串String[] javaStrings = { &quot;mouse&quot;, &quot;cat&quot;, &quot;dog&quot;, &quot;car&quot; };// 3. 在非堆上分配内存存储四个指针SegmentAllocator allocator = SegmentAllocator.implicitAllocator();MemorySegment offHeap = allocator.allocateArray(ValueLayout.ADDRESS, javaStrings.length );// 4. 将字符串从堆上复制到非堆上for (int i = 0; i &lt; javaStrings.length ; i++) {    // 分配一个字符串到非堆上，然后存储其指针    MemorySegment cString = allocator.allocateUtf8String(javaStrings[i]);    offHeap.setAtIndex(ValueLayout.ADDRESS, i, cString);}// 5. 调用外部函数对非堆上的数据进行排序radixSort.invoke (offHeap, javaStrings.length , MemoryAddress.NULL, &#39;\0&#39;);// 6. 将（重新排序的）字符串从非堆上复制回堆上for (int i = 0; i &lt; javaStrings.length ; i++) {    MemoryAddress cStringPtr = offHeap.getAtIndex(ValueLayout.ADDRESS, i);    javaStrings[i] = cStringPtr.getUtf8String(0);}assert Arrays.equals (javaStrings, new String[] {&quot;car&quot;, &quot;cat&quot;, &quot;dog&quot;, &quot;mouse&quot;}); // true</code></pre><blockquote><p>友情提示：官方的代码仅是示意，很多API还在调整，只有等到正式发行时才能暂时确定下来。</p></blockquote><h3 id="JEP-425-虚拟线程（预览）"><a href="#JEP-425-虚拟线程（预览）" class="headerlink" title="JEP 425: 虚拟线程（预览）"></a>JEP 425: 虚拟线程（预览）</h3><p>虚拟线程是一种轻量级的线程实现，旨在显著降低编写、维护和观察高吞吐量并发应用程序的难度。它们占用的资源少，不需要被池化，可以创建大量虚拟线程，特别适用于IO密集型任务，因为它们可以高效地调度大量虚拟线程来处理并发请求，从而显著提高程序的吞吐量和响应速度。</p><p>相比于传统现成，虚拟线程性能显著提升。</p><p>虚拟线程有下面几个特点：</p><ol><li>轻量级：虚拟线程是JVM内部实现的轻量级线程，不需要操作系统内核参与，创建和上下文切换的成本远低于传统的操作系统线程（即平台线程），且占用的内存资源较少。</li><li>减少CPU时间消耗：由于虚拟线程不依赖于操作系统平台线程，因此在进行线程切换时耗费的CPU时间会大大减少，从而提高了程序的执行效率。</li><li>简化多线程编程：虚拟线程通过结构化并发API来简化多线程编程，使得开发者可以更容易地编写、维护和观察高吞吐量并发应用程序。</li><li>适用于大量任务场景：虚拟线程非常适合需要创建和销毁大量线程的任务、需要执行大量计算的任务（如数据处理、科学计算等）以及需要实现任务并行执行以提高程序性能的场景。</li><li>提高系统吞吐量：通过对虚拟线程的介绍和与Go协程的对比，可以看出虚拟线程能够大幅提高系统的整体吞吐量。</li></ol><p>虽然虚拟线程在Java19中是预览功能，不妨碍我们一起试用下。</p><pre><code class="java">try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {    IntStream.range(0, 10_000).forEach(i -&gt; {        executor.submit(() -&gt; {            Thread.sleep(Duration.ofSeconds(1));            System.out.println(Thread.currentThread().getName() + &quot;: &quot; + i);            return i;        });    });}Thread.startVirtualThread(() -&gt; {    System.out.println(&quot;Hello from a virtual thread[Thread.startVirtualThread]&quot;);});final ThreadFactory factory = Thread.ofVirtual().factory();factory.newThread(() -&gt; {            System.out.println(&quot;Hello from a virtual thread[ThreadFactory.newThread]&quot;);        })        .start();</code></pre><p>虚拟线程为了降低使用门槛，直接提供了与原生线程类似的方法：</p><ul><li><code>Executors.newVirtualThreadPerTaskExecutor()</code>，可以像普通线程池一样创建虚拟线程。</li><li><code>Thread.startVirtualThread</code>，通过工具方法直接创建并运行虚拟线程。</li><li><code>Thread.ofVirtual().factory().newThread()</code>，另一个工具方法可以创建并运行虚拟线程。<code>Thread</code>还有一个<code>ofPlatform()</code>方法，用来构建普通线程。</li></ul><p>通过本地简单测试（在「公众号：看山的小屋」回复”java”获取源码），1w个模拟线程运行时，性能方面虚拟线程 &gt; 线程池。</p><h3 id="JEP-427-Switch模式匹配（第三次预览）"><a href="#JEP-427-Switch模式匹配（第三次预览）" class="headerlink" title="JEP 427: Switch模式匹配（第三次预览）"></a>JEP 427: Switch模式匹配（第三次预览）</h3><p>switch模式匹配是老朋友了，最终会在Java21转正。</p><p>switch模式匹配允许在switch语句中使用模式来测试表达式，每个模式都有特定的动作，从而可以简洁、安全地表达复杂的数据导向查询。</p><p>主要功能包括：</p><ul><li>增强表达性和适用性：允许在case标签中出现模式，扩展了switch表达式和语句的表达能力和适用范围。</li><li>放宽对null的敌意：当需要时，可以放宽switch对null的敌意。</li><li>提高安全性：要求模式switch语句覆盖所有可能的输入值，增加了switch语句的安全性。</li><li>保持兼容性：确保所有现有的switch表达式和语句继续无变化地编译并以相同的语义执行。</li></ul><pre><code class="java">static String formatValue(Object obj) {    return switch (obj) {        case null -&gt; &quot;null&quot;;        case Integer i -&gt; String.format(&quot;int %d&quot;, i);        case Long l -&gt; String.format(&quot;long %d&quot;, l);        case Double d -&gt; String.format(&quot;double %f&quot;, d);        case String s -&gt; String.format(&quot;String %s&quot;, s);        default -&gt; obj.toString();    };}public record Person(String name, String address) {}public static void main(String[] args) {    System.out.println(formatValue(10));    System.out.println(formatValue(20L));    System.out.println(formatValue(3.14));    System.out.println(formatValue(&quot;Hello&quot;));    System.out.println(formatValue(null));}// 运行结果// int 10// long 20// double 3.140000// String Hello// null</code></pre><h2 id="孵化功能"><a href="#孵化功能" class="headerlink" title="孵化功能"></a>孵化功能</h2><h3 id="JEP-426-向量API（第四次孵化）"><a href="#JEP-426-向量API（第四次孵化）" class="headerlink" title="JEP 426: 向量API（第四次孵化）"></a>JEP 426: 向量API（第四次孵化）</h3><p>向量API的功能是提供一个表达向量计算的API，这些计算在运行时可靠地编译成支持的CPU架构上的最优向量指令，从而实现比等效标量计算更优的性能。</p><p>下面这个是官方给的示例：</p><pre><code class="java">// 标量计算示例void scalarComputation(float[] a, float[] b, float[] c) {    for (int i = 0; i &lt; a.length ; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}// 使用向量API的向量计算示例static final VectorSpecies&lt;Float&gt; SPECIES = FloatVector.SPECIES_PREFERRED;void vectorComputation(float[] a, float[] b, float[] c) {    int i = 0;    int upperBound = SPECIES.loopBound(a.length);    for (; i &lt; upperBound; i += SPECIES.length()) {        // FloatVector va, vb, vc;        var va = FloatVector.fromArray(SPECIES, a, i);        var vb = FloatVector.fromArray(SPECIES, b, i);        var vc = va.mul(va).add(vb.mul(vb)).neg();        vc.intoArray(c, i);    }    for (; i &lt; a.length; i++) {        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;    }}</code></pre><p>在Java19中，向量API（Vector API）的性能改进主要体现在以下几个方面：</p><ul><li>向量计算的编译优化：Java 19中的Vector API能够将向量计算表达式在运行时可靠地编译为支持的CPU架构上的最佳向量指令。这意味着它能够利用特定CPU架构的向量指令集来执行计算，从而实现比等效标量计算更优的性能。</li><li>加载和存储向量操作：根据外部函数和内存API预览的定义，在MemorySegment之间加载和存储向量。这使得向量数据的处理更加高效，特别是在需要频繁访问大量数据的情况下。</li><li>新增交叉通道向量操作：Java 19还增加了两个新的交叉通道向量操作，即压缩和扩展。这些操作进一步增强了向量计算的能力，使其可以更灵活地处理不同类型的向量数据。</li><li>第四轮孵化：Vector API在Java 19中进行了第四轮孵化，这意味着经过前三轮孵化的反馈和改进后，该API已经变得更加成熟和稳定。这种持续的迭代和优化确保了其在实际应用中的可靠性和性能。</li></ul><p>向量API在Java中的独特优势在于其高效的并行计算能力、丰富的向量化指令集、跨平台的数据并行算法支持以及对机器学习的特别优化。</p><h3 id="JEP-428-结构化并发API-孵化"><a href="#JEP-428-结构化并发API-孵化" class="headerlink" title="JEP 428: 结构化并发API (孵化)"></a>JEP 428: 结构化并发API (孵化)</h3><p>结构化并发API，旨在简化多线程编程。结构化并发是一种多线程编程方法，通过使用新的API来管理多线程代码。将不同线程中的多个任务视为单个工作单元，简化错误处理和提高可靠性。</p><p>结构化并发API提供了明确的语法结构来定义子任务的生命周期，并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。</p><p>Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时，如何被传递和处理的过程称为异常传播。</p><p>在结构化并发环境中，异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。</p><p>在Java并发编程中，非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理，这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。</p><pre><code class="java">try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {    var task1 = scope.fork(() -&gt; {        Thread.sleep(1000);        return &quot;Result from task 1&quot;;    });    var task2 = scope.fork(() -&gt; {        Thread.sleep(2000);        return &quot;Result from task 2&quot;;    });    scope.join();    scope.throwIfFailed(RuntimeException::new);    System.out.println(task1.get());    System.out.println(task2.get());} catch (Exception e) {    e.printStackTrace();}</code></pre><p>在这个例子中，handle()方法使用StructuredTaskScope来并行执行两个子任务：task1和task2。通过使用try-with-resources语句自动管理资源，并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关，提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性：</p><ul><li>错误处理与短路：如果task1或task2子任务中的任何一个失败，另一个如果尚未完成则会被取消。（这由 ShutdownOnFailure 实现的关闭策略来管理；还有其他策略可能）。</li><li>取消传播：如果在运行上面方法的线程在调用 join() 之前或之中被中断，则线程在退出作用域时会自动取消两个子任务。</li><li>清晰性：设置子任务，等待它们完成或被取消，然后决定是成功（并处理已经完成的子任务的结果）还是失败（子任务已经完成，因此没有更多需要清理的）。</li><li>可观察性：线程转储清楚地显示了任务层次结构，其中运行task1或task2的线程被显示为作用域的子任务。</li></ul><p>上面的示例能够很好的解决我们的一个痛点，有两个可并行的任务A和B，A+B才是完整结果，任何一个失败，另外一个也不需要成功，结构化并发API就可以很容易的实现这个逻辑。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文介绍了 Java19 新增的特性，完整的特性清单可以从 <a href="https://openjdk.org/projects/jdk/19/" target="_blank" rel="noopener">https://openjdk.org/projects/jdk/19/</a> 查看。后续内容会发布在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/java/java-19-features">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java19 的新特性</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java19 的新特性</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      Java19 在 2022 年 9 月 20 日发布GA版本，共七大特性，这些新特性大多处于预览或孵化阶段，旨在简化并发编程、提高性能和增强语言表达能力。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java19" scheme="https://www.howardliu.cn/tags/Java19/"/>
    
  </entry>
  
  <entry>
    <title>Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java18 的新特性</title>
    <link href="https://www.howardliu.cn/java/java-18-features/"/>
    <id>https://www.howardliu.cn/java/java-18-features/</id>
    <published>2024-09-10T00:20:00.000Z</published>
    <updated>2024-12-05T02:30:53.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/bird-8922501_1920.jpg" alt="Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java18 的新特性"></p><p>你好，我是看山。</p><blockquote><p>本文收录在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">《从小工到专家的 Java 进阶之旅》</a> 系列专栏中。</p></blockquote><p>从 2017 年开始，Java 版本更新策略从原来的每两年一个新版本，改为每六个月一个新版本，以快速验证新特性，推动 Java 的发展。让我们跟随 Java 的脚步，配合示例讲解，看一看每个版本的新特性，本期是 Java18 的新特性。</p><a id="more"></a><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>Java18 是在 2022 年 3 月 22 日正式发布，有9大特性：</p><ul><li>JEP 400: 使用 UTF-8 作为默认字符集</li><li>JEP 408: 简单的网络服务器</li><li>JEP 413: Java API 文档中的代码片段</li><li>JEP 416: 使用方法句柄重新实现核心反射机制</li><li>JEP 417: Vector API（第三次孵化）</li><li>JEP 418: Internet-Address地址解析 SPI</li><li>JEP 419: 外部函数和内存 API (第二次孵化)</li><li>JEP 420: switch 模式匹配（第二次预览）</li><li>JEP 421: 弃用 Finalization</li></ul><p>接下来我们一起看看这些特性。</p><h2 id="JEP-400-使用-UTF-8-作为默认字符集"><a href="#JEP-400-使用-UTF-8-作为默认字符集" class="headerlink" title="JEP 400: 使用 UTF-8 作为默认字符集"></a>JEP 400: 使用 UTF-8 作为默认字符集</h2><p>Java从出生目标就是跨平台使用，“一处编码，处处运行”，所以有一些适配不同平台的逻辑，字符集就是其中一个。在Java 18之前，Java的默认字符集是基于系统环境的。</p><p>可以通过命令查看：</p><pre><code class="shell">java -XshowSettings:properties -version 2&gt;&amp;1 | grep file.encoding</code></pre><p>或者：</p><pre><code class="java">Charset.defaultCharset()</code></pre><p>但是在跨平台使用时，这种特性可能导致字符编码的问题。</p><p>比如：</p><ul><li>代码的编译字符集是UTF-8，运行字符集是其他的，假设是输出中文字符，比如<code>System.out.println(&quot;你好&quot;);</code>，在Mac和Windows上表现就会不一致，在Windows上就会出现乱码，这是因为，在Java18之前，Windows默认字符集是GBK；</li><li>IO文件流操作，比如文件是UTF-8字符集编写，在不同环境读取文件内容，表现就会不一致。最常见的就是乱码神兽“<strong>锟斤拷</strong>”。</li></ul><p>通过将UTF-8作为默认字符集，不再需要设置file.encoding，可以统一字符编码的处理方式，提高国际化应用的兼容性。开发者可以更方便地进行开发和部署，减少了因字符集不一致导致的调试和维护问题。</p><p>Java 18中默认使用UTF-8字符集不仅简化了开发流程，还显著提升了代码的可预测性和可移植性，同时增强了对全球化和多语言应用的支持。</p><p>这是一个ROI很高的特性。</p><h2 id="JEP-408-简单的网络服务器"><a href="#JEP-408-简单的网络服务器" class="headerlink" title="JEP 408: 简单的网络服务器"></a>JEP 408: 简单的网络服务器</h2><p>JEP 408是为开发者提供一个轻量级、简单易用的 HTTP 服务器，通过命令行工具<code>jwebserver</code>可以启动一个最小化的静态 Web 服务器，这个服务器仅支持静态资源的访问，不支持 CGI（Common Gateway Interface）或类似 servlet 的功能，主要用于原型制作、测试和开发环境中的静态文件托管与共享。</p><p>它的应用场景包括：</p><ul><li>原型开发：由于其简单易用的特性，jwebserver 可以作为快速原型开发工具，帮助开发者在短时间内搭建起一个可以访问静态资源的 Web 服务。这对于需要验证某个功能或概念的初期阶段非常有用。</li><li>快速部署：对于一些小规模的应用或者临时性的项目，使用 jwebserver 可以快速启动并运行一个简单的 Web 服务，而无需复杂的配置和环境搭建。这使得开发者能够迅速将想法转化为实际的可访问服务。</li><li>学习与教育：jwebserver 提供了一个直观的平台，让初学者可以轻松上手 Java Web 开发。通过简单的命令行操作，用户可以快速理解 Web 服务器的工作原理及其基本配置。</li><li>测试与调试：在进行 Web 应用的测试和调试时，jwebserver 可以作为一个独立的工具来提供静态文件的访问服务，从而方便开发者对应用进行测试和调试。</li><li>本地开发环境：在本地开发环境中，jwebserver 可以替代传统的 Web 服务器如 Apache Tomcat 或 Nginx，为开发者提供一个轻量级的选择，以减少系统资源的占用。</li></ul><p>我们可以简单试一下，在当前目录编写index.html：</p><pre><code class="html">&lt;html&gt;&lt;head&gt;    &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot; /&gt;&lt;/head&gt;&lt;body&gt;    &lt;h2&gt;欢迎参观&lt;/h2&gt;    &lt;h3&gt;&lt;a href=&quot;https://www.howardliu.cn/&quot;&gt;看山的小屋 howardliu.cn&lt;/a&gt;&lt;/h3&gt;    &lt;p&gt;        一起&lt;strong&gt;开心&lt;/strong&gt;学技术    &lt;/p&gt;    &lt;p&gt;        让我们一起&lt;strong&gt;扬帆起航&lt;/strong&gt;    &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</code></pre><p>运行<code>jwebserver</code>命令：</p><pre><code class="shell">$ ./bin/jwebserverBinding to loopback by default. For all interfaces use &quot;-b 0.0.0.0&quot; or &quot;-b ::&quot;.Serving /Users/liuxinghao/Library/Java/JavaVirtualMachines/temurin-18.0.2.1/Contents/Home and subdirectories on 127.0.0.1 port 8000URL http://127.0.0.1:8000/</code></pre><p>打开<code>http://127.0.0.1:8000/</code>就可以直接看到index.html的效果：</p><p><img src="https://static.howardliu.cn/20240803-114331.png" alt="看山的小屋 howardliu.cn"></p><p><code>jwebserver</code>还支持指定地址和端口等参数，具体使用可以通过命令查看：</p><pre><code class="shell">$ ./bin/jwebserver -hUsage: jwebserver [-b bind address] [-p port] [-d directory]                  [-o none|info|verbose] [-h to show options]                  [-version to show version information]Options:-b, --bind-address    - Address to bind to. Default: 127.0.0.1 (loopback).                        For all interfaces use &quot;-b 0.0.0.0&quot; or &quot;-b ::&quot;.-d, --directory       - Directory to serve. Default: current directory.-o, --output          - Output format. none|info|verbose. Default: info.-p, --port            - Port to listen on. Default: 8000.-h, -?, --help        - Prints this help message and exits.-version, --version   - Prints version information and exits.</code></pre><h2 id="JEP-413-Java-API-文档中的代码片段"><a href="#JEP-413-Java-API-文档中的代码片段" class="headerlink" title="JEP 413: Java API 文档中的代码片段"></a>JEP 413: Java API 文档中的代码片段</h2><p>JEP 413是Java 18中的一个新特性，旨在简化在Java API文档中嵌入示例源代码的过程。这一特性通过引入一个新的<code>@snippet</code>标签来实现，该标签可以用于标准的JavaDoc生成器。</p><p>在JEP 413之前，要在Java代码注释中添加示例代码非常麻烦，通常需要使用复杂的字符转义或预定义的标记（如 <code>{@code ...}</code>标记或<code>&lt;pre&gt;</code>标签），这不仅增加了编写和维护文档的复杂性，还可能导致文档中的代码片段无法正确显示。而JEP 413通过引入<code>@snippet</code>标签，使得在API文档中嵌入示例源代码变得更加简单和直观。</p><p><code>@snippet</code>标签有两种用法：</p><ol><li>内联片段（inline snippet），即代码片段直接包含在标签内；<pre><code class="java">/*** The following code shows how to use {@code Optional.isPresent}:* {@snippet :* if (v.isPresent()) {*     System.out.println(&quot;v: &quot; + v.get());* }* }*/</code></pre></li><li>外部片段（external snippet），即代码片段从单独的源文件中读取。<pre><code class="java">/*** The following code shows how to use {@code Optional.isPresent}:* {@snippet file=&quot;ShowOptional.java&quot; region=&quot;example&quot;}*/</code></pre>引用代码如下：<pre><code class="java">public class ShowOptional { void show(Optional&lt;String&gt; v) {     // @start region=&quot;example&quot;     if (v.isPresent()) {         System.out.println(&quot;v: &quot; + v.get());     }     // @end }}</code></pre></li></ol><blockquote><p>需要注意的，引入外部片段的时候，file参数指定的文件通常位于与源文件同一层级下的一级目录中的snippet-files目录内。</p></blockquote><p>JEP 413在Java API文档的增强主要是下面几个方面：</p><ul><li>引入<code>@snippet</code> JavaDoc标签：JEP 413引入了一个新的标准Doclet标签<code>@snippet</code>JavaDoc，这使得在API文档中嵌入示例源代码变得更加简单。这个标签允许开发者直接在Javadoc注释中包含可编译的源代码片段，从而提高了文档的可读性和实用性。</li><li>简化代码示例的嵌入：在JEP 413之前，要在JavaDoc中添加代码示例通常需要使用复杂的标记，如<pre>标签或通过<code>@code</code>标签包裹代码段。而JEP 413通过简化这些过程，使得开发者可以更方便地在文档中嵌入代码示例。</li><li>增强验证和维护能力：通过提供对源代码片段的API访问，JEP 413不仅简化了文档的编写，还增强了源代码片段的验证能力。尽管最终正确性仍由作者负责，但新的工具支持使得实现这一目标变得更加容易。</li><li>语法高亮和格式化支持：与之前的版本相比，JEP 413不仅支持代码片段的嵌入，还提供了语法高亮和格式化的功能。这意味着在生成的Javadoc文档中，代码示例会以更清晰、易读的方式展示，从而提高文档的可读性和用户体验。</li><li>提升用户体验：JEP 413的引入大大改善了在JavaDoc中嵌入代码示例的体验。例如，在之前版本中，开发者需要手动处理大量的HTML和XML标记，而JEP 413则通过标准化的方式减少了这些工作量。</li><li>提高开发效率和代码质量：通过使用规范且更新及时的API文档，开发者可以更快地理解和使用库中的功能。这种清晰的文档有助于减少错误和混淆，从而提高整体的开发效率和代码质量。</li></ul><blockquote><p>我只有在写基础工具的时候才会写这方面的实力，其他时候使用普通注释就行了。所以，这个功能使用的概率比较小，大家了解下就好。🐶</p></blockquote><h2 id="JEP-416-使用方法句柄重新实现核心反射机制"><a href="#JEP-416-使用方法句柄重新实现核心反射机制" class="headerlink" title="JEP 416: 使用方法句柄重新实现核心反射机制"></a>JEP 416: 使用方法句柄重新实现核心反射机制</h2><p>JEP 416 是 Java18 中的一个重要新特性，其核心内容是使用方法句柄（Method Handles）重新实现 Java 核心反射机制。具体来说，它在 java.lang.invoke 的方法句柄之上重构了 java.lang.reflect.Method 、Constructor 和 Field 等类的实现逻辑。</p><p>JEP 416主要目标是把反射API与JVM内部结构解耦，便于维护和更新，降低成本和复杂度，而且需要兼顾性能和兼容性。</p><p>这一改动主要体现在以下几个方面：</p><ol><li>性能提升：通过使用方法句柄来处理反射操作，Java18 显著提高了反射操作的性能和速度。这意味着在进行反射操作时，如调用方法、构造函数或访问字段等，可以更快地完成这些操作。</li><li>代码量减少：由于方法句柄的引入，开发者在编写反射相关的代码时，可以更简洁地表达意图，从而减少代码量。这不仅使得代码更加清晰易读，也进一步提升了开发效率。</li><li>无需修改现有 API：这项改动不会影响现有的 Java 反射 API，因此开发者无需对现有的反射相关代码进行修改即可体验到性能上的提升。这对于已经在使用反射功能的项目来说是一个非常友好的特性。</li><li>重构核心反射机制：在 java.lang.invoke 的方法句柄之上，Java 18 对 java.lang.reflect.Method 、Constructor 和 Field 等类进行了重构，使得它们的实现逻辑更加高效。这种重构不仅优化了内部实现，还可能带来一些未预见的性能提升。</li></ol><p>这项改动不会改变现有的反射相关 API，因此开发者可以在不修改现有代码的情况下体验到更好的反射性能。总的来说，JEP 416 的引入旨在简化 Java 反射机制的维护工作，并提升其运行效率。</p><p>官方给的基准测试数据如下：</p><p>基准线（Java18 之前实现）</p><pre><code>Benchmark                                     Mode  Cnt   Score  Error  UnitsReflectionSpeedBenchmark.constructorConst     avgt   10  68.049 ± 0.872  ns/opReflectionSpeedBenchmark.constructorPoly      avgt   10  94.132 ± 1.805  ns/opReflectionSpeedBenchmark.constructorVar       avgt   10  64.543 ± 0.799  ns/opReflectionSpeedBenchmark.instanceFieldConst   avgt   10  35.361 ± 0.492  ns/opReflectionSpeedBenchmark.instanceFieldPoly    avgt   10  67.089 ± 3.288  ns/opReflectionSpeedBenchmark.instanceFieldVar     avgt   10  35.745 ± 0.554  ns/opReflectionSpeedBenchmark.instanceMethodConst  avgt   10  77.925 ± 2.026  ns/opReflectionSpeedBenchmark.instanceMethodPoly   avgt   10  96.094 ± 2.269  ns/opReflectionSpeedBenchmark.instanceMethodVar    avgt   10  80.002 ± 4.267  ns/opReflectionSpeedBenchmark.staticFieldConst     avgt   10  33.442 ± 2.659  ns/opReflectionSpeedBenchmark.staticFieldPoly      avgt   10  51.918 ± 1.522  ns/opReflectionSpeedBenchmark.staticFieldVar       avgt   10  33.967 ± 0.451  ns/opReflectionSpeedBenchmark.staticMethodConst    avgt   10  75.380 ± 1.660  ns/opReflectionSpeedBenchmark.staticMethodPoly     avgt   10  93.553 ± 1.037  ns/opReflectionSpeedBenchmark.staticMethodVar      avgt   10  76.728 ± 1.614  ns/op</code></pre><p>当前（Java18 实现）</p><pre><code>Benchmark                                     Mode  Cnt    Score   Error  UnitsReflectionSpeedBenchmark.constructorConst     avgt   10   32.392 ± 0.473  ns/opReflectionSpeedBenchmark.constructorPoly      avgt   10  113.947 ± 1.205  ns/opReflectionSpeedBenchmark.constructorVar       avgt   10   76.885 ± 1.128  ns/opReflectionSpeedBenchmark.instanceFieldConst   avgt   10   18.569 ± 0.161  ns/opReflectionSpeedBenchmark.instanceFieldPoly    avgt   10   98.671 ± 2.015  ns/opReflectionSpeedBenchmark.instanceFieldVar     avgt   10   54.193 ± 3.510  ns/opReflectionSpeedBenchmark.instanceMethodConst  avgt   10   33.421 ± 0.406  ns/opReflectionSpeedBenchmark.instanceMethodPoly   avgt   10  109.129 ± 1.959  ns/opReflectionSpeedBenchmark.instanceMethodVar    avgt   10   90.420 ± 2.187  ns/opReflectionSpeedBenchmark.staticFieldConst     avgt   10   19.080 ± 0.179  ns/opReflectionSpeedBenchmark.staticFieldPoly      avgt   10   92.130 ± 2.729  ns/opReflectionSpeedBenchmark.staticFieldVar       avgt   10   53.899 ± 1.051  ns/opReflectionSpeedBenchmark.staticMethodConst    avgt   10   35.907 ± 0.456  ns/opReflectionSpeedBenchmark.staticMethodPoly     avgt   10  102.895 ± 1.604  ns/opReflectionSpeedBenchmark.staticMethodVar      avgt   10   82.123 ± 0.629  ns/op</code></pre><p>从基准测试来看，新实现有好有坏，效果差别不大，基本符合新扩展便于维护又对性能没有明显影响的预期。</p><h2 id="JEP-418-Internet-Address-地址解析-SPI"><a href="#JEP-418-Internet-Address-地址解析-SPI" class="headerlink" title="JEP 418: Internet-Address 地址解析 SPI"></a>JEP 418: Internet-Address 地址解析 SPI</h2><p>JEP 418旨在改进Internet-Address 地址解析的灵活性和可配置性。具体来说，它引入了一个服务提供者接口（SPI），使得<code>java.net.InetAddress</code>可以使用平台内置解析器之外的第三方解析器。</p><p>在Java18之前版本中，Internet-Address 地址解析主要依赖于内置的解析器，这些解析器通常会结合本地的hosts文件和DNS来解析主机名和IP地址。然而，这种设计限制了开发者对解析器的选择和定制能力。通过引入SPI，JEP 418允许开发者自定义网络地址解析策略，从而提高了系统的灵活性和可扩展性。</p><p>例如，开发者可以通过实现SPI接口，创建自己的解析器，并将其注册到系统中，这样java.net.InetAddress.getByName()方法就可以使用这些自定义解析器来进行地址解析了。这不仅增强了系统的灵活性，还为特定需求提供了更多的选择和控制。</p><h2 id="预览功能"><a href="#预览功能" class="headerlink" title="预览功能"></a>预览功能</h2><h3 id="JEP-420-switch-模式匹配（第二次预览）"><a href="#JEP-420-switch-模式匹配（第二次预览）" class="headerlink" title="JEP 420: switch 模式匹配（第二次预览）"></a>JEP 420: switch 模式匹配（第二次预览）</h3><p>switch 模式匹配第一次预览是在Java17中（JEP 406，参见<a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a>），旨在通过引入模式匹配来增强Java编程语言的表达能力，使得在switch语句和表达式中可以更灵活地进行条件判断和分支选择。</p><p>JEP 420允许在switch的选择器表达式中使用任何引用类型，并且case标签可以包含模式，包括条件模式。这意味着开发者可以在switch语句中直接匹配变量的不同状态或类型，从而简化代码并提高其可读性和健壮性。</p><p>基于这个特性，我们可以将一个复杂的if-else结构转换为更加简洁的switch模式匹配结构，这不仅提高了代码的可维护性，还减少了出错的可能性。</p><p>此外，JEP 420还扩展了模式匹配的语言特性，使得开发者能够对数据进行更复杂、更直观的查询和操作。这种改进特别适用于处理多样的数据结构和复杂的数据查询场景，有助于提升开发效率和代码质量。</p><p>总结而言，JEP 420通过引入switch模式匹配，进一步丰富了Java的控制流结构，使代码编写更加高效和易懂。</p><p>在JEP 420中，switch语句的模式匹配语法如下：</p><pre><code class="java">switch (表达式) {case 模式1 -&gt; 操作1;case 模式2 -&gt; 操作2;// 可以有多个case子句default -&gt; 默认操作;}</code></pre><h2 id="孵化功能"><a href="#孵化功能" class="headerlink" title="孵化功能"></a>孵化功能</h2><h3 id="JEP-417-Vector-API（第三次孵化）"><a href="#JEP-417-Vector-API（第三次孵化）" class="headerlink" title="JEP 417: Vector API（第三次孵化）"></a>JEP 417: Vector API（第三次孵化）</h3><p>Vector 向量计算 API 是在 Java16 引入（JEP 338，参见 <a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a>），可以在运行时借助 CPU 向量运算指令，实现更优的计算能力。在 Java17 第二次孵化（JEP 414，参见<a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a>），针对性能和实现进行了改进，包括字节向量与布尔数组之间进行转换。</p><p>Vector 向量计算 API是一个为了高性能计算设计的API，旨在利用CPU的向量指令集来优化性能，特别是在科学计算领域。其目标是提供一种更高效的编程方式，为Java语言的未来发展奠定基础。</p><blockquote><p>Vector 向量计算 API在最新的Java23中还在孵化，一时半会还用不了，我们就持续关注着吧。</p></blockquote><h3 id="JEP-419-外部函数和内存-API-第二次孵化"><a href="#JEP-419-外部函数和内存-API-第二次孵化" class="headerlink" title="JEP 419: 外部函数和内存 API (第二次孵化)"></a>JEP 419: 外部函数和内存 API (第二次孵化)</h3><p>外部函数和内存 API第一次孵化是在Java17中（JEP 412，参见<a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a>）。</p><p>JEP 419旨在使Java程序能够更安全、高效地与本地代码和数据进行互操作。通过这个API，开发者可以调用JVM之外的外部函数，并且能够安全地访问不受JVM管理的外部内存。</p><p>具体来说，该API定义了一系列类和接口，使得客户端代码可以在库和应用程序中分配外部内存（如MemorySegment、MemoryAddress和SegmentAllocator）、操作和访问结构化外部内存（如MemoryLayout和VarHandle），并管理外部资源的生命周期。这使得Java程序无需使用JNI（Java Native Interface）即可直接调用本地库和处理本地数据，从而避免了JNI带来的潜在风险和复杂性。</p><p>通过更加优雅的方式访问外部函数是从 Java14 开始的，经历了多个孵化版本：</p><ul><li>Java14 的 JEP 370：外部存储器访问 API（孵化）</li><li>Java15 的 JEP 383：外部存储器访问 API（第二版孵化功能）</li><li>Java16 的 JEP 389：外部链接器 API（孵化功能）</li><li>Java16 的 JEP 393：外部存储器访问 API（第三版孵化功能）</li><li>Java17 的 JEP 412：外部函数和内存 API</li></ul><p>可以看出来，虽然一直在孵化，但是功能越来越强大了。</p><h2 id="弃用和删除"><a href="#弃用和删除" class="headerlink" title="弃用和删除"></a>弃用和删除</h2><h3 id="JEP-421-弃用-Finalization"><a href="#JEP-421-弃用-Finalization" class="headerlink" title="JEP 421: 弃用 Finalization"></a>JEP 421: 弃用 Finalization</h3><p>将 Finalization 标记为过期：<code>@Deprecated(forRemoval=true)</code>，具体API包括：</p><ul><li>java.lang.Object.finalize()</li><li>java.lang.Enum.finalize()</li><li>java.awt.Graphics.finalize()</li><li>java.awt.PrintJob.finalize()</li><li>java.util.concurrent.ThreadPoolExecutor.finalize()</li><li>javax.imageio.spi.ServiceRegistry.finalize()</li><li>javax.imageio.stream.FileCacheImageInputStream.finalize()</li><li>javax.imageio.stream.FileImageInputStream.finalize()</li><li>javax.imageio.stream.FileImageOutputStream.finalize()</li><li>javax.imageio.stream.ImageInputStreamImpl.finalize()</li><li>javax.imageio.stream.MemoryCacheImageInputStream.finalize()</li></ul><p>了解过JVM GC逻辑的都知道，finalize虽然提供了资源回收，但其实并不可靠和安全，为了规范使用，开始主键淘汰这些过期的设计方案。</p><p>关于资源回收，推荐使用 try-with-resources 或者 java.lang.ref.Cleaner，这些都是简单直接高效的回收方式。</p><p>我们应该在日常开发中，应该有意识的写优雅的代码。代码功力，没有武功秘笈可以在短时间快速提升，都是日常一行一行积攒的，量变引起质变，低质量的编码，是无法引起质变的。</p><p>你我共勉。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文介绍了 Java18 新增的特性，完整的特性清单可以从 <a href="https://openjdk.org/projects/jdk/18/" target="_blank" rel="noopener">https://openjdk.org/projects/jdk/18/</a> 查看。后续内容会发布在 <a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a> 系列专栏中。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">从小工到专家的 Java 进阶之旅</a></li><li><a href="https://mp.weixin.qq.com/s/7f5dcjjKLdBYpzS5p7_v7Q" target="_blank" rel="noopener">一文掌握 Java8 Stream 中 Collectors 的 24 个操作</a></li><li><a href="https://mp.weixin.qq.com/s/JOLrw7cbVh8z2czePEfLjw" target="_blank" rel="noopener">一文掌握 Java8 的 Optional 的 6 种操作</a></li><li><a href="https://mp.weixin.qq.com/s/Vhw-2lgCvmhNI5aeXOWYNg" target="_blank" rel="noopener">使用 Lambda 表达式实现超强的排序功能</a></li><li><a href="https://mp.weixin.qq.com/s/Srv6733byTYDg7hTrW_tdw" target="_blank" rel="noopener">Java8 的时间库（1）：介绍 Java8 中的时间类及常用 API</a></li><li><a href="https://mp.weixin.qq.com/s/LRydpL89GsjOeTrLrihyYQ" target="_blank" rel="noopener">Java8 的时间库（2）：Date 与 LocalDate 或 LocalDateTime 互相转换</a></li><li><a href="https://mp.weixin.qq.com/s/KmBMLG1y73IUwFuSfUKaEQ" target="_blank" rel="noopener">Java8 的时间库（3）：开始使用 Java8 中的时间类</a></li><li><a href="https://mp.weixin.qq.com/s/PxC0WQMReIeG0Tyl5WiK1Q" target="_blank" rel="noopener">Java8 的时间库（4）：检查日期字符串是否合法</a></li><li><a href="https://mp.weixin.qq.com/s/rjMOq0QAdiWNacKYqHJqXA" target="_blank" rel="noopener">Java8 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/FHv963s0Qvh2K5r2tdLATA" target="_blank" rel="noopener">Java9 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/NY5-qkfZGdaOvz9iyJ3VSQ" target="_blank" rel="noopener">Java10 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/baBv0mP2JmPE4MoNYESqhA" target="_blank" rel="noopener">Java11 中基于嵌套关系的访问控制优化</a></li><li><a href="https://mp.weixin.qq.com/s/Bn4S2OfMoG19lI-L3nw8cg" target="_blank" rel="noopener">Java11 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/xBjXHC5UWy4JEKaRo6xQsg" target="_blank" rel="noopener">Java12 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/9EdRVPK0GUoQqI7eXcmwEA" target="_blank" rel="noopener">Java13 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/rEAHIf40j_UECLlxK5KW6w" target="_blank" rel="noopener">Java14 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/UESHDzuY3H7p5LLPJfwyAw" target="_blank" rel="noopener">Java15 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/By2JoZqAHA6U36PG8qvCcg" target="_blank" rel="noopener">Java17 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/W6Esovb7zzCL_seXgY84jA" target="_blank" rel="noopener">Java18 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/WutbAX_IQNqP4M3rud9f_g" target="_blank" rel="noopener">Java19 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/DvZYrZTtGfdXNsXGzmQN1A" target="_blank" rel="noopener">Java20 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/iiXa9rpJu1Vedpke0XpBzQ" target="_blank" rel="noopener">Java21 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/eHHFaJErH8XH4QSRrfcehQ" target="_blank" rel="noopener">Java22 的新特性</a></li><li><a href="https://mp.weixin.qq.com/s/XvB0D2vzhaAvHESupdALag" target="_blank" rel="noopener">Java23 的新特性</a></li><li><a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI4OTU5NTA1Ng==&action=getalbum&album_id=1732392238946533378" target="_blank" rel="noopener">Java24 的新特性</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/java/java-18-features">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java18 的新特性</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">Java 每半年就会更新一次新特性，再不掌握就要落伍了：Java18 的新特性</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      从 2017 年开始，Java 版本更新策略从原来的每两年一个新版本，改为每六个月一个新版本，以快速验证新特性，推动 Java 的发展。让我们跟随 Java 的脚步，配合示例讲解，看一看每个版本的新特性，本期是 Java18 的新特性。
    
    </summary>
    
    
      <category term="java" scheme="https://www.howardliu.cn/categories/java/"/>
    
    
      <category term="java" scheme="https://www.howardliu.cn/tags/java/"/>
    
      <category term="Java" scheme="https://www.howardliu.cn/tags/Java/"/>
    
      <category term="Java18" scheme="https://www.howardliu.cn/tags/Java18/"/>
    
  </entry>
  
  <entry>
    <title>羊不羊的关我什么事？</title>
    <link href="https://www.howardliu.cn/what-i-think-of-covid-19/"/>
    <id>https://www.howardliu.cn/what-i-think-of-covid-19/</id>
    <published>2023-01-07T14:40:56.000Z</published>
    <updated>2023-01-07T14:40:56.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/raving/b919bbc6863c45cb94c7f5a0ce376f18.jpg" alt="这段经历终将成为历史"></p><blockquote><p>本文共：1976字 预计阅读时间：5分钟</p></blockquote><p>你好，我是看山。</p><p>不出意外，看到这篇文章的人八成阳了。</p><p>先放个统计，12月23号的时候，部门发起了一次投票，480人参与，只有20%的壮士还没阳。</p><a id="more"></a><p><img src="https://static.howardliu.cn/raving/1.jpeg" alt="是否感染新冠统计"></p><p>在这一波高峰期前，我们公司已经居家好几个周了，所以投票结果基本上能够反应社会情况。</p><p>今天就想聊两件事：</p><ol><li>为啥突然放开了？</li><li>我对放开的看法。</li></ol><h2 id="为啥放开"><a href="#为啥放开" class="headerlink" title="为啥放开"></a>为啥放开</h2><p>什么情况下可以放开？基本上有三种情况：</p><ol><li>第一种情况是大家希望的，像SARS一样，新冠病毒消失了；</li><li>第二种情况是不需要防，新冠病毒致死率重症率降低到可容忍范围（比如无限接近0），我们与病毒共生了；</li><li>第三种情况是防不住，虽然病毒致死率还很高，但一个眼神交流就会传染，防不胜防了。</li></ol><p>现在显然不是第一种情况。</p><p>我们先来看个数据：</p><p>新冠病毒原始毒株的R0值在3-5；德尔塔毒株的R0值为6-8；奥密克戎BA.1的R0值约为9.5；奥密克戎BA.2的R0值约为13.3；奥密克戎BA.4/5的R0值可能达到18.6……</p><p>R0值（R naught）是基本传染数（basic reproductive number）的简称，又译作基本再生数，指的是在没有采取任何干预措施的情况下，平均每位感染者在传染期内使易感者个体致病的数量。数字越大说明传播能力越强，控制难度越大。</p><p>简单说就是，R0是2时，一个传染俩，两个传四个，四个传八个，以此类推……经过4个循环，病例数就增加到64人。</p><p>咱们把新冠病毒的R0值做个图：</p><p><img src="https://static.howardliu.cn/raving/B1234E4F-0095-4002-99A4-74530209393F.png" alt="新冠集中毒株的R0值曲线"></p><p>这就是所谓的指数爆炸：</p><ul><li>当R0等于3的时候，经过4次传播，结果是81；</li><li>当R0等于6的时候，经过4次传播，结果是1296；</li><li>当R0等于9的时候，经过4次传播，结果是6561；</li><li>当R0等于13的时候，经过4次传播，结果是28561；</li><li>……</li></ul><p>当R0超过一定范围，不是我们想防就能防得住的。就比如年初的上海，奥密克戎的高传播性迅速击溃了精准防控，演变成后来的封城。</p><p>奥密克戎的变异株的传染能力还在增强，据说现在是21了（没有查到官方数据）。</p><p>这个时候，我们如果还想像前几年似的，想通过封控来阻断传染，就要举全国之力了。那种情况下，大家就只能待在家里，各种行业停摆。显然是不现实的，不是每家都有能够吃一个月的战略储备粮，也不是每家都能够啥也不干白来一个月钱的。</p><p>也就是说，现在是防不住了。</p><p><img src="https://static.howardliu.cn/raving/1BE20C5B-4E99-462F-A95C-60550E27304A.png" alt="不用下楼会感染了新冠"></p><p>然后我们再看看致病性，鉴于大家或者周围人都阳过，就算是有经验。我身边人阳了之后的征兆基本上这这几种：</p><ol><li>发烧，有的低烧好几天（免疫力差）、有的高烧一两天（免疫力强）；</li><li>咳嗽，干咳或者湿咳，因人而异；</li><li>嗓子疼，戏称宝娟嗓，严重地说不出话，感觉像是吞刀片一样；</li><li>浑身疼，主要变现是肌肉酸疼，浑身没劲；</li></ol><p>可以看到，这些症状和普通的流感很像。甚至没有抗原的情况下，都不能断定自己是流感还是新冠。</p><p>也就是说，大部分人阳了之后症状都很轻，且属于上呼吸道的症状，基本上对肺部没啥影响。</p><p>这样就符合第二种情况了，我们大多数人可以与新冠病毒共生了。</p><p>从经济的角度看，我们不可能无限期的收紧防控措施，很多企业在这段封控期间艰难度日，每一次收紧，企业都会受到冲击，我们没有办法不计成本的封控，孩子要长大，成人要工作，企业要发展，人们要生活。时移世易，当封控成本远远大于收益时，当感染后对身体影响很小时，我们就要选择更适合的方式。</p><h2 id="我的看法"><a href="#我的看法" class="headerlink" title="我的看法"></a>我的看法</h2><p>再来说下我对放开的看法：坚决拥护，但也会做好个人防护。</p><p>首先说个人防护，相信大家从各种渠道了解过，病毒是RNA复制，变异方向多变且速度快，就有概率出现传染性强且致死率高的毒株。只要我们做好防护，别让病毒在我们体内有复制的机会，那变异的机会就会少很多。</p><p>然后说坚决拥护，就这几周看过来，商场在渐渐恢复，大家生活轻松很多，连前几天几乎瘫痪的物流快递业，现在也逐渐恢复了。一切都在向好的方向发展，有什么理由不拥护呢？</p><p>给大家看一下北京地铁最近15日客流量和往年日均客流量图。</p><p><img src="https://static.howardliu.cn/raving/B5237931-A4EE-47B1-8E68-0B1A7399572C.png" alt="北京地铁最近15日客流量"></p><p><img src="https://static.howardliu.cn/raving/375EB016-DA3D-4D34-A161-CCF332189190.png" alt="北京地铁往年日均客流量"></p><p>最近15日客流量每个工作日都在递增，在1月6日已经达到769万。再看年日均客流，2023年刚开始7天，日均客流量已经达到去年平均水平。</p><p>回想前段时间，2022年11月25日客流量跌到了108万，在放开之后，逐步恢复生机。</p><p>从上帝视角看，中央踩刹车打方向盘的举措是正确的。就这几天复工经历看，同事们带着口罩，偶尔听到几声咳嗽，也都没人在意，我们以较小的成本实现了与新冠共存。</p><p>放一个新华社的<a href="https://h5.xinhuaxmt.com/h5/m1/covid19/index.html" target="_blank" rel="noopener">新冠防疫手册</a>，虽然病毒毒性弱了，但是阳的时候还是很难受的，能不重阳就不重阳吧。</p><p>最后，多年之后，当我们回忆起这三年，可能有感慨、有怀念、有悲伤，更多的可能是对当下美好生活的珍惜。</p><p>祝大家身体健康。</p><p><img src="https://static.howardliu.cn/raving/1FE35223-7DD2-4910-89C7-081ACCF12BE3.jpg" alt="护体金钟罩"></p><p>青山不改，绿水长流，我们下次见。</p><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/what-i-think-of-covid-19/">羊不羊的关我什么事？</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;img src=&quot;https://static.howardliu.cn/raving/b919bbc6863c45cb94c7f5a0ce376f18.jpg&quot; alt=&quot;这段经历终将成为历史&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文共：1976字 预计阅读时间：5分钟&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;你好，我是看山。&lt;/p&gt;
&lt;p&gt;不出意外，看到这篇文章的人八成阳了。&lt;/p&gt;
&lt;p&gt;先放个统计，12月23号的时候，部门发起了一次投票，480人参与，只有20%的壮士还没阳。&lt;/p&gt;
    
    </summary>
    
    
      <category term="闲聊" scheme="https://www.howardliu.cn/categories/%E9%97%B2%E8%81%8A/"/>
    
    
      <category term="闲聊" scheme="https://www.howardliu.cn/tags/%E9%97%B2%E8%81%8A/"/>
    
      <category term="新冠" scheme="https://www.howardliu.cn/tags/%E6%96%B0%E5%86%A0/"/>
    
      <category term="covid-19" scheme="https://www.howardliu.cn/tags/covid-19/"/>
    
  </entry>
  
  <entry>
    <title>SpringBoot 实战：国际化组件 MessageSource 与 Nacos 组合实现动态配置能力</title>
    <link href="https://www.howardliu.cn/springboot-action-messagesource-nacos/"/>
    <id>https://www.howardliu.cn/springboot-action-messagesource-nacos/</id>
    <published>2022-08-14T02:52:50.000Z</published>
    <updated>2022-08-14T02:52:50.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/cover/jellyfish-g1ec9e22b7_1920.jpeg" alt="SpringBoot 实战：国际化组件 MessageSource 与 Nacos 组合实现动态配置能力"></p><p>你好，我是看山。</p><p>前面介绍了 Spring 的 MessageSource 组件的用法、执行逻辑和源码，本文我们将根据前面的知识，实现自己的动态刷新的国际化组件。</p><a id="more"></a><p>现在大家都用的是微服务，为了高可用，每个服务部署时最少两个实例。</p><ul><li>如果使用<code>ResourceBundleMessageSource</code>实现国际化，每次修改配置文件，都需要重启服务。</li><li>如果使用<code>ReloadableResourceBundleMessageSource</code>，我们可以借助多个服务挂在同一个磁盘或同一个卷读取同一个配置文件，借助远程工具，修改文件实现动态配置内容的修改。但是这就涉及到文本编辑，不能很好的实现审计记录和文件管控。</li></ul><p>所以，我们要实现一个适用于微服务架构、方便修改、具备审计功能的动态的国际化配置组件。本文选择 Nacos 实现，Nacos 有配置中心的能力，适合在微服务架构中使用，同时也具备方便修改和审计能力，只要我们实现从 Nacos 加载国际化配置的能力，就可以轻松实现目标。</p><h2 id="几个关键点"><a href="#几个关键点" class="headerlink" title="几个关键点"></a>几个关键点</h2><p>Spring 提供的默认实现中，<code>ReloadableResourceBundleMessageSource</code>实现了动态刷新的能力，只不过是从文件读取内容，我们可以借助<code>ReloadableResourceBundleMessageSource</code>的逻辑实现，只是将其改为从 Nacos 读取内容。</p><p>这个实现，通常可以有两种方案（假设我们新实现的类命名为<code>NacosBundleMessageSource</code>）：</p><ol><li>继承<code>ReloadableResourceBundleMessageSource</code>：重写读取配置的方法，然后通过 Spring 注入新的方法。这种方式有优点和缺点：<ol><li>优点 1：我们可以保持与<code>ReloadableResourceBundleMessageSource</code>相似的结构和执行逻辑，当 Spring 进行升级的时候，我们直接通过继承获取了能力；</li><li>优点 2：我们只需要覆盖几个关键方法，需要重写的方法比较少；</li><li>缺点 1：当我们期望在系统中引入多种类型的 MessageSource 组件时，就不能简单的通过类型加载了。比如<code>applicationContext.getBean(ReloadableResourceBundleMessageSource.class)</code>会找到<code>ReloadableResourceBundleMessageSource</code>和<code>NacosBundleMessageSource</code>两个 Bean，Spring 容器就不知道该返回哪个了；</li><li>缺点 2：虽然继承能够少写代码，但是一旦 Spring 修改了执行逻辑，我们的<code>NacosBundleMessageSource</code>就可能需要重写。</li></ol></li><li>模仿<code>ReloadableResourceBundleMessageSource</code>：完全实现自己的一个动态加载类。与第一种的优缺点正好相反：<ol><li>优点 1：完全不同的类，Bean 对应的 class 类型不同，<code>applicationContext.getBean</code>可以通过 class 类型获取；</li><li>优点 2：与<code>ReloadableResourceBundleMessageSource</code>的类定义没有关系，除非 Spring 修改底层逻辑，否则不会因为<code>ReloadableResourceBundleMessageSource</code>的变动出现不兼容的情况；</li><li>缺点 1：当 Spring 对<code>ReloadableResourceBundleMessageSource</code>进行升级，提出更加优化的写法，我们就需要重写<code>NacosBundleMessageSource</code>了；</li><li>缺点 2：既然是仿写，很多方法都是与<code>ReloadableResourceBundleMessageSource</code>完全相同的重复代码。</li></ol></li></ol><p>考虑到两种方案的优缺点，结合业务中的逻辑，最终选择方案二。</p><h2 id="文件名"><a href="#文件名" class="headerlink" title="文件名"></a>文件名</h2><p>我们要实现的国际化组件，在 Spring 的实现中，使用的是<code>Locale</code>表示指定的区域，在这个类中，定义了三个不同的维度<code>language</code>、<code>country</code>、<code>variant</code>，翻译过来是<code>语言</code>、<code>国家</code>、<code>变种</code>，结合<code>BaseLocale</code>的定义，可以将<code>country</code>看做是语言大类。</p><p>根据前面的介绍，我们的国际化配置文件定义格式是根据<code>Locale</code>格式定义的。比如，basename 是 messages，Locale 是 de_AT_oo 的话，对应的配置文件可以是“messages_de_AT_OO”、“messages_de_AT”、“messages_de”。</p><p>所以我们需要用到递归的方式获取文件名：</p><pre><code class="java">protected List&lt;String&gt; calculateFilenamesForLocale(String basename, Locale locale) {    List&lt;String&gt; result = new ArrayList&lt;&gt;(3);    String language = locale.getLanguage();    String country = locale.getCountry();    String variant = locale.getVariant();    StringBuilder temp = new StringBuilder(basename);    temp.append(&#39;_&#39;);    if (language.length() &gt; 0) {        temp.append(language);        result.add(0, temp.toString());    }    temp.append(&#39;_&#39;);    if (country.length() &gt; 0) {        temp.append(country);        result.add(0, temp.toString());    }    if (variant.length() &gt; 0 &amp;&amp; (language.length() &gt; 0 || country.length() &gt; 0)) {        temp.append(&#39;_&#39;).append(variant);        result.add(0, temp.toString());    }    return result;}</code></pre><h2 id="加载方式"><a href="#加载方式" class="headerlink" title="加载方式"></a>加载方式</h2><p>既然是从 Nacos 读取配置，那配置文件的内容就需要通过 Nacos 获取。这里使用了<code>NacosConfigManager</code>获取：</p><pre><code class="java">protected Properties loadProperties(String filename) throws IOException, NacosException {    final Properties props = newProperties();    final String dataId = filename + NacosConstants.PROPERTIES_SUFFIX;    logger.info(&quot;Loading properties for &quot; + dataId);    final String config = nacosConfigManager.getConfigService().getConfig(dataId, nacosGroup, 5000);    if (StringUtils.hasText(config)) {        logger.info(&quot;No properties found for &quot; + dataId);        throw new NoSuchFileException(dataId);    }    try (Reader reader = new StringReader(config)) {        this.propertiesPersister.load(props, reader);        logger.info(&quot;Loaded properties for &quot; + dataId);    }    return props;}</code></pre><p>方法传入的<code>filename</code>就是从上一节中获取的文件名。当然，文件名是我们计算出来的，可能不存在，此处直接抛出<code>NoSuchFileException</code>，由上层逻辑捕捉处理：</p><pre><code class="java">protected PropertiesHolder refreshProperties(String filename, @Nullable PropertiesHolder propHolder) {    long refreshTimestamp = (getCacheMillis() &lt; 0 ? -1 : System.currentTimeMillis());    try {        Properties props = loadProperties(filename);        propHolder = new PropertiesHolder(props, -1);    } catch (NacosException ex) {        if (logger.isWarnEnabled()) {            logger.warn(&quot;Could not get properties form nacos &quot;, ex);        }        // Empty holder representing &quot;not valid&quot;.        propHolder = new PropertiesHolder();    } catch (IOException ex) {        if (logger.isInfoEnabled()) {            logger.info(&quot;Could not get properties form nacos, the message is &quot; + ex.getMessage());        }        // Empty holder representing &quot;not valid&quot;.        propHolder = new PropertiesHolder();    }    propHolder.setRefreshTimestamp(refreshTimestamp);    this.cachedProperties.put(filename, propHolder);    logger.info(&quot;Refreshed properties for &quot; + filename);    return propHolder;}</code></pre><p>我们可以看到，从 Nacos 中读取配置逻辑的上层会捕捉<code>Exception</code>，然后创建一个空的配置管理器。此处会有两个异常：</p><ul><li><code>NacosException</code>：读取 Nacos 失败抛出的异常，包括 Nacos 链接不正确或者是读取失败等；</li><li><code>IOException</code>：没有指定文件名的配置时会抛出 IOException。</li></ul><h2 id="动态刷新"><a href="#动态刷新" class="headerlink" title="动态刷新"></a>动态刷新</h2><p>前一节讲了加载 Nacos 配置文件的方式，本节说一下怎么实现 Nacos 配置文件的监听，动态刷新配置内容。</p><p>我们先介绍一下实现 Nacos 监听的几个类：</p><ul><li>NacosRefreshHistory：nacos 配置文件刷新的历史记录，会保存 Nacos 配置文件的刷新历史，最多存储 20 个；</li><li>ConfigService：一般使用的时候是 NacosConfigService 类，这个类是使用<code>NacosConfigManager#getConfigService</code>通过反射创建的，用于存储 Nacos 监听器，实现监听逻辑；</li><li>Listener：这个就是具体的监听器了，在我们的例子中，使用的是<code>AbstractSharedListener</code>的匿名子类，实现动态刷新的逻辑就在这个类里。</li></ul><p>首先，我们当前组件是 MessageSource，用于实现国际化的组件。这个组件是在整个应用就绪后再加载就行，所以，我们监听 Spring 的<code>ApplicationReadyEvent</code>事件即可。</p><pre><code class="java">@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {    // many Spring context    if (this.ready.compareAndSet(false, true)) {        for (Locale defaultLocale : this.nacosBundleMessageSource.defaultLocales()) {            this.nacosBundleMessageSource.getMergedProperties(defaultLocale);        }        this.registerNacosListenersForApplications();    }}</code></pre><p>然后是我们需要对所有可能的配置文件进行监听，就需要用到前文的<code>calculateFilenamesForLocale</code>方法，计算所有可能出现的名字。</p><pre><code class="java">private void registerNacosListenersForApplications() {    if (!isRefreshEnabled()) {        return;    }    this.nacosBundleMessageSource.getBasenameSet().stream()            .map(basename -&gt; this.nacosBundleMessageSource.defaultLocales().stream()                    .map(locale -&gt; this.nacosBundleMessageSource.calculateAllFilenames(basename, locale))                    .flatMap(List::stream)                    .collect(Collectors.toList())            )            .flatMap(List::stream)            .forEach(x -&gt; registerNacosListener(nacosBundleMessageSource.getNacosGroup(), x + NacosConstants.PROPERTIES_SUFFIX));}</code></pre><p>最后，我们需要向 Nacos 的运行管理器中注册文件监听，实现动态刷新的能力。</p><pre><code class="java">private void registerNacosListener(final String groupKey, final String dataKey) {    final String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);    final Listener listener = listenerMap.computeIfAbsent(key, lst -&gt; new AbstractSharedListener() {        @Override        public void innerReceive(String dataId, String group, String configInfo) {            refreshCountIncrement();            nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);            try {                nacosBundleMessageSource.forceRefresh(dataId, configInfo);                if (log.isDebugEnabled()) {                    log.debug(&quot;Refresh Nacos config group={},dataId={},configInfo={}&quot;, group, dataId, configInfo);                }            } catch (IOException e) {                log.warn(&quot;Nacos refresh failed, dataId: {}, group: {}, configInfo: {}&quot;,                        dataId, group, configInfo, e);            }        }    });    try {        configService.addListener(dataKey, groupKey, listener);    } catch (NacosException e) {        log.warn(&quot;register fail for nacos listener ,dataId=[{}],group=[{}]&quot;, dataKey, groupKey, e);    }}</code></pre><p>至此，我们获取了国际化组件 MessageSource 与 Nacos 组合实现动态配置能力。本文中的实例已经传到 GitHub，关注公众号「看山的小屋」，回复<code>spring</code>获取源码。</p><h2 id="发散一下"><a href="#发散一下" class="headerlink" title="发散一下"></a>发散一下</h2><p>接下来，我们再重头想一下，我们最初想要实现一个适用于微服务架构、方便修改、具备审计功能的动态的国际化配置组件。选择 Nacos 作为例子，是因为 Nacos 本身实现了动态监听的能力，可以快速复刻<code>ReloadableResourceBundleMessageSource</code>的能力。那 Nacos 具备什么能力呢？</p><ol><li>中心化存储：因为我们想在微服务架构中使用，就不能采用与独立服务耦合的方式，需要所有服务可以读取的方式。比如数据库（包括关系型数据库、非关系型数据库等）、分布式缓存、远程磁盘（或者 Docker 的卷）等；</li><li>可监听：任何配置的修改能够被服务感知，想要做到这个，就是在配置发生修改时，发送一个修改时间，通知监听服务配置被修改。可以采用被动和主动两种方式：<ol><li>被动方式：这个比较简单，只要将组件中的缓存逻辑删除就可以了，每次查询配置都直接存储器中读取，但是这又与性能相悖，一般不采用这个方案；</li><li>主动方式：通过发送事件或者消息的方式，比如采用 CQRS 模式，发生修改时，发送一条消息，各个微服务监听这个消息，重新加载配置；</li></ol></li><li>可审计：这个能力简单说就是要记录每次修改的时间、人物、动作等信息，这个功能是常用功能，这里就不赘述了。</li></ol><p>只要具备上面三个特性，我们可以通过各种组合实现，比如：</p><ol><li>可挂在磁盘</li><li>Git+Hook</li><li>MySQL+Redis+MQ</li><li>……</li></ol><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文从实践角度出发，实现了一个适用于微服务架构、方便修改、具备审计功能的动态的国际化配置组件。文中的实例已经传到 GitHub，关注公众号「看山的小屋」，回复<code>spring</code>获取源码。如果是你，你会采用哪种方案呢？欢迎一起讨论。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/s/zUrx7duy0-OY1oYn8FeKOw" target="_blank" rel="noopener">SpringBoot 实战：一招实现结果的优雅响应</a></li><li><a href="https://mp.weixin.qq.com/s/H599Wri3VbjTuLBnf7pD3w" target="_blank" rel="noopener">SpringBoot 实战：如何优雅地处理异常</a></li><li><a href="https://mp.weixin.qq.com/s/fcVE-cE7tDuWKoNNX9rsoQ" target="_blank" rel="noopener">SpringBoot 实战：通过 BeanPostProcessor 动态注入 ID 生成器</a></li><li><a href="https://mp.weixin.qq.com/s/xQQbl6VNF2eCgmQPrMyc4w" target="_blank" rel="noopener">SpringBoot 实战：自定义 Filter 优雅获取请求参数和响应结果</a></li><li><a href="https://mp.weixin.qq.com/s/coKjuefWMmCA4bSkRDIRVA" target="_blank" rel="noopener">SpringBoot 实战：优雅地使用枚举参数</a></li><li><a href="https://mp.weixin.qq.com/s/pet_rA7-O1JD3QfQ1fqxAQ" target="_blank" rel="noopener">SpringBoot 实战：优雅地使用枚举参数（原理篇）</a></li><li><a href="https://mp.weixin.qq.com/s/2OXArNObVejXLvMOmE3oqw" target="_blank" rel="noopener">SpringBoot 实战：在 RequestBody 中优雅地使用枚举参数</a></li><li><a href="https://mp.weixin.qq.com/s/hdioIIwPA2lv2khfRmKhOQ" target="_blank" rel="noopener">SpringBoot 实战：在 RequestBody 中优雅地使用枚举参数（原理篇）</a></li><li><a href="https://mp.weixin.qq.com/s/vbccn0kTdyOlmznDr3RItw" target="_blank" rel="noopener">SpringBoot 实战：JUnit5+MockMvc+Mockito 做好单元测试</a></li><li><a href="https://mp.weixin.qq.com/s/s8PQXLT8Ni3dLqsOgf3syA" target="_blank" rel="noopener">SpringBoot 实战：加载和读取资源文件内容</a></li><li><a href="https://mp.weixin.qq.com/s/dGPhS5vdbnFdiTxkpr2mLg" target="_blank" rel="noopener">SpringBoot 实战：国际化组件 MessageSource</a></li><li><a href="https://mp.weixin.qq.com/s/FzX-nVMl96RwI6ITULhQcQ" target="_blank" rel="noopener">SpringBoot 实战：国际化组件MessageSource的执行逻辑与源码</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/springboot-action-messagesource-nacos/">SpringBoot 实战：国际化组件 MessageSource 与 Nacos 组合实现动态配置能力</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">SpringBoot 实战：国际化组件 MessageSource 与 Nacos 组合实现动态配置能力</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      前面介绍了 Spring 的 MessageSource 组件的用法、执行逻辑和源码，本文我们将根据前面的知识，实现自己的动态刷新的国际化组件。
    
    </summary>
    
    
      <category term="spring" scheme="https://www.howardliu.cn/categories/spring/"/>
    
    
      <category term="SpringBoot 实战" scheme="https://www.howardliu.cn/tags/SpringBoot-%E5%AE%9E%E6%88%98/"/>
    
      <category term="SpringBoot" scheme="https://www.howardliu.cn/tags/SpringBoot/"/>
    
      <category term="spring" scheme="https://www.howardliu.cn/tags/spring/"/>
    
      <category term="SpringBoot 手册" scheme="https://www.howardliu.cn/tags/SpringBoot-%E6%89%8B%E5%86%8C/"/>
    
  </entry>
  
  <entry>
    <title>SpringBoot 实战：国际化组件 MessageSource 的执行逻辑与源码</title>
    <link href="https://www.howardliu.cn/springboot-action-messagesource-principle/"/>
    <id>https://www.howardliu.cn/springboot-action-messagesource-principle/</id>
    <published>2022-08-07T13:04:19.000Z</published>
    <updated>2022-08-07T13:04:19.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/cover/squirrel-7025721_1920.jpeg" alt="SpringBoot 实战：国际化组件 MessageSource 的执行逻辑与源码"></p><p>你好，我是看山。</p><p>前文介绍了 SpringBoot 中的国际化组件<code>MessageSource</code>的使用，本章我们一起看下<code>ResourceBundleMessageSource</code>和<code>ReloadableResourceBundleMessageSource</code>的执行逻辑。SpringBoot 的 MessageSource 组件有很多抽象化，源码看起来比较分散，所以本文会通过流程图的方式进行讲解。</p><a id="more"></a><h2 id="配置文件"><a href="#配置文件" class="headerlink" title="配置文件"></a>配置文件</h2><p>配置文件是基础，会影响执行逻辑，我们先来看下配置项：</p><ul><li>basename：加载资源的文件名，可以多个资源名称，通过逗号隔开，默认是“messages”；</li><li>encoding：加载文件的字符集，默认是 UTF-8，这个不多说；</li><li>cacheDuration：文件加载到内存后缓存时间，默认单位是秒。如果没有设置，只会加载一次缓存，不会自动更新。这个参数在 ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 稍微有些差异，会具体说下。</li><li>fallbackToSystemLocale：这是一个兜底开关。默认情况下，如果在指定语言中找不到对应的值，会从 basename 参数（默认是 messages.properties）中查找，如果再找不到可能直接返回或抛错。该参数设置为 true 的话，还会再走一步兜底逻辑，从当前系统语言对应配置文件中查找。该参数默认是 true；</li><li>alwaysUseMessageFormat：MessageSource 组件通过<code>MessageFormat.format</code>函数对国际化信息格式化，如果注入参数，输出结果是经过格式化的。比如<code>MessageFormat.format(&quot;Hello, {0}!&quot;, &quot;Kanshan&quot;)</code>输出结果是“Hello, Kanshan!”。该参数控制的是，当输入参数为空时，是否还是使用<code>MessageFormat.format</code>函数对结果进行格式化，默认是 false；</li><li>useCodeAsDefaultMessage：当没有找到对应信息的时候，是否返回 code。也就是当找了所有能找的配置文件后，还是没有找到对应的信息，是否直接返回 code 值。默认是 false，即不返回 code，抛出<code>NoSuchMessageException</code>异常。</li></ul><p>这些配置参数都有各自的默认值。如果没有特殊的需求，可以直接直接按照默认约定使用。</p><h2 id="执行逻辑"><a href="#执行逻辑" class="headerlink" title="执行逻辑"></a>执行逻辑</h2><p>接下来我们看下流程图，下面的流程图绿色部分是 cacheDuration 没有配置的情况。对于 ResourceBundleMessageSource 是只加载一次配置文件，ReloadableResourceBundleMessageSource 会根据文件修改时间判断是否需要重新加载。</p><h3 id="ResourceBundleMessageSource-的流程图"><a href="#ResourceBundleMessageSource-的流程图" class="headerlink" title="ResourceBundleMessageSource 的流程图"></a>ResourceBundleMessageSource 的流程图</h3><p><img src="https://static.howardliu.cn/spring/springboot-messagesource-ResourceBundleMessageSource.png" alt="ResourceBundleMessageSource"></p><h3 id="ReloadableResourceBundleMessageSource-的流程图"><a href="#ReloadableResourceBundleMessageSource-的流程图" class="headerlink" title="ReloadableResourceBundleMessageSource 的流程图"></a>ReloadableResourceBundleMessageSource 的流程图</h3><p><img src="https://static.howardliu.cn/spring/springboot-messagesource-ReloadableResourceBundleMessageSource.png" alt="ReloadableResourceBundleMessageSource"></p><h3 id="AbstractMessageSource-的几个-getMessage-方法源码"><a href="#AbstractMessageSource-的几个-getMessage-方法源码" class="headerlink" title="AbstractMessageSource 的几个 getMessage 方法源码"></a>AbstractMessageSource 的几个 getMessage 方法源码</h3><pre><code class="java">@Overridepublic final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {    String msg = getMessageInternal(code, args, locale);    if (msg != null) {        return msg;    }    if (defaultMessage == null) {        return getDefaultMessage(code);    }    return renderDefaultMessage(defaultMessage, args, locale);}@Overridepublic final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {    String msg = getMessageInternal(code, args, locale);    if (msg != null) {        return msg;    }    String fallback = getDefaultMessage(code);    if (fallback != null) {        return fallback;    }    throw new NoSuchMessageException(code, locale);}@Overridepublic final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {    String[] codes = resolvable.getCodes();    if (codes != null) {        for (String code : codes) {            String message = getMessageInternal(code, resolvable.getArguments(), locale);            if (message != null) {                return message;            }        }    }    String defaultMessage = getDefaultMessage(resolvable, locale);    if (defaultMessage != null) {        return defaultMessage;    }    throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : &quot;&quot;, locale);}</code></pre><p>第一个<code>getMessage</code>方法，是可以传入默认值<code>defaultMessage</code>的，也就是当所有 basename 的配置文件中不存在 code 指定的值，就会使用<code>defaultMessage</code>值进行格式化返回。</p><p>第二个<code>getMessage</code>方法，是通过判断<code>useCodeAsDefaultMessage</code>配置，如果设置了 true，在所有 basename 的配置文件中不存在 code 指定的值的情况下，会返回 code 作为返回值。但是当设置为 false 时，code 不存在的情况下，会抛出<code>NoSuchMessageException</code>异常。</p><p>第三个<code>getMessage</code>方法，传入的是<code>MessageSourceResolvable</code>接口对象，查找的 code 更加多种多样。不过如果最后还是找不到，会抛出<code>NoSuchMessageException</code>异常。</p><h2 id="缓存的使用"><a href="#缓存的使用" class="headerlink" title="缓存的使用"></a>缓存的使用</h2><p>我们看源码不仅仅是为了看功能组件的实现，还是学习更加优秀的编程方式。比如下面这段内存缓存的使用，Spring 源码中很多地方都用到了这种内存缓存的使用方式：</p><pre><code class="java">// 两层 Map，第一层是 basename，第二层是 localeprivate final Map&lt;String, Map&lt;Locale, ResourceBundle&gt;&gt; cachedResourceBundles =        new ConcurrentHashMap&lt;&gt;();@Nullableprotected ResourceBundle getResourceBundle(String basename, Locale locale) {    if (getCacheMillis() &gt;= 0) {        // Fresh ResourceBundle.getBundle call in order to let ResourceBundle        // do its native caching, at the expense of more extensive lookup steps.        return doGetBundle(basename, locale);    }    else {        // Cache forever: prefer locale cache over repeated getBundle calls.        // 先从缓存中获取第一层 basename 的缓存        Map&lt;Locale, ResourceBundle&gt; localeMap = this.cachedResourceBundles.get(basename);        if (localeMap != null) {            // 如果命中第一层，在通过 locale 获取第二层的值            ResourceBundle bundle = localeMap.get(locale);            if (bundle != null) {                // 如果命中第二层缓存，直接返回                return bundle;            }        }        try {            // 走到这里，说明没有命中缓存，就根据 basename 和 locale 创建对象            ResourceBundle bundle = doGetBundle(basename, locale);            if (localeMap == null) {                // 如果 localeMap 为空，说明第一级就不存在，通过 Map 的 computeIfAbsent 方法初始化                localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -&gt; new ConcurrentHashMap&lt;&gt;());            }            // 将新建的 ResourceBundle 对象放入 localeMap 中            localeMap.put(locale, bundle);            return bundle;        }        catch (MissingResourceException ex) {            if (logger.isWarnEnabled()) {                logger.warn(&quot;ResourceBundle [&quot; + basename + &quot;] not found for MessageSource: &quot; + ex.getMessage());            }            // Assume bundle not found            // -&gt; do NOT throw the exception to allow for checking parent message source.            return null;        }    }}</code></pre><p>还有一种使用 Map 实现内存缓存的写法，比如我们就对上面的这个方法进行改写：</p><pre><code class="java">public class ResourceBundleMessageSourceExt extends ResourceBundleMessageSource {    private final Map&lt;BasenameLocale, ResourceBundle&gt; cachedResourceBundles = new ConcurrentHashMap&lt;&gt;();    @Override    protected ResourceBundle getResourceBundle(String basename, Locale locale) {        if (getCacheMillis() &gt;= 0) {            // Fresh ResourceBundle.getBundle call in order to let ResourceBundle            // do its native caching, at the expense of more extensive lookup steps.            return doGetBundle(basename, locale);        } else {            // Cache forever: prefer locale cache over repeated getBundle calls.            final BasenameLocale basenameLocale = new BasenameLocale(basename, locale);            ResourceBundle resourceBundle = this.cachedResourceBundles.get(basenameLocale);            if (resourceBundle != null) {                return resourceBundle;            }            try {                ResourceBundle bundle = doGetBundle(basename, locale);                this.cachedResourceBundles.put(basenameLocale, bundle);                return bundle;            } catch (MissingResourceException ex) {                if (logger.isWarnEnabled()) {                    logger.warn(&quot;ResourceBundle [&quot; + basename + &quot;] not found for MessageSource: &quot; + ex.getMessage());                }                // Assume bundle not found                // -&gt; do NOT throw the exception to allow for checking parent message source.                return null;            }        }    }    public record BasenameLocale(String basename, Locale locale) {        @Override        public boolean equals(Object o) {            if (this == o) {                return true;            }            if (o == null || getClass() != o.getClass()) {                return false;            }            BasenameLocale that = (BasenameLocale) o;            return basename.equals(that.basename) &amp;&amp; locale.equals(that.locale);        }        @Override        public int hashCode() {            return Objects.hash(basename, locale);        }    }}</code></pre><p>我们可以利用 Map 是通过<code>equals</code>判断 key 是否一致的原理，创建一个包含 basename、locale 的对象<code>BasenameLocale</code>，然后改写<code>cachedResourceBundles</code>为一层 Map，会简化一些判断逻辑。</p><blockquote><p>此处的<code>BasenameLocale</code>是<code>record</code>类型，具体语法可以参考 <a href="https://mp.weixin.qq.com/s/HExV39BU0lSbdtg0aMjbSA" target="_blank" rel="noopener">Java16 的新特性</a> 中的 Record 类型一节。</p></blockquote><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文先介绍了 MessageSource 的配置项，然后通过流程图的方式介绍了<code>ResourceBundleMessageSource</code>和<code>ReloadableResourceBundleMessageSource</code>的执行逻辑，最后分享了两个使用 Map 实现内存缓存的方式。</p><p>下一节我们将扩展 MessageSource，实现从 Nacos 加载配置内容，同时实现动态修改配置内容的功能。</p><p>本文中的实例已经传到 GitHub，关注公众号「看山的小屋」，回复<code>spring</code>获取源码。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/s/zUrx7duy0-OY1oYn8FeKOw" target="_blank" rel="noopener">SpringBoot 实战：一招实现结果的优雅响应</a></li><li><a href="https://mp.weixin.qq.com/s/H599Wri3VbjTuLBnf7pD3w" target="_blank" rel="noopener">SpringBoot 实战：如何优雅的处理异常</a></li><li><a href="https://mp.weixin.qq.com/s/fcVE-cE7tDuWKoNNX9rsoQ" target="_blank" rel="noopener">SpringBoot 实战：通过 BeanPostProcessor 动态注入 ID 生成器</a></li><li><a href="https://mp.weixin.qq.com/s/xQQbl6VNF2eCgmQPrMyc4w" target="_blank" rel="noopener">SpringBoot 实战：自定义 Filter 优雅获取请求参数和响应结果</a></li><li><a href="https://mp.weixin.qq.com/s/coKjuefWMmCA4bSkRDIRVA" target="_blank" rel="noopener">SpringBoot 实战：优雅的使用枚举参数</a></li><li><a href="https://mp.weixin.qq.com/s/pet_rA7-O1JD3QfQ1fqxAQ" target="_blank" rel="noopener">SpringBoot 实战：优雅的使用枚举参数（原理篇）</a></li><li><a href="https://mp.weixin.qq.com/s/2OXArNObVejXLvMOmE3oqw" target="_blank" rel="noopener">SpringBoot 实战：在 RequestBody 中优雅的使用枚举参数</a></li><li><a href="https://mp.weixin.qq.com/s/hdioIIwPA2lv2khfRmKhOQ" target="_blank" rel="noopener">SpringBoot 实战：在 RequestBody 中优雅的使用枚举参数（原理篇）</a></li><li><a href="https://mp.weixin.qq.com/s/vbccn0kTdyOlmznDr3RItw" target="_blank" rel="noopener">SpringBoot 实战：JUnit5+MockMvc+Mockito 做好单元测试</a></li><li><a href="https://mp.weixin.qq.com/s/s8PQXLT8Ni3dLqsOgf3syA" target="_blank" rel="noopener">SpringBoot 实战：加载和读取资源文件内容</a></li><li><a href="https://mp.weixin.qq.com/s/dGPhS5vdbnFdiTxkpr2mLg" target="_blank" rel="noopener">SpringBoot 实战：国际化组件 MessageSource</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/springboot-action-messagesource-principle/">SpringBoot 实战：国际化组件 MessageSource 的执行逻辑与源码</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">SpringBoot 实战：国际化组件 MessageSource 的执行逻辑与源码</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      掌握 MessageSource 的原理，我们才能更好的扩展。
    
    </summary>
    
    
      <category term="spring" scheme="https://www.howardliu.cn/categories/spring/"/>
    
    
      <category term="SpringBoot 实战" scheme="https://www.howardliu.cn/tags/SpringBoot-%E5%AE%9E%E6%88%98/"/>
    
      <category term="SpringBoot" scheme="https://www.howardliu.cn/tags/SpringBoot/"/>
    
      <category term="spring" scheme="https://www.howardliu.cn/tags/spring/"/>
    
      <category term="SpringBoot 手册" scheme="https://www.howardliu.cn/tags/SpringBoot-%E6%89%8B%E5%86%8C/"/>
    
  </entry>
  
  <entry>
    <title>SpringBoot 实战：国际化组件 MessageSource</title>
    <link href="https://www.howardliu.cn/springboot-action-messagesource/"/>
    <id>https://www.howardliu.cn/springboot-action-messagesource/</id>
    <published>2022-07-30T03:26:55.000Z</published>
    <updated>2022-07-30T03:26:55.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://static.howardliu.cn/cover/taiwan-3511017.jpeg" alt="SpringBoot 实战：国际化组件 MessageSource"></p><p>你好，我是看山。</p><p>咱们今天一起来聊聊 SpringBoot 中的国际化组件 MessageSource。</p><a id="more"></a><h2 id="初识-MessageSource"><a href="#初识-MessageSource" class="headerlink" title="初识 MessageSource"></a>初识 MessageSource</h2><p>先看一下类图：</p><p><img src="https://static.howardliu.cn/spring/MessageSource.png" alt="MessageSource 类图"></p><p>从类图可以看到，Spring 内置的<code>MessageSource</code>有三个实现类：</p><ul><li>ResourceBundleMessageSource：通过 JDK 提供的 ResourceBundle 加载资源文件；</li><li>ReloadableResourceBundleMessageSource：通过 PropertiesPersister 加载资源，支持 xml、properties 两个格式，优先加载 properties 格式的文件。如果同时存在 properties 和 xml 的文件，会只加载 properties 的内容；</li><li>StaticMessageSource：是手动注入国际化内容，相当于手写代码。因为比较简单，而且实际用处不大，所以暂时不做讨论。</li></ul><p>在 SpringBoot 中，默认创建 ResourceBundleMessageSource 实例实现国际化输出。标准的配置通过<code>MessageSourceProperties</code>类注入：</p><ul><li>basename：加载资源的文件名，可以多个资源名称，通过逗号隔开，默认是“messages”；</li><li>encoding：加载文件的字符集，默认是 UTF-8，这个不多说；</li><li>cacheDuration：文件加载到内存后缓存时间，默认单位是秒。如果没有设置，只会加载一次缓存，不会自动更新。这个参数在 ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 稍微有些差异，会具体说下。</li><li>fallbackToSystemLocale：这是一个兜底开关。默认情况下，如果在指定语言中找不到对应的值，会从 basename 参数（默认是 messages.properties）中查找，如果再找不到可能直接返回或抛错。该参数设置为 true 的话，还会再走一步兜底逻辑，从当前系统语言对应配置文件中查找。该参数默认是 true；</li><li>alwaysUseMessageFormat：MessageSource 组件通过<code>MessageFormat.format</code>函数对国际化信息格式化，如果注入参数，输出结果是经过格式化的。比如<code>MessageFormat.format(&quot;Hello, {0}!&quot;, &quot;Kanshan&quot;)</code>输出结果是“Hello, Kanshan!”。该参数控制的是，当输入参数为空时，是否还是使用<code>MessageFormat.format</code>函数对结果进行格式化，默认是 false；</li><li>useCodeAsDefaultMessage：当没有找到对应信息的时候，是否返回 code。也就是当找了所有能找的配置文件后，还是没有找到对应的信息，是否直接返回 code 值。默认是 false，即不返回 code，抛出<code>NoSuchMessageException</code>异常。</li></ul><h2 id="小试牛刀"><a href="#小试牛刀" class="headerlink" title="小试牛刀"></a>小试牛刀</h2><p>从上面我们知道了一些简单的配置，但是还是没有办法知道 MessageSource 到底是什么，本节我们举个例子小试牛刀。</p><p>首先从<a href="https://start.spring.io/" target="_blank" rel="noopener">https://start.spring.io/</a>创建一个最少依赖<code>spring-boot-starter-web</code>的 SpringBoot 项目。</p><p>然后在 resources 目录下定义一组国际化配置文件，我们这里使用默认配置，所以 basename 是 messages：</p><pre><code class="properties">## messages.propertiesmessage.code1=[DEFAULT]code onemessage.code2=[DEFAULT]code twomessage.code3=[DEFAULT]code threemessage.code4=[DEFAULT]code fourmessage.code5=[DEFAULT]code fivemessage.code6=[DEFAULT]code six## messages_en.propertiesmessage.code2=[en]code two## messages_en_US.propertiesmessage.code3=[en_US]code three## messages_zh.propertiesmessage.code4=[中文] 丁字号## messages_zh_CN.propertiesmessage.code5=[大陆区域中文] 戊字号## messages_zh_Hans.propertiesmessage.code6=[简体中文] 己字号</code></pre><p>一个定义了六个配置文件：</p><ul><li>messages.properties：默认配置文件</li><li>messages_en.properties：英文配置文件</li><li>messages_en_US.properties：英文美国配置文件</li><li>messages_zh.properties：中文配置文件</li><li>messages_zh_CN.properties：中文中国大陆区域配置文件</li><li>messages_zh_Hans.properties：简体中文配置文件</li></ul><p>从上面配置文件的命名可以看出，都是以 basename 开头，后面跟上语系和地区，三个参数以下划线分隔。</p><p>可以支持的语言和国家可以从<code>java.util.Locale</code>查找。</p><p>最后我们定义一个 Controller 实验：</p><pre><code class="java">@RestControllerpublic class HelloController {    @Autowired    private MessageSource messageSource;    @GetMapping(&quot;m1&quot;)    public List&lt;String&gt; m1(Locale locale) {        final List&lt;String&gt; multi = new ArrayList&lt;&gt;();        multi.add(messageSource.getMessage(&quot;message.code1&quot;, null, locale));        multi.add(messageSource.getMessage(&quot;message.code2&quot;, null, locale));        multi.add(messageSource.getMessage(&quot;message.code3&quot;, null, locale));        multi.add(messageSource.getMessage(&quot;message.code4&quot;, null, locale));        multi.add(messageSource.getMessage(&quot;message.code5&quot;, null, locale));        multi.add(messageSource.getMessage(&quot;message.code6&quot;, null, locale));        return multi;    }}</code></pre><p>我们通过不同的请求查看结果：</p><pre><code class="http">### 默认GET http://localhost:8080/m1### 结果是：[  &quot;[DEFAULT]code one&quot;,  &quot;[DEFAULT]code two&quot;,  &quot;[DEFAULT]code three&quot;,  &quot;[中文] 丁字号&quot;,  &quot;[大陆区域中文] 戊字号&quot;,  &quot;[简体中文] 己字号&quot;]### local: enGET http://localhost:8080/m1Accept-Language: en### 结果是：[  &quot;[DEFAULT]code one&quot;,  &quot;[en]code two&quot;,  &quot;[DEFAULT]code three&quot;,  &quot;[DEFAULT]code four&quot;,  &quot;[DEFAULT]code five&quot;,  &quot;[DEFAULT]code six&quot;]### local: en-USGET http://localhost:8080/m1Accept-Language: en-US### 结果是：[  &quot;[DEFAULT]code one&quot;,  &quot;[en]code two&quot;,  &quot;[en_US]code three&quot;,  &quot;[DEFAULT]code four&quot;,  &quot;[DEFAULT]code five&quot;,  &quot;[DEFAULT]code six&quot;]### local: zhGET http://localhost:8080/m1Accept-Language: zh### 结果是：[  &quot;[DEFAULT]code one&quot;,  &quot;[DEFAULT]code two&quot;,  &quot;[DEFAULT]code three&quot;,  &quot;[中文] 丁字号&quot;,  &quot;[DEFAULT]code five&quot;,  &quot;[DEFAULT]code six&quot;]### local: zh-CNGET http://localhost:8080/m1Accept-Language: zh-CN### 结果是：[  &quot;[DEFAULT]code one&quot;,  &quot;[DEFAULT]code two&quot;,  &quot;[DEFAULT]code three&quot;,  &quot;[中文] 丁字号&quot;,  &quot;[大陆区域中文] 戊字号&quot;,  &quot;[DEFAULT]code six&quot;]</code></pre><p>从上面的结果可以看出：</p><ol><li>默认情况下，HTTP 请求没有传语言，所以使用了系统语言组装，相当于传参是<code>zh-Hans</code>，所以结果是简体中文优先；</li><li>HTTP 请求定义的语言越精确，匹配的内容越精确；</li><li>默认情况下，指定语言配置文件找不到，会一次向上查找，地区 &gt; 国家 &gt; 语言 &gt; 默认。</li></ol><h2 id="带参数的国际化信息"><a href="#带参数的国际化信息" class="headerlink" title="带参数的国际化信息"></a>带参数的国际化信息</h2><p>我们在 message.properties 中添加一行配置：</p><pre><code class="properties">message.multiVars=var1={0}, var2={1}</code></pre><p>在刚才的 Controller 中增加一个请求：</p><pre><code class="java">@GetMapping(&quot;m2&quot;)public List&lt;String&gt; m2(Locale locale) {    final List&lt;String&gt; multi = new ArrayList&lt;&gt;();    multi.add(&quot;参数为 null: &quot; + messageSource.getMessage(&quot;message.multiVars&quot;, null, locale));    multi.add(&quot;参数为空：&quot; + messageSource.getMessage(&quot;message.multiVars&quot;, new Object[]{}, locale));    multi.add(&quot;只传一个参数：&quot; + messageSource.getMessage(&quot;message.multiVars&quot;, new Object[]{&quot;第一个参数&quot;}, locale));    multi.add(&quot;传两个参数：&quot; + messageSource.getMessage(&quot;message.multiVars&quot;, new Object[]{&quot;第一个参数&quot;, &quot;第二个参数&quot;}, locale));    multi.add(&quot;传超过两个参数：&quot; + messageSource.getMessage(&quot;message.multiVars&quot;, new Object[]{&quot;第一个参数&quot;, &quot;第二个参数&quot;, &quot;第三个参数&quot;}, locale));    return multi;}</code></pre><p>我们看看结果：</p><pre><code class="http">###GET http://localhost:8080/m2### 结果是：[  &quot;参数为 null: var1={0}, var2={1}&quot;,  &quot;参数为空：var1={0}, var2={1}&quot;,  &quot;只传一个参数：var1=第一个参数，var2={1}&quot;,  &quot;传两个参数：var1=第一个参数，var2=第二个参数&quot;,  &quot;传超过两个参数：var1=第一个参数，var2=第二个参数&quot;]</code></pre><p>我们可以看到，我们在配置文件中定义了带参数的配置信息，此时，我们可以不传参数、传少于指定数量的参数、传符合指定数量的参数、传超过指定数量的参数，都可以正常返回国际化信息。</p><p>此处可以理解为，<code>MessageFormat.format</code>执行过程是<code>for-index</code>循环，从配置值中找格式为<code>{数字}</code>的占位符，然后用对应下标的输入参数替换，如果属于参数没了，就保持原样。</p><h2 id="找不到配置内容"><a href="#找不到配置内容" class="headerlink" title="找不到配置内容"></a>找不到配置内容</h2><p>如果我们的配置文件中没有配置或者对应语言及其父级都没有配置呢？</p><p>这个就要靠前面说的<code>useCodeAsDefaultMessage</code>配置了，如果为 true，就会返回输入的 code，如果为 false，就会抛出异常。默认是 false，所以如果找不到会抛异常。比如：</p><pre><code class="java">@GetMapping(&quot;m3&quot;)public List&lt;String&gt; m3(Locale locale) {    final List&lt;String&gt; multi = new ArrayList&lt;&gt;();    multi.add(&quot;不存在的 code: &quot; + messageSource.getMessage(&quot;message.notExist&quot;, null, locale));    return multi;}</code></pre><p>这个时候我们执行 http 请求：</p><pre><code class="http">###GET http://localhost:8080/m3### 结果是：{  &quot;timestamp&quot;: &quot;2022-06-19T09:14:14.977+00:00&quot;,  &quot;status&quot;: 500,  &quot;error&quot;: &quot;Internal Server Error&quot;,  &quot;path&quot;: &quot;/m3&quot;}</code></pre><p>这是报错了，异常栈是：</p><pre><code class="log">org.springframework.context.NoSuchMessageException: No message found under code &#39;message.notExist&#39; for locale &#39;zh_CN_#Hans&#39;.    at org.springframework.context.support.AbstractMessageSource.getMessage(AbstractMessageSource.java:161) ~[spring-context-5.3.20.jar:5.3.20]    at cn.howardliu.effective.spring.springbootmessages.controller.HelloController.m3(HelloController.java:47) ~[classes/:na]    ……此处省略</code></pre><h2 id="自定义-MessageSource"><a href="#自定义-MessageSource" class="headerlink" title="自定义 MessageSource"></a>自定义 MessageSource</h2><p>本文开头说过，MessageSource 有三种实现，Spring 默认使用了 ResourceBundleMessageSource，我们可以自定义使用 ReloadableResourceBundleMessageSource。</p><p>既然是在 SpringBoot 中，我们可以依靠 SpringBoot 的特性定义：</p><pre><code class="java">@Configuration(proxyBeanMethods = false)@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)@ConditionalOnProperty(name = &quot;spring.messages-type&quot;, havingValue = &quot;ReloadableResourceBundleMessageSource&quot;)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@Conditional(ReloadResourceBundleCondition.class)@EnableConfigurationPropertiespublic class ReloadMessageSourceAutoConfiguration {    private static final Resource[] NO_RESOURCES = {};    @Bean    @ConfigurationProperties(prefix = &quot;spring.messages&quot;)    public MessageSourceProperties messageSourceProperties() {        return new MessageSourceProperties();    }    @Bean    public MessageSource messageSource(MessageSourceProperties properties) {        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();        if (StringUtils.hasText(properties.getBasename())) {            final String[] originBaseNames = StringUtils                    .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename()));            final String[] baseNames = new String[originBaseNames.length];            for (int i = 0; i &lt; originBaseNames.length; i++) {                if (originBaseNames[i].startsWith(&quot;classpath:&quot;)) {                    baseNames[i] = originBaseNames[i];                } else {                    baseNames[i] = &quot;classpath:&quot; + originBaseNames[i];                }            }            messageSource.setBasenames(baseNames);        }        if (properties.getEncoding() != null) {            messageSource.setDefaultEncoding(properties.getEncoding().name());        }        messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());        Duration cacheDuration = properties.getCacheDuration();        if (cacheDuration != null) {            messageSource.setCacheMillis(cacheDuration.toMillis());        }        messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());        messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());        return messageSource;    }    protected static class ReloadResourceBundleCondition extends SpringBootCondition {        private static final ConcurrentReferenceHashMap&lt;String, ConditionOutcome&gt; CACHE =                new ConcurrentReferenceHashMap&lt;&gt;();        @Override        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {            String basename = context.getEnvironment().getProperty(&quot;spring.messages.basename&quot;, &quot;messages&quot;);            ConditionOutcome outcome = CACHE.get(basename);            if (outcome == null) {                outcome = getMatchOutcomeForBasename(context, basename);                CACHE.put(basename, outcome);            }            return outcome;        }        private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {            ConditionMessage.Builder message = ConditionMessage.forCondition(&quot;ResourceBundle&quot;);            for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {                for (Resource resource : getResources(context.getClassLoader(), name)) {                    if (resource.exists()) {                        return ConditionOutcome.match(message.found(&quot;bundle&quot;).items(resource));                    }                }            }            return ConditionOutcome.noMatch(message.didNotFind(&quot;bundle with basename &quot; + basename).atAll());        }        private Resource[] getResources(ClassLoader classLoader, String name) {            String target = name.replace(&#39;.&#39;, &#39;/&#39;);            try {                return new PathMatchingResourcePatternResolver(classLoader)                        .getResources(&quot;classpath*:&quot; + target + &quot;.properties&quot;);            } catch (Exception ex) {                return NO_RESOURCES;            }        }    }}</code></pre><p>我们可以看到，我们在执行<code>messageSource.setBasenames(baseNames);</code>的时候，<code>baseNames</code>中的值都是设置成<code>classpath:</code>开头的，这是为了使<code>ReloadableResourceBundleMessageSource</code>能够读取 CLASSPATH 下的配置文件。当然也可以使用绝对路径或者相对路径实现，这个是比较灵活的。</p><p>我们可以通过修改配置文件内容，查看变化，这里就不再赘述。纸上得来终觉浅，绝知此事要躬行。</p><h2 id="文末总结"><a href="#文末总结" class="headerlink" title="文末总结"></a>文末总结</h2><p>本文通过几个小例子介绍了<code>MessageSource</code>的使用。这里做一下预告，下一章我们会从源码角度分析<code>MessageSourc</code>e 的实现类<code>ResourceBundleMessageSource</code>和<code>ReloadableResourceBundleMessageSource</code>的执行逻辑；然后我们自定义扩展，从 Nacos 中读取配置内容，实现更加灵活的配置。</p><p>本文中的实例已经传到 GitHub，关注公众号「看山的小屋」，回复<code>spring</code>获取源码。</p><p>青山不改，绿水长流，我们下次见。</p><h2 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h2><ul><li><a href="https://mp.weixin.qq.com/s/zUrx7duy0-OY1oYn8FeKOw" target="_blank" rel="noopener">SpringBoot 实战：一招实现结果的优雅响应</a></li><li><a href="https://mp.weixin.qq.com/s/H599Wri3VbjTuLBnf7pD3w" target="_blank" rel="noopener">SpringBoot 实战：如何优雅的处理异常</a></li><li><a href="https://mp.weixin.qq.com/s/fcVE-cE7tDuWKoNNX9rsoQ" target="_blank" rel="noopener">SpringBoot 实战：通过 BeanPostProcessor 动态注入 ID 生成器</a></li><li><a href="https://mp.weixin.qq.com/s/xQQbl6VNF2eCgmQPrMyc4w" target="_blank" rel="noopener">SpringBoot 实战：自定义 Filter 优雅获取请求参数和响应结果</a></li><li><a href="https://mp.weixin.qq.com/s/coKjuefWMmCA4bSkRDIRVA" target="_blank" rel="noopener">SpringBoot 实战：优雅的使用枚举参数</a></li><li><a href="https://mp.weixin.qq.com/s/pet_rA7-O1JD3QfQ1fqxAQ" target="_blank" rel="noopener">SpringBoot 实战：优雅的使用枚举参数（原理篇）</a></li><li><a href="https://mp.weixin.qq.com/s/2OXArNObVejXLvMOmE3oqw" target="_blank" rel="noopener">SpringBoot 实战：在 RequestBody 中优雅的使用枚举参数</a></li><li><a href="https://mp.weixin.qq.com/s/hdioIIwPA2lv2khfRmKhOQ" target="_blank" rel="noopener">SpringBoot 实战：在 RequestBody 中优雅的使用枚举参数（原理篇）</a></li><li><a href="https://mp.weixin.qq.com/s/vbccn0kTdyOlmznDr3RItw" target="_blank" rel="noopener">SpringBoot 实战：JUnit5+MockMvc+Mockito 做好单元测试</a></li><li><a href="https://mp.weixin.qq.com/s/s8PQXLT8Ni3dLqsOgf3syA" target="_blank" rel="noopener">SpringBoot 实战：加载和读取资源文件内容</a></li><li><a href="https://mp.weixin.qq.com/s/dGPhS5vdbnFdiTxkpr2mLg" target="_blank" rel="noopener">SpringBoot 实战：国际化组件 MessageSource</a></li></ul><hr><p>你好，我是看山。游于码界，戏享人生。如果文章对您有帮助，请点赞、收藏、关注。我还整理了一些精品学习资料，关注公众号「看山的小屋」，回复“资料”即可获得。</p><p>个人主页：<a href="https://www.howardliu.cn">https://www.howardliu.cn</a><br>个人博文：<a href="https://www.howardliu.cn/springboot-action-messagesource/">SpringBoot 实战：国际化组件 MessageSource</a><br>CSDN 主页：<a href="https://kanshan.blog.csdn.net/" target="_blank" rel="noopener">https://kanshan.blog.csdn.net/</a><br>CSDN 博文：<a href="https://kanshan.blog.csdn.net/article/details/126069965" target="_blank" rel="noopener">SpringBoot 实战：国际化组件 MessageSource</a></p><center><b>👇🏻欢迎关注我的公众号「看山的小屋」，领取精选资料👇🏻</b></center><p><img src="http://static.howardliu.cn/about/kanshanshuo.png" alt="公众号：看山的小屋"></p>]]></content>
    
    <summary type="html">
    
      咱们今天一起来聊聊 SpringBoot 中的国际化组件 MessageSource。
    
    </summary>
    
    
      <category term="spring" scheme="https://www.howardliu.cn/categories/spring/"/>
    
    
      <category term="SpringBoot 实战" scheme="https://www.howardliu.cn/tags/SpringBoot-%E5%AE%9E%E6%88%98/"/>
    
      <category term="SpringBoot" scheme="https://www.howardliu.cn/tags/SpringBoot/"/>
    
      <category term="spring" scheme="https://www.howardliu.cn/tags/spring/"/>
    
      <category term="SpringBoot 手册" scheme="https://www.howardliu.cn/tags/SpringBoot-%E6%89%8B%E5%86%8C/"/>
    
  </entry>
  
</feed>
