汇合容器概述
转载自:https://thinkwon.blog.csdn.ne…
什么是汇合
汇合框架:用于存储数据的容器。
汇合框架是为示意和操作汇合而规定的一种对立的规范的体系结构。
任何汇合框架都蕴含三大块内容:对外的接口、接口的实现和对汇合运算的算法。
接口:示意汇合的抽象数据类型。接口容许咱们操作汇合时不用关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来造成标准。
实现:汇合接口的具体实现,是重用性很高的数据结构。
算法:在一个实现了某个汇合框架中的接口的对象身上实现某种有用的计算的办法,例如查找、排序等。这些算法通常是多态的,因为雷同的办法能够在同一个接口被多个类实现时有不同的体现。事实上,算法是可复用的函数。
它缩小了程序设计的辛苦。
汇合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要局部上,而不是为了让程序能失常运行而将注意力于低层设计上。
通过这些在无关API之间的繁难的互用性,使你罢黜了为改编对象或转换代码以便联结这些API而去写大量的代码。 它进步了程序速度和品质。
汇合的特点
汇合的特点次要有如下两点:
- 对象封装数据,对象多了也须要存储。汇合用于存储对象。
- 对象的个数确定能够应用数组,对象的个数不确定的能够用汇合。因为汇合是可变长度的。
汇合和数组的区别
- 数组是固定长度的;汇合可变长度的。
- 数组能够存储根本数据类型,也能够存储援用数据类型;汇合只能存储援用数据类型。
- 数组存储的元素必须是同一个数据类型;汇合存储的对象能够是不同数据类型。
数据结构:就是容器中存储数据的形式。
对于汇合容器,有很多种。因为每一个容器的本身特点不同,其实原理在于每个容器的外部数据结构不同。
汇合容器在一直向上抽取过程中,呈现了汇合体系。在应用一个体系的准则:参阅顶层内容。建设底层对象。
应用汇合框架的益处
- 容量自增长;
- 提供了高性能的数据结构和算法,使编码更轻松,进步了程序速度和品质;
- 容许不同 API 之间的互操作,API之间能够来回传递汇合;
- 能够不便地扩大或改写汇合,进步代码复用性和可操作性。
- 通过应用JDK自带的汇合类,能够升高代码保护和学习新API老本。
罕用的汇合类有哪些?
Map接口和Collection接口是所有汇合框架的父接口:
- Collection接口的子接口包含:Set接口和List接口
- Map接口的实现类次要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
- Set接口的实现类次要有:HashSet、TreeSet、LinkedHashSet等
- 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
- List
- Arraylist: Object数组
- Vector: Object数组
- LinkedList: 双向循环链表
- 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值,是的话就返回遍历;否则抛出异样,终止遍历。
解决办法:
- 在遍历过程中,所有波及到扭转modCount值得中央全副加上synchronized。
- 应用CopyOnWriteArrayList来替换ArrayList
怎么确保一个汇合不能被批改?
能够应用 Collections. unmodifiableCollection(Collection c) 办法来创立一个只读汇合,这样扭转汇合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异样。
示例代码如下:
<code class="Java">`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 应用代码如下:
<code class="Java">`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() 办法,如下:
<code class="java">`Iterator<Integer> it = list.iterator(); while(it.hasNext()){ *// do something* it.remove(); }` * 1 * 2 * 3 * 4 * 5
一种最常见的谬误代码如下:
<code class="java">`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 遍历的最佳实际是什么?
遍历形式有以下几种:
- for 循环遍历,基于计数器。在汇合内部保护一个计数器,而后顺次读取每一个地位的元素,当读取到最初一个元素后进行。
- 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目标是屏蔽不同数据汇合的特点,对立遍历汇合的接口。Java 在 Collections 中反对了 Iterator 模式。
- 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() 办法。
代码示例:
<code class="Java">`// 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 办法将其转换成线程平安的容器后再应用。例如像上面这样:
<code class="java">`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 中的数组定义如下:
<code class="java">`private transient Object[] elementData;` * 1
再看一下 ArrayList 的定义:
<code class="java">`public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable` * 1 * 2
能够看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 反对序列化。transient 的作用是说不心愿 elementData 数组被序列化,重写了 writeObject 实现:
<code class="java">`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 局部源码:
<code class="java">`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()的相干规定:
- 如果两个对象相等,则hashcode肯定也是雷同的
- 两个对象相等,对两个equals办法返回true
- 两个对象有雷同的hashcode值,它们也不肯定是相等的
- 综上,equals办法被笼罩过,则hashCode办法也必须被笼罩
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即便这两个对象指向雷同的数据)。
==与equals的区别
- ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是雷同
- ==是指对内存地址进行比拟 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 异样。
代码示例:
<code class="Java">`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 算法实现的
- 当咱们往Hashmap中put元素时,利用key的hashCode从新hash计算出以后对象的元素在数组中的下标
- 存储时,如果呈现hash值雷同的key,此时有两种状况。(1)如果key雷同,则笼罩原始值;(2)如果key不同(呈现抵触),则将以后的key-value放入链表中
- 获取时,间接找到hash值对应的下标,在进一步判断key是否雷同,从而找到对应值。
- 了解了以上过程就不难明确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次要解决或优化了一下问题:
- resize 扩容优化
- 引入了红黑树,目标是防止单条链表过长而影响查问效率,红黑树算法请参考
- 解决了多线程死循环问题,但仍是非线程平安的,多线程时可能会造成数据失落问题。
不同
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的时候,首先计算 key
的hash
值,这里调用了 hash
办法,h来源gao.dai.ma.com搞@代*码网ash
办法理论是让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办法执行流程图
<code class="java">`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调配后,该元素的地位要么停留在原始地位,要么挪动到原始地位+减少的数组大小这个地位上
<code class="java">`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()函数如下:
<code class="java">`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碰撞的几率
- 都是final类型,即不可变性,保障key的不可更改性,不会存在获取hash值不同的状况
- 外部已重写了
equals()
、hashCode()
等办法,恪守了HashMap外部的标准(不分明能够去下面看看putValue的过程),不容易呈现Hash值计算错误的状况;
如果应用Object作为HashMap的Key,应该怎么办呢?
答:重写hashCode()
和equals()
办法
- 重写
hashCode()
是因为须要计算存储数据的存储地位,须要留神不要试图从散列码计算中排除掉一个对象的要害局部来进步性能,这样尽管能更快但可能会导致更多的Hash碰撞; - 重写
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()
计算出的哈希值可能不在数组大小范畴内,进而无奈匹配存储地位;
那怎么解决呢?
- HashMap本人实现了本人的
hash()
办法,通过两次扰动使得它本人的哈希值高下位自行进行异或运算,升高哈希碰撞概率也使得数据分布更均匀; - 在保障数组长度为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 有什么区别?
- 线程平安: HashMap 是非线程平安的,HashTable 是线程平安的;HashTable 外部的办法根本都通过
synchronized
润饰。(如果你要保障线程平安的话就应用 ConcurrentHashMap 吧!); - 效率: 因为线程平安的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 根本被淘汰,不要在代码中应用它;
- 对Null key 和Null value的反对: HashMap 中,null 能够作为键,这样的键只有一个,能够有一个或多个键所对应的值为 null。然而在 HashTable 中 put 进的键值只有有一个 null,间接抛NullPointerException。
- 初始容量大小和每次裁减容量大小的不同 : ①创立时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次裁减,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次裁减,容量变为原来的2倍。②创立时如果给定了容量初始值,那么 Hashtable 会间接应用你给定的大小,而 HashMap 会将其裁减为2的幂次方大小。也就是说 HashMap 总是应用2的幂作为哈希表的大小,前面会介绍到为什么是2的幂次方。
- 底层数据结构: JDK1.8 当前的 HashMap 在解决哈希抵触时有了较大的变动,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以缩小搜寻工夫。Hashtable 没有这样的机制。
- 举荐应用:在 Hashtable 的类正文能够看到,Hashtable 是保留类不倡议应用,举荐在单线程环境下应用 HashMap 代替,如果须要多线程应用则用 ConcurrentHashMap 代替。
如何决定应用 HashMap 还是 TreeMap?
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的抉择。然而,如果你须要对一个有序的key汇合进行遍历,TreeMap是更好的抉择。基于你的collection的大小,兴许向HashMap中增加元素会更快,将map换为TreeMap进行有序key的遍历。
HashMap 和 ConcurrentHashMap 的区别
- ConcurrentHashMap对整个桶数组进行了宰割分段(Segment),而后在每一个分段上都用lock锁进行爱护,绝对于HashTable的synchronized锁的粒度更精密了一些,并发性能更好,而HashMap没有锁机制,不是线程平安的。(JDK1.8之后ConcurrentHashMap启用了一种全新的形式实现,利用CAS算法。)
- 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的锁。
- 该类蕴含两个动态外部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
- Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行批改时,必须首先取得对应的 Segment 锁。
JDK1.8
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采纳Node + CAS + Synchronized来保障并发平安进行实现,synchronized只锁定以后链表或红黑二叉树的首节点,这样只有hash不抵触,就不会产生并发,效率又晋升N倍。
构造如下:
附加源码,有须要的能够看看
插入元素过程(倡议去看看源码):
如果相应地位的Node还没有初始化,则调用CAS插入相应的数据;
<code class="java">`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,则遍历链表更新节点或插入新节点;
<code class="java">`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
- 如果该节点是TreeBin类型的节点,阐明是红黑树结构,则通过putTreeVal办法往红黑树中插入节点;如果binCount不为0,阐明put操作对数据产生了影响,如果以后链表的个数达到8个,则通过treeifyBin办法转化为红黑树,如果oldVal不为空,阐明是一次更新操作,没有对元素个数产生影响,则间接返回旧值;
- 如果插入的是一个新节点,则执行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 中对函数式编程的反对)。