2019独角兽企业重金招聘Python工程师标准>>>

Map和Collection在结构层次上是没有任何关系的,通过查看源码可以发现map所有操作都是基于key-value对,而不是单独的元素。

下面以HashMap为例子,深入对Map的实现机制进行了解,在这个过程中,请打开jdk源码。

Hash算法

HashMap使用Hash算法,所以在解剖HashMap之间,需要先简单的了解Hash算法,Hash算法一般也成为散列算法,通过散列算法将任意的值转化成固定的长度输出,该输出就是散列值,这是一种压缩映射,也就是,散列值的空间远远小于输入的值空间。
简单的说,hash算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系,(每一个真实值只能有一个键值,但是一个键值可以对应多个真实值),这样可以快速在数组等里面存取数据。

下面我们建立一个HashMap,然后往里面放入12对key-value,这个HashMap的默认数组长度为16,我们的key分别存放在该数组的格子中,每个格子下面存放的元素又是以链表的方式存放元素。

     public   static   void  main(String[] args) {
        Map map  =   new  HashMap();
        map.put( " What " ,  " chenyz " );
        map.put( " You " ,  " chenyz " );
        map.put( " Don't " ,  " chenyz " );
        map.put( " Know " ,  " chenyz " );
        map.put( " About " ,  " chenyz " );
        map.put( " Geo " ,  " chenyz " );
        map.put( " APIs " ,  " chenyz " );
        map.put( " Can't " ,  " chenyz " );
        map.put( " Hurt " ,  " chenyz " );
        map.put( " you " ,  " chenyz " );
        map.put( " google " ,  " chenyz " );
        map.put( " map " ,  " chenyz " );
        map.put( " hello " ,  " chenyz " );
    }

当我们新添加一个元素时,首先我们通过Hash算法计算出这个元素的Hash值的hashcode,通过这个hashcode的值,我们就可以计算出这个新元素应该存放在这个hash表的哪个格子里面,如果这个格子中已经存在元素,那么就把新的元素加入到已经存在格子元素的链表中。

运行上面的程序,我们对HashMap源码进行一点修改,打印出每个key对象的hash值

What-->hash值:8
You-->hash值:3
Don't-->hash值:7
Know-->hash值:13
About-->hash值:11
Geo-->hash值:12
APIs-->hash值:1
Can't-->hash值:7
Hurt-->hash值:1
you-->hash值:10
google-->hash值:3
map-->hash值:8
hello-->hash值:0

计算出来的Hash值分别代表该key应该存放在Hash表中对应数字的格子中,如果该格子已经有元素存在,那么该key就以链表的方式依次放入格子中

从上表可以看出,Hash表是线性表和链表的综合所得,根据数据结构的定义,可以得出粗劣的结论,Hash算法的存取速度要比数组差一些,但是比起单纯的链表,在查找和存取方面却要好多。

如果要查找一个元素时,同样的方式,通过Hash函数计算出这个元素的Hash值hashcode,然后通过这个hashcode值,直接找到跟这个hash值相对应的线性格子,进如该格子后,对这个格子存放的链表元素逐个进行比较,直到找到对应的hash值。

在简单了解完Hash算法后,我们打开HashMap源码

初始化HashMap

下面我们看看Map map = new HashMap();这段代码究竟做了什么,发生了什么数据结构的变化。

HashMap中几个重要的属性

transient Entry[] table;
用来保存key-value的对象Entry数组,也就是Hash表

transient int size;
返回HashMap的键值对个数

final float loadFactor;
负载因子,用来决定Entry数组是否扩容的因子,HashMap默认是0.75f

int threshold;
重构因子,(capacity * load factor)负载因子与Entry[]数组容积的乘值

public   class  HashMap < K,V >
     extends  AbstractMap < K,V >
     implements  Map < K,V > , Cloneable, Serializable
{
     int  threshold;

final   float  loadFactor;

transient  Entry[] table;

static   final   float  DEFAULT_LOAD_FACTOR  =   0.75f ;

static   final   int  DEFAULT_INITIAL_CAPACITY  =   16 ;

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

以public HashMap(int initialCapacity, float loadFactor)构造函数为例,另外两个构造函数实际上也是以同种方式来构建HashMap.

首先是要确定hashMap的初始化的长度,这里使用的策略是循环查出一个大于initialCapacity的2的次方的数,例如initialCapacity的值是10,那么大于10的数是2的4次方,也就是16,capacity的值被赋予了16,那么实际上table数组的长度是16,之所以采用这样的策略来构建Hash表的长度,是因为2的次方运算对于计算机来说是有相当的效率。

loadFactor,被称为负载因子,HashMap的默认负载因子是0.75f

threshold,接下来是重构因子,由负载因子和容量的乘机组成,它表示当HashMap元素被存放了多少个之后,需要对HashMap进行重构。

通过这一系列的计算和定义后,初始化Entry[] table;

put(key,value)

接下来看一对key-value是如何被存放到HashMap中:put(key,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);
        System.out.println(key + " -->hash值: " + i); // 这就是刚才程序打印出来的key对应hash值
         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 ;
    }

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

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

这里是整个hash的关键,请打开源码查看一步一步查看。

hash(key.hashCode()) 计算出key的hash码 //对于hash()的算法,这里有一篇分析很透彻的文章<HashMap hash方法分析>
indexFor(hash, table.length) 通过一个与算法计算出来,该key应在存放在Hash表的哪个格子中。
for (Entry<K,V> e = table[i]; e != null; e = e.next) 然后再遍历table[i]格中的链表,判断是否已经存在一样的key,如果存在一样的key值,那么就用新的value覆盖旧的value,并把旧的value值返回。
addEntry(hash, key, value, i) 如果经过遍历链表没有发现同样的key,那么进行addEntry函数的操作,增加当前key到hash表中的第i个格子中的链表中

Entry<K,V> e = table[bucketIndex];  创建一个Entry对象来存放键值(ps:Entry对象是一个链表对象)
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 将Entry对象添加到链表中
if (size++ >= threshold) resize(2 * table.length); 最后将size进行自增,判断size值是否大于重构因子,如果大于那么就是用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);
    }

这里为什么是否需要扩容重构,其实是涉及到负载因子的性能问题

loadFactor负载因子
上面说过loadFactor是一个hashMap的决定性属性,HashSet和HashMap的默认负载因子都是0.75,它表示,如果哈希表的容量超过3/4时,将自动成倍的增加哈希表的容量,这个值是权衡了时间和空间的成本,如果负载因子较高,虽然会减少对内存空间的需求,但也会增加查找数据的时间开销,无论是put()和get()都涉及到对数据进行查找的动作,所以负载因子是不适宜设置过高

get(key)

接下来看看get(key)做了什么

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

这些动作似乎是跟put(key,value)相识,通过hash算法获取key的hash码,再通过indexFor定位出该key存在于table的哪一个下表,获取该下标然后对下标中的链表进行遍历比对,如果有符合就直接返回该key的value值。

keySet()

这里还涉及另一个问题,上面说了HashMap是跟set没有任何亲属关系,但map也一样实现了keySet接口,下面谱析一下keySet在hashMap中是如何实现的,这里给出部分代码,请结合源码查看

public  K next() {
             return  nextEntry().getKey();
        }

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

代码很简单,就是对每个格子里面的链表进行遍历,也正是这个原因,当我们依次将key值put进hashMap中,但在使用map.entrySet().iterator()进行遍历时候却不是put时候的顺序。

扩容
在前面说到put函数的时候,已经提过了扩容的问题

if  (size ++   >=  threshold)
resize( 2   *  table.length);

这里一个是否扩容的判断,当数据达到了threshold所谓的重构因子,而不是HashMap的最大容量,就进行扩容。


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

transfer方法实际上是将所有的元素重新进行一些hash,这是因为容量变化了,每个元素相对应的hash值也会不一样。

使用HashMap

1.不要再高并发中使用HashMap,HashMap是线程不安全,如果被多个线程共享之后,将可能发生不可预知的问题。
2.如果数据大小事固定的,最好在初始化的时候就给HashMap一个合理的容量值,如果使用new HashMap()默认构造函数,重构因子的值是16*0.75=12,当HashMap的容量超过了12后,就会进行一系列的扩容运算,重建一个原来成倍的数组,并且对原来存在的元素进行重新的hash运算,如果你的数据是有成千上万的,那么你的成千上万的数据也要跟这你的扩容不断的hash,这将产生高额的内存和cpu的大量开销。

当然啦,HashMap的函数还有很多,不过都是基于table的链表进行操作,当然也就是hash算法,Map & hashMap在平时我们的应用非常多,最重要的是我们要对每句代码中每块数据结构变化心中有数。

转载于:https://my.oschina.net/xiufeng/blog/115246

java数据结构-HashMap相关推荐

  1. Java数据结构---hashMap

    定义 HashMap 是一个散列表,它存储的内容是键值对(key-value)映射. HashMap 是无序的,即不会记录插入的顺序. 常见方法 定义 HashMap<Integer, Stri ...

  2. 【Java】HashMap的数据结构、源码解析 - 公开课笔记

    主要内容 Hashmap的数据结构 HashMap实现原理 HashMap源码解析 HashMap底层的数据结构? 1.7之前:数组+链表 1.8之后:数组+链表+红黑树 bucket 1.7之前: ...

  3. java队列_如何彻底搞懂 Java 数据结构?CSDN 博文精选

    作者 | 张振华.Jack 责编 | 郭芮 出品 | CSDN 博客 本文和大家一起来重温<Java数据结构>经典之作. Java数据结构 要理解Java数据结构,必须能清楚何为数据结构? ...

  4. java的hashmap排序_Java面试题:如何对HashMap按键值排序

    Java中HashMap是一种用于存储"键"和"值"信息对的数据结构.不同于Array.ArrayList和LinkedLists,它不会维持插入元素的顺序. ...

  5. Java数据结构类如何使用_Matlab如何使用Java的数据结构类型

    Matlab 2008b才开始引入containers.Map,这是Matlab唯一的数据结构(这里的数据结构是指自带一定逻辑性的数据结构,不包括普通数据类型).如果要有其它,比如Queue.Set等 ...

  6. Java数据结构Map遍历和排序

    map结构和list结合很好用,基本的遍历和排序每次都要查一下,用的不熟练,这里汇总下map的基本遍历和排序,参考的代码如下: package com.vip;import java.util.Arr ...

  7. Java基础 HashMap实现原理及方法

    1.什么是HashMap? HashMap通常提起他,我们想到的就是键值对方式存储(key-value型式),可以接收null键值和null值.基于Map接口的非同步实现(也就是线程不安全),并不保证 ...

  8. Java集合—HashMap底层原理

    原文链接:最通俗易懂搞定HashMap的底层原理 HashMap的底层原理面试必考题.为什么面试官如此青睐这道题?HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好o ...

  9. (7)Java数据结构--集合map,set,list详解

    MAP,SET,LIST,等JAVA中集合解析(了解) - clam_clam的专栏 - CSDN博---有颜色, http://blog.csdn.net/clam_clam/article/det ...

最新文章

  1. QIIME 2用户文档. 01简介和安装 Introduction Install(2020.2)
  2. Redux源码浅析系列(二):`combineReducer`
  3. 微信小程序button授权页面,用户拒绝后仍可再次授权
  4. fopen需要改写成fopen_s的时候
  5. c语言字符串加减_C语言中指针的介绍
  6. Mysql基本用法-left join、right join、 inner join、子查询和join-02
  7. Android应用实现文件关联
  8. 无忧开通了博客园博客主页
  9. 深入理解JavaScript系列:闭包(Closures)
  10. Docker02 基本命令、开发环境搭建、docker安装nginx、Dockerfile、路径挂载
  11. LED植物照明原理、推广难题与研究进展-徐志刚
  12. 操作系统linux入门,Linux操作系统基础(一)
  13. Windows下Tuxedo的安装与配置-无数据库
  14. 第十二章_网络搭建及训练
  15. php读取doc pdf文件,PHP读取创建txt,doc,xls,pdf类型文件
  16. div+css,表单和表格 学习笔记
  17. 悦诗风吟网络营销的目标_悦诗风吟产品网络营销推广策划方案
  18. CF685B Kay and Snowflake
  19. java 数字转英文_一个java的问题 讲输入的阿拉伯数字转换成英文
  20. 干草堆(寒假每日一题 2)

热门文章

  1. 71岁的亿万富翁坐自家飞船成功游览太空!此前已为太空旅行砸了超10亿美元
  2. 商汤组了「最强大脑」局,正儿八经解释为啥搞起电竞AI
  3. DCASE 2020权威声学比赛:腾讯多媒体实验室斩获双项指标国内第一
  4. 全球第一所人工智能大学成立:培养硕博研究生,全员全额奖学金
  5. 入门Go语言神器!超全学习资源+笔记,新手从零学习全过程资源汇总
  6. CycleGAN作者朱俊彦宣布重返CMU,担任助理教授
  7. Shell随机重命名所有当前目录一级子目录
  8. 字符串(一):char 数组
  9. 内存泄露从入门到精通三部曲之常见原因与用户实践
  10. 创新创新再创新(4)