Java集合框架--HashMap源码解析(JDK1.7)

本文通过查看Java7源码,分析了HashMap的结构原理和主要方法的源码实现。

定义

HashMap是Java Collection Framework的重要一员。它用于存储键值对(key-value),每个key值只能出现一次。它实现了Map接口,继承AbstractMap。

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

数据结构

在java中数组和引用(伪指针)是最常用的两种结构。HashMap就是利用这两种结构组合实现的。

在HashMap中查找或插入key-value对时,第一步是用哈希函数处理key得到哈希值作为索引,理想情况下不同的key会得到不同的哈希值。实际情况下,我们需要面对两个或多个key有相同的哈希值。第二步就是当哈希值相同时的冲突问题。

Java中通过数组和链表结合的方式,完成以上两步。数组每个元素都是一个链表,key的哈希值作为数据的索引,所以相同哈希值的key-value对存储在数组的同一个索引位置,然后每个key-value对存储在链表的一个结点上。如下就是它的数据结构图:
HashMap数据结构

通过使用这种结构,HashMap可以在时间和空间权衡的情况实现O(1)级别的查找和插入操作。
源码实现:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        ...
}

在HashMap中有一个内部类Entry,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。

构造函数

HashMap提供了三个构造函数:

  HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
  
  HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
  
  HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。

在这里提到了两个参数:初始容量加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建数组时的容量,加载因子是数组在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

当HashMap中元素的数量等于table数组长度*加载因子时,就会进行扩容操作。

下面为HashMap构造函数的源码:

public HashMap(int initialCapacity, float loadFactor) {
        // 初始容量(数组大小)不能为零
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 初始容量最大值为2^30
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 负载因子不能为负数
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        // 初始化数组,创建HashMap的散列表
        init();
    }

主要方法及实现

HashMap中我们常用的函数有如下几个:

  • V get(Object key)
    根据key获取value,为空时返回null
  • V put(K key,V value)
    插入、设置key-value对,允许插入null值
  • int size()   
    返回key-value对数量。大于Integer.MAX_VALUE时,返回Integer.MAX_VALUE
  • boolean isEmpty()
    是否为空
  • V remove(Object key)
    通过key删除,该key存在返回true,否则false
  • void clear()
    清除所有元素
  • boolean containsKey(Object key)
    是否包含该key
  • boolean containsValue(Object value)
    是否包含该value
  • Set setKey()
    将所有的key生成一个Set并返回

我们重点看一下put( )和get( )两个方法

存储实现:put(K key,V value )

先看源码实现:

public V put(K key, V value) {
        // 如果数组为空,初始化数组
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 如果Key为null,调putForNullKey方法将这个键值对存储在table数组第一个位置
        if (key == null)
            return putForNullKey(value);
        // 计算key的哈希值,并确定在table数组中的索引位置
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        // 遍历该索引位置的Entry链表,如果键值对已存在,直接覆盖并返回旧value,否则插入新的键值对返回Null
        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++;
        // 插入新的键值对返回Null
        addEntry(hash, key, value, i);
        return null;
    }

通过源码我们可以清晰看到HashMap保存数据的过程为:首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。

下面我们单独看一下hash( )和indexFor( )两个方法的源码:

// 计算哈希值
final int hash(Object k) {
        int h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

//计算该哈希值在table数组中对应的索引位置
static int indexFor(int h, int length) {
        return h & (length-1);
    }

这两个方法通过数学的方法保证了哈希值和对应数组索引在空间上的均匀分布。降低了哈希值碰撞发生的概率,提高了查询速度。

读取实现:get(key)

还是先看源码:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        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;
    }

相比于put( )方法,get( )方法实现就简单一点。通过key值找到table数组中索引位置,遍历链表寻找相同的key。

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值