HashSet、HashMap

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的存取效率高了。

时间: 2024-08-05 00:13:01

HashSet、HashMap的相关文章

java基础之问题:请说出hashCode方法、equals方法、HashSet、HashMap之间的关系

①HashSet是采用HashMap来实现的:这个HashMap的key就是放进HashSet中的对象,value就是一个Object类型的对象 ②当调用HashSet的add方法时,实际上是想HashMap中增加了一行(key-value对),该行的key就是往HashSet增加的那个对象,该行的value就是一个Object类型的常量. ③HashMap底层采用数组来维护 ④调用增加的那个对象的hashCode方法,来得到一个hashCode,然后根据该值来计算出一个数组的下标索引(计算出数

Java集合Set、Map、HashSet、HashMap、TreeSet、TreeMap等

1.Set和Map的关系: Set代表一种集合元素无序.不可重复的集合,Map代表一种由多个key-value对组成的集合. Set的集合继承体系: Map关系集合 Map集合的key特征:所有key不能重复,key之间没有顺序.Map集合的所有key将具有set集合的特征. 对Set做改造可将Set改造成Map集合: 2.HashSet和HashMap的区别和联系:    对于HashSet,系统采用Hash算法决定集合元素的存储位置:对于HashMap,系统将value当成key的附属物,系

深入源码剖析 HashSet、HashMap、HashTable

HashTable HashTable 是什么 public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable HashTable 是 Java 中哈希表的一种实现形式,它是 Dictionary 的子类,并且实现了 Map 接口. 注1: 哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接

[转]HashMap与HashTable的区别、HashMap与HashSet的关系

转自: http://blog.csdn.net/wl_ldy/article/details/5941770 HashTable的应用非常广泛,HashMap是新框架中用来代替HashTable的类,也就是说建议使用HashMap,不要使用HashTable.可能你觉得HashTable很好用,为什么不用呢?这里简单分析他们的区别. 一:HashMap与HashTable的区别 1.HashTable的方法是同步 的,在方法的前面都有synchronized来同步,HashMap未经同步,所以

JDK中ArrayList、HashMap和HashSet的equals方法源码分析

最近遇到个坑,在分别对ArrayList.HashMap等数据类型进行比较时,发现数据一样,但equals一直返回false.于是乎看了一下ArrayList和HashMap的源码,才恍然大悟.本文的代码摘自JDK 1.7.0. ArrayList的equals方法: public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof List)) return false; ListIterator<

HashMap与HashTable、HashSet与HashMap异同

1. HashMap与HashTable的区别: (1)HashMap允许将null作为一个entry的key或者value,而Hashtable不允许.当get()方法返回null值时,即可以表示HashMap中没有该键,也可以表示该键所对应的值为null.因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断. (2)HashMap把Hashtable的contains方法去掉了,改成containsvalue和cont

HashMap与HashTable的区别、HashMap与HashSet的关系

http://blog.csdn.net/wl_ldy/article/details/5941770 HashTable的应用非常广泛,HashMap是新框架中用来代替HashTable的类,也就是说建议使用HashMap,不要使用HashTable.可能你觉得HashTable很好用,为什么不用呢?这里简单分析他们的区别. 一:HashMap与HashTable的区别 1.HashTable的方法是同步 的,在方法的前面都有synchronized来同步,HashMap未经同步,所以在多线程

HashMap、HashSet、Hashtable key/value终极总结

涉及到Hash,即通过Key的哈希值,存取对应value. 注意,同一个key的哈希值必须唯一,不可变. 1.HashMap,key为null时,存在tab[0]中. value可为空,key的hash不同的话,可存多个. 2.HashSet,采用HashMap,add(value)即采用HashMap的put(value, false)方法, 就是将value作为HashMap的key存入.可存null,只能存一个. 3.Hashtable,多线程安全,synchronized修饰,底层采用m

对TreeSet中的元素&quot;HashSet&quot;、&quot;ArrayList&quot;、&quot;TreeMap&quot;、&quot;HashMap&quot;、&quot;TreeSet&quot;、&quot;LinkedList&quot;进行升序 * 使用静态内部类实现

/* * 对TreeSet中的元素"HashSet"."ArrayList"."TreeMap"."HashMap"."TreeSet"."LinkedList"进行升序 * 2. 使用静态内部类实现 */ import java.util.*; public class TreeTest4 { private static Set<String> treeSet; publ