hashmap1.8源码,hashmap菜鸟教程

  hashmap1.8源码,hashmap菜鸟教程

  Yyds干货库存

  深度解析HashMap源代码* HashMap底层数据结构(为什么要引入红黑树,存储数据的过程,哈希碰撞相关问题)

  * HashMap成员变量(什么是初始化容量,加载因子,为什么数组长度是2的n次方)

  * HashMap扩展机制(什么时候需要扩展?怎么拓展?)

  *与Jdk8相比,JDK7进行了优化?1定义基于哈希表的HashMap接口的实现,哈希表是以键值存储的形式存在的,即主要用来存储键值对。HashMap的实现不是同步的,这意味着它不是线程安全的。它的键和值可以为空。此外,HashMap中的映射是无序的。

  JDK1.7 HashMap数据结构:数组链表JDK1.8 HashMap数据结构:数组链表/红黑树思考:为什么1.8以后的HashMap数据结构要加入红黑树?

  2哈希表哈希表也叫哈希表,也直接翻译成哈希表。哈希表是一种可以根据键值直接访问的数据结构。也就是说,它通过将键值映射到表中的某个位置来访问记录,从而加快搜索速度。在链表、数组等数据结构中,通常需要遍历整个数据结构,也就是O(N)的时间级,但对于哈希表,只是O(1)的时间级。

  哈希表,通过将键值映射到表中的某个位置来访问记录,以加快搜索速度。这个映射函数叫做hash函数,存储记录的数组叫做* *哈希表,只需要O(1)**的时间级

  思考:多个键通过hash函数会得到相同的值。这时候我该怎么办?

  解决:

  (1)开放地址法

  (2)链地址法

  对于开放地址方法,可能会有两个冲突和三个冲突,所以需要一个好的哈希函数。分布越均匀越好。对于链地址法,虽然不会造成二次冲突,但是如果一次冲突很多,就会导致子数组或者子链表很长,我们搜索需要的遍历时间会很长。

  3哈希表在JDK1.8之前的数据结构在JDK8之前,哈希表在JDK 8之前的实现是数组链表。即使哈希函数实现的很好,也很难做到元素100%均匀分布。当HashMap中的大量元素存储在同一个桶中时,这个桶下就有一个很长的链表。在极端情况下,HashMap相当于单个链表。如果单个链表中有n个元素,遍历时间复杂度为O(n),完全失去优势。

  JDK1.8之后HashMap的数据结构JDK 8之后HashMap的实现是数组链表。红黑树桶中的结构可以是链表或红黑树。当链表的长度大于阈值(或红黑树的边界值,默认值为8)且当前数组的长度大于64时,该索引位置的所有数据将改为存储在红黑树中。

  5\.类构造函数公共类hashmap k,v扩展抽象映射k,v

  实现映射K,V,可克隆,可序列化{

  JDK为我们提供了一个抽象类AbstractMap,它继承了Map接口,所以如果我们不想实现所有的Map接口方法,可以选择继承抽象类AbstractMap。

  HashMap set实现了Cloneable接口和Serializable接口,分别用来克隆和序列化对象。

  注意:HashMap类继承了AbstractMap接口并实现了Map接口。这样做不是没有必要吗?

  根据java Collection Framework创始人Josh Bloch的说法,这种写法是一个错误。在java集合框架中,有很多方法可以这样写。在开始写java集合框架的时候,他认为有些地方可能是有价值的,直到他意识到自己的错误。显然,JDK的维护者认为这个小错误不值得修改,所以它存在了。6字段属性//序列化和反序列化时,通过该字段验证版本一致性。

  private static final long serialVersionUID=362498820763181265 l;

  //默认HashMap集初始容量是16(必须是2的倍数)

  static final int DEFAULT _ INITIAL _ CAPACITY=1 4;//又名16

  //集合的最大容量。如果参数construction指定的最大容量超过了这个数字,默认情况下仍将使用这个数字。

  静态最终int MAXIMUM _ CAPACITY=1 30

  //默认填充因子

  静态最终浮动DEFAULT _ LOAD _ FACTOR=0.75f

  //当桶上的节点数大于该值时,会变成红黑树(JDK1.8新增)

  静态最终int tree ify _ THRESHOLD=8;

  //当桶上的节点数小于该值时,会转换为链表(JDK1.8新增)

  静态最终int un treify _ THRESHOLD=6;

  /* *(JDK 1.8中的新功能)

  *当集合中的容量大于该值时,可以对表中的桶进行树化,否则桶中的元素过多会展开,

  *而不是爬树。为了避免容量扩展和树化之间的冲突,该值不能小于4 * TREEIFY_THRESHOLD。

  */

  静态最终int MIN _ TREEIFY _ CAPACITY=64

  /**

  *初始化时,长度总是2的幂。

  */

  暂态节点K,V []表;

  /**

  *保存缓存的entrySet()

  */

  瞬态集图。条目K,V entrySet

  /**

  *此映射中包含的键值映射的数量。(集合存储键值对的数量)

  */

  瞬态int大小;

  /**

  *与ArrayList和LinkedList集合中的字段modCount一样,记录集合被修改的次数。

  *主要用于迭代器中的快速失败。

  */

  瞬态int modCount

  /**

  resize的下一个大小值(容量*装载因子)。容量*负载系数

  */

  int阈值;

  /**

  *哈希表的加载因子。

  */

  最终浮动负载系数;让我们关注以上领域:

  、节点K,V []表

  我们说HashMap是由数组链表中的红黑树组成的,这里的数组就是表字段。最近一次初始化长度是DEFAULT_INITIAL_CAPACITY=16。此外,JDK指出,数组的长度总是2的n次方(它必须是一个合数)。为什么这里要求是合数?一般我们知道哈希算法为了避免冲突,要求长度是质数,这里要求是合数。这里先介绍一下HashMap的hashCode()方法(hash函数),然后再解释。

  **、尺寸**

  存储在集合中的键值的实时对数。

  、负载系数

  加载因子用于衡量哈希表的充满程度。HashMap的实时加载因子是用size/capacity的方法计算的,而不是用被占用的桶数除以容量。容量是桶的数量,也就是表的长度。

  默认的加载因子0.75是空间和时间效率的平衡选择。建议大家不要修改,除非有特殊的时空。如果内存空间很大,时间效率很高,可以降低load factor的值。相反,如果内存空间紧张,时间效率不高,可以增加loadFactor的值,可以大于1。

  、阈值

  计算公式:容量*负载系数。该值是当前占用数组的最大长度。在这个数字之后,再次resize (expand),扩展后的HashMap容量是之前容量的两倍。

  7构造函数,默认无参数构造函数

  /**

  *默认构造函数,初始化加载因子loadFactor=0.75

  */

  public HashMap() {

  this . LOAD FACTOR=DEFAULT _ LOAD _ FACTOR;

  } 、指定初始容量的建造师。

  /**

  *

  * @param initialCapacity指定初始容量。

  * @param loadFactor负载系数为0.75

  */

  public HashMap(int initial capacity,float loadFactor) {

  //初始化容量不能小于0,否则会抛出异常。

  if (initialCapacity 0)

  抛出新的IllegalArgumentException(非法初始容量:

  初始容量);

  //如果初始化容量大于2的30次方,则初始化容量为2的30次方。

  if(初始容量最大容量)

  initial CAPACITY=max _ CAPACITY;

  //如果加载因子小于0,或者加载因子是非数值,则引发异常

  if(load factor=0 float . isnan(load factor))

  抛出新的IllegalArgumentException(非法加载因子:

  负载系数);

  this.loadFactor=负载系数;

  this . threshold=tablesize for(initial capacity);

  }

  //返回大于或等于initialCapacity的最小功率值。

  //运算符表示无符号右移,高位取0。

  //按位或运算

  静态最终int tableSizeFor(int cap) {

  int n=cap-1;

  n =n

  n =n

  n =n

  n =n

  n =n

  返回(n 0)?1 : (n=MAXIMUM_CAPACITY)?MAXIMUM _ CAPACITY:n1;

  }8确定哈希桶数组的索引位置。当我们在前面解释哈希表时,我们知道哈希函数用于确定索引位置。散列函数设计得越好,元素分布得越均匀。HashMap是数组链表中红黑树的组合。我们希望当数组位置有限时,每个位置尽可能只有一个元素。那么当我们使用hash函数获取索引位置时,就可以立刻知道对应位置的元素是否是我们想要的,而不用遍历链表或者红黑树,这样会大大优化我们的查询效率。让我们看看HashMap中的哈希算法:

  静态最终int哈希(对象键){

  int h;

  return (key==null)?0:(h=key . hashcode())^(h 16);

  }

  i=(table.length - 1)哈希;//这一步是确定后面添加元素putVal()的方法中的位置。有三个步骤:

  取hashCode: key.hashCode()的值

  高位参与运算:h 16

  模运算:(n-1)哈希

  在这里,获取hashCode()方法的值是一个变量,但是我们知道,对于任何给定的对象,只要它的hashCode()返回值相同,那么通过调用hash(Object key)计算出来的hash代码值总是相同的。

  为了让数组元素均匀分布,我们首先想到的是用得到的哈希码(hash%length)对数组的长度取模。但是计算机都是二进制运算,模运算的相对成本还是很高的,那么如何优化呢?

  HashMap使用的方法很巧妙。它通过hash (table.length -1)获取对象的保存位。如前所述,HashMap底部数组的长度始终是2的n次方,这是对HashMap速度的优化。当length总是2的n次方时,hash (length-1)运算相当于模长度,即hash%length,但比%更高效。例如,n% 32=n (32 -1)

  这也解释了为什么数组的长度应该总是2的n次方。

  在JDK1.8中,有一个高位参与运算。hashCode()得到一个32位的int值,通过hashCode()的高16位或低16位的异或来实现:(h=k. Hashcode ()) (h 16),主要考虑速度、功效和质量。当数组表的长度相对较小时,可以做到这一点。

  例如,n是表的长度:

  添加9个元素//hash(key)就是上面提到的哈希方法,处理第一步和第二步。

  公共V put(K键,V值){

  返回putVal(hash(key),key,value,false,true);

  }

  /**

  *

  * @param哈希索引位置

  * @param key key

  * @param value值

  * @param onlyIfAbsent true表示不更改现有值。

  * @param evict false表示该表处于创建模式。

  * @返回

  */

  final V putVal(int hash,K key,V value,boolean onlyIfAbsent,

  布尔逐出){

  节点K,V[]tab;节点K,V int n,I;

  //如果表为空或长度为0,则初始化它

  ////resize()方法最初用于容量扩展。由于初始化时没有实际的空间分配,所以这里使用这个方法进行空间分配,后面会详细解释。

  if((tab=table)==null (n=tab . length)==0)

  n=(tab=resize())。长度;

  //注意:这里用的是前面解释的获取key哈希码的第三步,模运算。下面的if-else是tab[i]分别为null和not null。

  if ((p=tab[i=(n - 1) hash])==null)

  tab[i]=newNode(hash,key,value,null);//tab[i]为空,所以新键值直接插入到计算出的索引I位置。

  Else {//tab[i]不为null,表示该位置已经有值。

  节点K,V K k

  if (p.hash==hash

  ((k=p.key)==key (key!=null key.equals(k))))

  e=p;//节点键已经有值,直接用新值覆盖。

  //链条是一棵红黑树

  else if(p TreeNode的实例)

  e=((TreeNode K,V )p)。putTreeVal(this,tab,hash,key,value);

  //这个链是一个链表

  否则{

  for(int bin count=0;binCount) {

  if ((e=p.next)==null) {

  p.next=newNode(hash,key,value,null);

  //如果链表长度大于8,就转换成红黑树。

  第一个if(bin count=tree ify _ THRESHOLD-1)//-1

  treeifyBin(tab,hash);

  打破;

  }

  //键已经有直接重写值。

  如果(例如,哈希==哈希

  ((k=e.key)==key (key!=null key.equals(k))))

  打破;

  p=e;

  }

  }

  如果(e!=null) { //键的现有映射

  V oldValue=e.value

  如果(!onlyIfAbsent oldValue==null)

  e.value=值;

  afterNodeAccess(e);

  返回旧值;

  }

  }

  modCount//用作快速失败修改添加。

  If(大小阈值)//如果超过最大容量,请扩展容量。

  resize();

  afterNodeInsertion(驱逐);

  返回null

  } 、判断键值对数组表是否为空或null,否则执行resize()进行扩展;

  根据键值key计算哈希值得到插入的数组索引I,如果table[i]==null,直接新建一个节点添加,转,如果table[i]不为空,转;

  判断表[i]的第一个元素是否与key相同,如果相同,则直接覆盖value,否则转到,这里相同指hashCode和equals;

  判断table[i]是否为treeNode,即table[i]是否为红黑树,如果是,直接将键值对插入树中,否则转;

  遍历表[i],判断链表长度是否大于8。如果大于8,则将链表转换为红黑树,插入红黑树,否则插入链表;在遍历过程中,如果发现键已经存在,可以直接覆盖值;

  插入成功后,判断键-值对的实际数量大小是否超过最大容量阈值,如果是,则扩展容量。

  如果新插入的键不存在,则返回null如果新插入的键确实存在,它将返回对应于原始键的值(注意,新插入的值将覆盖原始值)

  1:其中代码:

  If(大小阈值)//如果超过最大容量,请扩展容量。

  resize();这里是一个测试点。我们知道HashMap是由红黑树(JDK1.8),一个数组链表组成的。如果添加元素时有冲突,冲突的数量将放在链表中。当链表长度超过8时,会自动转换为红黑树。

  然后有以下问题:数组上有5个元素,链表上有3个元素。这个散列表的大小是多少?

  我们分析代码的时候很容易知道,只要调用put()方法添加一个元素,那么就会调用size(这里有一个例外,就是插入一个有重复key的键值对,但是重复的key元素不会影响大小)。所以,上面的答案是7。

  10容量扩展机制(resize),我们知道集合是由数组链表中的红黑树组成的。在HashMap中插入元素时,如果HashMap集合的元素已经大于最大承载能力阈值(capacity * loadFactor),这里的阈值不是数组的最大长度。那么必须扩展数组的长度。在Java中,数组不能自动扩展。我们采用的方法是把这个小数组换成一个更大的,就像以前用小水桶盛水,现在小水桶装不下了。我们用一个更大的桶。

  JDK1.8融入了红黑树的机制,比较复杂。这里先介绍JDK1.7的扩展源代码,便于理解,再介绍JDK1.8的源代码。

  //参数newCapacity是新数组的大小。

  void resize(int newCapacity) {

  Entry[] oldTable=表;//在展开之前引用条目数组

  int old capacity=old table . length;

  If(old capacity==maximum _ capacity){//如果扩展前的数组大小已达到最大值(2 ^ 30)

  阈值=整数。MAX _ VALUE///修改int(2 ^ 31-1)的最大阈值,这样以后就不会扩大了。

  返回;

  }

  Entry[] newTable=新条目[new capacity];//初始化新的条目数组

  transfer(newTable,inithashseedasneed(new capacity));//将数组元素转移到新数组中

  table=newTable

  threshold=(int)math . min(new CAPACITY * load factor,MAXIMUM _ CAPACITY 1);//修改阈值

  }

  void transfer(Entry[] newTable,boolean rehash) {

  int new capacity=new table . length;

  For (Entry K,V e: table) {//遍历数组

  while(null!=e) {

  条目K,V next=e.next

  如果(再散列){

  e.hash=null==e.key?0:哈希(e . key);

  }

  int i=indexFor(e.hash,new capacity);//重新计算数组中每个元素的索引位置

  e . next=new table[I];//标记下一个元素,加法就是链表头的加法。

  new table[I]=e;//将元素放在链上

  e=下一个;//访问下一个条目链上的元素

  }

  }

  }通过方法我们可以看到,在JDK1.7中,首先创建一个新的大容量数组,然后依次重新计算原集合所有元素的索引,再重新赋值。如果数组中某个位置存在哈希冲突,则使用插入单个链表头的方法,相同位置的新元素总是放在链表头,这样与原来设置的链表相比,扩展后的链表可能是倒排链表。

  我们来看看JDK1.8。

  最终节点K,V [] resize() {

  节点K,V[]old tab=table;

  int oldCap=(oldTab==null)?0:old tab . length;//如果原数组为空,则长度赋为0。

  int oldThr=threshold

  int newCap,new thr=0;

  If (oldCap 0) {//如果原始数组长度大于0

  If (oldCap=MAXIMUM_CAPACITY) {//如果数组大小大于或等于最大值(2 ^ 30)

  阈值=整数。MAX _ VALUE//用int(2 ^ 31-1)的阈值修改最大值,这样以后就不会扩大了。

  返回oldTab

  }

  //原数组长度大于等于初始化长度16,如果原数组长度翻倍,也小于2的30次方。

  else if((new cap=old cap 1)MAXIMUM _ CAPACITY

  oldCap=DEFAULT _ INITIAL _ CAPACITY)

  new thr=old thr 1;//门槛翻倍。

  }

  Else if (oldThr 0) //如果旧阈值大于0,则新容量将直接等于旧阈值。

  newCap=oldThr

  Else {//阈值等于0,oldCap也等于0(集合未初始化)

  new cap=DEFAULT _ INITIAL _ CAPACITY;//数组长度初始化为16

  new thr=(int)(DEFAULT _ LOAD _ FACTOR * DEFAULT _ INITIAL _ CAPACITY);//阈值等于16*0.75=12

  }

  //计算新的上限阈值

  if (newThr==0) {

  float ft=(float)newCap * load factor;

  new thr=(newCap MAXIMUM _ CAPACITY ft(float)MAXIMUM _ CAPACITY?

  (int)ft:整数。MAX _ VALUE);

  }

  threshold=newThr

  @SuppressWarnings({rawtypes , unchecked})

  Node K,V [] newTab=(Node K,V[])new Node[newCap];

  table=newTab

  if (oldTab!=null) {

  //将每个存储桶移动到新的存储桶

  for(int j=0;j oldCapj) {

  节点K,V

  if ((e=oldTab[j])!=null) {

  old tab[j]=null;//元数据j位置设置为空,用于垃圾回收。

  If (e.next==null)//数组没有下一个引用(不是链表)

  new tab[e . hash(newCap-1)]=e;

  Else if (e instanceof TreeNode)//红黑树

  ((TreeNode K,V )e)。split(this,newTab,j,old cap);

  else { //保持顺序

  节点K,V loHead=null,loTail=null

  节点K,V hiHead=null,hiTail=null

  节点K,V接下来;

  做{

  next=e.next

  //原始索引

  if ((e.hash oldCap)==0) {

  if (loTail==null)

  loHead=e;

  其他

  lotail . next=e;

  loTail=e;

  }

  //原始索引oldCap

  否则{

  if (hiTail==null)

  hiHead=e;

  其他

  hitail . next=e;

  hiTail=e;

  }

  } while ((e=next)!=null);

  //将原始索引放入桶中

  如果(loTail!=null) {

  loTail.next=null

  new tab[j]=loHead;

  }

  //将原始索引oldCap放入bucket。

  如果(hiTail!=null) {

  hiTail.next=null

  new tab[j old cap]=hiHead;

  }

  }

  }

  }

  }

  返回newTab

  }这个方法分为两部分。首先计算新桶数组的容量newCap和新阈值newThr,然后将原集合的元素重新映射到新集合。

  与JDK1.7相比,1.8采用2次方扩展(即长度增加一倍)。因此,元素的位置要么在原始位置,要么从原始位置移动2次幂。我们在扩展HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,我们只需要看看原来hash值新增加的位是1还是0就可以了。如果为0,则索引保持不变,如果为1,则索引变为“原始索引oldCap”。

  1删除元素HashMap删除元素先找到桶的位置,然后如果是链表,遍历链表,找到要删除的元素后删除;如果是红黑树,也是树遍历。找到元素并删除后,进行平衡调整。注意,当红黑树的节点数小于6时,会转换成链表。

  public V get(对象键){

  节点K,V

  return (e=getNode(hash(key),key))==null?null:e . value;

  }

  最终节点K,V getNode(int hash,Object key) {

  节点K,V[]tab;节点K,V优先,e;int n;K k

  if ((tab=table)!=null (n=tab.length) 0

  (first=tab[(n - 1) hash])!=null) {

  //根据key计算的索引检查第一个索引

  if (first.hash==hash //始终检查第一个节点

  ((k=first.key)==key (key!=null key.equals(k))))

  先退;

  //不是第一个节点

  if ((e=first.next)!=null) {

  If(树的第一个实例)//遍历树以查找元素

  先返回((TreeNode K,V))。getTreeNode(hash,key);

  做{

  //遍历链表以查找元素

  如果(例如,哈希==哈希

  ((k=e.key)==key (key!=null key.equals(k))))

  返回e;

  } while ((e=e.next)!=null);

  }

  }

  返回null

  }12求元素。通过键查找值。

  首先,通过键找到计算索引和桶位置。首先,检查第一个节点。如果是,它将返回。如果没有,就遍历后面的链表或者红黑树。所有其他情况返回null。

  public V get(对象键){

  节点K,V

  return (e=getNode(hash(key),key))==null?null:e . value;

  }

  最终节点K,V getNode(int hash,Object key) {

  节点K,V[]tab;节点K,V优先,e;int n;K k

  if ((tab=table)!=null (n=tab.length) 0

  (first=tab[(n - 1) hash])!=null) {

  //根据key计算的索引检查第一个索引

  if (first.hash==hash //始终检查第一个节点

  ((k=first.key)==key (key!=null key.equals(k))))

  先退;

  //不是第一个节点

  if ((e=first.next)!=null) {

  If(树的第一个实例)//遍历树以查找元素

  先返回((TreeNode K,V))。getTreeNode(hash,key);

  做{

  //遍历链表以查找元素

  如果(例如,哈希==哈希

  ((k=e.key)==key (key!=null key.equals(k))))

  返回e;

  } while ((e=e.next)!=null);

  }

  }

  返回null

  }摘要

  基于JDK1.8的HashMap由数组链表红黑树组成。当链表长度超过8时,会自动转换为红黑树,当红黑树的节点数小于6时,会再次转换为链表。与早期版本的JDK HashMap相比,增加了新的红黑树作为底层数据结构,在数据量大、哈希碰撞多的情况下,可以大大增加检索效率。

  允许键和值都为空。重复的关键字将被覆盖,并且允许重复的值。

  非线程安全

  无序(遍历HashMap得到的元素顺序不是插入顺序)

  版权归作者所有:原创作品来自博主小二上九8,转载请联系作者取得转载授权,否则将追究法律责任。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: