Chinaunix首页 | 论坛 | 博客
  • 博客访问: 41659
  • 博文数量: 24
  • 博客积分: 10
  • 博客等级: 民兵
  • 技术积分: 130
  • 用 户 组: 普通用户
  • 注册时间: 2011-08-03 19:07
文章分类
文章存档

2012年(21)

2011年(3)

分类:

2012-04-12 00:36:00

原文地址:浅析HashSet 与 HashMap 作者:小小小黑猫

下面所有出现的源码,均来自java 的 src包。

昨天学习了HashSet 与 HashMap基本用法,为了更好地理解他们的实现机制,就大概看了看源码,因为还是菜鸟。


HashSet,存储object的集合,既然是集合,就不允许有重复元素。判断两个元素是否相同,是由hashCode与equals方法共同完成的。如果集合中对象的类没有重写这两个方法,那么就会使用从object继承的来的方法,即比较两个对象的引用是否相同,即比较地址。

HashSet使用什么数据结构来存储对象的呢,需要看一下源码

  1. private transient HashMap<E,Object> map;

  2.     // Dummy value to associate with an Object in the backing Map
  3.     private static final Object PRESENT = new Object();

  4.     /**
  5.      * Constructs a new, empty set; the backing HashMap instance has
  6.      * default initial capacity (16) and load factor (0.75).
  7.      */
  8.     public HashSet() {
  9.     map = new HashMap<E,Object>();
  10.     }
这里可以看到,HashSet类维护了一个HashMap引用作为自己的成员变量。并在其构造方法中将其实例化。
另一个成员变量是一个静态,final的object,后面就看到它的作用是什么了。

HashMap表达了一种映射,即关键字key到值value的映射,其中key的值唯一,即一个key某一时刻只能映射到唯一的值。

之前在分析HashSet时,提出一个问题,如何保证元素的唯一性?我们知道要通过使用两个方法来实现,但是,最终是哪里使用的这两个方法呢?

现在来看一下HashSet的add方法

  1. public boolean add(E e) {
  2.     return map.put(e, PRESENT)==null;
  3.     }
正如我们看到的,add方法调用了HashMap的put方法。这里我们看到了之前的成员变量PRESENT,即,我们存入到HashSet的对象作为key传给了map,而我们不关心这个key对应的value,所以才定义了一个静态final的object对象。
而保证集合中元素的唯一性的工作,也就顺理成章的交给了HashMap对完成。

至此,我们可以大概明白HashSet的实现本质,它封装了一个HashMap。

------------------------------------------------------------------------------------------------
看来,重点还是在HashMap这个类上。
先看看其成员变量



  1.     static final int DEFAULT_INITIAL_CAPACITY = 16;

  2.     static final int MAXIMUM_CAPACITY = 1 << 30;

  3.     static final float DEFAULT_LOAD_FACTOR = 0.75f;


  4.     transient Entry[] table;

  5.     transient int size;

  6.     int threshold;

  7.     final float loadFactor;
这个比起HashSet,成员变量多出不少。。
再对比下构造方法,先看下无参的

点击(此处)折叠或打开

  1. public HashMap() {
  2.         this.loadFactor = DEFAULT_LOAD_FACTOR;
  3.         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
  4.         table = new Entry[DEFAULT_INITIAL_CAPACITY];
  5.         init();
  6.     }
先不管这个loadFactor是神马,第四行我们可以看到,实例化了一个Entry型的数组,table是这个类里面为一个数组结构,我们有理由猜想,每对映射就是放在这里了。大小为默认初始大小。这个Entry是什么类型?

点击(此处)折叠或打开

  1. static class Entry<K,V> implements Map.Entry<K,V> {
  2.         final K key;
  3.         V value;
  4.         Entry<K,V> next;
  5.         final int hash;

  6.         /**
  7.          * Creates new entry.
  8.          */
  9.         Entry(int h, K k, V v, Entry<K,V> n) {
  10.             value = v;
  11.             next = n;
  12.             key = k;
  13.             hash = h;
  14.         }
-首先呢,这个Entry是HashMap的内部类。
如果看下他的成员变量,我们发现有key,value,还有一个Entry型引用next,这个next暂时不用管是做什么的。最后有一个final的int型变量hash。

-至此,我么知道Entry就是之前说的映射的具体实现。这个变量hash应该也是和key的hashcode有关的。下面我们就会看到。


我们现在最关心的方法是put。下面来分析。


点击(此处)折叠或打开

  1. public V put(K key, V value) {
  2.         if (key == null)
  3.             return putForNullKey(value);
  4.         int hash = hash(key.hashCode());
  5.         int i = indexFor(hash, table.length);
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  7.             Object k;
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  9.                 V oldValue = e.value;
  10.                 e.value = value;
  11.                 e.recordAccess(this);
  12.                 return oldValue;
  13.             }
  14.         }
  15.         modCount ;
  16.         addEntry(hash, key, value, i);
  17.         return null;
  18.     }

-貌似有些复杂,先看第4行,这里用到了key对象的hashCode,之后又用了一个叫做hash的方法对获得的key的hashcode又处理了一下


点击(此处)折叠或打开

  1. static int hash(int h) {
  2.         // This function ensures that hashCodes that differ only by
  3.         // constant multiples at each bit position have a bounded
  4.         // number of collisions (approximately 8 at default load factor).
  5.         h ^= (h >>> 20) ^ (h >>> 12);
  6.         return h ^ (h >>> 7) ^ (h >>> 4);
  7.     }
-我们看到,hash其实是一个哈希函数。至于为什么这么定义,不知道了。。。。经验吧。
再回头看put方法,第五行,用了个indexFor方法,来获取一个下标,这个下标就是要放在table里的位置。
至此,我们可以马上想到数据结构中的哈希表了。哈希表中,用来计算某个元素将存在表里的位置的函数叫做哈希函数,但是,这里,indexFor并不是那个哈希函数,虽然它是最直接算出table中存储位置的函数。

-其实这个哈希函数由3部分共同完成,首先,是hashCode方法,然后通过hash方法,最后由indexFor算出数组中的下标.



点击(此处)折叠或打开

  1. //找到对应链表
  2. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  3.             Object k;
  4.     //如果要插入的entry的key出现过,则替换成新插入entry的value,并返回oldValue
  5.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  6. //为什么后面有个或?因为key类可能重写了hashcode与euqls方法
  7. //若果没有重写,那就是默认的用引用作为hashcode,即第一个判断
  8.                 V oldValue = e.value;
  9.                 e.value = value;
  10.                 e.recordAccess(this);
  11.                 return oldValue;
  12.             }
  13.         }

-代码这段,说明这里使用了链地址法作为处理冲突的方法。
不同key值可能计算出相同的哈希表下标,即位置,这是即产生了冲突。
如果算出的下标相同,那就加到对应的链表里。

-这里有个疑问,为什么不适用再哈希法来处理冲突呢?-

-前面提到的loadFactor就是我们数据结构课中提到的填充因子。即,当前填充率达到这个数值时,一般会增加哈希表的大小,这样做的目的是降低产生冲突的概率。

-所以在put方法中的add方法中,我们可以看到,当table里面填入的entry数量到达threshold时,table会被resize
resize中有个transfer方法,用来将旧的table的内容拷贝到新的table中。新的table的大小事旧的大小的2倍。


点击(此处)折叠或打开

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2.     Entry<K,V> e = table[bucketIndex];
  3.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
  4.         if (size++ >= threshold)
  5.             resize(2 * table.length);//扩容
  6.     }

追踪resize方法

点击(此处)折叠或打开

  1. void resize(int newCapacity) {
  2.         Entry[] oldTable = table;
  3.         int oldCapacity = oldTable.length;
  4.         if (oldCapacity == MAXIMUM_CAPACITY) {//如果已达到 1 << 30,就无法再扩容了
  5.             threshold = Integer.MAX_VALUE;
  6.             return;
  7.         }

  8.         Entry[] newTable = new Entry[newCapacity];
  9.         transfer(newTable);  // 把当前table内容拷贝到新的table里
  10.         table = newTable;
  11.         threshold = (int)(newCapacity * loadFactor);//重新计算threshold
  12.     }


由于还很菜,就只能分析到这里了。
总结下我的感想
其实,这是第一次去学习源码,虽然只是看懂了个皮毛,但是收获还是很大的。
仅仅停留在知道如何使用一个类,是不够的,因为如果你不了解他实现的原理,你就无法真正的用好它,因为你不确定在一些场景是否可以使用。
其次,研究源码真的是一个优秀程序员必须培养的习惯,通过阅读这些代码,可以学到很多东西,对自己的进步起着很大最用。无论是设计思路,或者是优秀的编程风格,都是值得学习的。
虽然以现在的水平,我仅仅能看个大概,但是只要这么一点一滴的积累,总会有很大进步的,不能着急。

--------------------
先写到这里吧,出去跑跑步,身体是革命的本钱.




阅读(623) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~