zoukankan      html  css  js  c++  java
  • 程序猿的日常——HashMap的相关知识

    背景知识

    哈希冲突

    哈希是指通过某种方法把数据转变成特定的数值,数值根据mod对应到不同的单元上。比如在Java中,字符串就是通过每个字符的编码来计算、数字是本身对应的值等等,不过就算是再好的哈希方法,也有可能出现两个不同的对象hash值相同的情况。如果在HashMap中,hashcode相同,它们就会被分配到对应的存储位置,此时就会出现冲突——也叫做哈希冲突。

    解决哈希冲突的方法有很多种:

    1. 开放地址探测法:即如果出现哈希冲突,则按照一定的规则继续选择位置,如线性探测法再、二次探测再、伪随机探测等等。
    2. 链地址法:如果出现冲突,则在冲突的位置后面形成链表进行存储。HashMap就是通过这种方式实现的
    3. 再哈希法:这种方法是再换另一个哈希方法寻找存储的位置。

    hashCode和equals

    首先hashcode是经过一定的方法映射出的数值,而equals如果没有重写的话,是对比了每个内部的属性。总结的来说,如果两个对象hashcode相同,它们未必相等;如果hashcode不同,肯定不等。从另一个角度说,如果两个对象equals相等,它们肯定相等;如果equals不同,则它们不同。

    那么肯定会有人疑问,那还要hashcode干嘛咧?Hashcode其实就是在hashMap或者hashset进行快速比较的时候有用,可以快速的判断对像是否不同,如果hashcode相同,则再继续对比equals方法。这样可以节省大量的时间。

    HashMap

    HashMap允许null的key和value,HashMap根HashTable很像,只不过非线程安全并且允许Null值。

    有两个参数会影响Map的性能,分别是初始容量initial capacity和负载参数load facotr(确定了什么时间增加hash table的容量)。当容量超过load factor*initial capacity时,就会进行扩容,然后执行rehash操作。

    默认load factor时0.75,它基本已经能提供一个不错的性能效果了。不过在使用的初期可以预估一下数据量,直接设置一个比较适合的初始值。

    注意:HashMap不是线程安全的,可以通过

    Map m = Collections.synchronizedMap(new HashMap(...))
    

    实现线程安全的map.

    创建

    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    int threshold;
    final float loadFactor;
    

    新增

    如果key之前出现过,那么将会用新的value代替旧的value

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    主要的代码在这里:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果每个链表长度超过8,那么就转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    对于写操作的场景:

    1. 先会经过hash计算hashcode然后与size进行&操作,判断存储的位置
    2. 如果存储的位置没有节点,则直接写入
    3. 如果存储的位置有节点,且是树节点,则向树中插入节点
    4. 如果存储的位置有节点,不是树节点(而是普通的链表),则进行头插。但是会判断当前链表的长度,如果超过设置的阈值(默认是8),就会把链表转化成树。

    更新的时候也是上面的操作流程,只不过在对比hashcode相同时,还会检查key是否equals

    读取和删除基本上也是上面的套路。

    为什么非线程安全

    这个主要是因为在rehash的时候由于table[]后面接的是链表,而hashMap还是采用头插的形式。因此如果有不同的线程同时进行rehash,就可能导致链表形成环形,造成死循环。

    具体的可以参考网上的文章:https://coolshell.cn/articles/9606.html

  • 相关阅读:
    C#随笔
    C# 程序间通信的各种途径及解析
    C#控件根据窗体改变大小
    c# SqlHelper Class
    初始Java
    函数基础
    基本数据类型的高级特性:
    python基础
    C#double转化成字符串 保留小数位数, 不以科学计数法的形式出现。
    Oracle使用dblink导入数据
  • 原文地址:https://www.cnblogs.com/xing901022/p/7774741.html
Copyright © 2011-2022 走看看