一、集合概述
1、集合概念,特点
2、List,Set,Map三者的区别
Java容器分为Collection和Map两大类,Collection集合的子接口有Set,List,Queue三种子接口。
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。
3、集合框架底层数据结构
Collection:
(1)List:
ArrayList:Object数组。
Vector:Object数组。
LinkedList:双向链表
(2)Set:
HashSet(无序,唯一):基于HashMap实现的,底层采用HashMap来保存元素。
LinkedHashSet:LinkedHashSet继承与HashSet,并且其内部是通过LinkedHashMap来实现的。
TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)。
Map:
HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是由HashMap的主体,链表则是主要为了解决哈希冲突而存在(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突是有了较大变化,当链表长度阈值大于(默认8时)将链表转化为红黑树,以减少搜索时间。
LinkedHashMap:LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构即有数组+链表+红黑树组成。另外,LinkedHashMap在上面结构的基础上,增加了一个双向链表,使得上面的接口可以保持键值对的插入顺序。
HashTable:数据+链表,数组是HashMap的主题,链表则主要是为了解决哈希长途而存在的。
TreeMap:红黑树(自平衡的排序二叉树)
4、Java集合中的“fail-fast”
它是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量。集合在被遍历期间如果发生变化,就会改变modCount的值。没胆迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否WieexpectedmodCount
是的话遍历;否则抛出异常。
解决方法:遍历过程涉及modCount值的地方加上Synchronized;使用CopyOnWriteArrayList替代ArrayLIst。
5、只读集合:Collections.unmodifiableCollection(Collection c)
二、Collection接口
1、迭代器Iterator
Iterator接口提供遍历任何Collection的接口。可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器允许调用者在迭代过程中移除元素。
Iterator的特点是只能单向遍历,但是更加安全,确保在当前遍历的集合元素被更改的时候,就会抛出ConcurrentModifcationException。
Iterator可以遍历Set和List,而ListIterator只能遍历List。Iterator只能单向遍历,而ListIterator可以双向遍历(向前/后遍历)。ListIterator实现Iterator接口,然后添加了额外功能。
2、遍历List原理,list遍历的最佳实践
(1)for循环遍历:基于计数器,在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素停止。
(2)迭代器遍历:Iterator是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。
(3)foreach循环遍历:foreach内部也是采用Iterator方式实现,使用时不需要显示声明Iterator或计数器,优点是代码简洁,不易出错;缺点是只能做遍历,不能再遍历过程中操作数据集合,例如删除,替换。
最佳实践:Java Collections框架中提供了一个RandomAccess接口,用来表姐List实现是否支持RandomAccess。
(1)如果一个数据集合实现了该接口,就意味着它支持Random Access,按位置读取元素的平均时间复杂度为O(1),如ArraysList。
(2)如果没有该接口,标示不支持Random Access,如LinkedList。
(3)推荐的做法是,支持Random Access的列表可用for循环遍历,否则建议用Iterator或foreach遍历。
3、ArrayList优缺点
优点:底层已数组实现,是一种随机访问模式。ArrayList实现了RandomAccess接口,因此查找非常快。在顺序添加一个元素的时候非常方便。
缺点:删除元素的时候,需要做一次元素复制操作。如果要复制的元素多,就会比较耗性能。插入元素也需要做复制操作。
ArrayList比较适合顺序添加,随机访问的场景。
数组转List:Arrays.asList(array)。List转数组:使用List自带的toArray()方法。
4、ArrayList和LinkedList,ArrayList和Vector区别
ArrayList和LinkedList
(1)数据结构实现:ArrayList是动态数组的数据结构实现,而LinkedList是双向链表的数据结构实现。
(2)随机访问效率:ArrayList比LinkedList在随机访问的时候效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后一次查找。
(3)增加和删除效率:在非首尾的增加和删除操作,LinkedList要比ArrayList更高,因为ArrayList增删操作要影响数组内的其他数据的下标。
(4)内存空间占用:LinkedList比ArrayList更占内存,因为LinkedList的节点处理存储数据,还有两个引用,一个直向前一个元素,一个指向后一个元素。
(5)线程安全:ArrayList和LinkedList都是不同步的,不保证线程安全。
综合来说,在需要频繁读取元素集合时,推荐ArrayList,插入删除操作较多时,更推荐LinkedList。
ArrayList和Vector
(1)线程安全:Vector使用了Synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的。
(2)性能:ArrayList在性能方面优于Vector
(3)扩容:ArrayList和Vector都会根据实际的需要动态的调整容量,只不过Vector扩每次会增加1倍,而ArrayList只会增加50%。
Vector类的所有方法都是同步的。ArrayList不是同步的。
多线程场景下可以通过Collections的synchronizedList方法将其转换成线程安全的容器后在使用,List list=Collections.synchronizedList(mapList);
ArrayList中的数组定义:private transient Object[] elementData;transient的作用是说不希望elementData数组被序列化,重写了writeObject实现。每次序列化时,先调用defaultWriteObject()方法序列化非transient元素,
然后遍历elementData,只序列化已存入的元素,这样既加快了序列化速度,又减少了序列化的文件大小。
三、Set接口
1、HashSet
HashSet底层是基于HashMap实现的,HashSet的值存放在HashMap的key上,HashMap的Value统一为present。基本上都是直接调用底层HashMap的相关方法来完成的。
2、HashSet检查重复
向HashSet中add元素时,判断元素是否存在依据,不仅要比较hash值,同时还要结合equles方法比较
HashSet中的add方法会使用HashMap的put方法。
HashMap的可以是唯一的,由源码可以看出HashSet添加进去的值就是作为HashMap的Key,并且HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap比较Key是否相等是先比较hashCode在比较equels)。
3、hashCode()与equals()的相关规定:
(1)如果两个对象相等,则hashCode一定相同。hashCode是jdk根据对象的地址或者字符安川或者数字算出来的int类型的数值。
(2)两个对象相等,两个equals方法返回true。
(3)两个对象有相同的hashcode值,它们不一定是相等的。
(4)综上,equals方法被覆盖过,则hashCode方法也必须被覆盖。
(5)hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即时这两个对象指向相同的数据)。
==是判断两个变量或者实例是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是不是相同。
==是指对内存地址进行比较,equals是对字符串的内容进行比较。
四、Map
1、什么是Hash算法
哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值就叫做哈希值。
2、链表
链表是可以将物理上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等操作。
链表大致分为单链表和双向链表
(1)单链表:每个节点包含两个部分,一个部分存放数据变量的data,另一部分是指向下一节点的next指针。
(2)双向链表:除了包含单链表的部分,还增加了指向前一个节点的pre指针。
链表的优点:
(1)插入删除速度快(因为有next指针指向下一个节点,通过改变指针的指向可以方便的增加删除元素)。
(2)内存利用率高,不会浪费内存(可以利用内存中细小的不连续空间(大于node节点的大小)并且在需要空间的时候才创建空间)
(3)大小没有固定,扩展灵活。
链表缺点:不能随机查找,必须从第一个开始遍历,查找效率低。
3、HashMap的实现原理
概述:HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证顺序恒久不变。
HashMap的数据结构:java中最基本的两种结构,一个数组,一个模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap基于Hash算法实现的
(1)当往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标。
(2)存储时,如果出现hash值相同的可以,会有两种情况。
a.如果Key相同,则覆盖原始值。
b.如果Key不同(出现冲突),则将当前的key-value放入链表中
(3)获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
(4)HashMap如何解决hash冲突的,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表,一旦发现冲突就在链表中做进一步的对比。
4、HashMap在jdk1.7和jdk1.8区别,底层实现。
数组特点:寻址容易,插入和删除困难;链表:寻址困难,但插入和删除容易;所以HashMap将数组和链表结合在一起,发挥两者各自的优势,使用一种拉链法的方式可以解决哈希冲突。
HashMap JDK1.8之前:之前采用拉链法。拉链法就是将链表和数组结合。也就是说创建一个链表数组,数组中每一格就是链表。若遇到哈希冲突,则冲突的值加到链表中即可。
HashMap JDK1.8之后:相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认8)且数组长度大于阈值(64),将链表转化为红黑树,以减少搜索时间。
JDK1.7VSJDK1.8(前为1.7后为ie。8)
存储方式:jdk1.7:数组+链表;jdk1.8:数组+链表+红黑树。
初始化方式: jdk1.7:单独函数(inflateTable);jdk1.8:直接集成到扩容函数resize()中。
hash值计算方式:jdk1.7:扰动次数=9次扰动=4次位运算+5次异或运算;jdk1.8:扰动处理=2次扰动=1次位运算+1次异或运算。
存放数据规则:jdk1.7:无冲突时,存放数据组,有冲突时,存放链表。jdk1.8:无冲突时存放数组,冲突&&链表长度<8&&数组<64,扩容存放单链表。冲突&链表长度>8&&数据长度>64,树化并且存放黑红树。
插入数据方式:jdk1.7:头插法(先将原位置的数据移到后1为,在插入数据到该位置);jdk1.8:尾插法(直接插入到链表尾部/红黑树)。
扩容后计算存储位置方式:jdk1.7:全部全照原来方法进行计算(即hashcode->扰动函数->(h&length-1));jdk1.8:按照扩容后规律计算(即扩容后位置=原位置or原位置+旧容量)。
5、红黑树
红黑树是一种特殊的二叉查找树。红黑树的每个节点上都有存储为表示节点的颜色,可以是红或者黑。
红黑树的每个节点是黑色或者红色。根节点是黑色。叶子节点(叶子节点代表终结)也是黑色(注意:这里的叶子节点,指为空(NIL或NULL)的叶子节点)
如果一个节点是红色,则它的子节点必须是黑色
每个节点到叶子节点NIL所经过的黑色节点的个数一样。(去报没有一条路径会比其他路径长出两倍,所以红黑树是相对于接近平衡二叉树的)
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。通过旋转和变色,可以让这颗树重新编程红黑树,简单说,旋转变色就是让树保持红黑树的特性。
6、HashMap的put方法具体流程。
JDK1.7(1)定位Segement并确保定位的Segment已初始化(2)调用Segment的put方法。
JDK1.8当put的时候,首先计算key的hash值,这里调用了hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0。一个数和0异或不变,
所以hash函数的大概作用就是:高16bit不变,低16bit和高16bit做一个异或,目的是减少碰撞按照函数注释,因为bucket数组大小是2的幂,
计算下面index=(table.length-1)&hash,如果不做hash处理,如果不做hash处理,相当于散列生效的只有几个低bit位,为了减少散列的碰撞,设计者考虑速度,作用,质量之后,
使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度O(logn)的树结构提升碰撞的性能。
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;
}
}
// 结构性修改v
++modCount;
// 步骤⑥:超过最大容量就扩容
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();①
// 插入后回调
afterNodeInsertion(evict);
return null;
}
(1)判断键值对数组table是否为空或为null,否则执行resize()进行扩容。
(2)根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新节点添加,转向(6),如果table[i]不为空,转向(3)。
(3)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向(4),这里的想听指的hashCode以及equals。
(4)判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向(5)
(5)遍历table[i],判断链表长度是否大于8,大于8把链表转为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现Key已经存在直接覆盖Value即可;
(6)插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过进行扩容。
7、HashMap的get方法具体流程
jdk1.7中由于涉及的共享变量都使用voliate修饰,voliate可以保证内存可见性,所以不会读取过期数据。
jdk1.8中(1)计算key的hash值,并定位table索引。(2)若table索引下元素为普通链表,则按照链表形式迭代遍历。(3)若table索引下元素为红黑树TreeBin节点,则按照红黑树的方式查找。
7、HashMap的扩容操作
(1)在jdk1.8中,resize方法是在hashMap中的键值对大于阈值时或者初始化时,就调用resize方法进行扩容。
(2)每次扩容的时候,都是扩容2倍
(3)扩容后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
扩容的负载因子是0.75
当hashMap中的元素超过(数组大小*loadFactor)时,就会自动进行数组扩容,loadFactor的默认值是0.75,也就是说,默认情况下,数组大小为16,那么当hashMap中元素个数超过16*0.75=12时,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在存储中的位置。
而这是一个非常好性能的操作,所以如果已经知道hashMap中元素个数,那么预设元素的个数能够有效提高hashMap的性能,比如1000个元素new HashMap(1000),但是理论上来讲new HashMap(1024)更合适,不过即使1000,hashMap也会自动将其设置为1024(2^次方>1000)。
但是new HashMap(1024)还不是更合适的,因为0.75*1000<1000,也就是为了让0.75*size>1000,必须new HashMap(2048)才最合适,即考虑了&的问题,也避免了resize的问题。
此处省略源代码
为什么负载因子是0.75
一般来说,默认负载因子0.75在时间成本和空间成本之间提供了很好的权衡。更高的值(1)减少了空间开销,但增加了查找的成本(get和put)。更低的值(0.5)增加了空间成本。
loadFactor太大,比如1,那么就会有很高的哈希冲突的概率,会大大降低查询速度。
loadFactor太小,比如0.5,那么频繁扩容,就会大大浪费空间。
所以这个值介于0.5与1之间,这个值在log(2)的时候比较合理。
另外为了提升扩容效率,HashMap的容量有一个固定要求,就是一定是2的幂次方,所以如果loadFactor是3/4的话,那么和capacity的乘积可以是一个整数。所以一般不建议修改loadFactor值。
8、HashMap如何解决哈希冲突
当两个不同输入的值,根据同一散列函数计算出相同的散列值的现象,叫做碰撞,或哈希碰撞。
链地址法和开放地址法可以解决Hash冲突。
链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
但是相比于hashCode返回的int类型,HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY=1<<4(即2的四次方16)遥远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket(桶,槽位)将大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表。
HashMap的hash()函数获取槽位:
上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是米有起到任何作用的,所以让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使的数据分布根均匀,这种操作称为扰动
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)。
这比JDK1.7中更简洁,相比1.7中的4次位运算,5次异或运算(9次扰动),在1.8中只进行了1次位运算和1次异或运算(2次扰动)。
9、能否使用任何类作为Map的key
可以使用任何类,前提需要考虑:
如果类重写了equals()方法,也应该要重写hashCode()方法。
类的所有实例需要遵循与equals()和hashCode()相同的规则。
如果一个类没有使用equals(),不应该在hashCode()中使用它。
用户自定义Key类最佳实践是使只为不可变的,这样hashcode值可以缓存起来,不可变类也可以确保hashCode()和equals在未来不会改变。
10、HashMap中,String,Integer这样包装类适合作为K;如果用Object做key,需要怎么处理
String,Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效减少Hash碰撞几率。
(1)都是finale类型,即不可变性,保证key的不可更改性,不会存在获取hash不同的情况;
(2)内部已经重写了equals(),hashcode()等方法,遵守了HashMap内部规范。
重写hashCode()和equals()方法
(1)重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然更快,但是会导致更多的Hash碰撞。
(2)重写equals()方法,需要遵守自反性,对称性,传递性,一致性以及对于任何非null的应用值x,x,equals(null)必须返回false的几个特性,目的是为了保证key在哈希表中的唯一性。
11、为什么不直接用hashcode()处理后的哈希值作为下标。
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,
三类解决了“哈希值与数组大小范围不匹配”的问题。
12、HashMap长度为什么是2的幂次方
为了能让hashMap存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
算法如何设计
取余(%)操作中如果除数是2的幂次则等价于与其除数减一的(&)操作(也就是说hash%length==hash&(length-1)的前提是length是2的n次方)。并且采用二进制位操作&,相对于%能够提高运算效率。
为什么是两次扰动
这样就是加大哈希值低位的随机性,使得分布更均匀,从而挺高对应数组存储下标位置的随机性&均匀性,最终减少hash冲突,两次就够了,已经达到了高位低位同事参与与运算的目的。
HashMap的size是key的总数
13、HashMap与HashTable区别
(1)线程安全:HashMap是非线程安全的。HashTable是线程安全的;HashTable内部都经过synchronized修饰。如果要用线程安全可以使用ConcurrentHashMap
(2)效率:因为线程安全的问题,HashMap要比HashTable效率高一点。另外,HashTable基本淘汰,不在代码中使用
(3)对NULL Key和NULL Value的支持:HashMap中,null可以作为键,有且只能有一个,value可以有多个。HashTable中put进的键值只要有一个NULL,直接抛出NullPointException
(4)初始容量大小和每次扩容量大小
创事时如果不指定初始容量,HashTable默认初始大小为11,之后每次扩容,容量变为原来的2n+1。HashMap默认初始化大小为16,之后每次扩容,容量变为原来2倍。
创建时如果给定了初始值,HashTable会直接使用初始值,HashMap会将其扩充为2的幂次方大小。
(5)底层数据结构:JDK1.8中HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认8)时,将链表转为红黑树。
(6)推荐使用HashMap,如果使用线程安全可使用ConcurrentHashMap。
14、TreeMap
TreeMap是一个有序的key-value集合,它是通过红黑树实现。该映射根据其键的自然顺序进行排序,或者根绝创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。
TreeMap是线程非同步
对于在Map中插入,删除和定位元素这类操作,HashMap是最好的选择。然而你需要有序的Key集合进行遍历,TreeMap是更好的选择。
15、HashMap和ConcurrentHashMap区别
(1)ConcurrentHashMap对整个通数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细一些,并发性好。
(JDK1.8之后ConcurrentHashMap启用了全新的方式,利用CAS)。
size是通过baseCount和counterCell进行CAS计算,最终baseCount和遍历CounterCell数组得出size。
(2)HashMap的键值对允许有NULL,但是ConcurrentHashMap不允许。
size是Key的个数
16、ConcurrentHashMap原理
JDK1.7:
(1)首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问
(2)在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插入相应的数据;
else if((f=tabAt(tab,i=(n-1)&hash))==null){
if(casTabAt(tab,i,null,new Node<k,v>(hash,key,value,null)))
break;
}
如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash值不小于0,则遍历链表更新节点或插入新节点。
synchronized (f) {
if (tabAt(tab, i) == f) {
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)如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,
说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值。
(2)如果插入的是一个新节点,则执行addCount()方法尝试更新元素baseCount;
五、工具类
1、Array和ArrayList区别
Array可以存储基本数据类型和对象,ArrayList只能存储对象
Array是指定固定大小的,而ArrayList大小是自动扩展的
Array内置方法没有ArrayList多,比如addAll,removeAll等
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型时,这种方式相对比较慢。
Array转List:Array.asList(array);
List转Array:List的toArray()方法
2、comparable和comparator区别
comparable接口出自java.lang包,它有一个compareTo(Object obj)方法用来排序。
comparator接口出自java.util包,它有一个compare(Object obj1,Object obj2)方法来排序。
一般我们需要对一个集合使用自定义排序时,就需要重写compareTo方法或compare方法,当我们对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手分别采用一种排序方法的话,可以重写compareTo方法和使用自制的Comparetor方法或者
以两个Comparator来实现歌名排序和歌星排序,第二种代表我们只能使用两个参数版的Collections.sort();
3、Collection和Collections区别
java.util。Collection是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java类库中有很多具体实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List和Set。
Coleections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合元素进行排序,搜索以及线程安全等各种操作。
4、TreeMap和TreeSet排序时如何比价元素,Collections工具类中的sort方法如何比较
TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的comapreTo()方法,当插入元素时回调该方法比较元素的大小。Treemap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。
Collections工具类的sort方法有两种重载形式
(1)要求传入的待排序容器中存放的对象必须实现Comparable接口已实现元素比较。
(2)不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,
其实就是是通过接口注入比较元素大小的算法,也是对回调模式的应用。