前言:笔者参考了JavaGuide、三分恶等博主的八股文,结合Chat老师和自己的理解,整理了一篇关于Java集合的八股文。希望对各位读者有所帮助~~
常见集合有哪些?
Java集合相关类和接口都在java.util
包中,按照其存储结构集合可以分为两大类:单列集合 Collection 和双列集合 Map。Collection派生出了三个子接口:List、Set、Queue,因此Java集合大致也可分成List、Set、Queue、Map四种接口体系。
单列集合Collection
List
:元素有序、可重复,这里所谓的有序意思是:元素的存入顺序和取出顺序一致。例如,存储元素的顺序是 11、22、33,那么我们从 List 中取出这些元素的时候也会按照 11、22、33 这个顺序。List
接口的常用实现类有:
ArrayList
:底层数据结构是数组,线程不安全;LinkedList
:底层数据结构是链表,线程不安全;Set
:元素不可重复,不能通过整数索引来访问,并且元素无序。所谓无序也就是元素的存入顺序和取出顺序不一致。其常用实现类有:
HashSet
:底层基于 HashMap
实现,采用 HashMap
来保存元素;LinkedHashSet
:LinkedHashSet
是 HashSet
的子类,并且其底层是通过 LinkedHashMap
来实现的;Queue
:先进先出的队列。其常用实现类有:PriorityQueue
双列集合Map
Map:元素是成对存在的。每个元素由键(key)与值(value)两部分组成,通过键可以找对所对应的值。显然这个双列集合解决了数组无法存储映射关系的痛点。另外,需要注意的是,Map
不能包含重复的键,值可以重复;并且每个键只能对应一个值。
需要注意的是,这些容器都只能存储对象引用类型,也就是说当我们需要装载的数据是诸如 int
、float
等基本数据类型的时候,必须把它们转换成对应的包装类。
线程安全的集合有哪些?线程不安全的呢?
线程安全的:
Hashtable
:比HashMap多了个线程安全。ConcurrentHashMap
:是一种高效但是线程安全的集合。Vector
:比Arraylist多了个同步化机制。Stack
:栈,也是线程安全的,继承于Vector。线程不安全的:
HashMap
ArrayList
LinkedList
HashSet
TreeSet
TreeMap
ArrayList和LinkedList有什么区别?
讲讲ArrayList的扩容机制?
ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了再插入,就会数组溢出。所以在插入时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容。
扩容发生在啥时候?那肯定是我们往数组中新加入一个元素但是发现数组满了的时候。没错,我们去 add
方法中看看 ArrayList
是怎么做扩容的:
ensureExplicitCapacity
判断是否需要进行扩容,很显然,grow
方法是扩容的关键:
别的都不用看了,看上面图中的黄色框框就知道 ArrayList
是怎么扩容的了:扩容后的数组长度 = 当前数组长度 + 当前数组长度 / 2(也就是【1.5 倍】)。最后使用 Arrays.copyOf
方法直接把原数组中的数组 copy 过来,需要注意的是,Arrays.copyOf
方法会创建一个新数组然后再进行拷贝。
扩容操作需要调用 Arrays.copyOf()(底层 System.arraycopy())需要把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
Arraylist 扩容的数组长度为什么是原数组长度的 1.5 倍?
这种扩容方式能够有效地平衡内存空间的使用和程序性能之间的关系。如果每次扩容时都将数组长度翻倍,可能会导致数组长度过大,浪费内存空间,而如果每次扩容时只增加一个固定的长度,可能会导致频繁扩容,影响程序性能。因此,1.5倍左右的扩容因子是一个比较理想的选择。
ArrayList怎么序列化的知道吗? 为什么用transient修饰数组?
ArrayList的序列化不太一样,它使用 transient 修饰存储元素的 elementData 的数组, transient 关键字的作用是让被修饰的成员属性不被序列化。
为什么最ArrayList不直接序列化元素数组呢?
出于效率的考虑,数组可能长度100,但实际只用了50,剩下的50不用其实不用序列化,这样可以提高序列化和反序列化的效率,还可以节省内存空间。
那ArrayList怎么序列化呢?
ArrayList通过两个方法readObject
、writeObject
自定义序列化和反序列化策略,实际直接使用两个流ObjectOutputStream
和ObjectInputStream
来进行序列化和反序列化。
什么是CopyOnWriteArrayList?
CopyOnWriteArrayList就是线程安全版本的ArrayList。它的名字叫 CopyOnWrite ——写时复制,已经明示了它的原理。
CopyOnWriteArrayList采用了一种读写分离的并发策略。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
ArrayList 与Vector区别?
HashMap 有什么特点?
HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,主要用来存放键值对。
HashMap的底层数据结构是什么?
在JDK1.7 中,由“数组+链表”组成:数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8 中,由“数组+链表+红黑树”组成:当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O ( l o g n ) O(logn) O(logn),而链表是糟糕的 O ( n ) O(n) O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树。链表和红黑树在达到一定条件会进行转换。
其中,桶数组是用来存储数据元素,链表是用来解决冲突,红黑树是为了提高查询的效率。
了解红黑树嘛?
红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:
为什么HashMap不使用二叉树/平衡树/B树、B+树呢?
之所以不用二叉树:
红黑树是一种不严格的平衡二叉树,插入、删除、查找的最坏时间复杂度都为 O(logn),避免了二叉树最坏情况下的O(n)时间复杂度。
之所以不用平衡二叉树:
平衡二叉树是比红黑树更严格的平衡树,是一种高度平衡的二叉树,查询效率高。但是为了维持这种高度的平衡,需要旋转的次数更多,每次插入、删除都要做调整,就比较复杂、耗时,也就是说平衡二叉树保持平衡的效率更低。因此,红黑树的查询性能略微逊色于二叉平衡树, 但是红黑树在插入和删除上优于二叉平衡树。总体来说,红黑树的插入、删除、查找各种操作性能都比较稳定。
之所以不用B树、B+树:
- B+树在数据库中被应用的原因就是B+树比B树更加“矮胖”,B+树的非叶子结点不存储数据,所以每个结点能存储的关键字更多。所以B+树更能应对大量数据的情况。如果用B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面。这个时候遍历效率就退化成了链表;
- B和B+树主要用于数据存储在磁盘上的场景,比如数据库索引就是用B+树实现的。这两种数据结构的特点就是树比较矮胖,每个结点存放一个磁盘大小的数据,这样一次可以把一个磁盘的数据读入内存,减少磁盘转动的耗时,提高效率。而红黑树多用于内存中排序,也就是内部排序。
红黑树怎么保持平衡的知道吗?
HashMap的put流程知道吗?
HashMap怎么查找元素的呢?
HashMap的查找流程:
HashMap的哈希/扰动函数是怎么设计的?
HashMap的哈希函数是先拿到 key 的hashcode,是一个32位的int类型的数值,然后让hashcode的高16位和低16位进行异或操作。
static final int hash(Object key) {
int h;
// key的hashCode和key的hashCode右移16位做异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这么设计是为了降低哈希碰撞的概率。
你还知道哪些哈希函数的构造方法呢?
HashMap里哈希构造函数的方法叫除留取余法 :H(key) = key%p(p<=N)
,关键字除以一个不大于哈希表长度的正整数p,所得余数为地址,当然HashMap里进行了优化改造,效率更高,散列也更均衡。
除此之外,还有这几种常见的哈希函数构造方法:
解决哈希冲突有哪些方法呢?
解决Hash冲突方法有:开放寻址法、再哈希法、链地址法(拉链法)、建立公共溢出区。HashMap中采用的是链地址法 。
p = H(key)
出现冲突时,则以p为基础,再次hash,p1 = H(p)
,直到找到一个不冲突的哈希地址。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。R1 = H1(key1)
发生冲突时,再计算R2 =H2(key1)
,直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。为什么HashMap链表转红黑树的阈值为8呢?
这和统计学有关。理想情况下,使用随机哈希码,链表里的节点符合泊松分布,出现节点个数的概率是递减的,节点个数为8的情况,发生概率仅为0.00000006 。至于红黑树转回链表的阈值为什么是6,而不是8呢?这是因为如果这个阈值也设置成8,那么假如发生碰撞,节点增减刚好在8附近,则会发生链表和红黑树的不断转换,导致资源浪费。
**扩容在什么时候呢 **
为了减少哈希冲突发生的概率,当当前HashMap的元素个数达到一个临界值的时候,就会触发扩容,把所有元素rehash之后再放在扩容后的容器中,这是一个相当耗时的操作。
临界值threshold
就是由加载因子和当前容器的容量大小来确定的,假如采用默认的构造方法:
临界值(threshold )= 默认容量(DEFAULT_INITIAL_CAPACITY) * 默认扩 容因子(DEFAULT_LOAD_FACTOR)
那就是大于 16 × 0.75 = 12 16\times 0.75=12 16×0.75=12时,就会触发扩容操作。
为什么选择了0.75作为HashMap的默认加载因子呢?
简单来说,这是对空间成本和时间成本平衡的考虑。
我们都知道,HashMap的散列构造方式是Hash取余,负载因子决定元素个数达到多少时候扩容。
假如我们设的比较大,元素比较多,空位比较少的时候才扩容,那么发生哈希冲突的概率就增加了,查找的时间成本就增加了。
假设我们设的比较小的话,元素比较少,空位比较多的时候就扩容了,发生哈希碰撞的 概率就降低了,查找时间成本降低,但是就需要更多的空间去存储元素,空间成本就增加了。
总结,主要是在时间成本和空间成本上做的折衷:
具体到底为什么精确到 0.75 这个数值,可以看这篇文章的解释:博客 。
HashMap和Hashtable的区别?
HashMap jdk8与jdk7区别?
HashMap 是线程安全的吗? 多线程下会有什么问题?
HashMap不是线程安全的,可能会发生这些问题:
有什么办法能解决HashMap线程不安全的问题呢?
Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map。
HashMap 内部节点是有序的吗?
HashMap是无序的,根据 hash 值随机插入。如果想使用有序的Map,可以使用 LinkedHashMap 或者 TreeMap。
LinkedHashMap 怎么实现有序的?
LinkedHashMap维护了一个双向链表,有头尾节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。
TreeMap 怎么实现有序的?
TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了Comparator 接口的比较器,传给 TreeMap 用于 key 的比较。
讲讲HashSet的底层实现?
HashSet 底层就是基于 HashMap 实现的。(HashSet的源码非常少,因为除了clone()、writeObject()、readObject()是HashSet自己不得不实现之外,其他方法都是直接调用HashMap中的方法。)
HashSet的add方法,直接调用HashMap的put方法,将添加的元素作为key,new一个 Object作为value,直接调用HashMap的put方法,它会根据返回值是否为空来判断是否插入元素成功。
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
而在HashMap的putVal方法中,进行了一系列判断,最后的结果是,只有在key在table数组中不存在的时候,才会返回插入的值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}