在Java中创建HashMap的副本 - 最有效的方法是什么?

4
我有一个 HashMap 需要被复制100000次,每个副本都将独立扩展。由于复制100000次是很多的(而且这不是我代码中唯一发生这种情况的地方),所以这目前是实现中的一个主要瓶颈(事实上,它发生得如此频繁,以至于它占用了45%的运行时间,并且不幸的是没有办法限制这个数字),因此我正在寻找最有效的方法来解决这个问题。

我找到了以下创建 HashMap 原始浅层副本的选项:

//1
 HashMap<T> map = (HashMap<T>) original.clone()

并且。
//2
HashMap<T> map = new HashMap<T>();
map.putAll(original);

并且

//3
HashMap<T> map = new HashMap<T>(original);

根据您的经验,复制HashMap最有效的方法是什么?有其他选项吗(除了迭代原始方式,但我猜那并不是真正的选择)?


4
我能问一下为什么你需要100,000个HashMap的副本吗?我无法想象有哪种情况需要这么多副本。 - ControlAltDel
2
“享元模式”可能有助于您的情况。 - Pavlo Viazovskyy
@brso05 地图内的对象可能是相同的,复制地图中的所有对象会导致巨大的内存浪费。我只需要具有相同对象的地图,这样我就可以单独添加其他对象而不影响其他地图。 - Saftkeks
2
你确定你的副本会被修改吗?如果实际上有相当比例的副本未被修改,那么你可能会从推迟复制直到实际需要它中受益。有各种方法可以实现这一点,但我立即想到的所有方法都涉及在你的映射周围创建一个持有者或包装类。 - John Bollinger
1
我倾向于寻找一个支持这种操作高效的持久化数据结构库。 - Louis Wasserman
显示剩余8条评论
4个回答

3
考虑一下你是否真的需要副本。你说:“我只需要具有相同对象的地图,可以单独添加其他对象而不影响其他地图。”考虑到这一点,您可以创建一个Map的组合实现:
class MyCompositeMap<K, V> implements Map<K, V> {
  final Map<K, V> mapThatYouAddThingsTo;
  final Map<K, V> mapThatIsShared;
}

现在,您可以实现自己的方法。例如:
  • 您的containsKey方法可以首先检查mapThatYouAddThingsTo是否存在该键;如果存在,则返回mapThatYouAddThingsTo中的值。否则,它会检查mapThatIsShared
  • put方法只能将内容放入mapThatYouAddThingsTo,而不能放入mapThatIsShared

实现中有一些棘手的方面(如在keySet()entrySet()中去重键和值),但只要mapThatYouAddThingsTomapThatIsShared小得多,就可以使用更少的内存。


1

1 - 这是最糟糕的。2和3几乎相同。 您正在使用Map,并且它也被视为集合。 而为什么克隆是不好的做法,您可以在这里阅读:为什么人们如此害怕在集合和JDK类上使用clone()?

我会选择这个:

HashMap<T> map = new HashMap<T>(original);

因为当一个API让你写得更加优雅时,通常这个API会在幕后以最合适的方式处理其他事情。


0

这是一个老问题,但我认为还有其他要提到的事情。

如果你只想创建一个浅拷贝的映射,那么选项3是最推荐的。

然而,如果你需要复制一个被定义为 HashMap<Integer, List<Item>> 的映射,但你希望在复制品中更改某些内容时原始映射保持不变。也就是说,如果你从复制品的列表中删除了某些内容,则原始列表应该保持原值。

我有两个解决方案,一个是深度复制函数。目前Java 8没有提供本地实现。我们可以使用GuavaApache Commons Lang。但我们可以通过创建一个方法来创建新实例,使用foreach方法或Stream.collect()方法来找到一个解决方法。前者很简单,我们使用foreach来创建我们想要复制的对象的新实例,在这种情况下是List<T>。请查看此处的通用函数:

public static <T> HashMap<Integer, List<T>> deepCopy(HashMap<Integer, List<T>> original)
{
    HashMap<Integer, List<T>> copy = new HashMap<>();
    for (Map.Entry<Integer, List<T>> entry : original.entrySet()) {
        copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
    }

    return copy;
}

如果您不想处理泛型,那么我们将使用Stream.collect()。在这种情况下,我们使用流来提取数据并将其包装为映射,并创建一个新实例。

public static <T> Map<Integer, List<T>> deepCopyStream(Map<Integer, List<T>> original)
{
    return original
            .entrySet()
            .stream()
            .collect(Collectors.toMap(Map.Entry::getKey, valueMapper -> new ArrayList<>(valueMapper.getValue())));
}

注意

请注意,我没有在泛型中使用<K,V>,因为这不是一个适用于每个层级的嵌套克隆的正确深度复制方法。这种方法基于我们有一个HashMap<Integer, List<Item>>,其中Item类不包含需要克隆的属性。


-1
你需要循环遍历这些项目。最简单的方法是使用Stream。我将地图的键设置为字符串,并为您的“T”创建了一个“Pojo”类...
public void testMapCopy() {

    // build the orig map
    Map<String, Pojo> orig = new HashMap();
    for (int i = 0; i < 10; i++) {
        orig.put("k" + i, new Pojo("v"+i));
    }

    // make a copy
    Map<String, Pojo> mapCopy = orig.entrySet().stream()
            .collect(Collectors.toMap(e -> e.getKey(), new Pojo(e.getValue().getValue())));

    // change orig
    Pojo pojo = orig.get("k0");
    pojo.setValue("v0-updated!"); 

    // check values
    System.out.println("orig k0: " + orig.get("k0").getValue());
    System.out.println("copy k0: " + mapCopy.get("k0").getValue());
}

表示你的 "T" 的简单类

private class Pojo {

    private String value;

    public Pojo(String value) {
        this.value = value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

}

3
为什么你要使用Java流(Stream)来完成这个任务,而不是直接用 Map<String, Pojo> mapCopy = new HashSet<>(orig); 呢?还有为什么你要定义一个 String 的包装类(wrapper)? - Andy Turner
啊,谢谢。在回复你的时候我注意到我没有复制值。我想他希望值对象不是相同的。我更新了我的回复,为映射值提供了新值,并且还更新了原始的POJO。 - Matt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接