core java (一)HashMap的原理和应用

来源:互联网 发布:c语言 new 编辑:程序博客网 时间:2024/06/05 18:08
最近在比较源代码和自己写的代码之间的区别时,注意到了自己声明的一个成员变量 与源代码成员变量之间的区别,源代码使用的是LinkedHashMap,而我使用的是HashMap。所以仔细的分析了LinkeHashMap和
HashMap之间的却别,记录下来。
首先先看基础的HashMap:首先是百度百科上面的比较总的概括,HashMap是基于哈希表的 Map 接口的实现,并允许使用 null 值和 null 键。此实现提供所有可选的映射操作,(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。从这个地方我们知道:

1. HashMap不保证映射的顺序。意思就是说在访问的时候,不一定按照HashMap的put顺序进行取出,HashMap的get是随机的。


 HashMap<String,String> hashmap = new HashMap<String, String>();      String key = "key";      for (int i = 0; i <5; i++) {      hashmap.put(key+i, "value"+i);}             for(String key1:hashmap.keySet()){      System.out.println(key1+" "+hashmap.get(key1));      }
输出是:

key4 value4key3 value3key0 value0key2 value2key1 value1

2.HashMap的存储的结构
再来看一下HashMap的源代码:

/** * The table, resized as necessary. Length MUST Always be a power of two. */transient Entry[] table;static class Entry<K,V> implements Map.Entry<K,V> {    final K key;    V value;    Entry<K,V> next;    final int hash;//引用    ……}

HashMap可以看作是Java实现的哈希表。HashMap中存放的是key-value对,对应的类型为java.util.HashMap.Entry,从源码中我们可以看到,Entry就是数组中的元素的类型,每个 Map.Entry 其实就是一个key-value对,它的成员变量next指向下一个Entry元素的引用,这就构成了一个链表。所以在HashMap中数据都存放在一个Entry引用类型的数组table中。这里key是一个对象,为了把对象映射到table中的一个位置,这里的位置就是说table的下标,我们可以简单的设想通过求余法来找到这个位置,所以我们可以使用

[key的hashCode % table的长度]
来计算位置,当然在实际操作的时候由于需要考虑table上的key的均匀分布可能需要对key的hashCode做一些处理。

这里说明一下:在Java中每一个对象都有一个哈希码,这个值可以通过hashCode()方法获得,这里可以简单的理解为一个很大的int值。hashCode()的值和对象的equals方法息息相关,是两个对象的值是否相等的依据,所以当我们覆盖一个类的equals方法的时候也必须覆盖hashCode方法。关于hashcode是如何产生的,本文不在详解。

我们可以看一下 HashMap 中一个构造器的代码:

// 以指定初始化容量、负载因子创建 HashMap  public HashMap(int initialCapacity, float loadFactor)  {  // 初始容量不能为负数 if (initialCapacity < 0)  throw new IllegalArgumentException( "Illegal initial capacity: " +  initialCapacity);  if (initialCapacity > MAXIMUM_CAPACITY)  initialCapacity = MAXIMUM_CAPACITY;  // 负载因子必须大于 0 的数值 if (loadFactor <= 0 || Float.isNaN(loadFactor))  throw new IllegalArgumentException(  loadFactor);  // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。 int capacity = 1;  while (capacity < initialCapacity)  capacity <<= 1;  this.loadFactor = loadFactor;  // 设置容量极限等于容量 * 负载因子 threshold = (int)(capacity * loadFactor);  // 初始化 table 数组 table = new Entry[capacity];  // @ init();  } 

上面的代码包含了一个简洁的代码实现:找出大于 initialCapacity 的、最小的 2 的 n 次方值,并将其作为 HashMap 的实际容量(由 capacity 变量保存)。例如给定 initialCapacity 为 10,那么该 HashMap 的实际容量就是 16。
程序@号代码处可以看到:table 的实质就是一个数组,一个长度为 capacity 的数组。
对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,这个索引就是上面源代码中的成员变量:

final int hash;//引用


系统可以根据其索引快速访问该 bucket 里存储的元素。 无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数 final int hash; )用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。
所以HashMap的存储结构可以使用图形表示为:
3.HashMap的存储的实现
来看一下HashMap put方法的源代码 :
public V put(K key, V value) {        if (key == null)            return putForNullKey(value);        int hash = hash(key.hashCode());        int i = indexFor(hash, table.length);        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(hash, key, value, i);        return null;    }

我们来一步步的进行分析。在HashMap中我们的key可以为null,所以第一步就处理了key为null的情况,这样也能对应上百科上面所说的,值允许null,但就key也能够null。
当key为非null的时候,这个没有使用直接取余的方法,而是又好像做了一次哈希,为什么呢?这个还得先看indexFor(hash, table.length)方法,这个方法是决定存放位置的。

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

可以发现,因为在HashMap中table的长度为2^n(我们把运算都换成二进制进行考虑),所以h & (length-1)就等价于h%length,这也就是说,如果对原本的hashCode不做变换的话,其除去低length-1位后的部分不会对key在table中的位置产生任何影响,这样只要保持低length-1位不变,不管高数位如何都会冲突,所以就想办法使得高数位对其结果也产生影响,于是就对hashCode又做了一次哈希。

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);    }

for (Entry<K,V> e = table[i]; e != null; e = e.next) 

当找到key所对应的位置的时候,对对应位置的Entry的链表进行遍历,如果以及存在key的话,就更新对应的value,并返回老的value。如果是新的key的话,就将其增加进去,返回的是null。modCount是用来记录hashmap结构变化的次数的,这个在hashmap的fail-fast机制中需要使用(当某一个线程获取了map的游标之后,另一个线程对map做了结构修改的操作,那么原先准备遍历的线程会抛出异常)。
addEntry的方法如下:
void addEntry(int hash, K key, V value, int bucketIndex) {     // 获取指定 bucketIndex 索引处的 Entry     Entry<K,V> e = table[bucketIndex];  // ①    // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);     // 如果 Map 中的 key-value 对的数量超过了极限    if (size++ >= threshold)         // 把 table 对象的长度扩充到 2 倍。        resize(2 * table.length);  } 

上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

new Entry的代码:
Entry(int h, K k, V v, Entry<K,V> n) {            value = v;            next = n;//③            key = k;            hash = h;        }

这个时候,就可以在代码③处看到:在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。
4.HashMap的取值

HashMap的get方法的源码:
final Entry<K,V> getEntry(Object key) {        int hash = (key == null) ? 0 : hash(key.hashCode());        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 != null && key.equals(k))))                return e;        }        return null;    }

从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素,这个过程,也就再一次的验证了HashMap的存储的模型。

5.总结
HashMap 在底层是一个 Entry 对象,这个是一个键值对的形式。采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 Hash 算法来决定其存储位置;当需要取出一个 Entry 时,也会根据 Hash 算法找到其存储位置,直接取出该 Entry。由此可见:HashMap 之所以能快速存、取它所包含的 Entry,关键在于Entry散在不同的地方,这个时候就是hash算法的重要性了!

当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间,这样我们就能够根据我们的需要,调整负载因子。

如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap 中 Entry 的数量一直不会超过极限容量(capacity * load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为 capacity 的 Entry 数组),因此创建 HashMap 时初始化容量设置也需要小心对待。


参考:java中hashmap详解





原创粉丝点击