【问题标题】:Time for getting by comparable key from HashMap从 HashMap 获取可比较键的时间
【发布时间】:2018-04-26 01:44:50
【问题描述】:

AFAIK 因为 java 8 桶结构从链表更改为树。

所以如果 hashCode() 方法返回常量并且我们的关键类实现 Comparable 接口,那么从单个单元格获取元素的复杂度将从 O(n) 降低到 O(log n)。

我试着检查一下:

 public static void main(String[] args) {
    int max = 30000;
    int times = 100;
    HashMap<MyClass, Integer> map = new HashMap<>();
    HashMap<MyComparableClass, Integer> compMap = new HashMap<>();

    for(int i = 0; i < max; i++) {
        map.put(new MyClass(i), i);
        compMap.put(new MyComparableClass(i), i);
    }

    long startTime = System.nanoTime();
    for (int i = max; i > max - times; i--){
        compMap.get(new MyComparableClass(i));
    }
    System.out.println(String.format("Key is comparable:    %d", System.nanoTime() - startTime));

    startTime = System.nanoTime();
    for (int i = max; i > max - times; i--){
        map.get(new MyClass(i));
    }
    System.out.println(String.format("Key isn't comparable: %d", System.nanoTime() - startTime));
}

MyComparableClass:

public class MyComparableClass
        implements Comparable {
    public Integer value;

    public MyComparableClass(Integer value) {
        this.value = value;
    }

    @Override
    public int compareTo(Object o) {
        return this.value - ((MyComparableClass) o).value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        MyComparableClass myClass = (MyComparableClass) o;

        return value != null ? value.equals(myClass.value) : myClass.value == null;
    }

    @Override
    public int hashCode() {
        return 1;
    }
}

MyClass 与 MyComparableClass 相同,但没有实现 Comparable 接口。

而且出乎意料的是,我总是得到结果,其中通过不可比较的键获得价值的时间比通过可比较的要少。

Key is comparable:    23380708
Key isn't comparable: 10721718

谁能解释一下?

【问题讨论】:

  • 你是如何在MyClass中实现hashCode的?
  • (1) 避免使用原始类型:public class MyComparableClass implements Comparable&lt;MyComparableClass&gt; (2) 你的基准有缺陷(没有足够的迭代,没有预热等)阅读:stackoverflow.com/questions/504103/…
  • @Eran MyClass 与 MyComparableClass 相同,但没有实现 Comparable 接口。
  • @assylias 看似通用的compareTo 方法根本没有使用。显式设置类有很大帮助。

标签: java java-8 hashmap hashtable


【解决方案1】:

您的基准测试存在一些缺陷,但在这种情况下,仍然可以承认这种趋势。您的代码的问题是您正在实现原始类型 Comparable,但 HashMap 实现在决定使用它来解决哈希冲突之前会验证类型兼容性。

因此,在您的情况下,自然顺序从未在 HashMap 实现中使用,但您实现原始类型 Comparable 的类会导致更昂贵的检查。看看HashMap.comparableClassFor(Object x)

static Class<?> comparableClassFor(Object x) {
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        if ((ts = c.getGenericInterfaces()) != null) {
            for (int i = 0; i < ts.length; ++i) {
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                     Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
        }
    }
    return null;
}

对于没有实现Comparable 的关键类,测试在x instanceof Comparable 结束。对于另一个类,这个相当便宜的instanceof 测试评估为true,并且对泛型类型签名进行了更复杂的测试,然后将失败。而且这个测试的结果不会被记住,而是重复不仅对每个键

当你看HashMap.find

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        if ((ph = p.hash) > h)
            p = pl;
        else if (ph < h)
            p = pr;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if (pl == null)
            p = pr;
        else if (pr == null)
            p = pl;
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
            p = pl;
    } while (p != null);
    return null;
}

您会看到在测试comparableClassFor 失败后,find 被递归调用。它尝试使用kc 记住测试结果并将其传递下去,但在失败的情况下它是null,因此被视为尚未进行,换句话说,只要下降到子树,测试就会重复,但由于未使用自然顺序,因此如果在第一个子树中未找到键,则此代码可能还必须遍历另一个子树,并且在每次迭代中重复测试。

或者,换句话说,这个comparableClassFor(k) 可能会在最坏的情况下为该桶中的每个元素重复。 JVM 优化甚至可能改变结果以支持不实现 Comparable 的类,因为 JVM 可能会识别同一关键对象上重复的相同 instanceof 测试并优化它们,而泛型类型签名的测试不是一个内在的 JVM 操作及其结果不太可能被预测。

当然,当您的MyComparableClass 正确实现Comparable&lt;MyComparableClass&gt; 时,结果会发生巨大变化。然后,使用自然顺序将时间复杂度从O(n) 更改为O(log n),而且每次查找只进行一次测试,因为then-non-null 结果将被记住在kc .

【讨论】:

  • 我认为这很糟糕。当你糟糕地实现接口Comparable时,它不应该降低生产力。
  • @sann05:这是一个在同一个类中没有很好地实现Comparable 没有很好地实现hashCode() 的例子。尽管如此,我同意记住这个测试的结果是值得考虑的,特别是因为有 ClassValue 允许轻松实现这样的缓存(已经注意效率、线程安全并且仍然允许类收集垃圾) .
【解决方案2】:

即使您的类不可比较,它仍然使用二叉树!正如https://stackoverflow.com/a/30180593/638028 中所述,它使用较低级别的方法来比较两个对象(例如 System.identityHashCode),您的结果可能是以下几种的组合:

  1. 没有足够的迭代,因此无法比较 JIT 编译的版本。尝试颠倒时间(先做不可比较,然后做可比较),看看你是否得到不同的结果。使用 JMH 进行适当的基准测试。

  2. 调用您自己的 compareTo 方法不如 HashMap 内部用于比较不可比较对象的方法快。

  3. 1234563会将其转换为堆栈分配)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-10
    • 2015-01-30
    • 1970-01-01
    • 2017-12-20
    相关资源
    最近更新 更多