【问题标题】:How do I keep the iteration order of a List when using Collections.toMap() on a stream?在流上使用 Collections.toMap() 时如何保持 List 的迭代顺序?
【发布时间】:2015-05-19 08:40:21
【问题描述】:

我正在从List 创建一个Map,如下所示:

List<String> strings = Arrays.asList("a", "bb", "ccc");

Map<String, Integer> map = strings.stream()
    .collect(Collectors.toMap(Function.identity(), String::length));

我想保持与List 中相同的迭代顺序。如何使用Collectors.toMap() 方法创建LinkedHashMap

【问题讨论】:

  • 请在下面查看我的答案。对于 streamcollect 方法,使用自定义 SupplierAccumulatorCombiner 只需 4 行代码 :)
  • 问题已经回答了,我只是想列出找到这个问题答案的路径。(1)你想要在地图中排序,你必须使用LinkedHashMap(2)Collectors.toMap () 有很多实现,其中一个需要一个 Map。所以在它期望 Map 的地方使用 LinkedHashMap。

标签: java collections java-8 java-stream


【解决方案1】:

2-parameter version of Collectors.toMap() 使用HashMap

public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
    Function<? super T, ? extends K> keyMapper, 
    Function<? super T, ? extends U> valueMapper) 
{
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

要使用4-parameter version,可以替换:

Collectors.toMap(Function.identity(), String::length)

与:

Collectors.toMap(
    Function.identity(), 
    String::length, 
    (u, v) -> {
        throw new IllegalStateException(String.format("Duplicate key %s", u));
    }, 
    LinkedHashMap::new
)

或者为了更简洁,编写一个新的toLinkedMap() 方法并使用它:

public class MoreCollectors
{
    public static <T, K, U> Collector<T, ?, Map<K,U>> toLinkedMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends U> valueMapper)
    {
        return Collectors.toMap(
            keyMapper,
            valueMapper, 
            (u, v) -> {
                throw new IllegalStateException(String.format("Duplicate key %s", u));
            },
            LinkedHashMap::new
        );
    }
}

【讨论】:

  • 为何如此复杂?您可以轻松做到这一点,请在下面查看我的答案
  • mergeFunction 在 4 参数版本中,Collectors.toMap() 不知道要合并哪个键,uv 都是值。所以关于 IllegalStateException 的消息是不对的。
  • @hzitoun 因为如果你只是使用Map::put,你最终会得到(可能)同一个键不同的值。通过使用Map::put,您间接选择了您遇到的第一个值不正确。这是最终用户想要的吗?你确定吗?如果是,那么请使用Map::put。否则,您不确定也无法决定:让用户知道他们的流映射到两个具有不同值的相同键。我会评论你自己的答案,但它目前已锁定。
【解决方案2】:

制作自己的SupplierAccumulatorCombiner

List<String> myList = Arrays.asList("a", "bb", "ccc"); 
// or since java 9 List.of("a", "bb", "ccc");
    
LinkedHashMap<String, Integer> mapInOrder = myList
                        .stream()
                        .collect(
                          LinkedHashMap::new,                           // Supplier
                          (map, item) -> map.put(item, item.length()),  // Accumulator
                          Map::putAll);                                 // Combiner

System.out.println(mapInOrder);  // {a=1, bb=2, ccc=3}

【讨论】:

  • @Sushil 接受的答案允许重用逻辑
  • 你知道什么更快吗?使用您的版本或接受的答案?
  • @Nikolas 你能解释一下你对副作用的意思吗?我实际上有决定选择哪一个的问题:stackoverflow.com/questions/61479650/…
  • @NikolasCharalambidis,有什么副作用?是一个蓄能器。 java.util.stream.Collectors#uniqKeysMapAccumulator 做同样的事情。
【解决方案3】:

在 Kotlin 中,toMap() 是保序的。

fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V>

返回一个包含给定对集合中所有键值对的新映射。

返回的映射保留了原始集合的入口迭代顺序。如果两对中的任何一对具有相同的密钥,则最后一个将添加到地图中。

这是它的实现:

public fun <K, V> Iterable<Pair<K, V>>.toMap(): Map<K, V> {
    if (this is Collection) {
        return when (size) {
            0 -> emptyMap()
            1 -> mapOf(if (this is List) this[0] else iterator().next())
            else -> toMap(LinkedHashMap<K, V>(mapCapacity(size)))
        }
    }
    return toMap(LinkedHashMap<K, V>()).optimizeReadOnlyMap()
}

用法很简单:

val strings = listOf("a", "bb", "ccc")
val map = strings.map { it to it.length }.toMap()

map 的基础集合是 LinkedHashMap(按插入顺序排列)。

【讨论】:

    【解决方案4】:

    这个问题的正确解决方案是

    当前 ----> 2 参数版本

    Map&lt;Integer, String&gt; mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName));

    对 ----> 使用 Collectors.toMap 的 4 参数版本告诉供应商提供新的 LinkedHashMap:

    Map&lt;Integer, String&gt; mapping = list.stream().collect(Collectors.toMap(Entity::getId, Entity::getName, (u, v) -&gt; u, LinkedHashMap::new));

    这会有所帮助。

    【讨论】:

    • 爱 Java。在大多数其他编程语言中都是微不足道的。
    【解决方案5】:

    通过某个字段映射对象数组的简单函数:

    public static <T, E> Map<E, T> toLinkedHashMap(List<T> list, Function<T, E> someFunction) {
        return list.stream()
                   .collect(Collectors.toMap(
                       someFunction, 
                       myObject -> myObject, 
                       (key1, key2) -> key1, 
                       LinkedHashMap::new)
                   );
    }
    
    
    Map<String, MyObject> myObjectsByIdMap1 = toLinkedHashMap(
                    listOfMyObjects, 
                    MyObject::getSomeStringField()
    );
    
    Map<Integer, MyObject> myObjectsByIdMap2 = toLinkedHashMap(
                    listOfMyObjects, 
                    MyObject::getSomeIntegerField()
    );
    

    【讨论】:

      【解决方案6】:

      Java 9 开始,您可以按照与原始列表中相同的顺序收集 map entries 列表:

      List<String> strings = Arrays.asList("a", "bb", "ccc");
      
      List<Map.Entry<String, Integer>> entries = strings.stream()
              .map(e -> Map.entry(e, e.length()))
              .collect(Collectors.toList());
      
      System.out.println(entries); // [a=1, bb=2, ccc=3]
      

      或者你可以用同样的方式收集一个maps列表

      List<String> strings = Arrays.asList("a", "bb", "ccc");
      
      List<Map<String, Integer>> maps = strings.stream()
              .map(e -> Map.of(e, e.length()))
              .collect(Collectors.toList());
      
      System.out.println(maps); // [{a=1}, {bb=2}, {ccc=3}]
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-06-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-21
        相关资源
        最近更新 更多