一文搞懂所有Java集合面试题
Java集合
刚刚经历过秋招,看了大量的面经,顺便将常见的Java集合常考知识点总结了一下,并根据被问到的频率大致做了一个标注。一颗星表示知识点需要了解,被问到的频率不高,面试时起码能说个差不多。两颗星表示被问到的频率较高或对理解Java有着重要的作用,建议熟练掌握。三颗星表示被问到的频率非常高,建议深入理解并熟练掌握其相关知识,方便面试时拓展(方便装逼),给面试官留下个好印象。
推荐阅读:一文搞懂所有Java基础知识面试题
- Java集合
- 常用的集合类有哪些? ***
- List,Set,Map三者的区别? ***
- 常用集合框架底层数据结构 ***
- 哪些集合类是线程安全的? ***
- 迭代器 Iterator 是什么 *
- Java集合的快速失败机制 “fail-fast”和安全失败机制“fail-safe”是什么? ***
- 如何边遍历边移除 Collection 中的元素? ***
- Array 和 ArrayList 有何区别? ***
- comparable 和 comparator的区别? **
- Collection 和 Collections 有什么区别? **
- List集合
- Set集合
- Map集合
- HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 ***
- HashMap 的长度为什么是2的幂次方 ***
- HashMap的put方法的具体流程? **
- HashMap的扩容操作是怎么实现的? ***
- HashMap默认加载因子为什么选择0.75?
- 为什么要将链表中转红黑树的阈值设为8?为什么不一开始直接使用红黑树?
- HashMap是怎么解决哈希冲突的? ***
- HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标? ***
- 能否使用任何类作为 Map 的 key? ***
- 为什么HashMap中String、Integer这样的包装类适合作为Key? ***
- 如果使用Object作为HashMap的Key,应该怎么办呢? **
- HashMap 多线程导致死循环问题 ***
- ConcurrentHashMap 底层具体实现知道吗? **
- HashTable的底层实现知道吗?
- HashMap、ConcurrentHashMap及Hashtable 的区别 ***
 
- Java集合的常用方法 **
 
常用的集合类有哪些? ***
Map接口和Collection接口是所有集合框架的父接口。下图中的实线和虚线看着有些乱,其中接口与接口之间如果有联系为继承关系,类与类之间如果有联系为继承关系,类与接口之间则是类实现接口。重点掌握的抽象类有HashMap,LinkedList,HashTable,ArrayList,HashSet,Stack,TreeSet,TreeMap。注意:Collection接口不是Map的父接口。
List,Set,Map三者的区别? ***
- List:有序集合(有序指存入的顺序和取出的顺序相同,不是按照元素的某些特性排序),可存储重复元素,可存储多个- null。
- Set:无序集合(元素存入和取出顺序不一定相同),不可存储重复元素,只能存储一个- null。
- Map:使用键值对的方式对元素进行存储,- key是无序的,且是唯一的。- value值不唯一。不同的- key值可以对应相同的- value值。
常用集合框架底层数据结构 ***
- List:- ArrayList:数组
- LinkedList:双线链表
 
- Set:- HashSet:底层基于- HashMap实现,- HashSet存入读取元素的方式和- HashMap中的- Key是一致的。
- TreeSet:红黑树
 
- Map:- HashMap: JDK1.8之前- HashMap由数组+链表组成的, JDK1.8之后有数组+链表/红黑树组成,当链表长度大于8时,链表转化为红黑树,当长度小于6时,从红黑树转化为链表。这样做的目的是能提高- HashMap的性能,因为红黑树的查找元素的时间复杂度远小于链表。
- HashTable:数组+链表
- TreeMap:红黑树
 
哪些集合类是线程安全的? ***
- Vector:相当于有同步机制的- ArrayList
- Stack:栈
- HashTable
- enumeration:枚举
迭代器 Iterator 是什么 *
Iterator 是 Java 迭代器最简单的实现,它不是一个集合,它是一种用于访问集合的方法,Iterator接口提供遍历任何Collection的接口。
Java集合的快速失败机制 “fail-fast”和安全失败机制“fail-safe”是什么? ***
- 快速失败 - Java的快速失败机制是Java集合框架中的一种错误检测机制,当多个线程同时对集合中的内容进行修改时可能就会抛出 - ConcurrentModificationException异常。其实不仅仅是在多线程状态下,在单线程中用增强- for循环中一边遍历集合一边修改集合的元素也会抛出- ConcurrentModificationException异常。看下面代码- public class Main{
 public static void main(String[] args) {
 List<Integer> list = new ArrayList<>();
 for(Integer i : list){
 list.remove(i); //运行时抛出ConcurrentModificationException异常
 }
 }
 }
 - 正确的做法是用迭代器的 - remove()方法,便可正常运行。- public class Main{
 public static void main(String[] args) {
 List<Integer> list = new ArrayList<>();
 Iterator<Integer> it = list.iterator();
 while(it.hasNext()){
 it.remove();
 }
 }
 }
 - 造成这种情况的原因是什么?细心的同学可能已经发现两次调用的 - remove()方法不同,一个带参数据,一个不带参数,这个后面再说,经过查看ArrayList源码,找到了抛出异常的代码- final void checkForComodification() {
 if (modCount != expectedModCount)
 throw new ConcurrentModificationException();
 }
 - 从上面代码中可以看到如果 - modCount和- expectedModCount这两个变量不相等就会抛出- ConcurrentModificationException异常。那这两个变量又是什么呢?继续看源码- protected transient int modCount = 0; //在AbstractList中定义的变量
 - int expectedModCount = modCount;//在ArrayList中的内部类Itr中定义的变量
 - 从上面代码可以看到, - modCount初始值为0,而- expectedModCount初始值等于- modCount。也就是说在遍历的时候直接调用集合的- remove()方法会导致- modCount不等于- expectedModCount进而抛出- ConcurrentModificationException异常,而使用迭代器的- remove()方法则不会出现这种问题。那么只能在看看- remove()方法的源码找找原因了- public E remove(int index) {
 rangeCheck(index); modCount++;
 E oldValue = elementData(index); int numMoved = size - index - 1;
 if (numMoved > 0)
 System.arraycopy(elementData, index+1, elementData, index,
 numMoved);
 elementData[--size] = null; // clear to let GC do its work return oldValue;
 }
 - 从上面代码中可以看到只有 - modCount++了,而- expectedModCount没有操作,当每一次迭代时,迭代器会比较- expectedModCount和- modCount的值是否相等,所以在调用- remove()方法后,- modCount不等于- expectedModCount了,这时就了报- ConcurrentModificationException异常。但用迭代器中- remove()的方法为什么不抛异常呢?原来迭代器调用的- remove()方法和上面的- remove()方法不是同一个!迭代器调用的- remove()方法长这样:- public void remove() {
 if (lastRet < 0)
 throw new IllegalStateException();
 checkForComodification(); try {
 ArrayList.this.remove(lastRet);
 cursor = lastRet;
 lastRet = -1;
 expectedModCount = modCount; //这行代码保证了expectedModCount和modCount是相等的
 } catch (IndexOutOfBoundsException ex) {
 throw new ConcurrentModificationException();
 }
 }
 - 从上面代码可以看到 - expectedModCount = modCount,所以迭代器的- remove()方法保证了- expectedModCount和- modCount是相等的,进而保证了在增强- for循环中修改集合内容不会报- ConcurrentModificationException异常。- 上面介绍的只是单线程的情况,用迭代器调用 - remove()方法即可正常运行,但如果是多线程会怎么样呢?- 答案是在多线程的情况下即使用了迭代器调用 - remove()方法,还是会报- ConcurrentModificationException异常。这又是为什么呢?还是要从- expectedModCount和- modCount这两个变量入手分析,刚刚说了- modCount在- AbstractList类中定义,而- expectedModCount在- ArrayList内部类中定义,所以- modCount是个共享变量而- expectedModCount是属于线程各自的。简单说,线程1更新了- modCount和属于自己的- expectedModCount,而在线程2看来只有- modCount更新了,- expectedModCount并未更新,所以会抛出- ConcurrentModificationException异常。
- 安全失败 - 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会抛出 - ConcurrentModificationException异常。缺点是迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生了修改,迭代器是无法访问到修改后的内容。- java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用。
如何边遍历边移除 Collection 中的元素? ***
从上文“快速失败机制”可知在遍历集合时如果直接调用remove()方法会抛出ConcurrentModificationException异常,所以使用迭代器中调用remove()方法。
Array 和 ArrayList 有何区别? ***
- Array可以包含基本类型和对象类型,- ArrayList只能包含对象类型。
- Array大小是固定的,- ArrayList的大小是动态变化的。(- ArrayList的扩容是个常见面试题)
- 相比于Array,ArrayList有着更多的内置方法,如addAll(),removeAll()。
- 对于基本类型数据,ArrayList使用自动装箱来减少编码工作量;而当处理固定大小的基本数据类型的时候,这种方式相对比较慢,这时候应该使用Array。
comparable 和 comparator的区别? **
- comparable接口出自- java.lang包,可以理解为一个内比较器,因为实现了- Comparable接口的类可以和自己比较,要和其他实现了- Comparable接口类比较,可以使用- compareTo(Object obj)方法。- compareTo方法的返回值是- int,有三种情况:- 返回正整数(比较者大于被比较者)
- 返回0(比较者等于被比较者)
- 返回负整数(比较者小于被比较者)
 
- comparator接口出自- java.util包,它有一个- compare(Object obj1, Object obj2)方法用来排序,返回值同样是- int,有三种情况,和- compareTo类似。
它们之间的区别:很多包装类都实现了comparable接口,像Integer、String等,所以直接调用Collections.sort()直接可以使用。如果对类里面自带的自然排序不满意,而又不能修改其源代码的情况下,使用Comparator就比较合适。此外使用Comparator可以避免添加额外的代码与我们的目标类耦合,同时可以定义多种排序规则,这一点是Comparable接口没法做到的,从灵活性和扩展性讲Comparator更优,故在面对自定义排序的需求时,可以优先考虑使用Comparator接口。
Collection 和 Collections 有什么区别? **
- Collection是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。
- Collections是一个包装类。它包含有各种有关集合操作的静态多态方法,例如常用的- sort()方法。此类不能实例化,就像一个工具类,服务于Java的- Collection框架。
List集合
遍历一个 List 有哪些不同的方式? **
先说一下常见的元素在内存中的存储方式,主要有两种:
- 顺序存储(Random Access):相邻的数据元素在内存中的位置也是相邻的,可以根据元素的位置(如ArrayList中的下表)读取元素。
- 链式存储(Sequential Access):每个数据元素包含它下一个元素的内存地址,在内存中不要求相邻。例如LinkedList。
主要的遍历方式主要有三种:
- for循环遍历:遍历者自己在集合外部维护一个计数器,依次读取每一个位置的元素。
- Iterator遍历:基于顺序存储集合的- Iterator可以直接按位置访问数据。基于链式存储集合的- Iterator,需要保存当前遍历的位置,然后根据当前位置来向前或者向后移动指针。
- foreach遍历:- foreach内部也是采用了- Iterator的方式实现,但使用时不需要显示地声明- Iterator。
那么对于以上三种遍历方式应该如何选取呢?
在Java集合框架中,提供了一个RandomAccess接口,该接口没有方法,只是一个标记。通常用来标记List的实现是否支持RandomAccess。所以在遍历时,可以先判断是否支持RandomAccess(list instanceof RandomAccess),如果支持可用 for 循环遍历,否则建议用Iterator或 foreach 遍历。
ArrayList的扩容机制 ***
先说下结论,一般面试时需要记住,
ArrayList的初始容量为10,扩容时对是旧的容量值加上旧的容量数值进行右移一位(位运算,相当于除以2,位运算的效率更高),所以每次扩容都是旧的容量的1.5倍。
具体的实现大家可查看下ArrayList的源码。
ArrayList 和 LinkedList 的区别是什么? ***
- 是否线程安全:ArrayList和LinkedList都是不保证线程安全的
- 底层实现:ArrayList的底层实现是数组,LinkedList的底层是双向链表。
- 内存占用:ArrayList会存在一定的空间浪费,因为每次扩容都是之前的1.5倍,而LinkedList中的每个元素要存放直接后继和直接前驱以及数据,所以对于每个元素的存储都要比ArrayList花费更多的空间。
- 应用场景:ArrayList的底层数据结构是数组,所以在插入和删除元素时的时间复杂度都会收到位置的影响,平均时间复杂度为o(n),在读取元素的时候可以根据下标直接查找到元素,不受位置的影响,平均时间复杂度为o(1),所以ArrayList更加适用于多读,少增删的场景。LinkedList的底层数据结构是双向链表,所以插入和删除元素不受位置的影响,平均时间复杂度为o(1),如果是在指定位置插入则是o(n),因为在插入之前需要先找到该位置,读取元素的平均时间复杂度为o(n)。所以LinkedList更加适用于多增删,少读写的场景。
ArrayList 和 Vector 的区别是什么? ***
- 相同点 - 都实现了List接口
- 底层数据结构都是数组
 
- 都实现了
- 不同点 - 线程安全:Vector使用了Synchronized来实现线程同步,所以是线程安全的,而ArrayList是线程不安全的。
- 性能:由于Vector使用了Synchronized进行加锁,所以性能不如ArrayList。
- 扩容:ArrayList和Vector都会根据需要动态的调整容量,但是ArrayList每次扩容为旧容量的1.5倍,而Vector每次扩容为旧容量的2倍。
 
- 线程安全:
简述 ArrayList、Vector、LinkedList 的存储性能和特性? ***
- ArrayList底层数据结构为数组,对元素的读取速度快,而增删数据慢,线程不安全。
- LinkedList底层为双向链表,对元素的增删数据快,读取慢,线程不安全。
- Vector的底层数据结构为数组,用- Synchronized来保证线程安全,性能较差,但线程安全。
Set集合
说一下 HashSet 的实现原理 ***
HashSet的底层是HashMap,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT。
HashSet如何检查重复?(HashSet是如何保证数据不可重复的?) ***
这里面涉及到了HasCode()和equals()两个方法。
- equals() - 先看下 - String类中重写的- equals方法。- public boolean equals(Object anObject) {
 if (this == anObject) {
 return true;
 }
 if (anObject instanceof String) {
 String anotherString = (String)anObject;
 int n = value.length;
 if (n == anotherString.value.length) {
 char v1[] = value;
 char v2[] = anotherString.value;
 int i = 0;
 while (n-- != 0) {
 if (v1[i] != v2[i])
 return false;
 i++;
 }
 return true;
 }
 }
 return false;
 }- 从源码中可以看到: - equals方法首先比较的是内存地址,如果内存地址相同,直接返回- true;如果内存地址不同,再比较对象的类型,类型不同直接返回- false;类型相同,再比较值是否相同;值相同返回- true,值不同返回- false。总结一下,- equals会比较内存地址、对象类型、以及值,内存地址相同,- equals一定返回- true;对象类型和值相同,- equals方法一定返回- true。
- 如果没有重写equals方法,那么equals和==的作用相同,比较的是对象的地址值。
 
- hashCode - hashCode方法返回对象的散列码,返回值是- int类型的散列码。散列码的作用是确定该对象在哈希表中的索引位置。- 关于 - hashCode有一些约定:- 两个对象相等,则hashCode一定相同。
- 两个对象有相同的hashCode值,它们不一定相等。
- hashCode()方法默认是对堆上的对象产生独特值,如果没有重写- hashCode()方法,则该类的两个对象的- hashCode值肯定不同
 
- 两个对象相等,则
介绍完equals()方法和hashCode()方法,继续说下HashSet是如何检查重复的。
HashSet的特点是存储元素时无序且唯一,在向HashSet中添加对象时,首相会计算对象的HashCode值来确定对象的存储位置,如果该位置没有其他对象,直接将该对象添加到该位置;如果该存储位置有存储其他对象(新添加的对象和该存储位置的对象的HashCode值相同),调用equals方法判断两个对象是否相同,如果相同,则添加对象失败,如果不相同,则会将该对象重新散列到其他位置。
HashSet与HashMap的区别 ***
| HashMap | HashSet | 
|---|---|
| 实现了 Map接口 | 实现了 Set接口 | 
| 存储键值对 | 存储对象 | 
| key唯一,value不唯一 | 存储对象唯一 | 
| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值 | 
| 速度相对较快 | 速度相对较慢 | 
Map集合
HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 ***
- JDK1.7的底层数据结构(数组+链表)
- JDK1.8的底层数据结构(数组+链表)
- JDK1.7的Hash函数 - static final int hash(int h){
 h ^= (h >>> 20) ^ (h >>>12);
 return h^(h >>> 7) ^ (h >>> 4);
 }
 
- JDK1.8的Hash函数 - static final int hash(Onject key){
 int h;
 return (key == null) ? 0 : (h = key.hashCode())^(h >>> 16);
 }
 - JDK1.8的函数经过了一次异或一次位运算一共两次扰动,而JDK1.7经过了四次位运算五次异或一共九次扰动。这里简单解释下JDK1.8的hash函数,面试经常问这个,两次扰动分别是 - key.hashCode()与- key.hashCode()右移16位进行异或。这样做的目的是,高16位不变,低16位与高16位进行异或操作,进而减少碰撞的发生,高低Bit都参与到Hash的计算。如何不进行扰动处理,因为hash值有32位,直接对数组的长度求余,起作用只是hash值的几个低位。
HashMap在JDK1.7和JDK1.8中有哪些不同点:
| JDK1.7 | JDK1.8 | JDK1.8的优势 | |
|---|---|---|---|
| 底层结构 | 数组+链表 | 数组+链表/红黑树(链表大于8) | 避免单条链表过长而影响查询效率,提高查询效率 | 
| hash值计算方式 | 9次扰动 = 4次位运算 + 5次异或运算 | 2次扰动 = 1次位运算 + 1次异或运算 | 可以均匀地把之前的冲突的节点分散到新的桶(具体细节见下面扩容部分) | 
| 插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) | 解决多线程造成死循环地问题 | 
| 扩容后存储位置的计算方式 | 重新进行hash计算 | 原位置或原位置+旧容量 | 省去了重新计算hash值的时间 | 
HashMap 的长度为什么是2的幂次方 ***
因为HashMap是通过key的hash值来确定存储的位置,但Hash值的范围是-2147483648到2147483647,不可能建立一个这么大的数组来覆盖所有hash值。所以在计算完hash值后会对数组的长度进行取余操作,如果数组的长度是2的幂次方,(length - 1)&hash等同于hash%length,可以用(length - 1)&hash这种位运算来代替%取余的操作进而提高性能。
HashMap的put方法的具体流程? **
HashMap的主要流程可以看下面这个流程图,逻辑非常清晰。

HashMap的扩容操作是怎么实现的? ***
- 初始值为16,负载因子为0.75,阈值为负载因子*容量 
- resize()方法是在- hashmap中的键值对大于阀值时或者初始化时,就调用- resize()方法进行扩容。
- 每次扩容,容量都是之前的两倍 
- 扩容时有个判断 - e.hash & oldCap是否为零,也就是相当于hash值对数组长度的取余操作,若等于0,则位置不变,若等于1,位置变为原位置加旧容量。- 源码如下: - final Node<K,V>[] resize() {
 Node<K,V>[] oldTab = table;
 int oldCap = (oldTab == null) ? 0 : oldTab.length;
 int oldThr = threshold;
 int newCap, newThr = 0;
 if (oldCap > 0) {
 if (oldCap >= MAXIMUM_CAPACITY) { //如果旧容量已经超过最大值,阈值为整数最大值
 threshold = Integer.MAX_VALUE;
 return oldTab;
 }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 oldCap >= DEFAULT_INITIAL_CAPACITY)
 newThr = oldThr << 1; //没有超过最大值就变为原来的2倍
 }
 else if (oldThr > 0)
 newCap = oldThr; else {
 newCap = DEFAULT_INITIAL_CAPACITY;
 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 } if (newThr == 0) {
 float ft = (float)newCap * loadFactor;
 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
 (int)ft : Integer.MAX_VALUE);
 }
 threshold = newThr;
 @SuppressWarnings({"rawtypes","unchecked"})
 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 table = newTab;
 if (oldTab != null) {
 for (int j = 0; j < oldCap; ++j) {
 Node<K,V> e;
 if ((e = oldTab[j]) != null) {
 oldTab[j] = null;
 if (e.next == null)
 newTab[e.hash & (newCap - 1)] = e;
 else if (e instanceof TreeNode)
 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 else {
 Node<K,V> loHead = null, loTail = null;//loHead,loTail 代表扩容后在原位置
 Node<K,V> hiHead = null, hiTail = null;//hiHead,hiTail 代表扩容后在原位置+旧容量
 Node<K,V> next;
 do {
 next = e.next;
 if ((e.hash & oldCap) == 0) { //判断是否为零,为零赋值到loHead,不为零赋值到hiHead
 if (loTail == null)
 loHead = e;
 else
 loTail.next = e;
 loTail = e;
 }
 else {
 if (hiTail == null)
 hiHead = e;
 else
 hiTail.next = e;
 hiTail = e;
 }
 } while ((e = next) != null);
 if (loTail != null) {
 loTail.next = null;
 newTab[j] = loHead; //loHead放在原位置
 }
 if (hiTail != null) {
 hiTail.next = null;
 newTab[j + oldCap] = hiHead; //hiHead放在原位置+旧容量
 }
 }
 }
 }
 }
 return newTab;
 }
HashMap默认加载因子为什么选择0.75?
这个主要是考虑空间利用率和查询成本的一个折中。如果加载因子过高,空间利用率提高,但是会使得哈希冲突的概率增加;如果加载因子过低,会频繁扩容,哈希冲突概率降低,但是会使得空间利用率变低。具体为什么是0.75,不是0.74或0.76,这是一个基于数学分析(泊松分布)和行业规定一起得到的一个结论。
为什么要将链表中转红黑树的阈值设为8?为什么不一开始直接使用红黑树?
可能有很多人会问,既然红黑树性能这么好,为什么不一开始直接使用红黑树,而是先用链表,链表长度大于8时,才转换为红红黑树。
- 因为红黑树的节点所占的空间是普通链表节点的两倍,但查找的时间复杂度低,所以只有当节点特别多时,红黑树的优点才能体现出来。至于为什么是8,是通过数据分析统计出来的一个结果,链表长度到达8的概率是很低的,综合链表和红黑树的性能优缺点考虑将大于8的链表转化为红黑树。
- 链表转化为红黑树除了链表长度大于8,还要HashMap中的数组长度大于64。也就是如果HashMap长度小于64,链表长度大于8是不会转化为红黑树的,而是直接扩容。
HashMap是怎么解决哈希冲突的? ***
哈希冲突:hashMap在存储元素时会先计算key的hash值来确定存储位置,因为key的hash值计算最后有个对数组长度取余的操作,所以即使不同的key也可能计算出相同的hash值,这样就引起了hash冲突。hashMap的底层结构中的链表/红黑树就是用来解决这个问题的。
HashMap中的哈希冲突解决方式可以主要从三方面考虑(以JDK1.8为背景)
- 拉链法 - HasMap中的数据结构为数组+链表/红黑树,当不同的- key计算出的hash值相同时,就用链表的形式将Node结点(冲突的- key及- key对应的- value)挂在数组后面。
- hash函数 - key的hash值经过两次扰动,- key的- hashCode值与- key的- hashCode值的右移16位进行异或,然后对数组的长度取余(实际为了提高性能用的是位运算,但目的和取余一样),这样做可以让- hashCode取值出的高位也参与运算,进一步降低hash冲突的概率,使得数据分布更平均。
- 红黑树 - 在拉链法中,如果hash冲突特别严重,则会导致数组上挂的链表长度过长,性能变差,因此在链表长度大于8时,将链表转化为红黑树,可以提高遍历链表的速度。 
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标? ***
hashCode()处理后的哈希值范围太大,不可能在内存建立这么大的数组。
能否使用任何类作为 Map 的 key? ***
可以,但要注意以下两点:
- 如果类重写了 equals()方法,也应该重写hashCode()方法。
- 最好定义key类是不可变的,这样key对应的hashCode()值可以被缓存起来,性能更好,这也是为什么String特别适合作为HashMap的key。
为什么HashMap中String、Integer这样的包装类适合作为Key? ***
- 这些包装类都是final修饰,是不可变性的, 保证了key的不可更改性,不会出现放入和获取时哈希值不同的情况。
- 它们内部已经重写过hashcode(),equal()等方法。
如果使用Object作为HashMap的Key,应该怎么办呢? **
- 重写hashCode()方法,因为需要计算hash值确定存储位置
- 重写equals()方法,因为需要保证key的唯一性。
HashMap 多线程导致死循环问题 ***
由于JDK1.7的
hashMap遇到hash冲突采用的是头插法,在多线程情况下会存在死循环问题,但JDK1.8已经改成了尾插法,不存在这个问题了。但需要注意的是JDK1.8中的HashMap仍然是不安全的,在多线程情况下使用仍然会出现线程安全问题。基本上面试时说到这里既可以了,具体流程用口述是很难说清的,感兴趣的可以看这篇文章。HASHMAP的死循环
ConcurrentHashMap 底层具体实现知道吗? **
- JDK1.7 - 在JDK1.7中, - ConcurrentHashMap采用- Segment数组 +- HashEntry数组的方式进行实现。- Segment实现了- ReentrantLock,所以- Segment有锁的性质,- HashEntry用于存储键值对。一个- ConcurrentHashMap包含着一个- Segment数组,一个- Segment包含着一个- HashEntry数组,- HashEntry是一个链表结构,如果要获取- HashEntry中的元素,要先获得- Segment的锁。
- JDK1.8 - 在JDK1.8中,不在是 - Segment+- HashEntry的结构了,而是和- HashMap类似的结构,Node数组+链表/红黑树,采用- CAS+- synchronized来保证线程安全。当链表长度大于8,链表转化为红黑树。在JDK1.8中- synchronized只锁链表或红黑树的头节点,是一种相比于- segment更为细粒度的锁,锁的竞争变小,所以效率更高。
总结一下:
- JDK1.7底层是ReentrantLock+Segment+HashEntry,JDK1.8底层是synchronized+CAS+链表/红黑树
- JDK1.7采用的是分段锁,同时锁住几个HashEntry,JDK1.8锁的是Node节点,只要没有发生哈希冲突,就不会产生锁的竞争。所以JDK1.8相比于JDK1.7提供了一种粒度更小的锁,减少了锁的竞争,提高了ConcurrentHashMap的并发能力。
HashTable的底层实现知道吗?
HashTable的底层数据结构是数组+链表,链表主要是为了解决哈希冲突,并且整个数组都是synchronized修饰的,所以HashTable是线程安全的,但锁的粒度太大,锁的竞争非常激烈,效率很低。
HashMap、ConcurrentHashMap及Hashtable 的区别 ***
| HashMap(JDK1.8) | ConcurrentHashMap(JDK1.8) | Hashtable | |
|---|---|---|---|
| 底层实现 | 数组+链表/红黑树 | 数组+链表/红黑树 | 数组+链表 | 
| 线程安全 | 不安全 | 安全( Synchronized修饰Node节点) | 安全( Synchronized修饰整个表) | 
| 效率 | 高 | 较高 | 低 | 
| 扩容 | 初始16,每次扩容成2n | 初始16,每次扩容成2n | 初始11,每次扩容成2n+1 | 
| 是否支持Null key和Null Value | 可以有一个Null key,Null Value多个 | 不支持 | 不支持 | 
Java集合的常用方法 **
这些常用方法是需要背下来的,虽然面试用不上,但是笔试或者面试写算法题时会经常用到。
Collection常用方法
| 方法 | |
|---|---|
| booean add(E e) | 在集合末尾添加元素 | 
| boolean remove(Object o)  | 若本类集中有值与o的值相等的元素,移除该元素并返回true | 
| void clear() | 清除本类中所有元素 | 
| boolean contains(Object o) | 判断集合中是否包含该元素 | 
| boolean isEmpty() | 判断集合是否为空 | 
| int size() | 返回集合中元素的个数 | 
| boolean addAll(Collection c) | 将一个集合中c中的所有元素添加到另一个集合中 | 
| Object[] toArray() | 返回一个包含本集所有元素的数组,数组类型为Object[] | 
| `boolean equals(Object c)`` | 判断元素是否相等 | 
| int hashCode() | 返回元素的hash值 | 
List特有方法
| 方法 | |
|---|---|
| void add(int index,Object obj) | 在指定位置添加元素 | 
| Object remove(int index) | 删除指定元素并返回 | 
| Object set(int index,Object obj) | 把指定索引位置的元素更改为指定值并返回修改前的值 | 
| int indexOf(Object o) | 返回指定元素在集合中第一次出现的索引 | 
| Object get(int index) | 返回指定位置的元素 | 
| List subList(int fromIndex,int toIndex) | 截取集合(左闭右开) | 
LinkedList特有方法
| 方法 | |
|---|---|
| addFirst() | 在头部添加元素 | 
| addLast() | 在尾部添加元素 | 
| removeFirst() | 在头部删除元素 | 
| removeLat() | 在尾部删除元素 | 
| getFirst() | 获取头部元素 | 
| getLast() | 获取尾部元素 | 
Map
| 方法 | |
|---|---|
| void clear() | 清除集合内的元素 | 
| boolean containsKey(Object key) | 查询Map中是否包含指定key,如果包含则返回true | 
| Set entrySet() | 返回Map中所包含的键值对所组成的Set集合,每个集合元素都是Map.Entry的对象 | 
| Object get(Object key) | 返回key指定的value,若Map中不包含key返回null | 
| boolean isEmpty() | 查询Map是否为空,若为空返回true | 
| Set keySet() | 返回Map中所有key所组成的集合 | 
| Object put(Object key,Object value) | 添加一个键值对,如果已有一个相同的key,则新的键值对会覆盖旧的键值对,返回值为覆盖前的value值,否则为null | 
| void putAll(Map m) | 将制定Map中的键值对复制到Map中 | 
| Object remove(Object key) | 删除指定key所对应的键值对,返回所关联的value,如果key不存在返回null | 
| int size() | 返回Map里面的键值对的个数 | 
| Collection values() | 返回Map里所有values所组成的Collection | 
| boolean containsValue ( Object value) | 判断映像中是否存在值 value | 
Stack
| 方法 | |
|---|---|
| boolean empty()  | 测试堆栈是否为空。 | 
| E   peek()  | 查看堆栈顶部的对象,但不从堆栈中移除它。 | 
| E   pop()  | 移除堆栈顶部的对象,并作为此函数的值返回该对象。 | 
| E   push(E item)  | 把项压入堆栈顶部。 | 
| int search(Object o)  | 返回对象在堆栈中的位置,以 1 为基数。 | 
Queue
| 方法 | |
|---|---|
| boolean add(E e) | 将指定元素插入到队列的尾部(队列满了话,会抛出异常) | 
| boolean offer(E e) | 将指定元素插入此队列的尾部(队列满了话,会返回false) | 
| E remove() | 返回取队列头部的元素,并删除该元素(如果队列为空,则抛出异常) | 
| E poll() | 返回队列头部的元素,并删除该元素(如果队列为空,则返回null) | 
| E element() | 返回队列头部的元素,不删除该元素(如果队列为空,则抛出异常) | 
| E peek() | 返回队列头部的元素,不删除该元素(如果队列为空,则返回null) | 
一文搞懂所有Java集合面试题的更多相关文章
- 一文搞懂:java中的VO、PO、BO、DAO、POJO
		针对java工程里的各种带O的对象,进行分析,了解各自的作用. PO:persistent object,持久对象.与数据库里表字段一一对应.PO是一些属性,以及set和get方法组成.一般情况下,一 ... 
- 基础篇|一文搞懂RNN(循环神经网络)
		基础篇|一文搞懂RNN(循环神经网络) https://mp.weixin.qq.com/s/va1gmavl2ZESgnM7biORQg 神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子,只 ... 
- Web端即时通讯基础知识补课:一文搞懂跨域的所有问题!
		本文原作者: Wizey,作者博客:http://wenshixin.gitee.io,即时通讯网收录时有改动,感谢原作者的无私分享. 1.引言 典型的Web端即时通讯技术应用场景,主要有以下两种形式 ... 
- 一文搞懂Flink Window机制
		Windows是处理无线数据流的核心,它将流分割成有限大小的桶(buckets),并在其上执行各种计算. 窗口化的Flink程序的结构通常如下,有分组流(keyed streams)和无分组流(non ... 
- 一文搞懂RAM、ROM、SDRAM、DRAM、DDR、flash等存储介质
		一文搞懂RAM.ROM.SDRAM.DRAM.DDR.flash等存储介质 存储介质基本分类:ROM和RAM RAM:随机访问存储器(Random Access Memory),易失性.是与CPU直接 ... 
- 一文搞懂 Prometheus 的直方图
		原文链接:一文搞懂 Prometheus 的直方图 Prometheus 中提供了四种指标类型(参考:Prometheus 的指标类型),其中直方图(Histogram)和摘要(Summary)是最复 ... 
- 一文搞懂vim复制粘贴
		转载自本人独立博客https://liushiming.cn/2020/01/18/copy-and-paste-in-vim/ 概述 复制粘贴是文本编辑最常用的功能,但是在vim中复制粘贴还是有点麻 ... 
- 三文搞懂学会Docker容器技术(中)
		接着上面一篇:三文搞懂学会Docker容器技术(上) 6,Docker容器 6.1 创建并启动容器 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] --na ... 
- 三文搞懂学会Docker容器技术(下)
		接着上面一篇:三文搞懂学会Docker容器技术(上) 三文搞懂学会Docker容器技术(中) 7,Docker容器目录挂载 7.1 简介 容器目录挂载: 我们可以在创建容器的时候,将宿主机的目录与容器 ... 
随机推荐
- 浅析Java Web框架技术
			一.Java Web框架技术的概念 所谓的Java框架,简单理解是一个可复用的设计构件,它规定了应用的体系结构,阐明了整个设计.协作构件之间的依赖关系.责任分配和控制流程,表现为一组抽象类以及其实例之 ... 
- web应用部署(Tomcat,springboot部署方式)
			转载自:https://www.cnblogs.com/haimishasha/p/10791454.html 核心内容 1.在Tomcat中有四种部署Web应用的方式,分别是: (1)利用Tomca ... 
- B. Petya and Divisors 解析(思維)
			Codeforce 111 B. Petya and Divisors 解析(思維) 今天我們來看看CF111B 題目連結 題目 略,請看原題 前言 看了別人的解答就豁然開朗 @copyright p ... 
- confluence 4.2 升级至 6.10.x 记录
			confluence 4.2 升级至 6.10.x 记录 首先将线上环境中的 confluence 安装目录.数据目录以及数据库进行备份,相关信息如下: 安装目录:/opt/atlassian/con ... 
- 修改Anaconda中Jupyter Notebook默认工作路径
			修改Anaconda中Jupyter Notebook默认工作路径 1.打开 Anaconda Prompt 2.输入命令 jupyter notebook --generate-config 这个命 ... 
- Yum 命令出现[Errno 256] No more mirrors to try错误的解决方式
			今天我在虚拟机上安装 NetCore 的 SDK 的时候,出现错误,执行命令:"yum install dotnet-sdk-3.1",最后安装失败,很多安装包没有找到镜像.解决方 ... 
- Linux的进程、线程、文件描述符是什么
			说到进程,恐怕面试中最常见的问题就是线程和进程的关系了,那么先说一下答案:在 Linux 系统中,进程和线程几乎没有区别. Linux 中的进程就是一个数据结构,看明白就可以理解文件描述符.重定向.管 ... 
- 使用 Iceberg on Kubernetes 打造新一代云原生数据湖
			背景 大数据发展至今,按照 Google 2003年发布的<The Google File System>第一篇论文算起,已走过17个年头.可惜的是 Google 当时并没有开源其技术,& ... 
- .netcore3.1使用log4net/nlog记录日志
			.netcore3.1使用log4net/nlog记录日志 .netcore3.1与2.x之间很是有不少差异的.本来想通过ctrl+c,ctrl+v将在2.2中实现的简单日志记录搬到.netcore3 ... 
- AC86U kx上网
			AC86U收到很久了,为了能够kx上网免不了一番折腾. 准备 U盘一个, 读写速度要大于30M/s, 用于制作虚拟内存 步骤大致如下: 1.下载koolshare固件 我这里下载的是官改固件:http ... 
