乐趣区

关于java:Java集合容器

汇合容器概述

转载自:https://thinkwon.blog.csdn.ne…

什么是汇合

汇合框架:用于存储数据的容器。

汇合框架是为示意和操作汇合而规定的一种对立的规范的体系结构。
任何汇合框架都蕴含三大块内容:对外的接口、接口的实现和对汇合运算的算法。

接口:示意汇合的抽象数据类型。接口容许咱们操作汇合时不用关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来造成标准。

实现:汇合接口的具体实现,是重用性很高的数据结构。

算法 :在一个实现了某个汇合框架中的接口的对象身上实现某种有用的计算的办法,例如查找、排序等。这些算法通常是多态的,因为雷同的办法能够在同一个接口被多个类实现时有不同的体现。事实上,算法是可复用的函数。
它缩小了程序设计的辛苦。

汇合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要局部上,而不是为了让程序能失常运行而将注意力于低层设计上。
通过这些在无关 API 之间的繁难的互用性,使你罢黜了为改编对象或转换代码以便联结这些 API 而去写大量的代码。它进步了程序速度和品质。

汇合的特点

汇合的特点次要有如下两点:

  • 对象封装数据,对象多了也须要存储。汇合用于存储对象。
  • 对象的个数确定能够应用数组,对象的个数不确定的能够用汇合。因为汇合是可变长度的。

汇合和数组的区别

  • 数组是固定长度的;汇合可变长度的。
  • 数组能够存储根本数据类型,也能够存储援用数据类型;汇合只能存储援用数据类型。
  • 数组存储的元素必须是同一个数据类型;汇合存储的对象能够是不同数据类型。

数据结构:就是容器中存储数据的形式。

对于汇合容器,有很多种。因为每一个容器的本身特点不同,其实原理在于每个容器的外部数据结构不同。

汇合容器在一直向上抽取过程中,呈现了汇合体系。在应用一个体系的准则:参阅顶层内容。建设底层对象。

应用汇合框架的益处

  1. 容量自增长;
  2. 提供了高性能的数据结构和算法,使编码更轻松,进步了程序速度和品质;
  3. 容许不同 API 之间的互操作,API 之间能够来回传递汇合;
  4. 能够不便地扩大或改写汇合,进步代码复用性和可操作性。
  5. 通过应用 JDK 自带的汇合类,能够升高代码保护和学习新 API 老本。

罕用的汇合类有哪些?

Map 接口和 Collection 接口是所有汇合框架的父接口:

  1. Collection 接口的子接口包含:Set 接口和 List 接口
  2. Map 接口的实现类次要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap 以及 Properties 等
  3. Set 接口的实现类次要有:HashSet、TreeSet、LinkedHashSet 等
  4. List 接口的实现类次要有:ArrayList、LinkedList、Stack 以及 Vector 等

List,Set,Map 三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?

Java 容器分为 Collection 和 Map 两大类,Collection 汇合的子接口有 Set、List、Queue 三种子接口。咱们比拟罕用的是 Set、List,Map 接口不是 collection 的子接口。

Collection 汇合次要有 List 和 Set 两大接口

  • List:一个有序(元素存入汇合的程序和取出的程序统一)容器,元素能够反复,能够插入多个 null 元素,元素都有索引。罕用的实现类有 ArrayList、LinkedList 和 Vector。
  • Set:一个无序(存入和取出程序有可能不统一)容器,不能够存储反复元素,只容许存入一个 null 元素,必须保障元素唯一性。Set 接口罕用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

Map 是一个键值对汇合,存储键、值和之间的映射。Key 无序,惟一;value 不要求有序,容许反复。Map 没有继承于 Collection 接口,从 Map 汇合中检索元素时,只有给出键对象,就会返回对应的值对象。

Map 的罕用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

汇合框架底层数据结构

Collection

  1. List
  • Arraylist:Object 数组
  • Vector:Object 数组
  • LinkedList:双向循环链表
  1. Set
  • HashSet(无序,惟一):基于 HashMap 实现的,底层采纳 HashMap 来保留元素
  • LinkedHashSet:LinkedHashSet 继承与 HashSet,并且其外部是通过 LinkedHashMap 来实现的。有点相似于咱们之前说的 LinkedHashMap 其外部是基于 Hashmap 实现一样,不过还是有一点点区别的。
  • TreeSet(有序,惟一):红黑树(自均衡的排序二叉树。)

Map

  • HashMap:JDK1.8 之前 HashMap 由数组 + 链表组成的,数组是 HashMap 的主体,链表则是次要为了解决哈希抵触而存在的(“拉链法”解决抵触).JDK1.8 当前在解决哈希抵触时有了较大的变动,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以缩小搜寻工夫
  • LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层依然是基于拉链式散列构造即由数组和链表或红黑树组成。另外,LinkedHashMap 在下面构造的根底上,减少了一条双向链表,使得下面的构造能够放弃键值对的插入程序。同时通过对链表进行相应的操作,实现了拜访程序相干逻辑。
  • HashTable:数组 + 链表组成的,数组是 HashMap 的主体,链表则是次要为了解决哈希抵触而存在的
  • TreeMap:红黑树(自均衡的排序二叉树)

哪些汇合类是线程平安的?

  • vector:就比 arraylist 多了个同步化机制(线程平安),因为效率较低,当初曾经不太倡议应用。在 web 利用中,特地是前台页面,往往效率(页面响应速度)是优先思考的。
  • statck:堆栈类,先进后出。
  • hashtable:就比 hashmap 多了个线程平安。
  • enumeration:枚举,相当于迭代器。

Java 汇合的疾速失败机制“fail-fast”?

是 java 汇合的一种谬误检测机制,当多个线程对汇合进行构造上的扭转的操作时,有可能会产生 fail-fast 机制。

例如:假如存在两个线程(线程 1、线程 2),线程 1 通过 Iterator 在遍历汇合 A 中的元素,在某个时候线程 2 批改了汇合 A 的构造(是构造下面的批改,而不是简略的批改汇合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异样,从而产生 fail-fast 机制。

起因:迭代器在遍历时间接拜访汇合中的内容,并且在遍历过程中应用一个 modCount 变量。汇合在被遍历期间如果内容发生变化,就会扭转 modCount 的值。每当迭代器应用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异样,终止遍历。

解决办法:

  1. 在遍历过程中,所有波及到扭转 modCount 值得中央全副加上 synchronized。
  2. 应用 CopyOnWriteArrayList 来替换 ArrayList

怎么确保一个汇合不能被批改?

能够应用 Collections. unmodifiableCollection(Collection c) 办法来创立一个只读汇合,这样扭转汇合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异样。

示例代码如下:

`List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());` 

*   1
*   2
*   3
*   4
*   5

Collection 接口

List 接口

迭代器 Iterator 是什么?

Iterator 接口提供遍历任何 Collection 的接口。咱们能够从一个 Collection 中应用迭代器办法来获取迭代器实例。迭代器取代了 Java 汇合框架中的 Enumeration,迭代器容许调用者在迭代过程中移除元素。

Iterator 怎么应用?有什么特点?

Iterator 应用代码如下:

`List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){String obj = it. next();
  System. out. println(obj);
}` 

*   1
*   2
*   3
*   4
*   5
*   6

Iterator 的特点是只能单向遍历,然而更加平安,因为它能够确保,在以后遍历的汇合元素被更改的时候,就会抛出 ConcurrentModificationException 异样。

如何边遍历边移除 Collection 中的元素?

边遍历边批改 Collection 的惟一正确形式是应用 Iterator.remove() 办法,如下:

`Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();}` 

*   1
*   2
*   3
*   4
*   5

一种最常见的 谬误 代码如下:

`for(Integer i : list){list.remove(i)
}` 

*   1
*   2
*   3

运行以上错误代码会报 ConcurrentModificationException 异样。这是因为当应用 foreach(for(Integer i : list)) 语句时,会主动生成一个 iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 批改。Java 个别不容许一个线程在遍历 Collection 时另一个线程批改它。

Iterator 和 ListIterator 有什么区别?

  • Iterator 能够遍历 Set 和 List 汇合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 能够双向遍历(向前 / 后遍历)。
  • ListIterator 实现 Iterator 接口,而后增加了一些额定的性能,比方增加一个元素、替换一个元素、获取后面或前面元素的索引地位。

遍历一个 List 有哪些不同的形式?每种办法的实现原理是什么?Java 中 List 遍历的最佳实际是什么?

遍历形式有以下几种:

  1. for 循环遍历,基于计数器。在汇合内部保护一个计数器,而后顺次读取每一个地位的元素,当读取到最初一个元素后进行。
  2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目标是屏蔽不同数据汇合的特点,对立遍历汇合的接口。Java 在 Collections 中反对了 Iterator 模式。
  3. foreach 循环遍历。foreach 外部也是采纳了 Iterator 的形式实现,应用时不须要显式申明 Iterator 或计数器。长处是代码简洁,不易出错;毛病是只能做简略的遍历,不能在遍历过程中操作数据汇合,例如删除、替换。

最佳实际:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否反对 Random Access。

  • 如果一个数据汇合实现了该接口,就意味着它反对 Random Access,按地位读取元素的均匀工夫复杂度为 O(1),如 ArrayList。
  • 如果没有实现该接口,示意不反对 Random Access,如 LinkedList。

举荐的做法就是,反对 Random Access 的列表可用 for 循环遍历,否则倡议用 Iterator 或 foreach 遍历。

说一下 ArrayList 的优缺点

ArrayList 的长处如下:

  • ArrayList 底层以数组实现,是一种随机拜访模式。ArrayList 实现了 RandomAccess 接口,因而查找的时候十分快。
  • ArrayList 在程序增加一个元素的时候十分不便。

ArrayList 的毛病如下:

  • 删除元素的时候,须要做一次元素复制操作。如果要复制的元素很多,那么就会比拟消耗性能。
  • 插入元素的时候,也须要做一次元素复制操作,毛病同上。

ArrayList 比拟适宜程序增加、随机拜访的场景。

如何实现数组和 List 之间的转换?

  • 数组转 List:应用 Arrays. asList(array) 进行转换。
  • List 转数组:应用 List 自带的 toArray() 办法。

代码示例:

`// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();

// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7
*   8
*   9

ArrayList 和 LinkedList 的区别是什么?

  • 数据结构实现:ArrayList 是动静数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
  • 随机拜访效率:ArrayList 比 LinkedList 在随机拜访的时候效率要高,因为 LinkedList 是线性的数据存储形式,所以须要挪动指针从前往后顺次查找。
  • 减少和删除效率:在非首尾的减少和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其余数据的下标。
  • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个援用,一个指向前一个元素,一个指向后一个元素。
  • 线程平安:ArrayList 和 LinkedList 都是不同步的,也就是不保障线程平安;

综合来说,在须要频繁读取汇合中的元素时,更举荐应用 ArrayList,而在插入和删除操作较多时,更举荐应用 LinkedList。

补充:数据结构根底之双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,别离指向间接后继和间接前驱。所以,从双向链表中的任意一个结点开始,都能够很不便地拜访它的前驱结点和后继结点。

ArrayList 和 Vector 的区别是什么?

这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序汇合

  • 线程平安:Vector 应用了 Synchronized 来实现线程同步,是线程平安的,而 ArrayList 是非线程平安的。
  • 性能:ArrayList 在性能方面要优于 Vector。
  • 扩容:ArrayList 和 Vector 都会依据理论的须要动静的调整容量,只不过在 Vector 扩容每次会减少 1 倍,而 ArrayList 只会减少 50%。

Vector 类的所有办法都是同步的。能够由两个线程平安地拜访一个 Vector 对象、然而一个线程拜访 Vector 的话代码要在同步操作上消耗大量的工夫。

Arraylist 不是同步的,所以在不须要保障线程平安时时倡议应用 Arraylist。

插入数据时,ArrayList、LinkedList、Vector 谁速度较快?论述 ArrayList、Vector、LinkedList 的存储性能和个性?

ArrayList、LinkedList、Vector 底层的实现都是应用数组形式存储数据。数组元素数大于理论存储的数据以便减少和插入元素,它们都容许间接按序号索引元素,然而插入元素要波及数组元素挪动等内存操作,所以索引数据快而插入数据慢。

Vector 中的办法因为加了 synchronized 润饰,因而 Vector 是线程平安容器,但性能上较 ArrayList 差

LinkedList 应用双向链表实现存储,按序号索引数据须要进行前向或后向遍历,但插入数据时只须要记录以后项的前后项即可,所以 LinkedList 插入速度较快

多线程场景下如何应用 ArrayList?

ArrayList 不是线程平安的,如果遇到多线程场景,能够通过 Collections 的 synchronizedList 办法将其转换成线程平安的容器后再应用。例如像上面这样:

`List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {System.out.println(synchronizedList.get(i));
}` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7

为什么 ArrayList 的 elementData 加上 transient 润饰?

ArrayList 中的数组定义如下:

`private transient Object[] elementData;` 

*   1

再看一下 ArrayList 的定义:

`public class ArrayList<E> extends AbstractList<E>
     implements List<E>, RandomAccess, Cloneable, java.io.Serializable` 

*   1
*   2

能够看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 反对序列化。transient 的作用是说不心愿 elementData 数组被序列化,重写了 writeObject 实现:

`private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    *// Write out element count, and any hidden stuff*
        int expectedModCount = modCount;
    s.defaultWriteObject();
    *// Write out array length*
        s.writeInt(elementData.length);
    *// Write out all elements in the proper order.*
        for (int i=0; i<size; i++)
            s.writeObject(elementData[i]);
    if (modCount != expectedModCount) {throw new ConcurrentModificationException();
}` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7
*   8
*   9
*   10
*   11
*   12

每次序列化时,先调用 defaultWriteObject() 办法序列化 ArrayList 中的非 transient 元素,而后遍历 elementData,只序列化已存入的元素,这样既放慢了序列化的速度,又减小了序列化之后的文件大小。

List 和 Set 的区别

List , Set 都是继承自 Collection 接口

List 特点:一个有序(元素存入汇合的程序和取出的程序统一)容器,元素能够反复,能够插入多个 null 元素,元素都有索引。罕用的实现类有 ArrayList、LinkedList 和 Vector。

Set 特点:一个无序(存入和取出程序有可能不统一)容器,不能够存储反复元素,只容许存入一个 null 元素,必须保障元素唯一性。Set 接口罕用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

另外 List 反对 for 循环,也就是通过下标来遍历,也能够用迭代器,然而 set 只能用迭代,因为他无序,无奈用下标来获得想要的值。

Set 和 List 比照

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素地位扭转。
List:和数组相似,List 能够动静增长,查找元素效率高,插入删除元素效率低,因为会引起其余元素地位扭转

Set 接口

说一下 HashSet 的实现原理?

HashSet 是基于 HashMap 实现的,HashSet 的值寄存于 HashMap 的 key 上,HashMap 的 value 对立为 PRESENT,因而 HashSet 的实现比较简单,相干 HashSet 的操作,基本上都是间接调用底层 HashMap 的相干办法来实现,HashSet 不容许反复的值。

HashSet 如何查看反复?HashSet 是如何保证数据不可反复的?

向 HashSet 中 add ()元素时,判断元素是否存在的根据,不仅要比拟 hash 值,同时还要联合 equles 办法比拟。
HashSet 中的 add ()办法会应用 HashMap 的 put()办法。

HashMap 的 key 是惟一的,由源码能够看出 HashSet 增加进去的值就是作为 HashMap 的 key,并且在 HashMap 中如果 K / V 雷同时,会用新的 V 笼罩掉旧的 V,而后返回旧的 V。所以不会反复(HashMap 比拟 key 是否相等是先比拟 hashcode 再比拟 equals)。

以下是 HashSet 局部源码:

`private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {map = new HashMap<>();
}

public boolean add(E e) {
    // 调用 HashMap 的 put 办法,PRESENT 是一个至始至终都雷同的虚值
    return map.put(e, PRESENT)==null;
}` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7
*   8
*   9
*   10
*   11

hashCode()与 equals()的相干规定

  1. 如果两个对象相等,则 hashcode 肯定也是雷同的
  2. 两个对象相等, 对两个 equals 办法返回 true
  3. 两个对象有雷同的 hashcode 值,它们也不肯定是相等的
  4. 综上,equals 办法被笼罩过,则 hashCode 办法也必须被笼罩
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即便这两个对象指向雷同的数据)。

== 与 equals 的区别

  1. == 是判断两个变量或实例是不是指向同一个内存空间 equals 是判断两个变量或实例所指向的内存空间的值是不是雷同
  2. == 是指对内存地址进行比拟 equals()是对字符串的内容进行比拟 3.== 指援用是否雷同 equals()指的是值是否雷同

HashSet 与 HashMap 的区别

HashMap

HashSet

实现了 Map 接口

实现 Set 接口

存储键值对

仅存储对象

调用 put()向 map 中增加元素

调用 add()办法向 Set 中增加元素

HashMap 应用键(Key)计算 Hashcode

HashSet 应用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能雷同,所以 equals()办法用来判断对象的相等性,如果两个对象不同的话,那么返回 false

HashMap 绝对于 HashSet 较快,因为它是应用惟一的键获取对象

HashSet 较 HashMap 来说比较慢

Queue

BlockingQueue 是什么?

Java.util.concurrent.BlockingQueue 是一个队列,在进行检索或移除一个元素的时候,它会期待队列变为非空;当在增加一个元素时,它会期待队列中的可用空间。BlockingQueue 接口是 Java 汇合框架的一部分,次要用于实现生产者 - 消费者模式。咱们不须要放心期待生产者有可用的空间,或消费者有可用的对象,因为它都在 BlockingQueue 的实现类中被解决了。Java 提供了集中 BlockingQueue 的实现,比方 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue 等。

在 Queue 中 poll()和 remove()有什么区别?

  • 相同点:都是返回第一个元素,并在队列中删除返回的对象。
  • 不同点:如果没有元素 poll()会返回 null,而 remove()会间接抛出 NoSuchElementException 异样。

代码示例:

`Queue<String> queue = new LinkedList<String>();
queue. offer("string"); // add
System. out. println(queue. poll());
System. out. println(queue. remove());
System. out. println(queue. size());` 

*   1
*   2
*   3
*   4
*   5

Map 接口

说一下 HashMap 的实现原理?

HashMap 概述:HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作,并容许应用 null 值和 null 键。此类不保障映射的程序,特地是它不保障该程序恒久不变。

HashMap 的数据结构:在 Java 编程语言中,最根本的构造就是两种,一个是数组,另外一个是模仿指针(援用),所有的数据结构都能够用这两个根本构造来结构的,HashMap 也不例外。HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap 基于 Hash 算法实现的

  1. 当咱们往 Hashmap 中 put 元素时,利用 key 的 hashCode 从新 hash 计算出以后对象的元素在数组中的下标
  2. 存储时,如果呈现 hash 值雷同的 key,此时有两种状况。(1)如果 key 雷同,则笼罩原始值;(2)如果 key 不同(呈现抵触),则将以后的 key-value 放入链表中
  3. 获取时,间接找到 hash 值对应的下标,在进一步判断 key 是否雷同,从而找到对应值。
  4. 了解了以上过程就不难明确 HashMap 是如何解决 hash 抵触的问题,外围就是应用了数组的存储形式,而后将抵触的 key 的对象放入链表中,一旦发现抵触就在链表中做进一步的比照。

须要留神 Jdk 1.8 中对 HashMap 的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来进步查问效率,从原来的 O(n)到 O(logn)

HashMap 在 JDK1.7 和 JDK1.8 中有哪些不同?HashMap 的底层实现

在 Java 中,保留数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除艰难;链表的特点是:寻址艰难,但插入和删除容易;所以咱们将数组和链表联合在一起,施展两者各自的劣势,应用一种叫做 拉链法 的形式能够解决哈希抵触。

JDK1.8 之前

JDK1.8 之前采纳的是拉链法。拉链法:将链表和数组相结合。也就是说创立一个链表数组,数组中每一格就是一个链表。若遇到哈希抵触,则将抵触的值加到链表中即可。

JDK1.8 之后

相比于之前的版本,jdk1.8 在解决哈希抵触时有了较大的变动,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以缩小搜寻工夫。

JDK1.7 VS JDK1.8 比拟

JDK1.8 次要解决或优化了一下问题:

  1. resize 扩容优化
  2. 引入了红黑树,目标是防止单条链表过长而影响查问效率,红黑树算法请参考
  3. 解决了多线程死循环问题,但仍是非线程平安的,多线程时可能会造成数据失落问题。

不同

JDK 1.7

JDK 1.8

存储构造

数组 + 链表

数组 + 链表 + 红黑树

初始化形式

独自函数:inflateTable()

间接集成到了扩容函数 resize()

hash 值计算形式

扰动解决 = 9 次扰动 = 4 次位运算 + 5 次异或运算

扰动解决 = 2 次扰动 = 1 次位运算 + 1 次异或运算

存放数据的规定

无抵触时,寄存数组;抵触时,寄存链表

无抵触时,寄存数组;抵触 & 链表长度 < 8:寄存单链表;抵触 & 链表长度 > 8:树化并寄存红黑树

插入数据形式

头插法(先讲原地位的数据移到后 1 位,再插入数据到该地位)

尾插法(直接插入到链表尾部 / 红黑树)

扩容后存储地位的计算形式

全副依照原来办法进行计算(即 hashCode ->> 扰动函数 ->> (h&length-1))

依照扩容后的法则计算(即扩容后的地位 = 原地位 or 原地位 + 旧容量)

HashMap 的 put 办法的具体流程?

当咱们 put 的时候,首先计算 keyhash 值,这里调用了 hash办法,hash办法理论是让 key.hashCode()key.hashCode()>>>16进行异或操作,高 16bit 补 0,一个数和 0 异或不变,所以 hash 函数大略的作用就是:高 16bit 不变,低 16bit 和高 16bit 做了一个异或,目标是缩小碰撞。依照函数正文,因为 bucket 数组大小是 2 的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 解决,相当于散列失效的只有几个低 bit 位,为了缩小散列的碰撞,设计者综合思考了速度、作用、品质之后,应用高 16bit 和低 16bit 异或来简略解决缩小碰撞,而且 JDK8 中用了复杂度 O(logn)的树结构来晋升碰撞下的性能。

putVal 办法执行流程图

`public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 实现 Map.put 和相干办法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 步骤①:tab 为空则创立 
    // table 未初始化或者长度为 0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步骤②:计算 index,并对 null 做解决 
    // (n - 1) & hash 确定元素寄存在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中曾经存在元素
    else {
        Node<K,V> e; K k;
        // 步骤③:节点 key 存在,间接笼罩 value 
        // 比拟桶中第一个元素 (数组中的结点) 的 hash 值相等,key 相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给 e,用 e 来记录
                e = p;
        // 步骤④:判断该链为红黑树 
        // hash 值不相等,即 key 不相等;为红黑树结点
        // 如果以后元素类型为 TreeNode,示意为红黑树,putTreeVal 返回待寄存的 node, e 可能为 null
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 步骤⑤:该链为链表 
        // 为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 达到链表的尾部
                
                // 判断该链表尾部指针是不是空的
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    // 判断链表的长度是否达到转化红黑树的临界值,临界值为 8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 链表构造转树形构造
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的 key 值与插入的元素的 key 值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与后面的 e = p.next 组合,能够遍历链表
                p = e;
            }
        }
        // 判断以后的 key 曾经存在的状况下,再来一个雷同的 hash 值、key 值时,返回新来的 value 这个值
        if (e != null) { 
            // 记录 e 的 value
            V oldValue = e.value;
            // onlyIfAbsent 为 false 或者旧值为 null
            if (!onlyIfAbsent || oldValue == null)
                // 用新值替换旧值
                e.value = value;
            // 拜访后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性批改
    ++modCount;
    // 步骤⑥:超过最大容量就扩容 
    // 理论大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7
*   8
*   9
*   10
*   11
*   12
*   13
*   14
*   15
*   16
*   17
*   18
*   19
*   20
*   21
*   22
*   23
*   24
*   25
*   26
*   27
*   28
*   29
*   30
*   31
*   32
*   33
*   34
*   35
*   36
*   37
*   38
*   39
*   40
*   41
*   42
*   43
*   44
*   45
*   46
*   47
*   48
*   49
*   50
*   51
*   52
*   53
*   54
*   55
*   56
*   57
*   58
*   59
*   60
*   61
*   62
*   63
*   64
*   65
*   66
*   67
*   68
*   69
*   70
*   71
*   72
*   73
*   74
*   75
*   76
*   77
*   78
*   79
*   80
*   81
*   82
*   83
*   84
*   85
*   86
*   87

①. 判断键值对数组 table[i]是否为空或为 null,否则执行 resize()进行扩容;

②. 依据键值 key 计算 hash 值得到插入的数组索引 i,如果 table[i]==null,间接新建节点增加,转向⑥,如果 table[i]不为空,转向③;

③. 判断 table[i]的首个元素是否和 key 一样,如果雷同间接笼罩 value,否则转向④,这里的雷同指的是 hashCode 以及 equals;

④. 判断 table[i] 是否为 treeNode,即 table[i] 是否是红黑树,如果是红黑树,则间接在树中插入键值对,否则转向⑤;

⑤. 遍历 table[i],判断链表长度是否大于 8,大于 8 的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现 key 曾经存在间接笼罩 value 即可;

⑥. 插入胜利后,判断理论存在的键值对数量 size 是否超多了最大容量 threshold,如果超过,进行扩容。

HashMap 的扩容操作是怎么实现的?

①. 在 jdk1.8 中,resize 办法是在 hashmap 中的键值对大于阀值时或者初始化时,就调用 resize 办法进行扩容;

②. 每次扩大的时候,都是扩大 2 倍;

③. 扩大后 Node 对象的地位要么在原地位,要么挪动到原偏移量两倍的地位。

在 putVal()中,咱们看到在这个函数外面应用到了 2 次 resize()办法,resize()办法示意的在进行第一次初始化时会对其进行扩容,或者当该数组的理论大小大于其临界值值 (第一次为 12), 这个时候在扩容的同时也会随同的桶下面的元素进行从新散发,这也是 JDK1.8 版本的一个优化的中央,在 1.7 中,扩容之后须要从新去计算其 Hash 值,依据 Hash 值对其进行散发,但在 1.8 版本中,则是依据在同一个桶的地位中进行判断(e.hash & oldCap) 是否为 0,从新进行 hash 调配后,该元素的地位要么停留在原始地位,要么挪动到原始地位 + 减少的数组大小这个地位上

`final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//oldTab 指向 hash 桶数组
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {// 如果 oldCap 不为空的话,就是 hash 桶数组不为空
        if (oldCap >= MAXIMUM_CAPACITY) {// 如果大于最大容量了,就赋值为整数最大的阀值
            threshold = Integer.MAX_VALUE;
            return oldTab;// 返回
        }// 如果以后 hash 桶数组的长度在扩容后依然小于最大容量 并且 oldCap 大于默认值 16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold 双倍扩容阀值 threshold
    }
    // 旧的容量为 0,但 threshold 大于零,代表有参结构有 cap 传入,threshold 曾经被初始化成最小 2 的 n 次幂
    // 间接将该值赋给新的容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 无参结构创立的 map,给出默认容量和 threshold 16, 16*0.75
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的 threshold = 新的 cap * 0.75
    if (newThr == 0) {float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    // 计算出新的数组长度后赋给以后成员变量 table
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 新建 hash 桶数组
    table = newTab;// 将新数组的值复制给旧的 hash 桶数组
    // 如果原先的数组没有初始化,那么 resize 的初始化工作到此结束,否则进入扩容元素重排逻辑,使其平均的扩散
    if (oldTab != null) {
        // 遍历新数组的所有桶下标
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 旧数组的桶下标赋给长期变量 e,并且解除旧数组中的援用,否则就数组无奈被 GC 回收
                oldTab[j] = null;
                // 如果 e.next==null,代表桶中就一个元素,不存在链表或者红黑树
                if (e.next == null)
                    // 用同样的 hash 映射算法把该元素退出新的数组
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果 e 是 TreeNode 并且 e.next!=null,那么解决树中元素的重排
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // e 是链表的头并且 e.next!=null,那么解决链表中元素重排
                else { // preserve order
                    // loHead,loTail 代表扩容后不必变换下标,见注 1
                    Node<K,V> loHead = null, loTail = null;
                    // hiHead,hiTail 代表扩容后变换下标,见注 1
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历链表
                    do {             
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {if (loTail == null)
                                // 初始化 head 指向链表以后元素 e,e 不肯定是链表的第一个元素,初始化后 loHead
                                // 代表下标放弃不变的链表的头元素
                                loHead = e;
                            else                                
                                // loTail.next 指向以后 e
                                loTail.next = e;
                            // loTail 指向以后的元素 e
                            // 初始化后,loTail 和 loHead 指向雷同的内存,所以当 loTail.next 指向下一个元素时,// 底层数组中的元素的 next 援用也相应发生变化,造成 lowHead.next.next.....
                            // 追随 loTail 同步,使得 lowHead 能够链接到所有属于该链表的元素。loTail = e;                           
                        }
                        else {if (hiTail == null)
                                // 初始化 head 指向链表以后元素 e, 初始化后 hiHead 代表下标更改的链表头元素
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 遍历完结, 将 tail 指向 null,并把链表头放入新数组的相应下标,造成新的映射。if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7
*   8
*   9
*   10
*   11
*   12
*   13
*   14
*   15
*   16
*   17
*   18
*   19
*   20
*   21
*   22
*   23
*   24
*   25
*   26
*   27
*   28
*   29
*   30
*   31
*   32
*   33
*   34
*   35
*   36
*   37
*   38
*   39
*   40
*   41
*   42
*   43
*   44
*   45
*   46
*   47
*   48
*   49
*   50
*   51
*   52
*   53
*   54
*   55
*   56
*   57
*   58
*   59
*   60
*   61
*   62
*   63
*   64
*   65
*   66
*   67
*   68
*   69
*   70
*   71
*   72
*   73
*   74
*   75
*   76
*   77
*   78
*   79
*   80
*   81
*   82
*   83
*   84
*   85
*   86
*   87
*   88
*   89
*   90
*   91
*   92
*   93
*   94
*   95
*   96
*   97

HashMap 是怎么解决哈希抵触的?

答:在解决这个问题之前,咱们首先须要晓得 什么是哈希抵触 ,而在理解哈希抵触之前咱们还要晓得 什么是哈希 才行;

什么是哈希?

Hash,个别翻译为“散列”,也有间接音译为“哈希”的,这就是把任意长度的输出通过散列算法,变换成固定长度的输入,该输入就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输出的空间,不同的输出可能会散列成雷同的输入,所以不可能从散列值来惟一的确定输出值。简略的说就是一种将任意长度的消息压缩到某一固定长度的音讯摘要的函数

所有散列函数都有如下一个根本个性:依据同一散列函数计算出的散列值如果不同,那么输出值必定也不同。然而,依据同一散列函数计算出的散列值如果雷同,输出值不肯定雷同

什么是哈希抵触?

当两个不同的输出值,依据同一散列函数计算出雷同的散列值的景象,咱们就把它叫做碰撞(哈希碰撞)

HashMap 的数据结构

在 Java 中,保留数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除艰难;链表的特点是:寻址艰难,但插入和删除容易 ;所以咱们将数组和链表联合在一起,施展两者各自的劣势,应用一种叫做 链地址法 的形式能够解决哈希抵触:

这样咱们就能够将领有雷同哈希值的对象组织成一个链表放在 hash 值所对应的 bucket 下,但相比于 hashCode 返回的 int 类型,咱们 HashMap 初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即 2 的四次方 16)要远小于 int 类型的范畴,所以咱们如果只是单纯的用 hashCode 取余来获取对应的 bucket 这将会大大增加哈希碰撞的概率,并且最坏状况下还会将 HashMap 变成一个单链表,所以咱们还须要对 hashCode 作肯定的优化

hash()函数

下面提到的问题,次要是因为如果应用 hashCode 取余,那么相当于 参加运算的只有 hashCode 的低位 ,高位是没有起到任何作用的,所以咱们的思路就是让 hashCode 取值出的高位也参加运算,进一步升高 hash 碰撞的概率,使得数据分布更均匀,咱们把这样的操作称为 扰动 ,在JDK 1.8 中的 hash()函数如下:

`static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与本人右移 16 位进行异或运算(高下位异或)}` 

*   1
*   2
*   3
*   4

这比在 JDK 1.7 中,更为简洁,相比在 1.7 中的 4 次位运算,5 次异或运算(9 次扰动),在 1.8 中,只进行了 1 次位运算和 1 次异或运算(2 次扰动)

JDK1.8 新增红黑树

通过下面的 链地址法(应用散列表) 扰动函数 咱们胜利让咱们的数据分布更均匀,哈希碰撞缩小,然而当咱们的 HashMap 中存在大量数据时,退出咱们某个 bucket 下对应的链表有 n 个元素,那么遍历工夫复杂度就为 O(n),为了针对这个问题,JDK1.8 在 HashMap 中新增了红黑树的数据结构,进一步使得遍历复杂度升高至 O(logn);

总结

简略总结一下 HashMap 是应用了哪些办法来无效解决哈希抵触的:

1. 应用链地址法(应用散列表)来链接领有雷同 hash 值的数据;
2. 应用 2 次扰动函数(hash 函数)来升高哈希抵触的概率,使得数据分布更均匀;
3. 引入红黑树进一步升高遍历的工夫复杂度,使得遍历更快;

是否应用任何类作为 Map 的 key?

能够应用任何类作为 Map 的 key,然而在应用之前,须要思考以下几点:

  • 如果类重写了 equals() 办法,也应该重写 hashCode() 办法。
  • 类的所有实例须要遵循与 equals() 和 hashCode() 相干的规定。
  • 如果一个类没有应用 equals(),不应该在 hashCode() 中应用它。
  • 用户自定义 Key 类最佳实际是使之为不可变的,这样 hashCode() 值能够被缓存起来,领有更好的性能。不可变的类也能够确保 hashCode() 和 equals() 在将来不会扭转,这样就会解决与可变相干的问题了。

为什么 HashMap 中 String、Integer 这样的包装类适宜作为 K?

答:String、Integer 等包装类的个性可能保障 Hash 值的不可更改性和计算准确性,可能无效的缩小 Hash 碰撞的几率

  1. 都是 final 类型,即不可变性,保障 key 的不可更改性,不会存在获取 hash 值不同的状况
  2. 外部已重写了 equals()hashCode() 等办法,恪守了 HashMap 外部的标准(不分明能够去下面看看 putValue 的过程),不容易呈现 Hash 值计算错误的状况;

如果应用 Object 作为 HashMap 的 Key,应该怎么办呢?

答:重写 hashCode()equals()办法

  1. 重写 hashCode() 是因为须要计算存储数据的存储地位,须要留神不要试图从散列码计算中排除掉一个对象的要害局部来进步性能,这样尽管能更快但可能会导致更多的 Hash 碰撞;
  2. 重写 equals() 办法 ,须要恪守自反性、对称性、传递性、一致性以及对于任何非 null 的援用值 x,x.equals(null) 必须返回 false 的这几个个性,目标是为了保障 key 在哈希表中的唯一性

HashMap 为什么不间接应用 hashCode()解决后的哈希值间接作为 table 的下标?

答:hashCode()办法返回的是 int 整数类型,其范畴为 -(2 ^ 31)~(2 ^ 31 – 1),约有 40 亿个映射空间,而 HashMap 的容量范畴是在 16(初始化默认值)~2 ^ 30,HashMap 通常状况下是取不到最大值的,并且设施上也难以提供这么多的存储空间,从而导致通过 hashCode() 计算出的哈希值可能不在数组大小范畴内,进而无奈匹配存储地位;

那怎么解决呢?

  1. HashMap 本人实现了本人的 hash() 办法,通过两次扰动使得它本人的哈希值高下位自行进行异或运算,升高哈希碰撞概率也使得数据分布更均匀;
  2. 在保障数组长度为 2 的幂次方的时候,应用 hash() 运算之后的值与运算(&)(数组长度 – 1)来获取数组下标的形式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为 2 的幂次方时,h&(length-1)才等价于 h%length,三来解决了“哈希值与数组大小范畴不匹配”的问题;

HashMap 的长度为什么是 2 的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据调配平均,每个链表 / 红黑树长度大致相同。这个实现就是把数据存到哪个链表 / 红黑树中的算法。

这个算法应该如何设计呢?

咱们首先可能会想到采纳 % 取余的操作来实现。然而,重点来了:“取余 (%) 操作中如果除数是 2 的幂次则等价于与其除数减一的与 (&) 操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。”并且 采纳二进制位操作 &,绝对于 % 可能进步运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

那为什么是两次扰动呢?

答:这样就是加大哈希值低位的随机性,使得散布更平均,从而进步对应数组存储下标地位的随机性 & 平均性,最终缩小 Hash 抵触,两次就够了,曾经达到了高位低位同时参加运算的目标;

HashMap 与 HashTable 有什么区别?

  1. 线程平安:HashMap 是非线程平安的,HashTable 是线程平安的;HashTable 外部的办法根本都通过 synchronized 润饰。(如果你要保障线程平安的话就应用 ConcurrentHashMap 吧!);
  2. 效率:因为线程平安的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 根本被淘汰,不要在代码中应用它;
  3. 对 Null key 和 Null value 的反对:HashMap 中,null 能够作为键,这样的键只有一个,能够有一个或多个键所对应的值为 null。然而在 HashTable 中 put 进的键值只有有一个 null,间接抛 NullPointerException。
  4. 初始容量大小和每次裁减容量大小的不同 :①创立时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次裁减,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次裁减,容量变为原来的 2 倍。②创立时如果给定了容量初始值,那么 Hashtable 会间接应用你给定的大小,而 HashMap 会将其裁减为 2 的幂次方大小。也就是说 HashMap 总是应用 2 的幂作为哈希表的大小,前面会介绍到为什么是 2 的幂次方。
  5. 底层数据结构:JDK1.8 当前的 HashMap 在解决哈希抵触时有了较大的变动,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以缩小搜寻工夫。Hashtable 没有这样的机制。
  6. 举荐应用:在 Hashtable 的类正文能够看到,Hashtable 是保留类不倡议应用,举荐在单线程环境下应用 HashMap 代替,如果须要多线程应用则用 ConcurrentHashMap 代替。

如何决定应用 HashMap 还是 TreeMap?

对于在 Map 中插入、删除和定位元素这类操作,HashMap 是最好的抉择。然而,如果你须要对一个有序的 key 汇合进行遍历,TreeMap 是更好的抉择。基于你的 collection 的大小,兴许向 HashMap 中增加元素会更快,将 map 换为 TreeMap 进行有序 key 的遍历。

HashMap 和 ConcurrentHashMap 的区别

  1. ConcurrentHashMap 对整个桶数组进行了宰割分段(Segment),而后在每一个分段上都用 lock 锁进行爱护,绝对于 HashTable 的 synchronized 锁的粒度更精密了一些,并发性能更好,而 HashMap 没有锁机制,不是线程平安的。(JDK1.8 之后 ConcurrentHashMap 启用了一种全新的形式实现, 利用 CAS 算法。)
  2. HashMap 的键值对容许有 null,然而 ConCurrentHashMap 都不容许。

ConcurrentHashMap 和 Hashtable 的区别?

ConcurrentHashMap 和 Hashtable 的区别次要体现在实现线程平安的形式上不同。

  • 底层数据结构 :JDK1.7 的 ConcurrentHashMap 底层采纳 分段的数组 + 链表 实现,JDK1.8 采纳的数据结构跟 HashMap1.8 的构造一样,数组 + 链表 / 红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构相似都是采纳 数组 + 链表 的模式,数组是 HashMap 的主体,链表则是次要为了解决哈希抵触而存在的;
  • 实现线程平安的形式(重要):① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了宰割分段 (Segment),每一把锁只锁容器其中一部分数据,多线程拜访容器里不同数据段的数据,就不会存在锁竞争,进步并发访问率。(默认调配 16 个 Segment,比 Hashtable 效率进步 16 倍。) 到了 JDK1.8 的时候曾经摒弃了 Segment 的概念,而是间接用 Node 数组 + 链表 + 红黑树的数据结构来实现,并发管制应用 synchronized 和 CAS 来操作。(JDK1.6 当前 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程平安的 HashMap,尽管在 JDK1.8 中还能看到 Segment 的数据结构,然而曾经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) : 应用 synchronized 来保障线程平安,效率十分低下。当一个线程拜访同步办法时,其余线程也拜访同步办法,可能会进入阻塞或轮询状态,如应用 put 增加元素,另一个线程不能应用 put 增加元素,也不能应用 get,竞争会越来越强烈效率越低。

两者的比照图

HashTable:

JDK1.7 的 ConcurrentHashMap:

JDK1.8 的 ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

答:ConcurrentHashMap 联合了 HashMap 和 HashTable 二者的劣势。HashMap 没有思考同步,HashTable 思考了同步的问题。然而 HashTable 在每次同步执行时都要锁住整个构造。ConcurrentHashMap 锁的形式是略微细粒度的。

ConcurrentHashMap 底层具体实现晓得吗?实现原理是什么?

JDK1.7

首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁拜访其中一个段数据时,其余段的数据也能被其余线程拜访。

在 JDK1.7 中,ConcurrentHashMap 采纳 Segment + HashEntry 的形式进行实现,构造如下:

一个 ConcurrentHashMap 里蕴含一个 Segment 数组。Segment 的构造和 HashMap 相似,是一种数组和链表构造,一个 Segment 蕴含一个 HashEntry 数组,每个 HashEntry 是一个链表构造的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行批改时,必须首先取得对应的 Segment 的锁。

  1. 该类蕴含两个动态外部类 HashEntry 和 Segment;前者用来封装映射表的键值对,后者用来充当锁的角色;
  2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个 HashEntry 数组里得元素,当对 HashEntry 数组的数据进行批改时,必须首先取得对应的 Segment 锁。

JDK1.8

JDK1.8 中,放弃了 Segment 臃肿的设计,取而代之的是采纳 Node + CAS + Synchronized 来保障并发平安进行实现,synchronized 只锁定以后链表或红黑二叉树的首节点,这样只有 hash 不抵触,就不会产生并发,效率又晋升 N 倍。

构造如下:

附加源码,有须要的能够看看

插入元素过程(倡议去看看源码):

如果相应地位的 Node 还没有初始化,则调用 CAS 插入相应的数据;

`else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}` 

*   1
*   2
*   3
*   4

如果相应地位的 Node 不为空,且以后该节点不处于挪动状态,则对该节点加 synchronized 锁,如果该节点的 hash 不小于 0,则遍历链表更新节点或插入新节点;

`if (fh >= 0) {
    binCount = 1;
    for (Node<K,V> e = f;; ++binCount) {
        K ek;
        if (e.hash == hash &&
            ((ek = e.key) == key ||
             (ek != null && key.equals(ek)))) {
            oldVal = e.val;
            if (!onlyIfAbsent)
                e.val = value;
            break;
        }
        Node<K,V> pred = e;
        if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key, value, null);
            break;
        }
    }
}` 

*   1
*   2
*   3
*   4
*   5
*   6
*   7
*   8
*   9
*   10
*   11
*   12
*   13
*   14
*   15
*   16
*   17
*   18
*   19

  1. 如果该节点是 TreeBin 类型的节点,阐明是红黑树结构,则通过 putTreeVal 办法往红黑树中插入节点;如果 binCount 不为 0,阐明 put 操作对数据产生了影响,如果以后链表的个数达到 8 个,则通过 treeifyBin 办法转化为红黑树,如果 oldVal 不为空,阐明是一次更新操作,没有对元素个数产生影响,则间接返回旧值;
  2. 如果插入的是一个新节点,则执行 addCount()办法尝试更新元素个数 baseCount;

辅助工具类

Array 和 ArrayList 有何区别?

  • Array 能够存储根本数据类型和对象,ArrayList 只能存储对象。
  • Array 是指定固定大小的,而 ArrayList 大小是主动扩大的。
  • Array 内置办法没有 ArrayList 多,比方 addAll、removeAll、iteration 等办法只有 ArrayList 有。

对于根本类型数据,汇合应用主动装箱来缩小编码工作量。然而,当解决固定大小的根本数据类型的时候,这种形式绝对比较慢。

如何实现 Array 和 List 之间的转换?

  • Array 转 List:Arrays. asList(array);
  • List 转 Array:List 的 toArray() 办法。

comparable 和 comparator 的区别?

  • comparable 接口实际上是出自 java.lang 包,它有一个 compareTo(Object obj)办法用来排序
  • comparator 接口实际上是出自 java.util 包,它有一个 compare(Object obj1, Object obj2)办法用来排序

个别咱们须要对一个汇合应用自定义排序时,咱们就要重写 compareTo 办法或 compare 办法,当咱们须要对某一个汇合实现两种排序形式,比方一个 song 对象中的歌名和歌手名别离采纳一种排序办法的话,咱们能够重写 compareTo 办法和应用自制的 Comparator 办法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表咱们只能应用两个参数版的 Collections.sort().

Collection 和 Collections 有什么区别?

  • java.util.Collection 是一个汇合接口(汇合类的一个顶级接口)。它提供了对汇合对象进行基本操作的通用接口办法。Collection 接口在 Java 类库中有很多具体的实现。Collection 接口的意义是为各种具体的汇合提供了最大化的对立操作形式,其间接继承接口有 List 与 Set。
  • Collections 则是汇合类的一个工具类 / 帮忙类,其中提供了一系列静态方法,用于对汇合中元素进行排序、搜寻以及线程平安等各种操作。

TreeMap 和 TreeSet 在排序时如何比拟元素?Collections 工具类中的 sort()办法如何比拟元素?

TreeSet 要求寄存的对象所属的类必须实现 Comparable 接口,该接口提供了比拟元素的 compareTo()办法,当插入元素时会回调该办法比拟元素的大小。TreeMap 要求寄存的键值对映射的键必须实现 Comparable 接口从而依据键对元素进 行排 序。

Collections 工具类的 sort 办法有两种重载的模式,

第一种要求传入的待排序容器中寄存的对象比拟实现 Comparable 接口以实现元素的比拟;

第二种不强制性的要求容器中的元素必须可比拟,然而要求传入第二个参数,参数是 Comparator 接口的子类型(须要重写 compare 办法实现元素的比拟),相当于一个长期定义的排序规定,其实就是通过接口注入比拟元素大小的算法,也是对回调模式的利用(Java 中对函数式编程的反对)。

退出移动版