HashMap的存储与实现

 我们如果要保存一组对象,用我们之前学过的知识,会使用对象数组,但鉴于数组的局限性,数组长度一经定义就不能改变,所以我们使用链表、队列等数据结构操作,但是很麻烦。类集框架就是一个动态的数组,但不受数组长度的限制。 


HashMap允许key值为空,(在方法containsValue(Object value):如果指定值key==null,并且在键值对中有value为null时,也返回true)但是Hashtable不允许,否则会报“NullPointer Expection”异常。 

一、HashMap键值对的实现 

       HashMap是Map接口的实现子类,用于存放一对值,即类中的每一个元素都是以Key---->Value的形式存储。我们知道,在Java集合框架中,无论是将一个对象存放在数组中,还是队列中,其实并不是把这个对象存入其中,而是将对象的引用存入数组或者队列中。在HashMap中,数据的存储同样如此,我们通过调用put(K key,V value)方法存储键值对,方法如下:  
Java代码   收藏代码
  1. /** 
  2.      * 存储关联的键值对 
  3.      * @param key:键 
  4.      * @param value:值 
  5.      * @return 
  6.      */  
  7.      public V put(K key, V value) {  
  8.          //当键值为null时,调用putForNullKey(value)的方法存储,  
  9.          //在该方法中调用recordAccess(HashMap<K,V> m)的方法处理  
  10.             if (key == null)  
  11.                 return putForNullKey(value);  
  12.             //根据key的KeyCode,计算hashCode  
  13.             int hash = hash(key.hashCode());  
  14.             //调用indexFor方法,返回hash在对应table中的索引(Entry[] table)  
  15.             int i = indexFor(hash, table.length);  
  16.             //当i索引处的Entry不为null时,遍历下一个元素  
  17.             for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  18.                 Object k;  
  19.                 //如果遍历到的hash值等于根据Key值计算出的hash值并且  
  20.                 //key值与需要放入的key值相等时,存放与key对应的value值  
  21.                 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  22.                     //覆盖oldValue的值  
  23.                     V oldValue = e.value;  
  24.                     e.value = value;  
  25.                     e.recordAccess(this);  
  26.                     return oldValue;  
  27.                 }  
  28.             }  
  29.               
  30.             modCount++;  
  31.           //当i索引处的Entry为null时,将指定的key、value、hash条目放入到指定的桶i中  
  32.             //如果现有HashMap的大小大于容量*负载因子时,resize(2 * table.length);  
  33.             addEntry(hash, key, value, i);  
  34.             return null;  
  35.         }  


       在上面的put(K key,V value)方法中可知,当要存储Key---->Value对时,实际上是存储在一个Entry的对象e中,程序通过key计算出Entry对象的存储位置。换句话说,Key---->Value的对应关系是通过key----Entry----value这个过程实现的,所以就有我们表面上知道的key存在哪里,value就存在哪里。在Map接口中,有一个Entry接口,该接口用于处理key和value的set()和get()方法,所以在Map中存储数据,实际上是将Key---->value的数据存储在Map.Entry接口的实例中,再在Map集合中插入Map.Entry的实例化对象,如图示: 
 


二、HashMap的存储机制 

         HashMap的内部存储结构其实是数组和链表的结合。 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 
每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。
 
 
HashMap有四种方法: 
        HashMap():初始容量16,默认加载因子0.75 
        HashMap(int initialCapacity):自定义初始容量 
        HashMap(int initialCapacity,float loadFactor):自定义初始容量和加载因子 
        HashMap(Map<? extends K,? extends V> m) 
        这四个构造方法其实都受两个参数的影响:容量和加载因子。容量是哈希表中桶的数量,初始容量为16。加载因子是对哈希表的容量在自动增加resize()之前所达到尺度的描述。当哈希表中的条目数超过threshold(=Capacity*loadFactor) 的值时,要对哈希表进行rehash操作。 
        默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。 



三、HashMap的冲突处理问题 

        由于哈希函数是一个压缩映象,因此在一班情况下,很容易产生“冲突”现象,即key1 ≠ key2,而f(key1)=f(key2)。而且,由于关键字的集合比较大,这种冲突是不可避免的,所以必须采取合理的解决方案,找出尽量少产生冲突的哈希函数和处理冲突的方法。对于哈希函数的构造,通常有直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法等。而这里重点讲述处理冲突的两种方法。 
       1、 开放地址法 
        开放地址法是对那些发生冲突的记录,用hi=(h(key)+di)mod n方法再次确定Hash地址。 
        n:为哈希表长; 
       di:为增量序列,其取法有以下三种: 
        1)线性探测再散列      di= c * i  
        2)二次探测再散列      di = 12, -12, 22, -22, …, 
        3) 随机探测再散列      di是一组伪随机数列 或者 di=i×H2(key) (又称双散列函数探测) 
例如表长为11的哈希表中已填有关键字为17,60,29的记录,H(key)=key  MOD  11,现有第4个记录,其关键字为38 

       H(38)=38 MOD 11=5    冲突 
       H1=(5+1) MOD 11=6    冲突 
       H2=(5+2) MOD 11=7    冲突 
       H3=(5+3) MOD 11=8    不冲突 
对于其他增量序列的方法也是如此计算。 

         2、链地址法 
        将所有哈希地址相同的记录都链接在同一链表中图形类似于图2。也就是说,当HashMap中的每一个bucket里只有一个Entry,不发生冲突时,Hashmap是一个数组,根据索引可以迅速找到Entry,但是,当发生冲突时,单个的bucket里存储的是一个Entry链,系统必须按顺序遍历每个Entry,直到找到为止。为了减少数据的遍历,冲突的元素都是直接插入到第一个Entry后面的,所以,最早放入bucket中的Entry,位于Entry链中的最末端。这从put(K key,V value)中也可以看出,在同一个bucket存储Entry链的情况下,新放入的Entry总是位于bucket中。 

四、HashMap元素的输出 

        对于Map接口来说,其本身是不能直接使用迭代(Iteraor)进行输出的,因为Map接口的中的每个位置存放的是一对值(key---->value),而Iterator中每次只能找到一个值,如果要通过迭代的方法进行输出,主要分为以下几步: 
        1、将Map接口的实例通过Set<Entry<K,V>> entrySet();方法变为Set接口对象; 
        2、通过Set接口实例为Iterator实例化 
        3、通过Iterator迭代输出,输出的每个内容都是Map.Entry的对象 
        4、通过Map.Entry进行key---value的分离 
具体代码实现如下:  

Java代码   收藏代码
  1. /实例化HashMap对象  
  2.         HashMap<String,String> hashMap=new HashMap<String,String>();  
  3.         //1、将Map接口变为Set接口  
  4.         Set<Map.Entry<String,String>> set=hashMap.entrySet();  
  5.         //2、实例化Iterator接口  
  6.         Iterator it=set.iterator();  
  7.         while(it.hasNext()){  
  8.             //3、得到存储在HashMap中的Entry对象  
  9.             Map.Entry<String,String> me=(Entry<String, String>) it.next();  
  10.             //4、通过Entry得到key和value  
  11.             System.out.println("Key="+me.getKey()+"Value="+me.getValue());  
  12.         }  

         上面的Map的输出过程,entrySet()主要是返回此映射所包含的映射关系的 Set 视图,在HashMap中,还有一个keySet()方法用于返回此映射中所包含的键的 Set 视图,步骤都是一样的。根据key可以通过get(key)方法找到对应的value。如果存储的key值不是系统类,而是自定义的类,则需要注意以下两点: 
1)必须存储自定义类的实例化对象,如果使用匿名对象,就找不到对应值。 
例如,key值是一个Student的类型
 

Java代码   收藏代码
  1. HashMap<Student,String> map=new HashMap<Student,String>();  
  2.         map.put(new Student("1608100201","Jony"), "CSU");  
  3.         System.out.println(map.get(stu));  


这段代码是无法找到对应的value值的,会输出null;正确的代码应该是下面的写法,才能找到value值,因为在设置和取得的过程中,都使用的是Student的实例化对象,地址没有变化。
 
Java代码   收藏代码
  1. //实例化一个学生对象  
  2.         Student stu=new Student("1608100201","Jony");  
  3.         HashMap<Student,String> map=new HashMap<Student,String>();  
  4.         map.put(stu, "CSU");  
  5.         System.out.println(map.get(stu));  


        2)覆写equals()和hashCode()方法。我们在使用时,要想明确的知道其中一个key的引用地址,就得依靠这两个方法。
 
Java代码   收藏代码
  1. public class Student {  
  2.     //学生的学好属性  
  3.     public static String ID;  
  4.     //学生的姓名属性  
  5.     private String name;  
  6.     /* 
  7.      * 重载构造方法 
  8.      */  
  9.     public Student(String ID,String name){  
  10.         this.ID=ID;  
  11.         this.name=name;  
  12.     }  
  13.       
  14.     /** 
  15.      * 覆写equals()方法 
  16.      */  
  17.     public boolean equals(Object obj) {  
  18.         //判断地址是否相等  
  19.         if(this==obj){  
  20.             return true;  
  21.         }  
  22.         //传递进来用于比较的对象不是本类的对象  
  23.          if (!(obj instanceof Student))  
  24.              return false;  
  25.          //向下转型  
  26.         Student stu = (Student)obj;  
  27.         //比较属性内容是否相等  
  28.          if (this.ID.equals(stu.ID)&&this.name.equals(stu.name)) {  
  29.              return true;  
  30.          }  
  31.          return false;  
  32.     }  
  33.     /** 
  34.      * 覆写hashCode()方法 
  35.      */  
  36.     public int hashCode() {  
  37.         return this.ID.hashCode();  
  38.     }  
  39. }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值