绿源和天下 發表於 2025-12-26 11:23:00

使用Java Stream,将集合转换为一对一Map

<p>在日常的开发工作中,我们经常使用到Java Stream,特别是Stream API中提供的<code>Collectors.toList()</code>收集器,</p>
<p>但有些场景下,我们需要将集合转换为Map,这时候就需要使用到Stream API中提供的另一个收集器:</p>
<p><code>Collectors.toMap</code>,它可以将流中的元素映射为键值对,并收集到一个Map中。</p>
<h2 id="1-三种主要的重载方法">1. 三种主要的重载方法</h2>
<p><code>Collectors.toMap</code>有3种重载方法,分别是:</p>
<p>1)<strong>两个参数的重载方法(最简单的形式)</strong></p>
<pre><code class="language-java">public static &lt;T, K, U&gt; Collector&lt;T, ?, Map&lt;K,U&gt;&gt; toMap(Function&lt;? super T, ? extends K&gt; keyMapper,
                              Function&lt;? super T, ? extends U&gt; valueMapper) {
        return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
</code></pre>
<p>2)<strong>三个参数的重载方法(包含冲突处理)</strong></p>
<pre><code class="language-java">public static &lt;T, K, U&gt; Collector&lt;T, ?, Map&lt;K,U&gt;&gt; toMap(Function&lt;? super T, ? extends K&gt; keyMapper,
                              Function&lt;? super T, ? extends U&gt; valueMapper,
                              BinaryOperator&lt;U&gt; mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
</code></pre>
<p>3)<strong>四个参数的重载方法(指定Map实现)</strong></p>
<pre><code class="language-java">public static &lt;T, K, U, M extends Map&lt;K, U&gt;&gt;
   Collector&lt;T, ?, M&gt; toMap(Function&lt;? super T, ? extends K&gt; keyMapper,
                            Function&lt;? super T, ? extends U&gt; valueMapper,
                            BinaryOperator&lt;U&gt; mergeFunction,
                            Supplier&lt;M&gt; mapSupplier) {
    BiConsumer&lt;M, T&gt; accumulator
            = (map, element) -&gt; map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);
    return new CollectorImpl&lt;&gt;(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}
</code></pre>
<p>接下来,我们结合使用示例详细讲解。</p>
<h2 id="2-使用示例">2. 使用示例</h2>
<h3 id="21-将对象的某些属性转换为map">2.1 将对象的某些属性转换为Map</h3>
<p>假设有一个城市列表,需要将其转换为Map,其中Key为城市ID、Value为城市名称,转换方法如下所示:</p>
<pre><code class="language-java">@Getter
@Setter
public class City {
    private Integer cityId;

    private String cityName;

    public City(Integer cityId, String cityName) {
      this.cityId = cityId;
      this.cityName = cityName;
    }
}
</code></pre>
<pre><code class="language-java">List&lt;City&gt; cityList = Arrays.asList(
      new City(1, "北京"),
      new City(2, "上海"),
      new City(3, "广州"),
      new City(4, "深圳")
);
Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println(cityMap);
</code></pre>
<p>输出结果:</p>
<blockquote 1="北京," 2="上海," 3="广州," 4="深圳">
<p></p>
</blockquote>
<h3 id="22-将对象列表转换为mapid---对象">2.2 将对象列表转换为Map(ID -&gt; 对象)</h3>
<p>仍然使用上面的城市列表,需要将其转换为Map,其中Key为城市ID、Value为城市对象,转换方法如下所示:</p>
<pre><code class="language-java">List&lt;City&gt; cityList = Arrays.asList(
      new City(1, "北京"),
      new City(2, "上海"),
      new City(3, "广州"),
      new City(4, "深圳")
);
Map&lt;Integer, City&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, city -&gt; city));
City city = cityMap.get(1);
System.out.println("城市ID: " + city.getCityId());
System.out.println("城市名称: " + city.getCityName());
</code></pre>
<p>输出结果如下所示:</p>
<blockquote>
<p>城市ID: 1<br>
城市名称: 北京</p>
</blockquote>
<p>上面的写法等价于:</p>
<pre><code class="language-java">Map&lt;Integer, City&gt; cityMap = cityList.stream()
            .collect(Collectors.toMap(City::getCityId, Function.identity()));
</code></pre>
<p>因为<code>Function.identity()</code>内部实现是下面这样的:</p>
<pre><code class="language-java">static &lt;T&gt; Function&lt;T, T&gt; identity() {
    return t -&gt; t;
}
</code></pre>
<h3 id="23-键冲突处理">2.3 键冲突处理</h3>
<p>假设上面的城市列表中有一个ID重复的城市:</p>
<pre><code class="language-java">List&lt;City&gt; cityList = Arrays.asList(
      new City(1, "北京"),
      new City(2, "上海"),
      new City(3, "广州"),
      new City(4, "深圳"),
      new City(4, "天津")
);
Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println("城市ID: 4, 城市名称: " + cityMap.get(4));
</code></pre>
<p>此时运行代码,会抛出<code>java.lang.IllegalStateException</code>异常,如下图所示:</p>
<p><img src="https://images.zwwhnly.com/picture/2025/snipaste_20251225_172725.png" alt="" loading="lazy"></p>
<p>有3种常见的键冲突处理方式,分别是保留旧值、使用新值和合并值,接下来一一讲解。</p>
<p>1)方式一:保留旧值</p>
<pre><code class="language-java">Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -&gt; oldValue));
</code></pre>
<p>输出结果:</p>
<blockquote>
<p>城市ID: 4, 城市名称: 深圳</p>
</blockquote>
<p>2)方式二:使用新值</p>
<pre><code class="language-java">Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -&gt; newValue));
</code></pre>
<p>输出结果:</p>
<blockquote>
<p>城市ID: 4, 城市名称: 天津</p>
</blockquote>
<p>3)方式三:合并值</p>
<pre><code class="language-java">Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName,
                      (oldValue, newValue) -&gt; oldValue + ", " + newValue));
</code></pre>
<p>输出结果:</p>
<blockquote>
<p>城市ID: 4, 城市名称: 深圳, 天津</p>
</blockquote>
<h3 id="24-数据分组聚合">2.4 数据分组聚合</h3>
<p>假设有一个销售记录列表,需要将其转换为Map,其中Key为销售员、Value为该销售员的总销售额,转换方法如下所示:</p>
<pre><code class="language-java">@Getter
@Setter
public class SalesRecord {
    private String salesPerson;

    private BigDecimal amount;

    public SalesRecord(String salesPerson, BigDecimal amount) {
      this.salesPerson = salesPerson;
      this.amount = amount;
    }
}
</code></pre>
<pre><code class="language-java">List&lt;SalesRecord&gt; salesRecordList = Arrays.asList(
      new SalesRecord("张三", new BigDecimal("1000")),
      new SalesRecord("李四", new BigDecimal("2000")),
      new SalesRecord("张三", new BigDecimal("980"))
);

Map&lt;String, BigDecimal&gt; salesRecordMap = salesRecordList.stream()
      .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::add));
System.out.println(salesRecordMap);
</code></pre>
<p>输出结果:</p>
<blockquote>
<p></p>
</blockquote>
<p>上面的例子是销售额累加,也可以只取最小值:</p>
<pre><code class="language-java">Map&lt;String, BigDecimal&gt; salesRecordMap = salesRecordList.stream()
      .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::min));
</code></pre>
<p>此时的输出结果:</p>
<blockquote>
<p></p>
</blockquote>
<p>或者只取最大值:</p>
<pre><code class="language-java">Map&lt;String, BigDecimal&gt; salesRecordMap = salesRecordList.stream()
      .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::max));
</code></pre>
<p>此时的输出结果:</p>
<blockquote>
<p></p>
</blockquote>
<h3 id="25-指定map实现">2.5 指定Map实现</h3>
<p>默认情况下,<code>Collectors.toMap</code>是将结果收集到HashMap中,如果有需要,我们也可以指定成TreeMap或者LinkedHashMap。</p>
<p>如果想要保持插入顺序,可以指定使用LinkedHashMap:</p>
<pre><code class="language-java">List&lt;City&gt; cityList = Arrays.asList(
      new City(2, "上海"),
      new City(1, "北京"),
      new City(4, "深圳"),
      new City(3, "广州")
);
Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName,
                (existing, replacement) -&gt; existing, LinkedHashMap::new));
System.out.println(cityMap);
</code></pre>
<p>输出结果:</p>
<blockquote 2="上海," 1="北京," 4="深圳," 3="广州">
<p></p>
</blockquote>
<p>如果想要按键排序,可以指定使用TreeMap:</p>
<pre><code class="language-java">List&lt;City&gt; cityList = Arrays.asList(
      new City(2, "上海"),
      new City(1, "北京"),
      new City(4, "深圳"),
      new City(3, "广州")
);
Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName,
                (existing, replacement) -&gt; existing, TreeMap::new));
System.out.println(cityMap);
</code></pre>
<p>输出结果:</p>
<blockquote 1="北京," 2="上海," 3="广州," 4="深圳">
<p></p>
</blockquote>
<h2 id="3-注意事项">3. 注意事项</h2>
<h3 id="31-空异常">3.1 空异常</h3>
<p>如果valueMapper中取出的值有null值,会抛出<code>java.lang.NullPointerException</code>异常,如下示例:</p>
<pre><code class="language-java">List&lt;City&gt; cityList = Arrays.asList(
      new City(1, "北京"),
      new City(2, "上海"),
      new City(3, "广州"),
      new City(4, "深圳"),
      new City(5, null)
);
Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println(cityMap);
</code></pre>
<p>运行以上代码会抛出异常,如下图所示:</p>
<p><img src="https://images.zwwhnly.com/picture/2025/snipaste_20251226_104553.png" alt="" loading="lazy"></p>
<p>有两种解决方案,第一种解决方案是过滤null值:</p>
<pre><code class="language-java">Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .filter(city -&gt; city.getCityName() != null)
      .collect(Collectors.toMap(City::getCityId, City::getCityName));
</code></pre>
<p>第二种解决方案是提供默认值:</p>
<pre><code class="language-java">Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId,
                city -&gt; Optional.ofNullable(city.getCityName()).orElse("未知")));
</code></pre>
<h3 id="32-键重复异常">3.2 键重复异常</h3>
<p>如果出现重复键,且没有提供mergeFunction参数,会抛出<code>java.lang.IllegalStateException</code>异常,如下示例:</p>
<pre><code class="language-java">List&lt;City&gt; cityList = Arrays.asList(
      new City(1, "北京"),
      new City(2, "上海"),
      new City(3, "广州"),
      new City(4, "深圳"),
      new City(4, "天津")
);
Map&lt;Integer, String&gt; cityMap = cityList.stream()
      .collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println(cityMap);
</code></pre>
<p>运行以上代码会抛出异常,如下图所示:</p>
<p><img src="https://images.zwwhnly.com/picture/2025/snipaste_20251225_172725.png" alt="" loading="lazy"></p>
<p>解决方案见本篇文章<strong>2.3 键冲突处理</strong>部分。</p>
<h2 id="4-总结">4. 总结</h2>
<p><code>Collectors.toMap</code>是Stream API中提供的一个非常方便的收集器,它可以将流中的元素映射为键值对,并收集到一个Map中。</p>
<p>它适用于一对一映射的场景,但在使用时,要注意避免<code>java.lang.NullPointerException</code>异常和</p>
<p><code>java.lang.IllegalStateException</code>异常。</p>
<blockquote>
<p>文章持续更新,欢迎关注微信公众号「申城异乡人」第一时间阅读!</p>
</blockquote><br><br>
来源:https://www.cnblogs.com/zwwhnly/p/19403765
頁: [1]
查看完整版本: 使用Java Stream,将集合转换为一对一Map