基元值的映射替代项

2022-09-01 12:57:17

我对我的应用程序进行了一些分析,其中一个结果显示堆上大约18%的内存由类型对象使用。事实证明,这些对象是s中的值,我不能使用基元类型。DoubleMap

我的理由是,基元类型的 比对象 消耗更少的内存 。有没有办法有一个像数据结构这样的映射,可以接受任何类型作为键,将基元作为值?doubleDoubledouble

主要操作将是:

  • 插入(可能只有一次)
  • 查找(按键包含)
  • 检索(按键)
  • 迭 代

我拥有的典型地图是:

  • HashMap<T, HashMap<NodeData<T>, Double>> graph
  • HashMap<Point2D, Boolean> onSea(虽然不是双精度值)
  • ConcurrentHashMap<Point2D, HashMap<Point2D, Double>>

全部与Java 8一起使用。

补遗

我主要对能够解决这些类型地图的框架不感兴趣,而是对解决这些问题时必须考虑的因素感兴趣。如果你愿意,任何这样的框架背后的概念/想法/方法是什么?或者解决方案也可能在另一个层面上,地图被替换为遵循某种模式的物体,如@Ilmari Karonen在他的答案中指出的那样。


答案 1

其他人已经提出了一些原始值映射的第三方实现。为了完整起见,我想提一些您可能要考虑的完全摆脱地图的方法。这些解决方案并不总是可能的,但是当它们成为可能时,它们通常比任何地图都更快,内存效率更高。

备选方案 1:使用普通的旧阵列。

一个简单的数组可能不像花哨的地图那样性感,但很少有数组可以在紧凑性和访问速度上击败它。double[]

当然,数组有一堆限制:它们的大小是固定的(尽管你总是可以创建一个新数组并将旧数组的内容复制到其中),它们的键只能是小的正整数,为了提高效率,它们应该是相当密集的(即,使用的键的总数应该是最高键值的合理大一部分)。但是,如果您的键碰巧是这种情况,或者您可以安排这种情况,那么基元值数组可以非常有效。

特别是,如果可以为每个键对象分配一个唯一的小整数 ID,则可以将该 ID 用作数组的索引。同样,如果您已经将对象存储在数组中(例如,作为某个更复杂的数据结构的一部分)并按索引查找它们,那么您可以简单地使用相同的索引来查找另一个数组中的任何其他元数据值。

如果您实现了某种冲突处理机制,您甚至可以免除ID唯一性要求,但是在这一点上,您正在实现自己的哈希表。在某些情况下,这可能实际上有意义,但通常在这一点上,使用现有的第三方实现可能更容易。

备选方案 2:自定义对象。

为什么不将这些值转换为对象本身的属性,而不是维护从关键对象到基元值的映射呢?毕竟,这就是面向对象编程的全部意义所在——将相关数据分组到有意义的对象中。

例如,与其维护 一个 ,为什么不直接给你的点一个布尔属性呢?当然,您需要为此定义自己的自定义点类,但是没有理由不能根据需要使其扩展标准类,以便可以将自定义点传递到任何需要 .HashMap<Point2D, Boolean> onSeaonSeaPoint2DPoint2D

同样,这种方法可能并不总是直接工作,例如,如果您需要使用无法修改的类(但见下文),或者如果要存储的值与多个对象相关联(如您的)。ConcurrentHashMap<Point2D, HashMap<Point2D, Double>>

但是,对于后一种情况,您仍然可以通过适当地重新设计数据表示来解决问题。例如,您可以将类定义为Map<Node, Map<Node, Double>>Edge

class Edge {
    Node a, b;
    double weight;
}

,然后向包含连接到该节点的任何边的每个节点添加一个 (or ) 属性。Edge[]Vector<Edge>

备选方案 3:将多个地图合并为一个。

如果您有多个具有相同键的映射,并且不能像上面建议的那样将这些值转换为键对象的新属性,请考虑将它们分组到单个元数据类中,并从键创建单个映射到该类的对象。例如,考虑定义单个元数据类,而不是 a 和 a,如下所示:Map<Item, Double> accessFrequencyMap<Item, Long> creationTime

class ItemMetadata {
    double accessFrequency;
    long creationTime;
}

并有一个存储所有元数据值的单个。这比拥有多个映射更节省内存,并且还可以通过避免冗余映射查找来节省时间。Map<Item, ItemMetadata>

在某些情况下,为方便起见,您可能还希望在每个元数据对象中包括对其相应主对象的引用,以便您可以通过对元数据对象的单个引用来访问这两个对象。这自然而然地演变成...

备选方案 4:使用装饰器。

作为前两种替代方法的组合,如果无法直接将额外的元数据属性添加到关键对象中,请考虑使用可以保存额外值的装饰器包装它们。因此,例如,与其直接创建具有额外属性的点类,不如简单地执行以下操作:

class PointWrapper {
    Point2D point;
    boolean onSea;
    // ...
}

如果您愿意,您甚至可以通过实现方法转发将此包装器变成一个完整的装饰器,但即使只是一个简单的“哑”包装器也可能足以满足许多目的。

如果随后可以安排仅存储和使用包装器,则此方法最有用,这样您就永远不需要查找与未包装对象对应的包装器。当然,如果你确实需要偶尔这样做(例如,因为你只从其他代码接收未包装的对象),那么你可以用单个,但这样你就可以有效地回到以前的替代方案。Map<Point2D, PointWrapper>


答案 2

Eclipse Collections具有对象基元映射,并且两者都有可变和不可变版本。

MutableObjectDoubleMap<String> doubleMap = ObjectDoubleMaps.mutable.empty();
doubleMap.put("1", 1.0d);
doubleMap.put("2", 2.0d);

MutableObjectBooleanMap<String> booleanMap = ObjectBooleanMaps.mutable.empty();
booleanMap.put("ok", true);

ImmutableObjectDoubleMap<String> immutableMap = doubleMap.toImmutable();
Assert.assertEquals(doubleMap, immutableMap);

A 可以用作 Eclipse Collections 中 a 的工厂,就像我在上面的示例中所做的那样调用。可变映射和不可变映射共享一个公共父接口,在及以上情况下,该父接口被命名为 。MutableMapImmutableMaptoImmutableMutableObjectDoubleMapImmutableObjectDoubleMapObjectDoubleMap

Eclipse Collections 还为库中的所有可变容器提供了同步和不可修改的版本。以下代码将为您提供一个围绕基元映射的同步视图。

MutableObjectDoubleMap<String> doubleMap = 
        ObjectDoubleMaps.mutable.<String>empty().asSynchronized();
doubleMap.put("1", 1.0d);
doubleMap.put("2", 2.0d);

MutableObjectBooleanMap<String> booleanMap = 
        ObjectBooleanMaps.mutable.<String>empty().asSynchronized();
booleanMap.put("ok", true);

这种大型地图的性能比较是在几年前发布的。

大型哈希地图概述:JDK,FastUtil,Goldman Sachs,HPPC,Koloboke,Trove – 2015年1月版本

GS Collections已经迁移到Eclipse基金会,现在是Eclipse Collections。

注意:我是 Eclipse Collections 的提交者。