hashMap 底层原理+LinkedHashMap 底层原理+常见面试题

1.源码  java

 

 

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

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

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

    在hashmap 中有这样一个结构数组

        Node implenets Map.entity{安全

          hash数据结构

          key多线程

          valueide

          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喽。

相关文章
相关标签/搜索