【问题标题】:Lowercase all HashMap keys小写所有 HashMap 键
【发布时间】:2017-05-04 15:34:26
【问题描述】:

我遇到了一个场景,我想将 HashMap 的所有键都小写(不要问为什么,我只需要这样做)。 HashMap 有几百万个条目。

起初,我以为我只是创建一个新地图,遍历要小写的地图条目,然后添加相应的值。这个任务应该每天只运行一次或类似的东西,所以我想我可以忍受这个。

Map<String, Long> lowerCaseMap = new HashMap<>(myMap.size());
for (Map.Entry<String, Long> entry : myMap.entrySet()) {
   lowerCaseMap.put(entry.getKey().toLowerCase(), entry.getValue());
}

然而,当我的服务器在我即将复制地图的这段时间超载时,这会导致一些 OutOfMemory 错误。

现在我的问题是,我怎样才能以最小的内存占用完成这项任务?

是否会在小写后删除每个键 - 添加到新的地图帮助中?

我可以利用 java8 流来加快速度吗? (例如这样的)

Map<String, Long> lowerCaseMap = myMap.entrySet().parallelStream().collect(Collectors.toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue));

更新 好像是Collections.unmodifiableMap 所以我没有选择

小写后删除每个键 - 添加到新地图

【问题讨论】:

  • 你不能一开始就插入小写的键吗?
  • 您尝试做的事情很危险,因为在原始的Map 中可能有keyKey。那么你的地图应该如何表现呢?
  • a) 不,这不太可能从并行处理中受益,b) 并行处理不能解决您的内存问题,c) 因为您自己已经回答了 remove 选项,所以只有一个解决方案:增加更多的堆内存。
  • @sestus:不,这将不会从并行处理中受益。解释原因将超出此问题的范围。基本上,标准的toMap 收集器必须合并部分结果,合并成本与任何节省一样高。 String.toLowerCase() 操作太便宜了,无法补偿开销。
  • 我真的,真的认为你应该告诉我们你想这样做的原因。这可能是“我的指甲太长所以我不得不剪掉我的手(不要问我为什么必须这样做)”的情况。也许有比将不可修改的映射键小写更好的解决方案。

标签: java hashmap java-8 java-stream


【解决方案1】:

不确定内存占用。如果使用 Kotlin,可以尝试以下方法。

val lowerCaseMap = myMap.mapKeys { it.key.toLowerCase() }

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/map-keys.html

【讨论】:

    【解决方案2】:

    上述答案中的问题是正确的,您可能需要重新考虑更改您正在使用的数据结构。

    对我来说,我有一个简单的地图,我需要将其键更改为小写

    看看我的 sn-p,它是一个简单的解决方案,而且性能很差

    private void convertAllFilterKeysToLowerCase() {
        HashSet keysToRemove = new HashSet();
        getFilters().keySet().forEach(o -> {
            if(!o.equals(((String) o).toLowerCase()))
                keysToRemove.add(o);
        });
        keysToRemove.forEach(o -> getFilters().put(((String) o).toLowerCase(), getFilters().remove(o)));
    }
    

    【讨论】:

      【解决方案3】:

      您可以尝试使用不区分大小写的TreeMap,而不是使用HashMap。这将避免创建每个键的小写版本的需要:

      Map<String, Long> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
      map.putAll(myMap);
      

      构建此映射后,put()get() 将不区分大小写,因此您可以使用全小写键保存和获取值。遍历键将返回它们的原始形式,可能是大写形式。

      这里有一些类似的问题:

      【讨论】:

      • 这确实是一种有趣的方法。它不会更改 API,只需要不同的 Map 实例化。我尝试利用它。
      • 看来这完成了这项工作。改变是微不足道的——只是 Map 的实现被改变了。谢谢!
      【解决方案4】:

      您无法在遍历地图时删除该条目。如果您尝试这样做,您将遇到 ConcurentModificationException。

      由于问题是 OutOfMemoryError,而不是性能错误,因此使用并行流也无济于事。

      尽管最近将在 Stream API 上完成一些任务,但这仍然会导致内存中有两个映射,因此您仍然会遇到问题。

      为了解决这个问题,我只看到了两种方法:

      • 为您的进程提供更多内存(通过在 Java 命令行上增加 -Xmx)。现在内存很便宜;)
      • 拆分地图并以块的形式工作:例如,您将地图的大小除以 10,然后一次处理一个块,并在处理新块之前删除已处理的条目。这样一来,内存中的地图就不是两倍,而是地图的 1.1 倍。

      对于拆分算法,您可以使用 Stream API 尝试这样的操作:

      Map<String, String> toMap = new HashMap<>();            
      int chunk = fromMap.size() / 10;
      for(int i = 1; i<= 10; i++){
          //process the chunk
          List<Entry<String, String>> subEntries = fromMap.entrySet().stream().limit(chunk)
              .collect(Collectors.toList());  
      
          for(Entry<String, String> entry : subEntries){
              toMap.put(entry.getKey().toLowerCase(), entry.getValue());
              fromMap.remove(entry.getKey());
          }
      }
      

      【讨论】:

      • 我只想引用@xenteros。如果映射中有key 键和Key 键,则只有小写映射的整个逻辑将失败,并且该任务将不再有意义,因为最终状态永远无法实现(如果您不只是删除其中一个)
      • 假设这段时间没有使用原始地图。
      • (更新的)问题表明源映射根本不可变(即Collections.unmodifiableMap 返回的映射),因此无论您尝试删除块还是单个都没有关系条目(如果您使用Iterator.remove(),则可以同时迭代和删除)。
      • 不,它没有被使用。我想分裂是我唯一的选择。我已经为我的 JVM 提供了尽可能多的内存。避免在内存中加载完整地图可以解决此问题。
      • @davidxxx 答案是在提供此附加信息之前提交的(或者至少它们彼此接近,所以我猜 loicmathieu 那时还不知道)
      猜你喜欢
      • 1970-01-01
      • 2014-04-13
      • 2014-03-15
      • 2020-03-14
      • 1970-01-01
      • 2021-04-25
      • 2012-08-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多