目录
3.3 HashSet、LinkedHashSet和TreeSet的异同
4.8 ConcurrentHashMap和HashTable的区别
4.9 ConcurrentHashMap线程安全的具体实现方式
1 集合概述
1.1 集合和数组的区别
- 数组长度固定,而集合长度可变;
- 数组既可以存储基本数据类型,又可以存储引用数据类型,而集合只能存储引用数据类型;
- 数组存储的元素必须是同一个数据类型,而集合存储的对象可以是不同数据类型;
1.2 Java集合概览
1.3 List、Set和Map的区别
- List:单列集合,存储的元素有序(元素存入和取出的顺序一致),有索引,可重复,可为null;
- Set:单列集合,存储的元素无序(元素存入和取出的顺序可能不一致),无索引,不可重复,只有一个null元素;
- Map:双列集合,使用键值对(key-value)存储元素,key无序,不可重复,value无序,可重复,key与value一一对应。
1.4 集合底层数据结构
List:
- ArrayList/Vecctor:Object[] 数组
- LinkedList:双向链表
Set:
- HashSet:底层采用HashMap来保存元素(无序,唯一)
- LinkedHashSet:底层采用LinkedHashMap(有序)
- TreeSet:红黑树(有序,唯一)
Map:
- HashMap:JDK1.8之前,数组+链表(解决哈希冲突);JDK1.8之后,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间(注意:将链表转换成红黑树前会先判断,若当前数组长度小于64,则进行数组扩容,而不是红黑树转换)。
- LinkedHashMap:在数组+链表/红黑树的基础上,增加了一条双向链表,以保证键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- HashTable:数组+链表
- TreeMap:红黑树
2 List接口
2.1 ArrayList与Vector的区别
- ArrayList线程不安全,线程不同步,效率高,每次扩容的大小是原来的1.5倍;
- Vector线程安全,线程同步,效率低,可以指定扩容的大小,默认是原来的2倍。
2.2 ArrayList和LinkedList的区别
- 线程安全:ArrayList和LinkedList都不同步,都不保证线程安全;
- 底层数据结构:ArrayList底层使用Object数组,LinkedList底层采用双向链表;
- 插入和删除是否受元素位置的影响:ArrayList采用动态数组存储元素,所以插入和删除元素的时间复杂度受元素位置的影响(列表末尾为O(1),指定位置i为O(n-i));LinkedList采用链表存储元素,所以插入和删除元素的时间复杂度不受元素位置的影响,都是近似O(1);
- 是否支持快速随机访问:ArrayList支持快速随机访问,而LinkedList不支持(快速随机访问即通过元素的索引快速获取元素对象);
- 内存空间占用:ArrayList的空间浪费主要体现在列表的结尾会预留一定的容量空间,而 LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
2.3 ArrayList扩容机制
详细内容请参考浅谈 ArrayList 及其扩容机制
//扩容函数grow()
//minCapacity=oldCapacity+1
private Object[] grow(int minCapacity) {
// 获取老容量,也就是当前容量
int oldCapacity = elementData.length;
// 如果当前容量大于0 或者 数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//ArraySupport.newLength()函数的作用是创建一个大小为oldCapacity+max(minimium growth, prferred growth)的数组
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
//创建一个新数组,将旧数组拷贝到新数组,并赋给elementData
return elementData = Arrays.copyOf(elementData, newCapacity);
// 如果 数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(容量等于0的话,只剩这一种情况了)
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
扩容可分为两种情况:
第一种情况,当ArrayList的容量为0时,此时添加元素的话,需要扩容,三种构造方法创建的ArrayList在扩容时略有不同:
1.无参构造,创建ArrayList后容量为0,添加第一个元素后,容量变为10,此后若需要扩容,则正常扩容(对应else情况)。
2.传容量构造,当参数为0时,创建ArrayList后容量为0,添加第一个元素后,容量为1,此时ArrayList是满的,下次添加元素时需正常扩容。
3.传列表构造,当列表为空时,创建ArrayList后容量为0,添加第一个元素后,容量为1,此时ArrayList是满的,下次添加元素时需正常扩容。
第二种情况,当ArrayList的容量大于0,并且ArrayList是满的时,此时添加元素的话,进行正常扩容,每次扩容到原来的1.5倍(oldCapacity+oldCapacity>>1)。
2.4 List的遍历方式
- for循环遍历:基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止;
- Iterator遍历:Iterator是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java在Collections中支持了Iterator模式。
- for each遍历:foreach内部也是采用了Iterator的方式实现,使用时不需要显式声明Iterator或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
3 Set接口
3.1 Comparable和Comparator的区别
package java.lang;
public interface Comparable<T> {
public int compareTo(T o); // 比较此对象与指定对象的顺序,如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
}
———————————————————————————————————————————————————————————————————————————————————————————
packet java.util;
public interface Comparator<T> {
int compare(T o1, T o2);
//还有很多其他方法...
}
Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
3.2 无序性和不可重复性的含义
- 无序性:无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的;
- 不可重复性:指添加的元素按照equals()判断时 ,返回false(需要同时重写equals()方法和HashCode()方法)。
3.3 HashSet、LinkedHashSet和TreeSet的异同
- HashSet:底层采用HashMap,线程不安全,可以存储null值;
- LinkedHashSet:HashSet的子类,能够按照添加元素的顺序遍历;
- TreeSet:底层使用红黑树,能够按照添加元素的顺序遍历,排序方式有自然排序和定制排序。
3.4 HashSet如何检查重复
当把对象加入HashSet时,首先计算对象的hashcode值来判断对象的加入位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相同的hashcode值,则将元素加入到相应位置;若发现有相同hashcode值的对象,则会调用equals()方法来检查哈希值相等的对象是否真的相同。两者相同,则加入操作失败,否则,加入操作成功。
注意:
- 两个对象相等,hashcode值一定相同;
- 两个对象相等,equals()方法返回true;
- 两个对象hashcode值相同,两个对象不一定相等;
- 重写equals()方法,必须重写hashcode()方法。
3.5 Set的遍历方式
只能采用Iterator迭代器遍历和for each遍历。
4 Map接口
4.1 HashMap和HashTable的区别
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
————————————————————————————————————————————————————————————————
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable
- 线程是否安全:HashMap是非线程安全的,HashTable是线程安全的,因为HashTable内部的方法基本都经过synchronized修饰;
- 效率:HashMap比HashTable效率高(HashTable基本被淘汰,不要在代码中使用它);
- 对Null key和Null value的支持:HashMap可以存储null的key和value,但null作为key只能有一个,null作为value可以有多个。HashTable不允许有null的key和null的value,否则会抛出NullPointerException异常;
- 初始容量大小和每次扩充容量大小:1)创建时如果不指定容量初始值,HashTable默认的初始大小为11,之后每次扩充,容量变为原来的 2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的 2 倍。2)创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小(通过tableSizeFor()方法保证)。也就是说HashMap总是使用2的幂作为哈希表的大小;
- 底层数据结构:HashMap在JDK1.8之后增加了链表转红黑树的机制,HashTable则没有。
4.2 HashMap和HashSet的区别
HashMap | HashSet |
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存储对象 |
调用put()向map中添加元素 | 调用add()方法向set中添加元素 |
HashMap使用key计算hashcode | HashSet使用成员对象计算hashcode值,对于两个对象来说,hashcode可能相同,所以还需用equals()方法判断对象的内容 |
4.3 HashMap和TreeMap的区别
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
————————————————————————————————————————————————————————————————————————————————————
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
从类的定义来看,HashMap和TreeMap都继承自AbstractMap,不同的是HashMap实现的是Map接口,而TreeMap实现的是NavigableMap接口。除此之外,TreeMap还实现了SortedMap接口。
- NavigableMap接口:让TreeMap有了对集合内元素的搜索的能力;
- SortedMap接口:让TreeMap有了对集合内的元素根据键排序的能力(默认按key的升序排序)。
4.4 HashMap的底层实现
JDK1.8之前:数组+链表(即,链表散列)。HashMap首先计算key的hashcode值,然后通过扰动函数处理这个值得到hash值,再通过(n-1)&hash判断当前元素的存放位置(n为数组长度),如果当前位置存在元素,判断该元素与要存入的元素的hash值及key是否相同,如果相同,则直接覆盖,不同则通过拉链法解决冲突。
所谓扰动函数指的就是HashMap的hash方法。使用扰动函数是为了防止一些实现比较差的hashCode()方法,也就是说使用扰动函数可以减少碰撞。所谓 “拉链法” 就是指将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后:当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
详细分析请参考最通俗易懂搞定HashMap的底层原理
4.5 HashMap的长度为什么是2的幂次方
哈希值的范围很大(-2147483648 到 2147483647),内存是放不下的,因此不能直接作为数组下标。需要让哈希值对数组的长度做取模运算(hash%length),得到的余数才能是对应的数组下标,也就是要存放的位置。
取余(%)操作中如果除数(length)是2的幂次方,则等价于和其除数减一的数值做与(&)操作,即hash%length==hash&(length-1)。因为二进制操作&,相对于%能提高运算效率,因此,当数组长度(length)是2的幂次方时,可以利用&操作提高计算效率。
4.6 HashMap的遍历方式
补充:Map的遍历方式?
键找值方式,即通过元素中的键,获取键所对应的值
- 获取Map中所有的key,返回一个Set集合存储所有的key(keyset()方法);
- 遍历Set集合,获取每个key;
- 根据key,获取key所对应的value(get(K key)方法)。
Entry将键值对的对应关系封装成了对象,即键值对对象,每一对键值对对象,称为一个Entry项。在创建Map集合的时候,也会在Map集合中创建一个Entry对象,用来记录Key和Value。
键值对方式:即通过集合中每个键值对(Entry)对象,获取键值对(Entry)对象中的键与值
- 获取Map集合中,所有的键值对(Entry)对象,以Set集合形式返回(entrySet()方法);
- 遍历Set集合,得到每一个键值对(Entry)对象;
- 通过键值对(Entry)对象,获取Entry对象中的键与值(getkey()、getValue()方法)
4.7 HashMap多线程操作导致死循环问题
主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
详细分析请参考JAVA HASHMAP的死循环
4.8 ConcurrentHashMap和HashTable的区别
ConcurrentHashMap和HashTable的区别主要体现在实现线程安全的方式上。
- 底层数据结构:JDK1.7的ConcurrentHashMap采用分段的数组+链表实现,JDK1.8则采用数组+链表/红黑树实现。HashTable采用数组+链表实现;
- 实现线程安全的方式:1)JDK1.7,采用分段锁(ConcurrentHashMap)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器的一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。JDK1.8摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。2)HashTable
(
同一把锁)
:使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用 get,竞争会越来越激烈效率越低。
JDK1.7 的 ConcurrentHashMap:
JDK1.8 的 ConcurrentHashMap:
4.9 ConcurrentHashMap线程安全的具体实现方式
JDK1.7:首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap由Segment数组结构和HashEntry数组结构组成。
Segment实现了ReentranLock,
所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。
一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁。
JDK1.8:
ConcurrentHashMap取消了Segment分段锁,采用 CAS 和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。