HashMap源码分析笔记

以前经常用HashMap,但是对它具体怎么实现一点也不了解,这几天看了看它的源码,整理笔记如下做一个记录,其中有些内容是参考一些大神的博客,鉴于本人功力尚浅,可能有写的不正确的地方,欢迎批评指正。

一 HashMap概述

HashMap是基于散列表的Map接口的实现。它允许插入null键和null值,如果要用自己实现的类作为HashMap的键,则必须同时重写hashCode()和equals()这两个方法。HashMap插入“键值对”的开销是固定的。

此外,HashMap不是线程安全的,多线程环境下可以采用concurrent并发包下的concurrentHashMap可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap

二 HashMap数据结构

学过数据结构的都知道哈希表这种数据结构,它其中一个重要的概念就是构造哈希函数,把关键字转换成数组下标。如果两个不同的关键字哈希化后得到的值相同,这种情况称为冲突。解决冲突的办法通常有两种:开放地址法和链地址法。其中开放地址法通过探测方法找到数组的一个空位来插入数据。链地址法是创建一个存放链表的数组,将发生冲突的数据直接接到这个下标所指的链表中。

HashMap就是基于哈希表这种数据结构,底层通过一个Entry类型(放入HashMap的“键值对”都会封装成Entry对象)的数组来实现,它通过key的hashCode()方法计算hash值,从而确定对象的存储位置,这也是HashMap会特别快的原因。HashMap通过链地址法来解决冲突问题(通过Entry的next来添加,下面的源码分析会进一步分析)。



三 HashMap源码分析

首先看它的一些属性:

    static final int DEFAULT_INITIAL_CAPACITY = 16; //默认初始容量
    static final int MAXIMUM_CAPACITY = 1 << 30;   //最大容量
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默认负载因子
    transient Entry<K,V>[] table;   // 存放“键值对”数组
    transient int size;  //实际存放元素的个数
    int threshold;   //阈值 threshold=capacity*loadFactor,当实际存放元素个数size超过阈值需要扩充容量
    final float loadFactor;   //负载因子

      负载因子表示数组可以填满的程度,负载因子很大时,数组的利用率很高,但是也很容易产生冲突,查找的性能也会下降。.因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。一般情况下就使用默认的0.75。

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);
        int capacity = 1;
         //确保容量为仅大于参数initialCapacity的2的指数次方。这样取是为了可用掩码代替除法和取余数等很慢的操作。这个在后面indexFor(int h, int length)这个方 法中有所体现。
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

其他构造函数如果没有指定 initialCapacity或者 loadFactor,则使用默认的容量16和负载因子0.75 :

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

对于Entry类,HashMap将存入的“键值对”都封装成Entry类型的对象

   static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;    //key键
        V value;      //value键
        Entry<K,V> next;      //指向下一个Entry对象,这个属性将有冲突的对象连接起来构成一个链表,解决冲突问题
        int hash;      //散列码
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    .....
}


对于put方法:

   public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);  //计算key的散列值
        int i = indexFor(hash, table.length);//计算该对象存放的数组位置下标
        //在key散列值对应的数组下标位置的链表中找是否已经有相同的key键
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果已经有相同的Key,则value的值用新传入的value值替代
            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); //新增的Entry对象的hash值传入的是key的散列值。
        return null;
}

可以发现,将“键值对”封装为Entry对象时,传入的hash值取的是Key的hash值,而Key的hash值是通过hashCode()方法计算的;并且在判断是够已经存在相同的key键时,是通过比较hash的值和equals()方法执行的。所以如果要将自己写的类作为HashMap的key键时,一定要重写hashCode()和equals()方法,如果不重写,则默认使用父类Object的hashCode()和equals(),它默认是使用对象的地址来计算散列码,是否equals也是通过地址来判断。

   private V putForNullKey(V value) {
      //将key键为null值的Entry对象放在数组下标为0的链表中
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
           //如果已经有null值的Key键,则value的值用新传入的value值替代
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

     void addEntry(int hash, K key, V value, int bucketIndex) {
         //判断是否需要扩充容量
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }
     void createEntry(int hash, K key, V value, int bucketIndex) {
        //将新传入的“键值对”增添到链表的头部
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

对于下面这个方法,起初只是认为它就是将hash值压缩到数组大小的范围内,后来看了书和一些大神写的博客才发现远不止这么简单。

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

我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低。之前说HashMap容量取2的整数次幂,这样length为2的整数次幂,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率。其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。


对于get方法:

  public V get(Object key) {
        //如果为Null值的key键,则在table[0]位置的链表中查找
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
   }
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    final Entry<K,V> getEntry(Object key) {
        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;
    }
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

可以发现,HashMap查找时,不需要遍历整个数组,它根据散列值快速的调到数组的某个位置,并对很少的元素进行比较就可以完成。



参考文章:http://blog.csdn.net/ns_code/article/details/36034955

                    http://www.cnblogs.com/liuling/p/2013-8-22-01.html








HashMapJava 中非常重要的数据结构之一,它实现了 Map 接口,提供了快速的键值对的查找和存储功能。下面是 HashMap源码分析: 1. 数据结构 HashMap 内部实现了一个数组,每个数组元素是一个单向链表,称为桶(bucket)。当我们向 HashMap 中添加一对键值对时,会根据键的哈希值(hashcode)计算出该键值对应该存储在哪个桶中。如果该桶中已经有了该键值对,就将该键值对添加到桶的末尾(Java 8 中是添加到桶的头部),否则就创建一个新的节点添加到桶的末尾。 2. 哈希冲突 如果两个键的哈希值相同,就称为哈希冲突。HashMap 采用链表法解决哈希冲突,即将哈希值相同的键值对存储在同一个桶中,通过单向链表组织起来。当我们根据键查找值时,先根据键的哈希值找到对应的桶,然后遍历该桶中的链表,直到找到目标键值对或者链表为空。 3. 扩容机制 当 HashMap 中的键值对数量超过了桶的数量的时候,就需要对 HashMap 进行扩容。扩容会重新计算每个键值对的哈希值,并将它们存储到新的桶中。Java 8 中,HashMap 的扩容机制发生了一些变化,采用了红黑树等优化方式。 4. 线程安全 HashMap 是非线程安全的,如果多个线程同时操作同一个 HashMap,就有可能导致数据不一致的问题。如果需要在多线程环境下使用 HashMap,可以使用 ConcurrentHashMap。 以上就是 HashMap源码分析,希望对你有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值