Java面试题-容器
1. 说说常见的集合有哪些?
- Collection
- List
ArrayList
LinkedList
Vector
Stack - Set
HashSet
LinkedHashSet
TreeSet - Map
HashMap
LinkedHashMap
TreeMap
ConcurrentHashMap
Hashtable
2. 哪些集合类可对元素随机访问?
ArrayList、HashMap、TreeMap、Hashtable 类提供对元素的随机访问。
3. Comparable 和 Comparator 接口的区别?
- Comparable 和 Comparator 接口用来对对象集合或者数组进行排序;
- Comparable 接口用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序;
- Comparator 用来提供不同的排序算法,我们可以选择需要使用的 Comparator 来对给定的对象集合进行排序;
4. Collection 和 Collections的区别?
- Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
- Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。
5. Enumeration 和 Iterator 接口的区别?
- Enumeration 只能读取集合的数据,而不能对数据进行修改,Iterator除了能读取集合的数据之外,还能删除集合中的数据;
- 与 Enumeration 相比,Iterator更加安全,因为当一个集合正在遍历的时候,它会阻止其他线程去修改集合(fail-fast机制);
6. 集合使用范型有什么优点?
- 范型规定了一个集合中可以容纳的对象类型,添加其他类型的对象将编译失败;
- 避免了运行时可能出现的 ClassCastException 异常;
- 范型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符;
- 它也给运行时带来了好处,因为不会产生类型检查的字节码指令;
7. List、Set、Map 之间的区别是什么?
8. 为什么 Map 接口不继承 Collection 接口?
尽管 Map 接口和它的实现也是集合框架的一部分,但 Map 不是集合,集合也不是 Map ,因此 Map 继承 Collection 是毫无意义的。
如果 Map 继承 Collection 接口,那么元素去哪儿?Map 包含 key-value 对,它提供抽取 key 或 value 列表集合的方法,但是它不适合“一组对象”规范。
9. 常用的线程安全的 Map 有哪些?
- Hashtable
Hashtable的 get/put 方法都被 synchronized 修饰,说明他们是方法级别阻塞的,他们占用共享资源锁,效率低,不推荐使用; - SynchronizedMap
使用 Collections 工具类创建出来的同步集合,通过对象锁实现,每次调用方法必须先获取对象锁,效率低,不推荐使用; - ConcurrentHashMap
JKD1.7 之前使用分段锁方法,分成 16 个桶,每次只加锁其中一个桶,而在 JDK1.8 中又加入了红黑树和 CAS 算法,同步效率很高,推荐使用;
10. HashMap 与 Hashtable 的区别?
- 存储:HashMap 允许一个 key 和 多个 value 为 null,而 Hashtable 不允许。
- 线程安全:Hashtable 是线程安全的,而 HashMap 是非线程安全的。
- 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
11. HashMap 与 TreeMap 怎么选?
- 对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的 选择,因为相对而言 HashMap 的插入会更快;
- 如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择;
12. HashMap 的数据结构是什么?
- JDK 1.7:数据 + 链表
- JDK 1.8:数据 + 链表 + 红黑树(如果数组的长度大于 64 并且链表的长度大于 8 将转换为红黑树)
13. HashMap 在 JDK 8 中有哪些改变?
- 在 JDK 1.8 中,如果链表长度超过了 8,那么链表将转换为红黑树(桶数组长度必须大于 64,小于 64 只会扩容);
- 发生 hash 碰撞时,JDK 1.7 会在链表的头部插入,而 JDK 1.8 会在链表的尾部插入;
- 在 JDK 1.8 中,Entry 被 Node 替代;
14. HashMap 的 put 方法逻辑?
- 通过hash()函数对key进行hash运算得到当前key的hash值;
- 通过indexFor(hash, table.length)函数获取在table中的实际位置;
- 如果当前位置为空,则创建新的Entry对象并插入实际位置;
- 如果当前位置不为空,则遍历当前链表,如果对应数据已经存在则覆盖如果对应数据不存在则创建新的Entry对象并将当前链表最后一个元素的next指针指向新创建的对象;
源码:
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);//位运算
}
put方法流程给图:
15. HashMap 的 get 方法逻辑?
- 通过hash()函数对key进行hash运算得到当前key的hash值;
- 通过indexFor(hash, table.length)函数获取在table中的实际位置;
- 比较参数key的hash值和当前位置元素的hash值及key的内容是否相等,如果相等则直接返回该元素,否则遍历当前链表。
源码:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {//获取的下标位置不为空
if (first.hash == hash && //总是检查第一个元素是否相等
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {//如果该链表有下一个节点
if (first instanceof TreeNode)//如果是红黑树,则调用getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//遍历当前链表获取元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
16. HashMap 是线程安全的吗?
- 当用在方法内部的局部变量时,局部变量属于当前线程级别的变量,其他线程访问不了,所以不存在线程安全问题;
- 如果是成员变量,则不是线程安全的,两条线程的 put 操作可能发生覆盖(当两个值的 hashCode 相同时);
17. HashMap 是怎么解决 hash 冲突的?
HashMap 采用了一种链表数据结构来解决 hash 冲突的情况,当两个对象的 hashCode 相同时,它们会放到当前数组索引位置的链表中。
18. HashMap 是怎么扩容的?
- table 数组的大小是由 capacity 这个参数确定的,默认是 16,也可以构造传入,最大限制是 1 << 30;
- loadFactor 是装载因子,主要目的是用来确认 table 数组是否需要动态扩展,默认值是 0.75,如 table 数组大小为16,装载因子为 0.75 时, threshould 就是 12,当 table 的实际大小超过 12 时,table 就需要动态扩容;
- 扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold);
- 如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能是致命的;
源码:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
newTable[i] = e;
e = next;
}
}
}
19. 为何HashMap的数组长度一定是2的次幂?
- 获取元素下表位置的方式为:(n - 1) & hash,这个操作如果在 n 为 2 的 N 次幂的情况下是等同于 hash % n 取余数的值;
- 至于为什么要使用与(&)运算呢:因为与运算的效率要高于 hash % n 取余的运算,这也就解释了为什么 HashMap 的数组长度是 2 的 N 次幂;
20. HashMap 是如何实现同步的?
- 使用 Collections.synchronizedMap(...) 来同步 Map;
- 使用 ConcurrentHashMap;
21. Hashtable 为什么不叫 HashTable?
Hashtable 实在 JDK 1.0 的时候创建的,而集合统一规范命名是在后来的 JDK 1.2 开始约定的,为了兼容老版本所以就没有改变;
22. ConcurrentHashMap 的数据结构?
- JDK 1.7 中,采用分段锁机制,实现并发更新操作,底层采用 数组 + 链表 的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。
1)Segment 继承 ReentrantLock(可重入锁)用来充当锁的角色,每个Segment 对象守护每个散列表的若干个桶,Segment 数组默认大小为 16,并且不会扩容。
2)HashEntry 用来封装映射表的键-值对;
3)每个桶是由若干个 HashEntry 对象链接起来的链表; - JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 Node 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。
23. ArrayList 是线程安全的吗?
不是线程安全的,多线程操作时可能存在以下问题:
- 发生 ArrayIndexOutOfBoundsException异常;
- add 时程序正常运行,结果实际存储的数据少于存入的数据;
24. 常用的线程安全的 List 集合有哪些?
- Vector
- SynchronizedList
- CopyOnWriteArrayList
CopyOnWriteArrayList 和 CopyOnWriteArraySet 是在 JDK 1.5 时加入的在 java.util.concurrent 包下。
25. 循环删除 List 集合可能会发生什么异常?
- ArrayIndexOutOfBoundsException:数组下标越界异常。
- ConcurrentModificationException:使用增强 for 循环遍历删除时会报该异常,通过 Iterator 遍历时则不会,因为取下个元素时会判断要修改的数量和期待修改的数量是否一致,不一致会报错,而迭代器的remove方法会同步该值。
26. ArrayList 和 LinkedList 的区别?
-
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
-
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
-
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
27. ArrayList 和 Vector 的区别?
- 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
- 性能:ArrayList 在性能方面要优于 Vector。
- 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
28.如何实现数组和 List 之间的转换?
- 数组转 List:使用 Arrays. asList(array) 进行转换。
- List 转数组:使用 List 自带的 toArray() 方法。
例如:
List<String> list = new ArrayList<>();
list.add("abc");
list.add("bcd");
Object[] objects = list.toArray();
for (Object object : objects) {
System.out.println(object);
}
List<String> list1 = Arrays.asList("abc", "bcd");
System.out.println(list1.toString());
29.Iterator 和 ListIterator 有什么区别?
- Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
- Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
- ListIterator 从 Iterator 接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
30.怎么确保一个集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix