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
有人可以解释一下吗?
答案 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),结果可能是以下组合:
没有足够的迭代次数,因此无法比较JIT编译的版本。尝试颠倒时间(首先进行不可比较,然后进行比较)并查看是否得到不同的结果。使用JMH进行适当的基准测试。
调用您自己的compareTo方法的速度不如HashMap内部用于比较不可比较的方法的速度快。
您还可以计算内存分配器的性能,因为您要在每次迭代中创建一个新类以传递给HashMap.get
(尽管JVM最终会在JIT编译器启动时执行转义分析,并将其转换为堆栈分配。)