HashMap学习随笔

HashMap用的常有,但深入了解HashMap源码,数据结构,实现原理的人应该相对较少。今天闲暇之余,对HashMap的源码研究了一下。

 

HashMap在JAVA开发中,经常用到,需要使用键值对,但无需同步的,可以快速查找其中的元素,那么HashMap是怎么达到其快速定位无素的的呢?

 

1 首先谈一下HashMap的数据结构

HashMap是一个由数据与链表构成的一种存储结构,水平方向是一种数组,页每个数组都是一个链表的头部。如下图:



HashMap学习随笔

上图中,X轴方向为HashMap的数组容器,Y轴方向,为每个相应位置的单向链表。每一个元素都是一个包含如图所示四个属性,key,value,hash,next的一种数据结构,即EntrySet,其中的next指向Y轴方向其下一个元素.

现在知道HashMap的数据结构了,那么其实现又是怎么样的?

 

 

2 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;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }
 

 

loadFactor :加载因子,加载因子与HashMap resize有关。默认为0.75

capacity:容器大小,默认值为16 其大小为上面所说的数据结构中数组的长度。

table:即为上面数据结构图中,X方向的数组(transient Entry[] table;

threshold :resize的临界值,即当HashMap中无素个数达到该值时,HashMap就会调用其resize方法,重新扩充大小。

 

从下面的代码中:

 

 

  while (capacity < initialCapacity)
            capacity <<= 1;

 

可以看出,capacity的值是2的倍数,为什么?为什么?在下面我会讲到。

 

创建HashMap之后,我们得到了一个空的hashMap容器,我们接下来的可能就是往容器里面放我们的元素了。

public V put(K key, V value) 

 

 

 

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

当你的key为null时,会调用putForNullKey,HashMap允许key为null,这样的对像是放在table[0]中。

如果不为空,则调用int hash = hash(key.hashCode());这是hashmap的一个自定义的hash,在key.hashCode()基础上进行二次hash

 

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

这样做的目的就是为了改进传统的hash方法,而且尽量保证key的每一位都会影响到最后的hash值,以达到减少hash冲突的目的.再看indexFor方法:

hashMap是根据这个方法定位到某个元素,将要存储在X方向,即table数组的哪个位置。

 

 

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

 

就一行代码,将key的二次hash值,与长度减一进行与操作,这一步可谓经典,通常我们会用取模的方式来定位数组中的某个位置,但是hashMap却用这种方法,而且length即capacity的值,面capacity又是2的倍数,减1之后,表示成二进制就全部是1了,那么与全部为1的一个数进行与操作,速度会大大提升了。这就是为什么"capacity的值是2的倍数"

 

位定到具体的列之后,hashMap会遍历该列的所有元素,当以该key的无素已经存在是,会将其value替换,并返回其原值。否则会重新创建一个新的EntrySet并放在table[indexFor(hash, table.length)]的位置上,并将之前该列的链表,设为其next。最后是判断hashmap中元素的个数是否达到了threshold ,如果达到了则将其容器扩大到原来的2倍。如下所示:

 

 resize(2 * table.length);
 

 

当然hashMap的get 方法,是首先通过通过key的两次hash值与数组的长度(capacity)进行于操作,定位到数组的某个位置,然后对该列的链表进行遍历,一般情况下,hashMap的这种查找速度是非常快的,hash值相同的元素过多,就会造成链表中数据很多,而链表中的数据查找是通过遍历所有链表中的元素进行的,这可能会影响到查找速度,找到即返回,这里需要注意的是,返回为null时,你不能判断是找到了,还是在hashmap中存着一个value为null的元素,因为hashmap允许value为null.

 

HashMap有一些比较重新的方法,比如resize,putAll,remove等,我都认真的看过。感觉代码写的非常精致。希望有兴趣的朋友也认直的去看一下。

 

 

说到HashMap不是线程完全的情况,可以用Map Collections.synchronizedMap(Map m) 或者HashTable去实现。

 

你可能感兴趣的:(数据结构)