【问题标题】:Java Map with two keys - speed comparison of solutions带两个键的Java Map - 解决方案的速度比较
【发布时间】:2012-11-25 19:42:49
【问题描述】:

在我的程序中,我想使用带有两个键(整数)的 Map。我的第一个想法是以某种方式将整数连接成一个字符串,例如:

String key = k1.toString()+"-"+k2.toString();

这个解决方案不适合我:1)丑陋; 2) 慢(将数字作为文本处理)。

我在 stackoverflow 上发现了其他方法。它们基于将整数封装在一个类中——一个目的类(MyKey),或更通用的一个(Pair)。

我尝试运行一些速度测试,我的虚拟解决方案似乎是最快的。第一次拍摄后,我尝试将转换整数字符串封装在一个新类(MyString)中,并针对此解决方案运行测试。

地图定义是:

Map<Pair<Integer,Integer>,String> map1 = new HashMap<>();
Map<MyKey,String> map2 = new HashMap<>();
Map<String,String> map3 = new HashMap<>();
Map<MyString,String> map4 = new HashMap<>();

测试结果是(运行多次,看起来很稳定):

  map: put+get=total
  1: 52+154=206
  2: 29+77=106
  3: 23+49=72
  3: 17+55=72

使用字符串的解决方案更快。字符串键的直接连接在搜索时更快,在输入时更慢。

我的问题是:

1) 为什么使用 String 的解决方案更快? (一次调用 hashCode()?)

2) 有什么理由不应该使用 String 的解决方案?


附加信息:

Map 中的记录数约为 6000。

Test 还尝试获取许多不存在的键的值。它会改变测试结果吗?

在我的程序中,我生成 boolean[N] 的排列,其中 M 值为真。一次,我得到某个 N,M 的结果;我想保存它们以备不时之需。

这是我的示例中使用的类的完整代码:

  class Pair<L,R> {

    private final L left;
    private final R right;

    public Pair(L left, R right) {
      this.left = left;
      this.right = right;
    }

    public L getLeft() { return left; }
    public R getRight() { return right; }

    @Override
    public int hashCode() { return left.hashCode() ^ right.hashCode(); }

    @Override
    public boolean equals(Object o) {
      if (o == null) return false;
      if (!(o instanceof Pair)) return false;
      Pair pairo = (Pair) o;
      return this.left.equals(pairo.getLeft()) &&
             this.right.equals(pairo.getRight());
    }
  }

  class MyKey {
      public Integer k1;
      public Integer k2;

      public MyKey(Integer k1, Integer k2) {
          this.k1 = k1;
          this.k2 = k2;
      }

      @Override
      public int hashCode() {
          return k1.hashCode() + 17 * k2.hashCode();
      }

      @Override
      public boolean equals(Object o) {
          if (o == this) {
              return true;
          }
          if (o == null || !(o instanceof MyKey)) {
              return false;
          }
          MyKey cp = MyKey.class.cast(o);
          return k1.equals(cp.k1) && k2.equals(cp.k2);
      }
  }

  class MyString  {
      private String value;

      public MyString(Integer k1, Integer k2) {
          value=k1+"-"+k2;
      }

      @Override
      public int hashCode() {
          return value.hashCode();
      }

      @Override
      public boolean equals(Object o) {
          return o.equals(value);
      }
  }

【问题讨论】:

    标签: java performance map


    【解决方案1】:

    这应该是性能最高的双整数键:

    class MyKey {
      public final int k1, k2;
      MyKey(int k1, int k2) { this.k1 = k1; this.k2 = k2; }
      public int hashCode() { return k1 ^ k2; }
      public boolean equals(Object o) { 
        MyKey that;
        return o instanceof MyKey && (that = (MyKey)o).k1 == k1 && that.k2 == k2;
      }
    

    至于您的测试结果,您应该非常小心地进行微基准测试。你确定你做了所有的咒语,比如热身、GC-ing、仔细编写 JIT 无法编译出来的代码等吗?如果没有,我强烈推荐 Google Caliper,而不是重新发明轮子。

    【讨论】:

    • +1。根据用于密钥的 k1/k2 对,您可能需要更改 hashCode() 以避免 (a,b) 和 (b,a) 的结果相同。
    • @Axel 我的实现取自Long.hashCode,但它当然是最适合OP的情况的理想选择。
    • 测试结果:不,我不确定。我意识到测试不是 100% 准时的,但是 2-3 次的差异对我来说似乎已经足够不同了,即使使用的方法不是最好的。无论如何,我对 Google Caliper 做了一个简短的了解,它看起来很酷,我会更深入地了解一下。谢谢!
    • 解释代码和编译代码之间的差异远不止2-3倍,因此始终需要谨慎。
    【解决方案2】:

    您遇到的最大问题是构建字符串,或者创建对象只是为了执行查找。

    解决此问题的一种方法是使用 Map 或 Map 值。由于您的密钥是原语,因此您最好使用 trove 库。 TObjectIntHashMapTIntIntHashMap

    例如

    TObjectIntHashMap<TIntIntHashMap> map = ...
    int val = map.get(k1).get(k2);
    

    使用这种方法不需要对象来创建键或值。

    如果你想配对密钥,你可以使用follow

    TLongIntHashMap map = ...
    int val = map.get(((long) k1 << 32) | k2);
    

    例如

    long key = ((long) k1 << 32) | k2;
    map.adjustOrPut(key, 1, 1); // a counter for this key.
    

    【讨论】:

    • 正如我在第二个代码示例中看到的那样,我只需要为键号设置一些限制(它甚至可以小于 int 的范围),〜按位连接数字并将其用作一个数字。那正确吗?第一个代码示例:我不确定创建地图地图是否是一种好方法。
    • 你是对的,但无论哪种方式,你都没有收获太多。对于你的问题,我会做你认为在概念上更清楚的事情。
    【解决方案3】:

    2) 有什么理由不应该使用 String 的解决方案?

    如果您询问给定的方法:

    String key = k1.toString()+"-"+k2.toString();
    

    问题是:

    k1 = "a-b"
    k2 = "c"
    

    k1 = "a"
    k2 = "b-c"
    

    (和类似的)

    拥有相同的键。

    如果您询问有关使用类的问题:

    有一个处理这个的类更干净。因为那时你的班级关心的是实现,而不是调用者。 这意味着,您不必考虑是使用“-”还是“。”或“#”或任何现在正确的东西,如果你想改变它,你可以在课堂上改变它。不在源代码周围的不同位置。

    哈希码实现的正确方法取决于您的数据。 Eclipse 建议一个通用的方法:

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((k1 == null) ? 0 : k1.hashCode());
        result = prime * result + ((k2 == null) ? 0 : k2.hashCode());
        return result;
    }
    

    这对我来说看起来不错。

    问题 1 有点复杂。这在很大程度上取决于输入数据。

    一般建议:不要关心性能,只要您不必关心它。这意味着,只有当解决方案太慢时,才开始对其进行分析并改进最重要的部分。除此之外,可读性和可维护性始终是首要目标。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-22
      • 1970-01-01
      • 1970-01-01
      • 2021-03-28
      • 1970-01-01
      • 2017-04-09
      相关资源
      最近更新 更多