HashMap和HashSet解析

------------------------------------------------HashMap------------------------------------------------------

一、---概念---

HashMap继承自AbstractMap,实现了Map接口。下面从定义入手来开始分析:

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable  

(1)AbstractMap类提供Map接口的骨干实现

(2)Map接口定义了键映射到值的规则。

(3)实现了Cloneable接口的类,可以调用Object.clone方法返回该对象的浅拷贝。

二、---属性---

HashMap中有多个属性:包括初始容量、最大容量和装载因子等

 //默认的初始容量,必须是2的幂。
 static final int DEFAULT_INITIAL_CAPACITY = 16;

 //最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
 static final int MAXIMUM_CAPACITY = 1 << 30;

 //默认装载因子,这个后面会做解释
 static final float DEFAULT_LOAD_FACTOR = 0.75f;

 //存储数据的Entry数组,长度是2的幂。看到数组的内容了,接着看数组中存的内容就明白为什么博文开头先复习数据结构了
 transient Entry[] table;

 //map中保存的键值对的数量
 transient int size;

 //需要调整大小的极限值(容量*装载因子)
 int threshold;

 //装载因子
 final float loadFactor;

 //map结构被改变的次数
 transient volatile int modCount;

三、---构造方法---

HashMap中为我们提供了三个构造方法。

(1)HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

(2)HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。

(3)HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。

在这里提到了两个参数:初始容量,加载因子。其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

 public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR;
     threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//计算下次需要调整大小的极限值
     table = new Entry[DEFAULT_INITIAL_CAPACITY];//根据默认容量(16)初始化table
     init();
 }

 //根据给定的初始容量的装载因子创建一个空的HashMap
 //初始容量小于0或装载因子小于等于0将报异常
 public HashMap(int initialCapacity, float loadFactor) {
     //初始容量不能<0
     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal initial capacity: "
                 + initialCapacity);
     //初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
     if (initialCapacity > MAXIMUM_CAPACITY)
         initialCapacity = MAXIMUM_CAPACITY;
     //负载因子不能 < 0
     if (loadFactor <= 0 || Float.isNaN(loadFactor))
         throw new IllegalArgumentException("Illegal load factor: "
                 + loadFactor);  

     // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
     int capacity = 1;
     while (capacity < initialCapacity)
         capacity <<= 1;  

     this.loadFactor = loadFactor;
     //设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
     threshold = (int) (capacity * loadFactor);
     //初始化table数组
     table = new Entry[capacity];
     init();
 }  

 //根据指定容量创建一个空的HashMap
 public HashMap(int initialCapacity) {
     this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上面的构造方法,容量为指定的容量,装载因子是默认值
 }

 //通过传入的map创建一个HashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值
 public HashMap(Map<? extends K, ? extends V> m) {
     this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                   DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
     putAllForCreate(m);
 }

如上面的构造函数的源代码,其中第二个是重点,必须要掌握

从源码中可以看出,每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Entry节点。

四、---Entry类---

初始化table时均使用了Entry,table的数组元素为Entry结点。Entry是HashMap的一个内部类,实现Map接口的内部接口

Entry。下面给出Map.Entry接口及HashMap.Entry类的内容。Map.Entry接口定义的方法如下:

 K getKey();//获取Key
 V getValue();//获取Value
 V setValue();//设置Value,至于具体返回什么要看具体实现
 boolean equals(Object o);//定义equals方法用于判断两个Entry是否相同
 int hashCode();//定义获取hashCode的方法

如下是HashMap.Entry的具体实现:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;  

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        .......
    } 

其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。

五、---其他方法---

5.1put(E key, V value)

如下是put( )函数的源代码:

public V put(K key, V value) {
        //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
        if (key == null)
            return putForNullKey(value);
        //计算key的hash值
        int hash = hash(key.hashCode());                  ------(1)
        //计算key hash 值在 table 数组中的位置
        int i = indexFor(hash, table.length);             ------(2)
        //从i出开始迭代 e,找到 key 保存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断该条链上是否有hash值相同的(key相同)
            //若存在相同,则直接覆盖value,返回旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    //旧值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回旧值
            }
        }
        //修改次数增加1
        modCount++;
        //将key、value添加至i位置处
        addEntry(hash, key, value, i);
        return null;
    } 

通过源码我们可以清晰看到HashMap保存数据的过程为:首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。

获得hash值的函数如下:

static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }  

我们知道对于HashMap的table而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。那么如何做到这一点呢,我们使用的是下面的方法:

static int indexFor(int h, int length) {
        return h & (length-1);
    }  

HashMap的底层数组长度总是2的n次方,在构造函数中存在:capacity
<<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。至于为什么是2的n次方下面解释。

5.2 get(Object key)

通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。

public V get(Object key) {
        // 若为null,调用getForNullKey方法返回相对应的value
        if (key == null)
            return getForNullKey();
        // 根据该 key 的 hashCode 值计算它的 hash 码
        int hash = hash(key.hashCode());
        // 取出 table 数组中指定索引处的值
        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            //若搜索的key与查找的key相同,则返回相对应的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

该方法分为key为null和不为null两块。先看不为null的情况。先获取key的hash值,之后通过hash值及table.length获取key对

应的table数组的索引,遍历索引的链表,所找到key相同的元素,则返回元素的value,否者返回null。不为null的情况调用了

getForNullKey()方法。

private V getForNullKey() {
         for (Entry<K,V> e = table[0]; e != null; e = e.next) {
             if (e.key == null)
                 return e.value;
         }
         return null;
     }

这是一个私有方法,只在get中被调用。该方法判断table[0]中的链表是否包含key为null的元素,包含则返回value,不包含则

返回null。为什么是遍历table[0]的链表?因为key为null的时候获得的hash值都是0。

以上是对ArrayList中几个典型方法的源代码分析,还有很多就不一一列举了。

-------------------------------------------------HashSet-------------------------------------------------------

一、---概念---

存入Set的每个元素必须是惟一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set不

保证维护元素的次序。Set与Collection有完全一样的接口。

在没有其他限制的情况下需要Set时应尽量使用HashSet,因为它对速度进行了优化。

<span style="font-size:14px;">public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable  </span>

HashSet继承AbstractSet类,实现Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作。Set接口是一种不包括重复元素的Collection,它维持它自己的内部排序,所以随机访问没有任何意义。

二、---属性---

在LinkList中提供了两个基本的属性,一个是map,一个是PRESENT,其中map代表的是保存的元素,PRESENT代表的是value

//基于HashMap实现,底层使用HashMap保存所有元素
private transient HashMap<E,Object> map;  

 //定义一个Object对象作为HashMap的value
 private static final Object PRESENT = new Object();  

通过一个HashMap存储元素,元素是存放在HashMap的Key中,而Value统一使用一个Object对象。

这样看来HashSet应该很简单,应该只是使用了HashMap的部分内容来实现。

三、---构造方法---

LinkedList提高了两个构造方法:LinkedLis()和LinkedList(Collection<? extends E> c)。

 // 构造方法一:调用默认的HashMap构造方法初始化map
 public HashSet() {
     map = new HashMap<E,Object>();
 }
 // 构造方法二:根据给定的Collection参数调用HashMap(int initialCapacity)的构造方法创建一个HashMap(这个构造方法的HashMap的源码分析里已经描述过了)
 // 调用addAll方法将c中的元素添加到HashSet对象中
 public HashSet(Collection<? extends E> c) {
     map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
     addAll(c);
 }
 // 构造方法三:构造一个指定初始化容量和负载因子的HashMap
 public HashSet(int initialCapacity, float loadFactor) {
     map = new HashMap<E,Object>(initialCapacity, loadFactor);
 }
 // 构造方法四:构造一个指定初始化容量的HashMap
 public HashSet(int initialCapacity) {
     map = new HashMap<E,Object>(initialCapacity);
 }
 // 构造方法五:构造一个指定初始化容量和负载因子的LinkedHashMap
 // dummy参数被忽略,只是用于区分其他的,包含一个int、float参数的构造方法
 HashSet(int initialCapacity, float loadFactor, boolean dummy) {
     map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
 }

从构造函数中可以看出HashSet所有的构造都是构造出一个新的HashMap,其中最后一个构造函数,为包访问权限是不对外公开,仅仅只在使用LinkedHashSet时才会发生作用。

四、---其他方法---

4.1 Iterator()

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

iterator()方法返回对此 set 中元素进行迭代的迭代器。返回元素的顺序并不是特定的。底层调用HashMap的keySet返回所有的key,这点反应了HashSet中的所有元素都是保存在HashMap的key中,value则是使用的PRESENT对象,该对象为static
final。

4.2 size()

public int size() {
        return map.size();
    }  

size()返回此 set 中的元素的数量(set 的容量)。底层调用HashMap的size方法,返回HashMap容器的大小。

4.3add(E e)

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

add(E e)方法只是调用了HashMap(构造方法中提供了创建LinkedHashMap的方式,但是LinkedHashMap是继承

HashMap的,put方法也是调用HashMap的put方法)的put方法将e当做Key,PERSENT当做Value加入到map中并根据返回值判

断是否添加成功。

五、---LinkedHashSet---

LinkedHashSet具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结

果会按照元素的插入次序显示。

 public class LinkedHashSet<E>
     extends HashSet<E>
     implements Set<E>, Cloneable, java.io.Serializable {

     public LinkedHashSet(int initialCapacity, float loadFactor) {
             super(initialCapacity, loadFactor, true);
     }

     public LinkedHashSet(int initialCapacity) {
         super(initialCapacity, .75f, true);
     }

     public LinkedHashSet() {
         super(16, .75f, true);
     }

     public LinkedHashSet(Collection<? extends E> c) {
         super(Math.max(2*c.size(), 11), .75f, true);
         addAll(c);
     }
 }

从上面可以看出还是间接地调用HashSet中的构造函数

注意这里的构造方法,都调用了父类HashSet的第五个构造方法:HashSet(int initialCapacity,
float loadFactor, boolean

dummy)。如果还记得上面的内容应该明白为什么是基于链表,下面再给出这个构造方法的内容。

 HashSet(int initialCapacity, float loadFactor, boolean dummy) {
     map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
 }

区别于其他的HashSet的构造方法,这个方法创建的是一个LinkedHashMap。LinkedHashMap继承自HashMap,同时自身

有一个链表结构用于维护元素顺序,默认情况使用的是插入元素,所以LinkedHashSet既有HashSet的访问速度(因为访问的时候

都是通过HashSet的方法访问的),同时可以维护顺序。

尊重作者,尊重原创,参考文章:

http://blog.csdn.net/jzhf2012/article/details/8540670

http://blog.csdn.net/chenssy/article/details/18323767

时间: 2024-10-10 16:33:17

HashMap和HashSet解析的相关文章

HashMap、HashSet源代码分析其 Hash 存储机制

集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并不是真正的把 Java 对象放入数组中,只是把对象的引用放入数组中,每个数组元素都是一个引用变量. 实际上,HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算法决定集合元素的存储位置,这样可以保证能快速存.取集合元素:对于 HashMap 而言,系统 key-value 当成一个整体进行处理,系统总是根据 Hash 算法来计算 key-value 的存储位置,这样可

JDK之HashMap、HashSet分析

HashMap主要分析key.value的放入Map和取出Map操作以及他的遍历器.个人觉得在HashMap中有个很重要的内部类Entry,Map的put,get等重要方法都是依靠这个Entry的.先来分析下这个内部类Entry,Entry中有几个重要的变量key.value.next,不用说大家也会明白这几个变量的含义,当然也自然会有get和set方法了.当我们在遍历Map的时候,有一种方法就是获取这个内部类的对象entry,然后去这个entry中取值.现在就以这个为例,遍历一个Map.首先获

【转】Java HashMap 源码解析(好文章)

- .fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wrapper iframe, .fluid-width-video-wrapper object, .fluid-width-video-wrapper embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } [

Java容器 HashMap与HashSet的学习

Java学习中,看到HashMap,HashSet类,本着不止要停留在用的层面( 很多公司面试都要问底层 ),学习了JDK源码,记录下笔记. 源码来自jdk1.7下的src.zip HashMap是一种键值对类型,它提供一种Key-Value对应保存的数据结构,实现了Map接口,其中key的值唯一,即一个key某一时刻只能映射到唯一的值. 看其中几个成员(没列全) static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 sta

HashMap和HashSet的源代码分析

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认的扩容倍数 static final Entry<?,?>[] EMPTY_TABLE = {}; //就比较用的 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;/

[转]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未经同步,所以

java该HashTable,HashMap和HashSet

同一时候我们也对HashSet和HashMap的核心方法hashcode进行了具体解释,见<探索equals()和hashCode()方法>. 万事俱备,那么以下我们就对基于hash算法的三个集合HashTable,HashSet和HashMap具体解释. 本文文件夹: 1. HashTable和HashMap的差别 2. HashSet和HashMap的差别 3. HashMap,HashSet工作原理 4. HashSet工作原理 5. 常见问题 1. HashTable和HashMap的

HashMap和HashSet的区别http://www.importnew.com/6931.html

HashMap和HashSet的区别是Java面试中最常被问到的问题.如果没有涉及到Collection框架以及多线程的面试,可以说是不完整.而Collection框架的问题不涉及到HashSet和HashMap,也可以说是不完整.HashMap和HashSet都是collection框架的一部分,它们让我们能够使用对象的集合.collection框架有自己的接口和实现,主要分为Set接口,List接口和Queue接口.它们有各自的特点,Set的集合里不允许对象有重复的值,List允许有重复,它

java中的HashTable,HashMap和HashSet

目录(?)[+] 上篇博客中我们详细的分析了java集合<java中Map,List与Set的区别>. 同时我们也对HashSet和HashMap的核心方法hashcode进行了详解,见<探索equals()和hashCode()方法>. 万事俱备,那么下面我们就对基于hash算法的三个集合HashTable,HashSet和HashMap详解. 本文目录: 1. HashTable和HashMap的区别 2. HashSet和HashMap的区别 3. HashMap,HashS