zoukankan      html  css  js  c++  java
  • HashSet与HashMap

    目的:了解HashSet的内部结构和使用

    1:Hash表:要了解HashSet,先要了解Hash表这一数据结构,包括Hash计算、装载因子、扩容等知识点。

    2:HashSet的继承关系图

                                     

      对于接口Set,是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。请注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。

    HashSet的API: 

    public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
    // 使用 HashMap 的 key 保存 HashSet 中所有元素
    private transient HashMap<E,Object> map; 
    
    // 定义一个虚拟的 Object 对象作为 HashMap 的 value 
     private static final Object PRESENT = new Object(); 
    
    // 默认构造函数
    public HashSet() 
         
    // 带集合的构造函数
    public HashSet(Collection<? extends E> c) 
         
    // 指定HashSet初始容量和加载因子的构造函数
    public HashSet(int initialCapacity, float loadFactor) 
         
    // 指定HashSet初始容量的构造函数
    public HashSet(int initialCapacity) 
         
    // 指定HashSet初始容量和加载因子的构造函数,dummy没有任何作用
    HashSet(int initialCapacity, float loadFactor, boolean dummy)

    由上面源程序可以看出,HashSet 的实现其实非常简单,它只是封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

    往hashset中插入对象其实只不过是内部做了

                public boolean add(Object o) {

                          return map.put(o, PRESENT)==null;
                }

    HashSet 的绝大部分方法都是通过调用 HashMap 的方法来实现的,因此 HashSet 和 HashMap 两个集合在实现本质上是相同的。

    3:HashMap

                       

    上图为Hashmap的数据结构图,具体实线是采用数组结合链表实现,链表是为了解决在hash过程中因hash值一样导致的碰撞问题。
    所以在使用自定义对象做key的时候,一定要去实现hashcode方法,不然HashMap就成了纯粹的链表,查找性能非常的慢,添加节点元素也非常的慢
    HashMap的成员变量:
    //默认初始容量,总为2的次方值  
    static final int DEFAULT_INITIAL_CAPACITY = 16;  
      
    //最大容量  
    static final int MAXIMUM_CAPACITY = 1 << 30;  
      
    //默认加载因子    
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  
      
    //Entry数组,每一个Entry是一个键值对实体  
    transient Entry[] table;  
      
    //实际存的Entry个数    
    transient int size;  
      
    //数组扩容的阀值,当size+1 > threshold时,扩充到以前容量的两倍  
    //threshold = table.length * loadFactor  
    int threshold;  
      
    //负载比率  
    final float loadFactor;  
      
    //Map结构一旦变化,如put remove clear等操作的时候,modCount随之变化  
    transient volatile int modCount; 

    可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

    Entry对象:

    //很简单的一个键值对实体而已  
    static class Entry<K,V> implements Map.Entry<K,V> {  
            final K key;          //key  
            V value;              //value  
            Entry<K,V> next;  //next Entry  
            final int hash;       //计算出key的hash值  
      
            /** 
             * Creates new entry. 
             */  
            Entry(int h, K k, V v, Entry<K,V> n) {  
                value = v;  
                next = n;  
                key = k;  
                hash = h;  
            }  
            .....  
    }  

    构造函数:

    // 构造函数  
    public HashMap(int initialCapacity, float loadFactor) {  
        if (initialCapacity < 0)  
            throw new IllegalArgumentException("Illegal initial capacity: "  
                    + initialCapacity);  
        if (initialCapacity > MAXIMUM_CAPACITY)  
            initialCapacity = MAXIMUM_CAPACITY;  
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  
            throw new IllegalArgumentException("Illegal load factor: "  
                    + loadFactor);  
      
        // 将传入的initialCapacity值,转变成2的次方值capacity去实例化hashmap的属性  
        // 比喻传入initialCapacity = 100,则算出来capacity = 2 << 7 = 128,  
        // 最终threshold = 128 * 0.75 = 96,table = new Entry[128]  
        int capacity = 1;  
        while (capacity < initialCapacity)  
            capacity <<= 1;  
      
        this.loadFactor = loadFactor;  
        threshold = (int) (capacity * loadFactor);  
        table = new Entry[capacity];  
        // 模板方法模式,子类想在init里面做点什么重写init就好了  
        init();  
    }  

    hash算法:

    /** 
     * 让hashMap里面的元素尽量分布均需,方便查找 
     * @param h entry中key的hash值 
     * @return 打散后的hash值 
     */  
    static int hash(int h) {  
        // This function ensures that hashCodes that differ only by  
        // constant multiples at each bit position have a bounded  
        // number of collisions (approximately 8 at default load factor).  
        h ^= (h >>> 20) ^ (h >>> 12);  
        return h ^ (h >>> 7) ^ (h >>> 4);  
    }   

     HashMap的功能是通过“键(key)”能够快速的找到“值”。下面我们分析下HashMap存数据的基本流程:

      (1):当调用put(key,value)时,首先获取key的hashcode,int hash = key.hashCode(); 

      (2): 2、 再把hash通过一下运算得到一个int h.  

         h ^= (h >>> 20) ^ (h >>> 12);  
            return h ^ (h >>> 7) ^ (h >>> 4); 
    为什么要经过这样的运算呢?这就是HashMap的高明之处。先看个例子,一个十进制数32768(二进制1000 0000 0000 0000),经过上述公式运算之后的结果是35080(二进制1000 1001 0000 1000)。看出来了吗?或许这样还看不出什么,再举个数字61440(二进制1111 0000 0000 0000),运算结果是65263(二进制1111 1110 1110 1111),现在应该很明显了,它的目的是让“1”变的均匀一点,散列的本意就是要尽量均匀分布。那这样有什么意义呢?看第3步。 
        (3) :得到h之后,把h与HashMap的承载量(HashMap的默认承载量length是16,可以自动变长。在构造HashMap的时候也可以指定一个长 度。这个承载量就是上图所描述的数组的长度。)进行逻辑与运算,即 h & (length-1),这样得到的结果就是一个比length小的正数,我们把这个值叫做index。其实这个index就是索引将要插入的值在数组中的 位置。第2步那个算法的意义就是希望能够得出均匀的index,这是HashTable的改进,HashTable中的算法只是把key的 hashcode与length相除取余,即hash % length,这样有可能会造成index分布不均匀。还有一点需要说明,HashMap的键可以为null,它的值是放在数组的第一个位置。 

     

     (4) :我们用table[index]表示已经找到的元素需要存储的位置。先判断该位置上有没有元素(这个元素是HashMap内部定义的一个类Entity, 基本结构它包含三个类,key,value和指向下一个Entity的next),没有的话就创建一个Entity<K,V>对象,在 table[index]位置上插入,这样插入结束;如果有的话,通过链表的遍历方式去逐个遍历,看看有没有已经存在的key,有的话用新的value替 换老的value;如果没有,则在table[index]插入该Entity,把原来在table[index]位置上的Entity赋值给新的 Entity的next,这样插入结束。
    总结: Key -> hashCode -> h -> index -> 遍历链表 -> 插入

     

    存储实现:put(key,vlaue)

    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在该处没有元素,则直接保存

    要同时复写equals方法和hashCode方法。

    按照散列函数的定义,如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同。

    如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希表的操作。

     文章出处:http://blog.csdn.net/o9109003234/article/details/44107811

  • 相关阅读:
    Atitit. visual studio vs2003 vs2005 vs2008  VS2010 vs2012 vs2015新特性 新功能.doc
    Atitit. C#.net clr 2.0  4.0新特性
    Atitit. C#.net clr 2.0  4.0新特性
    Atitit.通过null 参数 反射  动态反推方法调用
    Atitit.通过null 参数 反射  动态反推方法调用
    Atitit..net clr il指令集 以及指令分类  与指令详细说明
    Atitit..net clr il指令集 以及指令分类  与指令详细说明
    Atitit.变量的定义 获取 储存 物理结构 基本类型简化 隐式转换 类型推导 与底层原理 attilaxDSL
    Atitit.变量的定义 获取 储存 物理结构 基本类型简化 隐式转换 类型推导 与底层原理 attilaxDSL
    Atitit.跨语言反射api 兼容性提升与增强 java c#。Net  php  js
  • 原文地址:https://www.cnblogs.com/myseries/p/5203239.html
Copyright © 2011-2022 走看看