Java基础:HashMap和HashSet解析

来源:互联网 发布:bt.gg新域名 编辑:程序博客网 时间:2024/06/05 15:54


一、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)

直接看代码:

[java] view plaincopyprint?
  1. public V put(K key,V value) {  
  2.         if (table == EMPTY_TABLE) {  
  3.             inflateTable(threshold);  
  4.         }  
  5.          
  6.         //key为null时的插入  
  7.         if (key == null)  
  8.             return putForNullKey(value);  
  9.          
  10.         //根据key计算hash值  
  11.         int hash = hash(key);  
  12.          
  13.         //返回哈希表索引位置  
  14.         int i = indexFor(hash, table.length);  
  15.          
  16.         //在哈希表中该索引处的链表中查找相同key的Entry<K,V>  
  17.         //注意table[i]是指向Entry<K,V>链表头结点的指针  
  18.         for (Entry<K,V> e = table[i]; e!= null; e = e.next) {  
  19.             Object k;  
  20.             if (e.hash == hash && ((k =e.key) == key || key.equals(k))) {  
  21.                    //通过equals方法判断找到相同key的节点,用新value覆盖旧value并返回旧value  
  22.                 V oldValue = e.value;  
  23.                 e.value = value;  
  24.                 e.recordAccess(this);  
  25.                 return oldValue;  
  26.             }  
  27.         }  
  28.    
  29.         modCount++;  
  30.          
  31.         //创建一个新的Entry<K,V>实体,头插法插入到位置i处  
  32.         addEntry(hash, key, value, i);  
  33.         return null;  
  34.     }  

其中针对keynull的处理如下:

[java] view plaincopyprint?
  1. private V putForNullKey(V value) {  
  2. //在hash表的第0个位置开始找是否已经有了key为null的节点  
  3.         for (Entry<K,V> e = table[0]; e!= null; e = e.next) {  
  4.             if (e.key == null) {  
  5.                 V oldValue = e.value;  
  6.                 e.value = value;  
  7.                 e.recordAccess(this);  
  8.                 return oldValue;  
  9.             }  
  10.         }  
  11.         modCount++;  
  12.          
  13.         //在hash表的第0个位置用头插法插入key为null的这个节点  
  14.         addEntry(0null, value, 0);  
  15.         return null;  
  16.     }  

其中通过key计算hash值方法如下:

[java] view plaincopyprint?
  1. final inthash(Object k) {  
  2.         int h = hashSeed;  
  3.         if (0 != h && k instanceofString) {  
  4.             returnsun.misc.Hashing.stringHash32((String) k);  
  5.         }  
  6.    
  7.         //调用Key的hashCode()方法计算hash值  
  8.         h ^= k.hashCode();  
  9.    
  10.         // This function ensures that hashCodesthat differ only by  
  11.         // constant multiples at each bitposition have a bounded  
  12.         // number of collisions (approximately8 at default load factor).  
  13.         h ^= (h >>> 20) ^ (h>>> 12);  
  14.         return h ^ (h >>> 7) ^ (h>>> 4);  
  15.     }  

由此可以得出以下结论:

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方法就相对简单:

[java] view plaincopyprint?
  1. public V get(Objectkey) {  
  2.         if (key == null)  
  3.             return getForNullKey();  
  4.         Entry<K,V> entry = getEntry(key);  
  5.    
  6.         return null == entry ? null :entry.getValue();  
  7.     }  

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

[java] view plaincopyprint?
  1. final Entry<K,V> getEntry(Object key) {  
  2.         if (size == 0) {  
  3.             return null;  
  4.         }  
  5.    
  6.         //根据Key计算hash值  
  7.         int hash = (key == null) ? 0 :hash(key);  
  8.          
  9.         //indexFor(hash,table.length)根据hash值返回其在hash表中索引位置  
  10.         //在该索引位置指向的链表中查找Key相同的节点并返回其Value  
  11.         for (Entry<K,V> e =table[indexFor(hash, table.length)];  
  12.              e != null;  
  13.              e = e.next) {  
  14.             Object k;  
  15.             if (e.hash == hash &&  
  16.                 ((k = e.key) == key || (key !=null && key.equals(k))))  
  17.                 return e;  
  18.         }  
  19.         return null;  
  20.     }  

3、散列表容量

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

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

下面看看resize操作时如何进行的:

[java] view plaincopyprint?
  1. void resize(intnewCapacity) {  
  2.         Entry[] oldTable = table;  
  3.         int oldCapacity = oldTable.length;  
  4.         if (oldCapacity == MAXIMUM_CAPACITY) {  
  5.             threshold = Integer.MAX_VALUE;  
  6.             return;  
  7.         }  
  8.    
  9.         //根据新的容量重新创建hash表  
  10.         Entry[] newTable = newEntry[newCapacity];  
  11.          
  12.         //逐个将节点拷贝至新hash表,较为耗时  
  13.         transfer(newTable,initHashSeedAsNeeded(newCapacity));  
  14.         table = newTable;  
  15.         threshold = (int)Math.min(newCapacity *loadFactor, MAXIMUM_CAPACITY + 1);  
  16.     }  

4、类似结构

1Hashtable

HashMap的早期版本,底层和HashMap类似,也是hash表存储,链接法解决冲突,通过synchronized关键字保证线程安全,下面看看其put方法,和HashMapput方法很像:

[java] view plaincopyprint?
  1. public synchronizedV put(K key, V value) {  
  2.         //不允许Key为null的情况  
  3.         if (value == null) {  
  4.             throw new NullPointerException();  
  5.         }  
  6.    
  7.         Entry tab[] = table;  
  8.          
  9.         //利用key的hashCode()方法计算hash值  
  10.         int hash = hash(key);  
  11.          
  12.         //除留余数法计算索引  
  13.         int index = (hash & 0x7FFFFFFF) %tab.length;  
  14.          
  15.         //通过equals()方法找到key相同的节点覆盖  
  16.         for (Entry<K,V> e = tab[index] ;e != null ; e = e.next) {  
  17.             if ((e.hash == hash) &&e.key.equals(key)) {  
  18.                 V old = e.value;  
  19.                 e.value = value;  
  20.                 return old;  
  21.             }  
  22.         }  
  23.    
  24.         modCount++;  
  25.         if (count >= threshold) {  
  26.             // Rehash the table if thethreshold is exceeded  
  27.             rehash();  
  28.    
  29.             tab = table;  
  30.             hash = hash(key);  
  31.             index = (hash & 0x7FFFFFFF) %tab.length;  
  32.         }  
  33.    
  34.         //插入新节点  
  35.         Entry<K,V> e = tab[index];  
  36.         tab[index] = new Entry<>(hash,key, value, e);  
  37.         count++;  
  38.         return null;  
  39.     }  

2)ConcurrentHashMap

HashMap的线程安全版本,底层和HashMap几乎类似,也是采用hash表存储,链接法解决冲突,通过KeyhashCode()方法计算hash表索引,通过keyequals()方法判断两个Key是否相同。

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

 

总结:三者底层实现都是一样,但是不同之处在于是否线程安全,以及实现线程安全的方式。HashMap不支持线程安全,是一个简单高效的版本,Hashtable通过synchronized关键字简单粗暴地实现了一个线程安全的HashMap,而新的ConcurrentHashMap通过一种叫做分段的灵活的方式实现了线程安全的HashMap

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

 

二、HashSet

HashSet是为独立元素的存放而设计的哈希存储,优点是快速存取。

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

[java] view plaincopyprint?
  1. public classHashSet<E>  
  2.     extends AbstractSet<E>  
  3.     implements Set<E>, Cloneable,java.io.Serializable  
  4. {  
  5.     static final long serialVersionUID =-5024744406713321676L;  
  6.    
  7.     private transient HashMap<E,Object>map;  
  8.    
  9.     // Dummy value to associate with an Objectin the backing Map  
  10.     private static final Object PRESENT = newObject();  

可以看到底层就是一个HashMapKey存放放入集合的元素,而对应的Value则是一个任意对象PRESENT

下面是其Put方法:

[java] view plaincopyprint?
  1. public boolean add(Ee) {  
  2.         return map.put(e, PRESENT)==null;  
  3.     }  

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

[java] view plaincopyprint?
  1. publicIterator<E> iterator() {  
  2.         return map.keySet().iterator();  
  3.     }  
0 0
原创粉丝点击