阅读HashMap(1.6)源码所做的一些记录

HashMap是用来存放键值对对的,其底层实现是数组+链表(JDK1.6),而且在多线程的情况下它是不安全的,下面是基于JDK1.6的常用功能的源码分析。

一、构造方法和一些重要的属性

1、几个属性

// 默认的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储数据的数组
transient Entry[] table;
// HashMap的长度
transient int size;
// 扩容的临界值,就是说当HashMap的长度达到这个值得时候,就要扩容了,其大小是容量*加载因子
int threshold;
// 实际的加载因子
final float loadFactor;

2、指定初始容量和加载因子的构造方法

public HashMap(int initialCapacity, float loadFactor) {
    // 初始容量不能小于0
    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);

    int capacity = 1;
    // 通过位移运算让容量为2的指数倍(大于等于指定容量)。比如如果你指定初始容量为9,实际的初始容量为16,如果指定初始容量为16,实际初始容量也为16
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    // 计算扩容的临界值
    threshold = (int)(capacity * loadFactor);
    // 初始化数组
    table = new Entry[capacity];
    // init是个空方法
    init();
}

3、指定初始容量的构造方法

调用上面的那个构造方法,并指定加载因子为默认加载因子0.75

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

4、无参构造方法

加载因子默认0.75,默认初始容量16

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}

5、依据给定的集合构造HashMap

加载因子采用默认的加载因子,而初始容量就取(默认初始容量(16))和(传入的集合长度除以默认加载因子后再加1)中比较大的那个值。然后通过迭代器循环取出m(传入的集合)中的键值然后放到HashMap中

public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                    DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    putAllForCreate(m);
}

private void putAllForCreate(Map<? extends K, ? extends V> m) {
    // 通过迭代器循环
    for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
        Map.Entry<? extends K, ? extends V> e = i.next();
        putForCreate(e.getKey(), e.getValue());
    }
}

private void putForCreate(K key, V value) {
    // 如果key为null,则直接让其hash为0,之后会把其散列到table[0]上
    int hash = (key == null) ? 0 : hash(key.hashCode());
    // 通过key的hash和table的长度进行按位与运算,得到这个键值应该放到数组table哪个位置的下标
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 检测key是否已经存在,如果已经存在,则替换此key对应的value
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }
    createEntry(hash, key, value, i);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    // 此处是头插(在头部插入,JDK1.8的时候变成了尾插)
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    size++;// 更新HashMap的长度值
}

二、增

1、单个键值对的添加

public V put(K key, V value) {
    // 如果key为null,则有单独的处理
    if (key == null)
        return putForNullKey(value);
    // 计算hash
    int hash = hash(key.hashCode());
    // 计算散列到table的那个位置,得到的i即为要散列到数组table的下标
    int i = indexFor(hash, table.length);
    // 查找此key是否已经存在,如果存在就修改value,并把旧的value返回
    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);// HashMap中的recordAccess方法是空的
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
    // 此处是头插
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);// 扩容
}

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    // 新的数组
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);// 计算新的扩容点
}

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    // 循环取出原数组中的数据,并重新计算每条数据散列位置,注意,每个hash桶中的数据,如果扩容以后两条数据还落在同一个桶中,则其在链表中的前后顺序会倒置(因为是头插,但取出的时候也是从头部开始取得),因此在多线程下可能造成死循环
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

// key为null的插入
private V putForNullKey(V value) {
    // key为null的数据固定散列到table[0]桶中,同样,如果有key为null的数据,则把其value替换,并把旧的value返回,如果不存在key为null的数据,则正常插入
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

2、把一个集合添加到HashMap中

整体的做法是遍历m,获取到每一条数据,然后调用HashMap的put方法,把数据加入到HashMap中

public void putAll(Map<? extends K, ? extends V> m) {
    int numKeysToBeAdded = m.size();
    // 如果m为空集合,则直接返回
    if (numKeysToBeAdded == 0)
        return;
    if (numKeysToBeAdded > threshold) {
        int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
        if (targetCapacity > MAXIMUM_CAPACITY)
            targetCapacity = MAXIMUM_CAPACITY;
        int newCapacity = table.length;
        while (newCapacity < targetCapacity)
            newCapacity <<= 1;
        if (newCapacity > table.length)
            resize(newCapacity);
    }

    // 遍历添加
    for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
        Map.Entry<? extends K, ? extends V> e = i.next();
        put(e.getKey(), e.getValue());
    }
}

三、删

1、指定键的值来删除

通过key的值来计算该key所散列的位置,然后循环取匹配,如果存在指定的key,则删除,并返回key所对应的原值

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}

final Entry<K,V> removeEntryForKey(Object key) {
    // 计算hash
    int hash = (key == null) ? 0 : hash(key.hashCode());
    // 找到所在桶的下标
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;
    // 循环匹配
    while (e != null) {
        Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            modCount++;
            size--;
            // 如果要删除的是链表的第一个元素就直接把table[i]指向后一个元素就行了,如果删除的是中间的某个元素,直接要删除的元素的上个元素的next直接指向要删除的下个元素
            if (prev == e)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }
    return e;
}

2、清空集合

通过循环置空table数组

public void clear() {
    modCount++;
    Entry[] tab = table;
    for (int i = 0; i < tab.length; i++)
        tab[i] = null;
    size = 0;
}

四、查

通过给定的key计算出数组的下标,然后通过循环对应下标处的链表对key进行匹配,如果匹配到就返回其对应的值,否则返回null

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = 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.equals(k)))
            return e.value;
    }
    return null;
}

private V getForNullKey() {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

五、其他一些方法

1、长度
size属性就是其长度

public int size() {
    return size;
}

2、是否为空集合

长度为0,就是空集合

public boolean isEmpty() {
    return size == 0;
}

3、是否包含指定的key

和get方法的逻辑基本上是一样

public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

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

4、是否包含指定的value

主要就是遍历匹配,因为不能根据value值来确定所在的桶,这个操作比较费时。

public boolean containsValue(Object value) {
	if (value == null)
            return containsNullValue();

	Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
	return false;
}

private boolean containsNullValue() {
	Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
	return false;
}

六、遍历

一些测试代码

public void test(){
    Map<String, String> map = new HashMap<String, String>();
    map.put("a", "aa");
    map.put("b", "bb");
    map.put("c", "cc");
    map.put("d", "dd");
    // 遍历key
    for(String key : map.keySet()){
        System.out.println(key);
    }
    // 遍历value
    for(String val : map.values()){
        System.out.println(val);
    }
    // 遍历key和value
    for(Map.Entry<String, String> entry : map.entrySet()){
        System.out.println(entry.getKey());
        System.out.println(entry.getValue());
    }
    // 迭代器遍历 (在遍历的时候删除特定的值可以用迭代器)
    Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
    while(iterator.hasNext()){
        Map.Entry<String, String> e = iterator.next();
        System.out.println(e.getKey());
        System.out.println(e.getValue());
    }
}

关于以上代码,下面结合源码稍作分析

1、遍历key

先来看一下keySet方法

public Set<K> keySet() {
    Set<K> ks = keySet;
    return (ks != null ? ks : (keySet = new KeySet()));
}
// keySet属性的定义
transient volatile Set<K> keySet = null;

首先第一次调用keySet方法的时候,keySet的值为null,然后就会new一个KeySet,并赋给keySet变量。下面是KeySet类的结构

private final class KeySet extends AbstractSet<K> {
    public Iterator<K> iterator() {
        return newKeyIterator();
    }
    public int size() {
        return size;
    }
    public boolean contains(Object o) {
        return containsKey(o);
    }
    public boolean remove(Object o) {
        return HashMap.this.removeEntryForKey(o) != null;
    }
    public void clear() {
        HashMap.this.clear();
    }
}

我们知道,foreach的实现是基于迭代器来实现的,所以会调用KeySet的iterator方法,看到其中是直接调用的newKeyIterator放,这个方法在哪儿呢?在HashMap中有这么个方法

Iterator<K> newKeyIterator()   {
    return new KeyIterator();
}

在这个方法中直接new了一个KeyIterator,我们继续看KeyIterator这个类,这个类是HashMap的内部类

private final class KeyIterator extends HashIterator<K> {
    public K next() {
        return nextEntry().getKey();
    }
}

这个类又继承了HashIterator类

private abstract class HashIterator<E> implements Iterator<E> {
    Entry<K,V> next;	// next entry to return
    int expectedModCount;	// For fast-fail
    int index;		// current slot
    Entry<K,V> current;	// current entry

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();

        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
        current = e;
        return e;
    }

    public void remove() {
        if (current == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Object k = current.key;
        current = null;
        HashMap.this.removeEntryForKey(k);
        expectedModCount = modCount;
    }
}

HashIterator类中有hasNext方法,KeyIterator类中有next方法,这就构成了迭代器循环的基础
在这儿说一个遇到过的问题。就是在调试的时候,第一次调用keySet方法的时候,发现keySet的居然有值,我就想可能是在其他地方对keySet有初始化,但是找了一圈都没有找到初始化的地方,然后查了一下资料说是idea在调试的时候会调用toString方法,然后我就写了一段测试代码。

public void testDebugger(){
    System.out.println("before");
    DebuggerTest debuggerTest = new DebuggerTest();
    System.out.println("after");
}

public class DebuggerTest {
    @Override
    public String toString() {
        System.out.println("toString");
        return super.toString();
    }
}

这段代码在调试的时候会输出toString,但在运行的时候不会输出,这个基本上就验证了问题所在。后来我在eclipse中调试了一下keySet方法,发现进去的时候keySet确实是null.

2、values和entrySet

values和entrySet基本上和keySet是一样的套路,自己看一下代码就明白了

七、多线程下扩容死循环问题

在多线程的情况下,JDK1.6中HahsMap在扩容的时候可能会形成死循环(jJDK1.8不存在这种情况,因其使用的是尾插法,但其仍然不是线程安全的),我们分析死循环形成的原因,死循环是发生在扩容的时候,把数据从旧的数组移到新的数组的时候。看下代码

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;// ①
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

比说说有个链表是a->b->null(并且假设这两个Entry在扩容后还落在同一个桶中),然后有个线程(A)在执行完①这一行的时候,此时next指向了b,被剥夺了cpu的执行权限,然后另个一个线程(B)也执行了扩容,扩容后链表变成了b->a->null,当A线程再获得执行权限后,会形成a->b再次指向b这种情况。这样就会形成a<->b(a的next指向b,b的next指向a)的死循环。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值