`
crd1991
  • 浏览: 23029 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

HashMap原理、源码解析

    博客分类:
  • java
阅读更多

一、前言

 

HashMap是Map实现中最常使用的,具有快速存取的优点,所以很有必要深入到源码去了解其实现原理。

本文的内容包括:分析HashMap的数据结构和HashMap的常用方法的源码分析。

 

二、HashMap的数据结构

 

HashMap 可以理解为由数组链表组成的存储结构,如图

 


 

X轴方向上是一个数组,Y方向是链表。一个节点的信息包括hashkeyvaluenext

 

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

 

 

三、常用方法源码分析

 

1、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[] table;  

 transient int size;     

 int threshold;     

 final float loadFactor;   

   DEFAULT_INITAL_CAPACITY:默认初始化容量,16

    MAXIMUM_CAPACITY :最大容量,即230次方

   DEFAULT_LOAD_FACTOR :默认负载系数

    Entry[] table:Entry类型的一维数组,长度必须是2的幂,也就是上图的table数组

    int size: 当前HashMap的元素个数

    int threshold: 扩容的临界值,即capacity*loadFactor

    final float loadFactor:hash表的负载系数

 

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

 }

 

 HashMap的初始化主要是initialCapacityloadFactor这两个参数。前者表示上图中X轴方向table的长度,即不同hashkey的长度,后者是负载系数,扩充的临界值threshold=capacity*loadFactor,当达到扩充临界值时,HashMap会自动扩容。比如默认情况是16*0.75=12当填充到12时,HashMap会自动将X轴方向的table

的长度扩充到32HashMap里面的capacity,长度是2的幂,即使你初始化时不是2的幂,它也会帮你转为2

。从上面的代码可以看出来。

 

2、添加元素

 

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

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

 }
  

基本思路:通过key拿到hash,然后通过hash找到table的索引值,找到之后比较该索引值所在列(Y轴方向

有没有相同的key,有的话就替换掉原来的vaule,返回旧的value;如果没有相同的key,则把当前元素加进该列的前面,即链表的表头。当容量大于等于扩容的临界值时则扩充为原来table长度的2倍。

注意:HashMap允许key的值为null

 

那它是怎么根据hash值返回该hashtable里面的索引呢?你可能会说,很简单啊,取模就可以了,没错,但取模效率是比较低的,来看看HashMap里面是怎么实现的。

 

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

 

length,table的长度,也就是HashMap里面的capacity,长度是2的幂,&是与运算,例如length为16,则length-1的二进制为01111,当h为17,即10001,01111&10001=00001,通过与运算取代了取模运算,效率大大提高,这也是为什么table的长度为2的幂。

 

 

3、获取元素

 

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;

 }
  

基本思路:根据传过来的key获取其hash,快速定位到table的索引,再遍历该索引链表,用equals方法找出

该元素的value

 

注意:一般table里的同个的索引值的元素不会太多,这关系到查找性能的问题,当达到扩容的临界值时会自动扩容,然后rehash,即重新分配所有的元素来尽可能达到哈希均匀。但rehash是很耗性能的,所以如果确定HashMap的容量,最好一开始就初始化好。

 

 

4、删除元素

 

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

final Entry<K,V> removeEntryForKey(Object key) {
        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--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
}
  

其实删除不难,定义三个指针,一个指向当前元素,另外两个分别指向当前元素的前一个和后一个元素,这主要是链表的操作。

 

 

5、扩容的实现

 

当达到的临界值时,系统会调用resize函数来实现扩容。

 

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

基本思路:新建一个更大的数组,然后遍历旧数组,修改指针,把一个个的元素放到新数组上,每次插入都是插到链表的表头,并把旧数组设为null。不算太难,但算法精致,值得慢慢品味

 

 

四、小结

 

1、HashMap采用数组和链表组成的存储结构,用数组方式存储hash、key、value和next指针构成的Entry对象,对于冲突采用链表的方式解决;

 

2、HashMap插入时是插入到链表的表头,可能要扩大数组的容量,扩容时需要重新计算hash,并复制对象到新数组中,即rehash;

 

3、HashMap是非线程安全的。

 

 

ps:如写得不好或存在严重错误,欢迎批评指正。

 

 

 

 

  • 描述: HashMap数据结构图
  • 大小: 51.9 KB
分享到:
评论
1 楼 sprite311 2014-04-24  

相关推荐

    hashmap实现原理

    hashmap的底层及源码解析,很适合大家的学习,不要积分。

    源码解析jdk8.0集合:HashMap的底层实现原理.pdf

    源码解析jdk8.0集合:HashMap的底层实现原理.pdf

    源码解析jdk7.0集合(3):HashMap的底层实现原理.pdf

    源码解析jdk7.0集合(3):HashMap的底层实现原理.pdf

    HashMap新增数据原理.docx

    深入理解HASHMAP底层原理,通过对源码进行解析分析HASHMAP执行过程,一篇文章帮助你深刻理解HASHMAP

    蚂蚁java架构师(第七/八期含项目) |课件完整|完结无秘

    08HashMap与ConcurrenthashMap源码解读 09MySQL深度原理解析 10Netty深度源码解读 11SpringCloud微服务框架源码解读 12彻底搞懂分布式锁架构设计原理 13分布式数据一致性设计原理 14分布式消息中间件 15实战新零售...

    TheirNotes::ledger: 《互联网后端知识大全》「前人栽树, 后人乘凉; 它山之石, 可以攻玉」java

    Java IOJVM垃圾收集CMSG1ZGC元空间字节码操作JVM 调优分布式分布式算法PaxosRaftBFT分布式锁Redis 分布式锁分布式事务MySQL查询语句基本原理innodb 存储引擎:sparkles:缓存Redis底层原理开源框架Spring源码解析:...

    learning-notes:学习一些东西

    learning-notes分布式1.Lambda表达式Java基础JVM设计模式数据库并发微服务消息队列1.RabbitMq缓存1.Redis集群方式...限流3.HashMap源码解析4.CDN5.多模块构建过程6.高性能7.高性能18.中间件网络1.CDN其他1.Mybatis

    JAVA高并发高性能高可用高扩展架构视频教程

    前后台数据验证架构源码级解析 session跨域共享 JAVANIO原理详解 高并发数据库(Mysql数据库性能优化) 软件质量管控 企业常用框架springMVC基于注解+xml配置方式实现链接 WEB服务器优化之Tomcat7性能调优 JVM概述 ...

    KnowledgeSummary

    Android相关总结啥也不是JAVA相关知识点链接基础泛型注解反射并发序列化Json解析IO网络数据结构相关知识点链接HashMap阻塞队列JVM相关知识点链接JVMView相关知识点链接自定义View事件分发机制RecyclerView解析...

    安卓java读取网页源码-interview:安卓面试

    安卓java读取网页源码 Android-Interview Java 基础 父类的静态方法能否被子类重写? 静态属性和静态方法是否可以被继承?是否可以被重写?为什么? 什么是内部类?内部类、静态内部类、局部内部类和匿名内部类的...

    spring-annotation:1.Spring 5.X源码分析2.手写框架3.设计模式4.Springcloud2 5.互联网高并发场景6.互联网安全架构

    天道酬请,一步一个坑弹簧注释...源码分析1.1 Spring 5.X源码分析1.1.1 Spring5源码深度解析(一)之理解配置注解1.1.2 Spring5源码深度分析(二)之理解@ Conditional,@ Import注解1.1.3 Spring5深度源码分析(三)之...

    Nakoruru:娜可relu是手游《王者荣耀》中的一名日本少女英雄角色

    安卓 Android基础 回收站视图 图片 文件 Kotlin基础 Kotlin协程 ... HashMap原始解析 LeakCanary原理解析 吉特: Android Studio快捷键 Ctrl + F12查看类的方法 Ctrl + Alt +←返回到上一步 抓包工具 查理斯

    c语言难点分析整理,C语言

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    免费下载:C语言难点分析整理.doc

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    java基础案例与开发详解案例源码全

    2.3.5 常见错误解析24 2.4 Java类库组织结构和文档27 2.5 Java虚拟机简介28 2.6 Java技术两种核心运行机制29 2.7 上机练习30 第3章 3.1 变量32 3.1.1 什么是变量32 3.1.2 为什么需要变量32 3.1.3 变量的声明和赋值33...

    C语言难点分析整理.doc

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组...

    高级C语言 C 语言编程要点

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    高级进阶c语言教程..doc

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

    史上最强的C语言资料

    38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针...

Global site tag (gtag.js) - Google Analytics