zoukankan      html  css  js  c++  java
  • Java容器:HashMap和HashSet解析

    转载请注明出处:jiq•钦's technical Blog 


    一、HashMap

    HashMap,基于散列(哈希表)存储“Key-Value”对象引用的数据结构。

    存入的键必须具备两个关键函数:

    1equals():  推断两个Key是否同样,用来保证存入的Key的唯一性。

    2hashCode(): 依据k-v对象的Key来计算其引用在散列表中存放的位置;

     

    HashMap底层结构是一个数组:

    transientEntry<K,V>[] table

    而当中Entry<K,V>定义例如以下:

    static classEntry<K,V> implements Map.Entry<K,V> {

            final K key;

            V value;

            Entry<K,V> next;

            int hash;

    }

    包括了keyvalue以及hash值。更重要的是另一个指向下一个节点的next指针。

    结合以下将要介绍的put方法可知HashMap底层是一个哈希表。以链接法解决冲突。

    在网上找到一张画的比較好的图片:


     

    1public V put(K key, V value)

    直接看代码:

    public V put(K key,V value) {
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);
            }
           
            //key为null时的插入
            if (key == null)
                return putForNullKey(value);
           
            //依据key计算hash值
            int hash = hash(key);
           
            //返回哈希表索引位置
            int i = indexFor(hash, table.length);
           
            //在哈希表中该索引处的链表中查找同样key的Entry<K,V>
            //注意table[i]是指向Entry<K,V>链表头结点的指针
            for (Entry<K,V> e = table[i]; e!= null; e = e.next) {
                Object k;
                if (e.hash == hash && ((k =e.key) == key || key.equals(k))) {
                       //通过equals方法推断找到同样key的节点,用新value覆盖旧value并返回旧value
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
     
            modCount++;
           
            //创建一个新的Entry<K,V>实体。头插法插入到位置i处
            addEntry(hash, key, value, i);
            return null;
        }

    当中针对keynull的处理例如以下:

    private V putForNullKey(V value) {
    //在hash表的第0个位置開始找是否已经有了key为null的节点
            for (Entry<K,V> e = table[0]; e!= null; e = e.next) {
                if (e.key == null) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            modCount++;
           
            //在hash表的第0个位置用头插法插入key为null的这个节点
            addEntry(0, null, value, 0);
            return null;
        }

    当中通过key计算hash值方法例如以下:

    final inthash(Object k) {
            int h = hashSeed;
            if (0 != h && k instanceofString) {
                returnsun.misc.Hashing.stringHash32((String) k);
            }
     
            //调用Key的hashCode()方法计算hash值
            h ^= k.hashCode();
     
            // This function ensures that hashCodesthat differ only by
            // constant multiples at each bitposition have a bounded
            // number of collisions (approximately8 at default load factor).
            h ^= (h >>> 20) ^ (h>>> 12);
            return h ^ (h >>> 7) ^ (h>>> 4);
        }

    由此能够得出下面结论:

    1)当插入一个<Key, Value>,发现此Key已经存在时,将用新的value覆盖旧的value

    2)当插入的<Key, Value>keynull时,将插入到hash表的位置0处,而且仅仅会有一个keynull的节点;

    3)当插入一个<Key, Value>时通过KeyhashCode()方法计算在hash表中的索引。通过Keyequals()方法推断两个Entry<K,V>Key是否同样,同样会覆盖。所以说插入的<Key, Value>中的Key必须实现这两个方法。

     

    2public V get(Object key)

    依据Key返回Value方法就相对简单:

    public V get(Objectkey) {
            if (key == null)
                return getForNullKey();
            Entry<K,V> entry = getEntry(key);
     
            return null == entry ? null :entry.getValue();
        }

    当中getEntry(key)方法为主要实现:

    final Entry<K,V> getEntry(Object key) {
            if (size == 0) {
                return null;
            }
     
            //依据Key计算hash值
            int hash = (key == null) ? 0 :hash(key);
           
            //indexFor(hash,table.length)依据hash值返回其在hash表中索引位置
            //在该索引位置指向的链表中查找Key同样的节点并返回其Value
            for (Entry<K,V> e =table[indexFor(hash, table.length)];
                 e != null;
                 e = e.next) {
                Object k;
                if (e.hash == hash &&
                    ((k = e.key) == key || (key !=null && key.equals(k))))
                    return e;
            }
            return null;
        }

    3、散列表容量

    HashMap有默认的装载因子loadFactor=0.75,默认的entry数组的长度为16。装载因子的意义在于使得entry数组有冗余,默认即同意25%的冗余,当HashMap的数据的个数超过12(16*0.75)时即会对entry数组进行第一次扩容,后面的再次扩容依次类推。

    HashMap每次扩容一倍,resize时会将已存在的值从新进行数组下标的计算,这个是比較浪费时间的。在平时使用中,假设能预计出大概的HashMap的容量。能够合理的设置装载因子loadFactor和entry数组初始长度即能够避免resize操作,提高put的效率。

    以下看看resize操作时怎样进行的:

    void resize(intnewCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
     
            //依据新的容量又一次创建hash表
            Entry[] newTable = newEntry[newCapacity];
           
            //逐个将节点拷贝至新hash表,较为耗时
            transfer(newTable,initHashSeedAsNeeded(newCapacity));
            table = newTable;
            threshold = (int)Math.min(newCapacity *loadFactor, MAXIMUM_CAPACITY + 1);
        }

    4、相似结构

    1Hashtable

    HashMap的早期版本号,底层和HashMap相似,也是hash表存储,链接法解决冲突,通过synchronizedkeyword保证线程安全,以下看看其put方法,和HashMapput方法非常像:

    public synchronizedV put(K key, V value) {
            //不同意Key为null的情况
            if (value == null) {
                throw new NullPointerException();
            }
     
            Entry tab[] = table;
           
            //利用key的hashCode()方法计算hash值
            int hash = hash(key);
           
            //除留余数法计算索引
            int index = (hash & 0x7FFFFFFF) %tab.length;
           
            //通过equals()方法找到key同样的节点覆盖
            for (Entry<K,V> e = tab[index] ;e != null ; e = e.next) {
                if ((e.hash == hash) &&e.key.equals(key)) {
                    V old = e.value;
                    e.value = value;
                    return old;
                }
            }
     
            modCount++;
            if (count >= threshold) {
                // Rehash the table if thethreshold is exceeded
                rehash();
     
                tab = table;
                hash = hash(key);
                index = (hash & 0x7FFFFFFF) %tab.length;
            }
     
            //插入新节点
            Entry<K,V> e = tab[index];
            tab[index] = new Entry<>(hash,key, value, e);
            count++;
            return null;
        }

    2)ConcurrentHashMap

    HashMap的线程安全版本号,底层和HashMap差点儿相似。也是採用hash表存储。链接法解决冲突,通过KeyhashCode()方法计算hash表索引。通过keyequals()方法推断两个Key是否同样。

    不同点在于。ConcurrentHashMap针对hash表提出了一个“分段”的概念。每次插入一个<KeyValue>的时候,都先逐个分段请求获取锁。获取成功之后再运行在该hash表分段的插入操作。

     

    总结:三者底层实现都是一样,可是不同之处在于是否线程安全,以及实现线程安全的方式。

    HashMap不支持线程安全,是一个简单高效的版本号。Hashtable通过synchronizedkeyword简单粗暴地实现了一个线程安全的HashMap。而新的ConcurrentHashMap通过一种叫做分段的灵活的方式实现了线程安全的HashMap

    所以不管出于什么原因,旧的Hashtable不建议再使用。若没有并发訪问需求,推荐HashMap,否则推荐线程安全的ConcurrentHashMap。

     

    二、HashSet

    HashSet是为独立元素的存放而设计的哈希存储。长处是高速存取。

    HashSet的设计较为“偷懒”。其直接在HashMap上封装而成

    public classHashSet<E>
        extends AbstractSet<E>
        implements Set<E>, Cloneable,java.io.Serializable
    {
        static final long serialVersionUID =-5024744406713321676L;
     
        private transient HashMap<E,Object>map;
     
        // Dummy value to associate with an Objectin the backing Map
        private static final Object PRESENT = newObject();

    能够看究竟层就是一个HashMapKey存放放入集合的元素。而相应的Value则是一个随意对象PRESENT

    以下是其Put方法:

    public boolean add(Ee) {
            return map.put(e, PRESENT)==null;
        }

    以下是返回迭代器的方法:

    publicIterator<E> iterator() {
            return map.keySet().iterator();
        }

  • 相关阅读:
    清除浮动
    版心和布局流程
    浮动(float)
    盒子模型(CSS重点)
    CSS 三大特性
    CSS 背景(background)
    移动web开发之rem布局
    .移动端常见布局
    移动端基础
    CSS高级技巧
  • 原文地址:https://www.cnblogs.com/tlnshuju/p/6811148.html
Copyright © 2011-2022 走看看