HashSet、HashMap

原创 2016年08月29日 08:59:39
HashSet:

很多开发者,初学者都知道HashSet无序,不可重复,线程非同步。底层是哈希表结构。
但它是怎么做到的?什么是散列表数据结构(哈希表)?有什么特性?都清楚吗?不清楚继续往下看。

它是这样做到的:

先来看HashSet的源码,首先看默认构造器:

public HashSet() {  
    map = new HashMap<E,Object>();  
}  
// ok,我们看到构造器中new了一个HashMap。key使用了泛型,value使用Object。  
再来看add方法源码:
private static final Object PRESENT = new Object();  
public boolean add(E e) {  
    return map.put(e, PRESENT)==null;  
}  
// PRESENT是一个Object类型的常量,用来当做map的value. 也就是说,你以后在HashSet中存储的元素都是HashMap中key,value全部使用Object。  
HashMap的key是不可以重复的,保证元素唯一的依据是对象的hashCode跟equals方法。
而HashSet不就是用HashMap的key来存储元素嘛,也就保证了元素的唯一性。包括迭代器也是HashMap中keySet方法取得的iterator。
public Iterator<E> iterator() {  
    return map.keySet().iterator();  
}
通过上面的介绍,已经对HashSet比较了解了,我们知道HashSet底层是用了HashMap。
要想知道怎么做到存取速度快的,我们直接看HashMap就好了。

散列表数据结构(哈希表)

散列表(Hash table,也叫哈希表),是根据关键字(Key value)而直接进行访问的数据结构。
也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

HashMap底层就是散列表数据结构,即数组和链表的结合体,
底层是一个数组结构,数组中的每一项又是一个链表。这样做有什么好处呢?
数组能够提供对元素的快速访问但不易于扩展(如果不知道元素脚标,还得进行遍历查找),链表易于扩展但不能对其元素进行快速访问。
怎样做到两全其美,就是散列表数据结构。

我们来看看HashMap中元素存跟取的实现方式:

首先明白,HashMap根据key的hashCode计算出元素在Entry数组中的位置,然后再Entry内部链表中存放key,value。

先看构造方法源码:
static final int DEFAULT_INITIAL_CAPACITY = 16; // 默认初始化容量  
static final float DEFAULT_LOAD_FACTOR = 0.75f;   
final float loadFactor; // 用于计算扩容阀值  
 /* The next size value at which to resize (capacity * load factor) */  
int threshold; // Entry扩容阀值  
// The table, resized as necessary. Length MUST Always be a power of two.  
transient Entry[] table;// 存放键值对的Entry数组  
public HashMap() {   
    this.loadFactor = DEFAULT_LOAD_FACTOR;  
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); // 计算扩容阀值  
    table = new Entry[DEFAULT_INITIAL_CAPACITY]; // 初始化Entry<K,V>数组  
    init();  
}  
/* 在默认构造方法中,初始化了一个容量为16的HashMap(Entry数组),当元素超过75%(16*0.75f=12个)的时候开始自动扩容*/
put方法源码: eg: map.put("a","abc");
public V put(K key, V value) {  
    if (key == null)  
        return putForNullKey(value);  
    int hash = hash(key.hashCode()); // 获取key的hash值  
    int i = indexFor(hash, table.length); // h & (length-1); 通过hashcode取模数组长度, 定位hash值在table数组中的索引  
    // 如果table数组中i索引所在位置有元素,循环遍历该链表中的下一个元素  
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
        Object k;  
        // 遍历到了hash值相同并且equals也相同的key,把value用新值替换掉  
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
      
    modCount++;  
    // table中i索引所在位置没有元素,添加key、value到指定索引处。  
    addEntry(hash, key, value, i);  
    return null;  
}  
addEntry()方法源码:
void addEntry(int hash, K key, V value, int bucketIndex) {  
    // 下面两行代码将entry保存进了table数组中Entry内部链表的第一个位置。  
    Entry<K,V> e = table[bucketIndex];  
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
    if (size++ >= threshold) // 需要扩容了  
        resize(2 * table.length); // 重新计算数组大小  
}  

由于元素的位置是通过hashcode取模数组长度而得, 现在由于需要扩容,数组长度会发生变化,
所以会在resize方法跟transfer方法中进行元素位置的重新分配。

resize()方法源码: // 重新计算数组长度
void resize(int newCapacity) {  
    Entry[] oldTable = table;  
    int oldCapacity = oldTable.length;  
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;  
        return;  
    }  
  
  
    Entry[] newTable = new Entry[newCapacity];  
    transfer(newTable);  
    table = newTable;  
    threshold = (int)(newCapacity * loadFactor); // 新的扩容阀值  
}  
transfer()方法源码:// 重新分配
void transfer(Entry[] newTable) {  
    Entry[] src = table;  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) {  
        Entry<K,V> e = src[j];  
        if (e != null) {  
            src[j] = null;  
            do {  
                Entry<K,V> next = e.next;  
                int i = indexFor(e.hash, newCapacity);  
                e.next = newTable[i];  
                newTable[i] = e;  
                e = next;  
            } while (e != null);  
        }  
    }  
}  
上面是HashMap存元素的实现方式,再来看看取元素的方式:

// get方法源码
public V get(Object key) {  
    if (key == null)  
        return getForNullKey();  
    int hash = hash(key.hashCode()); // 还是计算key的hashcode,  
    // 定位hash值在table数组中的索引,并通过equals方法定位元素在链表中的位置。  
    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.equals(k)))  
            return e.value;  
    }  
    return null;  
}  
我们来对HashMap存取元素的过程来做一个小的总结:

元素(key,value)在HashMap中被封装进Entry数组。
put元素的时候,根据key的hash值定位元素在Entry数组中的索引,如果当前索引有元素,就通过equals方法进行比较,将元素存进当前Entry的链表中适当的位置。
get元素的时候,根据key的hash值定位元素在Entry数组中的索引,然后通过equals方法定位元素在链表中的位置,取出该元素。

性能分析:

因为元素的存取是通过hash算法进行的,所以速度都没的说。
在查找操作中,唯一影响性能的是在链表中,但实际只要优化好了key对象hashCode跟equals方法,就会避免链表中的数据过多而导致查找性能变慢。

再一个非常影响性能的是数组扩容操作,当使用默认的DEFAULT_INITIAL_CAPACITY对HashMap进行初始化的时候,
如果元素个数非常多,会导致扩容次数增加,每次扩容都会进行元素位置的重新分配,这是相当耗费性能的。
如果能预算好元素个数,就应该避免使用默认的DEFAULT_INITIAL_CAPACITY,可在HashMap的构造函数中为其指定一个初始值。

问题解决:

hashCode必须和equals保持兼容(equals方法的判断依据和计算hashCode的依据相同),这样做是为了避免链表中的数据过多。

举例:  
public class Person {    
       public int id;    
       public String name="";    
  
       public int hashCode() {     
            return id;    
       }    
       // equals必须比较id    
       public boolean equals(Person p) {    
            if(this.id == p.id)  
                return true;    
            else   
                return false;    
       }  
}  

如果元素很多,应该使用这个构造函数public HashMap(int initialCapacity){}对HashMap进行初始化。
举例:HashMap map = new HashMap(1024);

了解了HashMap的存储原理之后,自然也就明白了为什么说HashSet的存取效率高了。

















版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

HashMap与HashTable和HashSet的区别

  • 2008年04月23日 10:23
  • 4KB
  • 下载

ArrayList、Vector、HashMap、HashTable、HashSet的默认初始容量、加载因子、扩容增量

这里要讨论这些常用的默认初始容量和扩容的原因是: 当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的...

ArrayList、Vector、HashMap、HashSet的默认初始容量、加载因子、扩容增量

原文转自http://www.cnblogs.com/xiezie/p/5511840.html   这里要讨论这些常用的默认初始容量和扩容的原因是: 当底层实现涉及到扩容时,容器或...

Java集合HashSet-ArrayList-HashMap的线程同步控制方法和区别

Collections类中提供了多个synchronizedXxx,该方法返回指定集合对象对应的同步对象,从而可以解决多线程并发访问集合时的线程安全问题.    正如Java中常用的集合框架推荐使用...

HashSet、HashMap,散列表数据结构(哈希表)

很多开发者,初学者都知道HashSet无序,不可重复,线程非同步。底层是哈希表结构。 但它是怎么做到的?什么是散列表数据结构(哈希表)?有什么特性?都清楚吗?不清楚继续往下看。 它是这样做到的...

J2SE学习笔记:J2SE重点难点,数组排序、HashSet去重、HashMap遍历

package cn.itheima; /** * 数组排序 * @author Cuilitang * */ public class ArraySort { public stati...

HashMap和HashSet的区别

HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题。HashMap的工作原理、ArrayList与Vect...
  • qian_ch
  • qian_ch
  • 2016年12月21日 09:54
  • 226

HashSet和HashMap分析

1.HashSet可以支持null元素,但最多放一个,HashSet不支持重复元素,因为元素是内部HashMap的Key; 2.HashSet由HashMap支持,HashMap支持null,但最多只...

集合框架(HashSet,HashMap,HashTable)

一,Map集合的基本操作 package fighting; import java.util.Collection; import java.util.HashMap; import j...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:HashSet、HashMap
举报原因:
原因补充:

(最多只允许输入30个字)