1.源码

java1.7    hashMap 底层实现是数组+链表

java1.8 对上面进行优化  数组+链表+红黑树

2.hashmap  是怎么保存数据的。

    在hashmap 中有这样一个结构

        Node implenets Map.entity{

          hash

          key

          value

          next

        }

当我们像hashMap 中放入数据时,其实就是一个

Enity{

  key

  vaue

}

在存之前会把这个Entity  转成Node

    怎么转的如下:

根据Entity 的key   通过hash  算出一个值  当成Node 的 hash  ,key vlaue  ,复制到Node 中,对于没有产生hash 冲突前,Node 的next 是null.

复制完成后,还需要通过Entity 对象的hash  算出  该 Entiry对象 具体应该放在 hashMap 的那个位置。计算如下  Hash&(lenth-1) 得到的值就是hashMap 对应具体的位置。(lentth是当前hashMap 的长度)。‘

 解决hash 冲突

    就是不同的元素通过   Hash&(lenth-1) 公式算出的位置相同,现在就启动了链表(单项链表),挂在了当前位置的下面,而链表的元素怎么关联呢,就用到了Node  的next  ,next的值就是该链表下一个元素在内存中的地址。

    jdk1.7  就是这样处理的,而到了 1.8  以后,就引用了红黑树,1.8以后这个链表只让挂7个元素,超过七个就会转成一个红黑树进行处理(最多是64,超多64 就会重新拆分)。

    当红黑树下挂的节点小于等于6的时候,系统会把红黑树转成链表。  Node 在jdk1.8之前是插入l链表头部的,在jdk1.8中是插入链表的尾部的。

hashMap 扩容:

    hashMap  会根据  当前的hashMap 的存储量达到 16*0.75=12 的时候,就是扩容  16*2=32  依次类推下去。2倍扩容。

    扩容后元素是如何做到均匀分的。

      

对上面的总结:

LinkedHashMap 源码详细分析(JDK1.8)

 这位大哥写的很好,可以看一下    https://segmentfault.com/a/1190000012964859

  我针对LinkedHashMap 的总结有一下几点

  1.LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构。该结构由数组和链表+红黑树 在此基础上LinkedHashMap 增加了一条双向链表,保持遍历顺序和插入顺序一致的问题。

  2. 在实现上,LinkedHashMap 很多方法直接继承自 HashMap(比如put  remove方法就是直接用的父类的),仅为维护双向链表覆写了部分方法(get()方法是重写的)。

  3.LinkedHashMap使用的键值对节点是Entity 他继承了hashMap 的Node,并新增了两个引用,分别是 before 和 after。这两个引用的用途不难理解,也就是用于维护双向链表.

  4.链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新

  5.LinkedHashMap 允许使用null值和null键, 线程是不安全的,虽然底层使用了双线链表,但是增删相快了。因为他底层的Entity 保留了hashMap  node 的next 属性。

  6.如何实现迭代有序?

  重新定义了数组中保存的元素Entry(继承于HashMap.node),该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。仍然保留next属性,所以既可像HashMap一样快速查找,

  用next获取该链表下一个Entry,也可以通过双向链接,通过after完成所有数据的有序迭代.

  7.竟然inkHashMap 的put 方法是直接调用父类hashMap的,但在 HashMap 中,put 方法插入的是 HashMap 内部类 Node 类型的节点,该类型的节点并不具备与 LinkedHashMap 内部类 Entry 及其子类型节点组成链表的能力。那么,LinkedHashMap 是怎样建立链表的呢?

   虽然linkHashMap 调用的是hashMap中的put 方法,但是linkHashMap 重写了,了一部分方法,其中就有  newNode(int hash, K key, V value, Node<K,V> e)linkNodeLast(LinkedHashMap.Entry<K,V> p)

   这两个方法就是 第一个方法就是新建一个 linkHasnMap 的Entity 方法,而 linkNodeLast 方法就是为了把Entity 接在链表的尾部。

  8.链表节点的删除过程

   与插入操作一样,LinkedHashMap 删除操作相关的代码也是直接用父类的实现,但是LinkHashMap 重写了removeNode()方法 afterNodeRemoval()方法,该removeNode方法在hashMap 删除的基础上有调用了afterNodeRemoval 回调方法。完成删除。

  删除的过程并不复杂,上面这么多代码其实就做了三件事:

  1. 根据 hash 定位到桶位置
  2. 遍历链表或调用红黑树相关的删除方法
  3. 从 LinkedHashMap 维护的双链表中移除要删除的节点

TreeMap 和SortMap

  1.TreeMap实现了SortedMap接口,保证了有序性。默认的排序是根据key值进行升序排序,也可以重写comparator方法来根据value进行排序具体取决于使用的构造方法。不允许有null值null键。TreeMap是线程不安全的。

   2.TreeMap基于红黑树(Red-Black tree)实现TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。

public class SortedMapTest {

public static void main(String[] args) {

SortedMap<String,String> sortedMap = new TreeMap<String,String>();
sortedMap.put("1", "a");
sortedMap.put("5", "b");
sortedMap.put("2", "c");
sortedMap.put("4", "d");
sortedMap.put("3", "e");
Set<Entry<String, String>> entry2 = sortedMap.entrySet();
for(Entry<String, String> temp : entry2){
System.out.println("修改前 :sortedMap:"+temp.getKey()+" 值"+temp.getValue());
}
System.out.println("\n");

//这里将map.entrySet()转换成list
List<Map.Entry<String,String>> list =
new ArrayList<Map.Entry<String,String>>(entry2);

Collections.sort(list, new Comparator<Map.Entry<String,String>>(){

@Override
public int compare(Entry<String, String> o1, Entry<String, String> o2) {
// TODO Auto-generated method stub
return o1.getValue().compareTo(o2.getValue());
}

});

for(Map.Entry<String,String> temp :list){
System.out.println("修改后 :sortedMap:"+temp.getKey()+" 值"+temp.getValue());
}
}

}

 附加点上面没有讲到的面试题:

1 HashMap特性?
  HashMap的特性:HashMap存储键值对,实现快速存取数据;允许null键/值;线程不安全;不保证有序(比如插入的顺序)。

2 HashMap中hash函数怎么是是实现的?还有哪些 hash 的实现方式?
  1. 对key的hashCode做hash操作(高16bit不变,低16bit和高16bit做了一个异或); 
  2. h & (length-1); //通过位操作得到下标index。

3. 扩展问题1:当两个对象的hashcode相同会发生什么?
  因为两个对象的Hashcode相同,所以它们的bucket位置相同,会发生“碰撞”。HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

4 扩展问题2:抛开 HashMap,hash 冲突有那些解决办法?
  开放定址法、链地址法、再哈希法。

5如果两个键的hashcode相同,你如何获取值对象?
  重点在于理解hashCode()与equals()。 
  通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。两个键的hashcode相同会产生碰撞,则利用key.equals()方法去链表或树(java1.8)中去查找对应的节点。

6 针对 HashMap 中某个 Entry 链太长,查找的时间复杂度可能达到 O(n),怎么优化?
  将链表转为红黑树,实现 O(logn) 时间复杂度内查找。JDK1.8 已经实现了。

7.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
  扩容。这个过程也叫作rehashing,因为它重建内部数据结构,并调用hash方法找到新的bucket位置。 
  大致分两步: 
  1.扩容:容量扩充为原来的两倍(2 * table.length); 
  2.移动:对每个节点重新计算哈希值,重新计算每个元素在数组中的位置,将原来的元素移动到新的哈希表中。 (如何计算上面讲的有)
  

8 为什么String, Interger这样的类适合作为键?
  String, Interger这样的类作为HashMap的键是再适合不过了,而且String最为常用。 
  因为String对象是不可变的,而且已经重写了equals()和hashCode()方法了。 
  1.不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。 
  注:String的不可变性可以看这篇文章《【java基础】浅析String》。 
  2.因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

9.hashmap.put 为什么是线程不安全的。(很重要)

正常情况下  hashmap 在保存数据时,底层是数组+链表+红黑树  但是 你去源码中看时,i发现子啊hashMap 底层没有加任何的多线程中的锁机制,比如: synchronize修饰  ,所以在多线程的情况下  hashMap 的单项链表,

可能会变成一个环形的链表,所以这个链表上的Next元素的指向永远不为null, 所以在遍历的时候就是死循环啊。

 9.1HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的

 9.2 HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。在hashmap做put操作的时候会调用到以上的方法。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失

   

10 ,hashmap 初始化时就生了一个长度为16 的数组。

  1.1 为什么初始化时16   而不是别的数字,

    1.其实是4或者8 只要是2的N次幂就行,因为hashMap put 元素时,会根据key 进行运算得到一个位置,运算就是,根据key的hash值&hashMap的长度-1(hash&length-1)  ,

    假如hashMap的长度为16     补充:&运算时,都是1才为1,其他情况都为0

    hash值   1010 1010 1000 0000 ....   1010

    &

    lennth-1             0111

    你会发现不管hash值为多少,只要 length 的长度是2的N次幂, 那么length-1 的二进制最后一位就是1,所以  hash值&上length-1 最后得到的二进制数字的末位,可能是1 也可能是0,

    如果 其长度不是2的n次幂,比如 15 ,那么15-1 =14 的 二进制 0110,那么遇上hash  的到二进制末位,永远就是0了 ,这就侧面的表明了通过计算出来的元素位置的分散性。

    为什么不选4,8 这些也是2的N次幂作为扩容初始化值呢?其实8 也行4 也行,但是 我的java 是c语言写的,而c语言是由汇编语言,而汇编的语言来源是机器语言,而汇编的语言使用的就是16进制,对于经验而言,当然就选16喽。

最新文章

  1. [开源]QuickSwitchSVNClient,快速完成SVN Switch的工具
  2. 【burp】配置HTTPS抓包方法
  3. Tomcat部署web项目,虚拟目录,上下文(Context),WEB-INF,web.xml,servlet,404
  4. [ERROR] Failed to execute goal org.apache.maven.plugins:maven-archetype-plugin:2.4:create (default-cli) on project standalone-pom: Unable to parse configuration of 3: mojo org.apache.maven.plugins:
  5. PMP 第十三章 项目干系人管理
  6. Mysql学习笔记(十四)备份与恢复
  7. SQL server performance - tempdb
  8. Socket 两平台互相 通信 .NET
  9. spring使用aop
  10. 触摸事件 Touch MotionEvent ACTION
  11. 第二章SignalR所支持的平台
  12. English - 英文写作中的最常见“十大句式”
  13. java复习 --集合类
  14. 关于sqlserver还原不了数据库的原因
  15. 【Android】 分享一个完整的项目,适合新手!
  16. ESLint 使用入门 - 来自推酷
  17. leetcode math类型题目解题总结
  18. mysql 数据库查看表的信息
  19. Xamarin Essentials教程使用指南针Compass
  20. springbank 开发日志 一次因为多线程问题导致的applicationContext.getBean()阻塞

热门文章

  1. hive 存储格式对比
  2. 【leetcode】610. Triangle Judgement
  3. 清除keil编译中间文件的脚本
  4. 哲学家就餐问题 C语言实现
  5. Linux教程 Yum命令的使用
  6. Ubuntu系统---C++之Eclipse 开始工程项目
  7. Windows Media Player播放视频导致程序闪退
  8. Qt中使用匿名函数lambda表达式
  9. Spring入门篇——AOP基本概念
  10. mybatis+mysql批量插入和批量更新