【问题标题】:Why is hashCode() called once in case of iterating through one bucket with multiply values?为什么在迭代一个具有多个值的桶的情况下调用一次 hashCode() ?
【发布时间】:2019-10-10 09:26:02
【问题描述】:

我有以下课程

public class Animal {

    private int hash;

    public Animal(int hash) {
        this.hash = hash;
    }

    @Override
    public int hashCode() {
        System.out.println(hash);
        return hash;
    }
}

还有这段代码

    public static void main(String[] args) {
        Map<Animal, Integer> map = new HashMap<>();
        for (int i = 0; i < 4; i++) {
            map.put(new Animal(16 * i), i);
        }

        Animal an = new Animal(16*4);
        map.put(an, 1);

        for (int i = 5; i < 9; i++) {
            map.put(new Animal(16 * i), i);
        }
        Integer value = map.get(an);
    }

据我了解,所有这些值都应该放在一个存储桶中(由于它们的哈希码)。在最后一次调用map.get(an) 时,hashCode() 仅被调用一次(根据控制台),但在遍历存储桶并找到具有正确 hashCode() 的条目时不应该多次调用它吗?

EDIT1:如果我实现equals(带有控制台日志记录),它也不会被调用(再次根据控制台),只有当有两个具有 same 哈希码的对象时才会调用它(例如,如果我将它添加到我的代码 map.put(new Animal(16*3), 4);,并且在这种情况下,hashCode() 在从地图中获取对象时被调用 两次

【问题讨论】:

  • 请注意,您没有实现equals
  • 为什么你认为它们都应该在同一个桶中?您创建的 Animal 实例的 hashCode 值大多不同,为什么它们会有哈希冲突?
  • 除此之外:HashMap 缓存了它存储的键的哈希码,因此它只需要在添加键时调用一次hashCode
  • @JoachimSauer index_of_bucket= hashCode(key) & (n-1), 我的 hashCodes 是 0, 16, 32 ... 而 n (map 的大小) 默认是 16
  • @BogdanTimofeev:这不是 Java HashMap 使用哈希的方式,它实际上在使用它之前对其进行了操作。检查您最喜欢的 JVM 实现中的私有 HashMap.hash(Object) 方法。

标签: java equals hashcode


【解决方案1】:

单个存储桶可能包含具有不同hashCode 的键,并将相关存储桶的键的hashCode 与您正在添加/搜索的键进行比较。但是hashCode是缓存在Map.Entry中的,所以对于已经在Entrys中的Entrys,不需要调用key的hashCode方法:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash; // here the hash code is cached
        this.key = key;
        this.value = value;
        this.next = next;
    }
    ...
}

不过,这是一个实现细节。

您可以在此处查看用于定位给定hashkeyEntry 的代码:

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // <--- here a cached hash is compared to hash
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&  // <--- here a cached hash is compared to hash
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

hash 与缓存的键的hash 值进行比较,这意味着无需再次调用hashCode()

【讨论】:

    【解决方案2】:

    不,哈希码用于查找存储桶,这需要一次调用 hashcode(在 map.get(an) 的参数上调用)。

    然后将桶中的元素与equals() 进行比较以找到正确的对象。

    【讨论】:

    • equals 也没有被调用(参见我的编辑 1),看起来根本没有碰撞
    • @BogdanTimofeev 好吧,如果没有必要,它不会被调用。这个问题是关于碰撞还是关于何时使用equals()hashCode()
    【解决方案3】:

    每个人都已经回答了这个问题。我想从 HashMap 中捕获代码以提供更多信息

    当我们从 Map 调用 .put 时,它们在内部首先调用 hashKey

    在 hash() 方法中,你会看到 hashCode() 被调用了。

    然后在 putVal 方法中,有几个地方 equals 被调用如下

    这就是为什么我们会多次调用 hashCode()。

    如果您需要了解更多,请查看此链接https://github.com/frohoff/jdk8u-jdk/blob/master/src/share/classes/java/util/HashMap.java以了解HashMap的实现

    【讨论】:

      【解决方案4】:

      您最多可以期望hashCode() 被调用。调用次数是HashMap 的实现细节,您不应期望任何特定行为,也不应期望观察到的行为是稳定的。由于hashCode() 的实现方式可能不同,甚至代价高昂,因此只调用一次是合理的选择,即使需要多次调用,也可以使用返回值而不是新调用hashCode()。但这只是猜测,不应该假设。

      【讨论】:

        【解决方案5】:

        一旦确定了存储桶,HashMap 将使用 .eqauls 来确定与提供的对象匹配的链表节点。查看实现:

        /**
         * Implements Map.get and related methods
         *
         * @param hash hash for key
         * @param key the key
         * @return the node, or null if none
         */
        final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
                if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
                if ((e = first.next) != null) {
                    if (first instanceof TreeNode)
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
            }
            return null;
        }
        

        【讨论】:

          【解决方案6】:

          map.put(new Animal(16 * i), i);

          当您这样做时,Animal 对象是键,其哈希值将是值 i 的存储桶。可以把它想象成 Animal 对象甚至没有存储在地图中。

          当您添加所有 9 个项目时,具有值的存储桶为 0,16,32,48,...,(16*8)

          当您执行map.get(aa) 时,an 的哈希值从 an 对象返回,即 64。

          tl;dr 所有对象都属于不同的桶(因为它们的 hashValues 不同)

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-10-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多