是时候从HashMap获得可比较的密钥了

时间:2017-11-13 11:28:49

标签: java java-8 hashmap hashtable

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

有人可以解释一下吗?

2 个答案:

答案 0 :(得分:4)

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

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

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.comparableClassFor(Object x)时:

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<MyComparableClass>时,结果会发生显着变化。然后,使用自然顺序将时间复杂度从O(n)更改为O(log n),但每次查找只进行一次测试,因为当时非null结果将记住kc

答案 1 :(得分:2)

即使你的班级不具有可比性,它仍然使用二叉树!正如https://stackoverflow.com/a/30180593/638028中所解释的,它使用较低级别的方式来比较两个对象(例如System.identityHashCode),结果可能是以下组合:

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

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

  3. 您还可以计算内存分配器的性能,因为您要在每次迭代中创建一个新类以传递给HashMap.get(尽管JVM最终会在JIT编译器启动时执行转义分析,并将其转换为堆栈分配。)