面试🤺🤺🤺<持续更新
>
- 🗡️JAVA
- ⚔️ Java基础篇
- ⚔️ I/O、泛型、反射、异常篇
- ⚔️ Java容器篇
- ⚔️ JUC
- ⚔️ JVM
- ⚔️ 新特性
- ⚔️ 补充点
⚔️ Java容器篇
- 前言
- 一、🕐 Collection
- 二、🕐 Map
- 📦 [HashMap put、get方法详解【https://blog.csdn.net/qq_44833552/article/details/124006289】](https://blog.csdn.net/qq_44833552/article/details/124006289)
- 📦 HashMap与ConcurrentHashMap 区别?
- 📦 HashMap与HashTable区别?
- 📦 HashMap与TreeMap区别?
- 📦 HashMap与HashSet区别?
- 📦 [JAVA HASHMAP的死循环【https://coolshell.cn/articles/9606.html】](https://coolshell.cn/articles/9606.html)
- 📦 HashMap 有哪几种常见的遍历方式?
- 📦 ConcurrentHashMap与HashTable区别?
- 📦 JDK 1.7与JDK 1.8 HashMap区别?
- 📦 [说一说红黑树【https://zhuanlan.zhihu.com/p/273829162】](https://zhuanlan.zhihu.com/p/273829162)
- 📦 说一说“fail-fast”
- 总结</font>
前言
通过本文,你将了解如下内容:
- 🕐 Collection
- 🕑 List、Set、Queue
- 🕒 Map
穿插在文章内的补充知识点(希望也能看看,说不定有不一样的收获❤️❤️❤️)`
提示:以下是本篇文章正文内容
一、🕐 Collection
List
📦 ArrayList与LinkedList区别?
- 线程安全:都不是同步的,都是线程不安全的
- 数据结构:ArrayList底层是Object数组;LinkedList底层是双向链表(JDK 1.7以后,JDK 1.6以前1是双向循环链表)
- 查询、插入、删除、增加性能:
– 查询:数组结构的查询速度快于链表,在随机访问上ArrayList支持,LinkedList不支持
– 插入、删除、增加:ArrayList在末尾操作的时间复杂度为O(1),在中间操作是O(n - i),i为操作位置,数组向前(后)移动n-i个元素;LinkedList在两端操作的时间复杂度为O(1),在中间操作是O(n)- 内存占用:ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)
📦 数组为什么比链表查询效率高?
图片来源【https://blog.csdn.net/jizhu4873/article/details/84341884】
通过上图可以看到CPU缓存的执行速度是远快于内存和磁盘的。
在CPU需要数据的时候,遵循一级缓存〉二级缓存〉内存的顺序,从而尽量提高读取速度。
- CPU缓存会把一片连续的内存空间读入, 因为数组结构是连续的内存地址,所以数组全部或者部分元素被连续存在CPU缓存里面;链表的节点是分散在堆空间里面的,地址跨度可能很大,这时候CPU只能是去读取内存
- 根据上图访问缓存和内存的时间比,可以大概知道,数组的查询是快于链表的
📦 双端链表和双向链表与双向循环链表
- 双端链表与单链表十分相似,不同的是它新增一个对尾结点的引用。双端链表不是双向链表
- 每个结点除了保存了对下一个结点的引用,同时还保存着对前一个结点的引用
- 在双向链表基础上, 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环
📦 RandomAccess接口作用?
RandomAccess接口中什么都没有定义。 RandomAccess 接口用于标识实现这个接口的类具有随机访问功能。
Collections类里面二分查找方法中根据对象是否实现了RandomAccess而采用不同的方法
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
📦 ArrayList扩容机制
Set
📦 无序性和不可重复性的含义是什么?
1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法
📦 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同?
相同点:
- 顶层接口:都实现了Set接口
- 唯一性:都保证数据唯一
- 线程安全:都不是线程安全的
不同点:
- 数据结构:HashSet底层结构是hash表(基于HashMap);LinkedHashSet底层结构是链表+hash表;TreeSet底层结构是红黑树
- 应用场景:HashSet 用于不需要保证元素插入和取出顺序的场景;LinkedHashSet 用于保证元素的插入和取出顺序满足 先进先出(FIFO) 的场景;TreeSet 用于支持对元素自定义排序规则的场景
📦 HashSet 如何检查重复?
HashSet先计算对象的HashCode,如果相同,会调用equals比较对象内容,如果相同,则不会
Queue
📦 Queue 与 Deque 的区别?
Queue :
- Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
- Queue 扩展了 Collection 的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。
Deque :
- Deque 是双端队列,在队列的两端均可以插入或删除元素。
- Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法
📦 ArrayDeque 与 LinkedList 的区别?
ArrayDeque 和 LinkedList 都实现了 Deque 接口,两者都具有队列的功能,两者主要区别:
- 数据结构:ArrayDeque 是基于可变长的数组和双指针来实现,而 LinkedList 则通过链表来实现
- 插入数据性能:ArrayDeque 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢
- NULL值:ArrayDeque 不能存入NULL,而LinkedList 可以存入NULL
- JDK引入时间:ArrayDeque 是在 JDK1.6 才被引入的,而LinkedList 早在 JDK1.2 时就已经存在
📦 说一说 PriorityQueue
- 优先级队列,默认是小根堆,底层数据结构是二叉堆,底层采用数组
- 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素
- 不支持存储 NULL 和 non-comparable 的对象
- 通过Comparator 作为构造参数,自定义元素优先级的先后
📦 说一说 二叉堆
二叉堆定义:堆是完全二叉树,底层数据结构是数组。父结点的键值总是大于等于(小于等于)任何一个子节点的键值。
大根堆:父结点的键值总是大于等于任何一个子节点的键值
小根堆:父结点的键值总是小于等于任何一个子节点的键值每次增加数据都会保证父节点为最大值(最小值),下面以大根堆为例:
保证堆顶是最大值:
// 父节点:index >> 1
public void heapInsert(int[] arr, int index) {
int indexNext;
// (x + (1 << k) -1) >> k ===> [(index - 1) + (1 << 1) - 1] >> 1 = index >> 1
// 值大于父节点值,进入循环,交换值
while (arr[index] > arr[(indexNext = ((index - 1) < 0 ? index : (index - 1))) >> 1]) {
// 交换位置
sortUtil.arrayToSwap1(arr, index, indexNext >> 1);
index = indexNext >> 1;
}
}
二、🕐 Map
HashMap、HashTable、TreeMap、ConcurrentHashMap
📦 HashMap put、get方法详解【https://blog.csdn.net/qq_44833552/article/details/124006289】
📦 HashMap与ConcurrentHashMap 区别?
- ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
- HashMap的键值对允许有null,但是ConCurrentHashMap都不允许
📦 HashMap与HashTable区别?
- 线程安全:HashMap线程不安全,HashTable方法基本都经过synchronized 修饰,线程安全
- 性能:因为synchronized的原因,HashMap性能比HashTable高一点
- 数据结构:HashMap数据结构是数组+链表+红黑树(JDK 8后,JDK 7是数组+链表);HashTable数据结构是数组+链表
- 对NULL key和NULL value支持:HashMap支持存在一个NULL key,多个NULL value;HashTable不支持,会抛出 NullPointerException
- 初始容量大小和每次扩充容量大小:
📦 HashMap与TreeMap区别?
HashMap | HashSet |
---|---|
实现了 Map 接口 | 实现 Set 接口 |
存储键值对 | 仅存储对象 |
调用 put()向 map 中添加元素 | 调用 add()方法向 Set 中添加元素 |
HashMap 使用键(Key)计算 hashcode | HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性 |
📦 HashMap与HashSet区别?
TreeMap 和HashMap 都继承自AbstractMap
TreeMap实现了NavigableMap接口,获得了对集合内元素的搜索的能力
TreeMap实现了SortedMap 接口,获得了根据对象属性自定义排序
📦 JAVA HASHMAP的死循环【https://coolshell.cn/articles/9606.html】
📦 HashMap 有哪几种常见的遍历方式?
- 迭代器(Iterator)方式遍历:
– 使用迭代器(Iterator)EntrySet 的方式进行遍历:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(8);
map.put(1, "d1");
map.put(2, "d2");
map.put(3, "d3");
map.put(4, "d4");
map.put(5, "d5");
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
}
}
– 使用迭代器(Iterator)KeySet 的方式进行遍历:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(8);
map.put(1, "d1");
map.put(2, "d2");
map.put(3, "d3");
map.put(4, "d4");
map.put(5, "d5");
Iterator<Integer> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
Integer key = iterator.next();
System.out.println("key:" + key + ", value:" + map.get(key));
}
}
- For Each 方式遍历:
– 使用 For Each EntrySet 的方式进行遍历:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(8);
map.put(1, "d1");
map.put(2, "d2");
map.put(3, "d3");
map.put(4, "d4");
map.put(5, "d5");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
}
}
– 使用 For Each KeySet 的方式进行遍历:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(8);
map.put(1, "d1");
map.put(2, "d2");
map.put(3, "d3");
map.put(4, "d4");
map.put(5, "d5");
for (Integer key : map.keySet()) {
System.out.println("key:" + key + ", value:" + map.get(key));
}
}
- Lambda 表达式遍历(JDK 1.8+):
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(8);
map.put(1, "d1");
map.put(2, "d2");
map.put(3, "d3");
map.put(4, "d4");
map.put(5, "d5");
map.forEach((key, value) -> {
System.out.println("key:" + key + ", value:" + value);
});
}
- Streams API 遍历(JDK 1.8+):
– 使用 Streams API 单线程的方式进行遍历:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(8);
map.put(1, "d1");
map.put(2, "d2");
map.put(3, "d3");
map.put(4, "d4");
map.put(5, "d5");
map.entrySet().stream().forEach((entry) -> {
System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
});
}
– 使用 Streams API 多线程的方式进行遍历:
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(8);
map.put(1, "d1");
map.put(2, "d2");
map.put(3, "d3");
map.put(4, "d4");
map.put(5, "d5");
map.entrySet().parallelStream().forEach((entry) -> {
System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
});
}
📦 ConcurrentHashMap与HashTable区别?
- 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的HashMap 的底层数据结构类似都是采用 数组+链表 的形式
- 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),Segment 数组 + HashEntry 数组 + 链表,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;②Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低
HashTable全表锁
concurrenthashmap JDK 7
concurrenthashmap JDK 8(TreeBin: 红黑二叉树节点 Node: 链表节点)
📦 JDK 1.7与JDK 1.8 HashMap区别?
📦 说一说红黑树【https://zhuanlan.zhihu.com/p/273829162】
📦 说一说“fail-fast”
而fail-fast 机制是Java集合(Collection)中的一种错误机制。当多个线程(单线程也可能)对同一个集合的内容进行操作时,就可能会产生fail-fast(快速失败)事件
- 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
- 解决办法:
- 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
- 使用CopyOnWriteArrayList来替换ArrayList
总结
以上就是今天要记录的内容,本文仅仅对Java集合知识点进行初步总结。
资料来源:
- guide哥.【JavaGuide】. 面试指北. JAVA,.
- 陈皓.【酷 壳 – COOLSHELL】. 疫苗:JAVA HASHMAP的死循环. JAVA,.
- 敖丙.【知乎】. 图解:什么是红黑树?. JAVA,.