该文章为面试精华版,如果是初学者,建议先从专栏学习:Java集合专栏
一、 主要容器概述
注意Collection是一个接口,是List和Set的公共接口,Collections是一个工具类
Java容器主要有三个:
- List:是一个有序集合,可以放重复的数据
- Set:是一个无序集合,不允许放重复的数据
- Map:是一个无序集合,集合中包含一个键对象,-一个值对象,键对象不允许重复,值对象可以重复(身份证号-姓名)
size (),length,length()区别?
- size()是集合中使用,统计集合中元素的个数
- length是数组的一个成员属性
- length()是String的一个方法
Iterator
方法 | 描述 |
---|---|
boolean hasNext() | 判断集合中是否还有下一个元素 |
Object next() | 返回下一个元素 |
void remove() | 从集合中删除一个由 next()方法返回的元素 |
二、 List
- ArrayList:查询数据比较快,添加和删除数据比较慢(基于可变数组)
- LinkedList:查询数据比较慢,添加和删除数据比较快(基于链表数据结构)
1. ArrayList
查找比较快,增删比较慢
- 底层是基于数组。
- 默认容量是10
- 每次扩容1.5倍
- 如果增加 0.5 倍后的新容量超过限制的容量,则用所需的最小容量与限制的容量进行判断,超过则指定为 Integer 的最大值
- 通过数组的复制将原数据复制到一个更大(新的容量大小)的数组
如何保证线程安全?
- 继承ArrayList,重写其中的方法
List list = Collections.synchronizedList(new ArrayList());
2. LinkedList
增删比较快,查找比较慢
-
基于链表的,除了存放数据,还需要存放指针,所以占用的空间会大一些
-
它实现了 Deque 接口,使得 LinkedList 类也具有队列的特性
-
不是线程安全的
3. Vector
Vector与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一 个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。
4. ArrayList 与 LinkedList 异同
-
是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
-
底层数据结构: Arraylist 底层使用的是 Object 数组; LinkedList 底层使用的是双向链表数据结构JDK1.6之前为循环链表, JDK1.7 取消了循环。注意双向链表和双向循环链表的区别:节约空间);
-
插入和删除是否受元素位置的影响:
- ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(Ee) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
-
LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
-
是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。
5. ArrayList和Vector的区别
- Vector 类的所有方法都是同步的。可以由两个线程安全地访问一个 Vector 对象、但是一个线程访问 Vector的话代码要在同步操作上耗费大量的时间。
- Arraylist 不是同步的,所以在不需要保证线程安全时时建议使用 ArrayList。【CopyOnWriteArrayList 是同步的】。
- 扩容倍数不一样:Vector 每次扩容请求其大小的 2 倍空间 ,而 ArrayList 是 1.5 倍。
6. System.arraycopy()和 Arrays.copyOf()
- arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置
- copyOf()是系统自动在内部新建一个数组,并返回该数组。
7. CopyOnWriteArrayList
是一种读写分离的结构:
- 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
- 写操作需要加锁,防止并发写入时导致写入数据丢失。
- 写操作结束之后需要把原始数组指向新的复制数组。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。但是 CopyOnWriteArrayList 有其缺陷:
- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
三、 Set
在Set家族中,常用的有三种:
- HashSet: 采用 Hashmap 的 key 来储存元素,主要特点是无序的,基本操作都是 O(1) 的时间复杂度,很快。
- LinkedHashSet: 这个是一个 HashSet + LinkedList 的结构,特点就是既拥有了 O(1) 的时间复杂度,又能够保留插入的顺序。
- TreeSet: 采用红黑树结构,特点是可以有序,可以用自然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。
1. 哈希表
哈希表是一种数据结构,哈希表能够提供快速存取操作。哈希表是基于数组的,所以也存在缺点,数
组一旦创建将不能扩展。
正常的数组,如果需要查询某个值,需要对数组进行遍历,只是一种线性查找,查找的速度比较慢。
如果数组中的元素值和下标能够存在明确的对应关系,那么通过数组元素的值就可以换算出数据元素的下
**标,通过下标就可以快数定位数组元素,这样的数组就是哈希表。**一张哈希表:
元素值 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
---|---|---|---|---|---|---|---|---|---|
元素下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
以上我们的示例元素值和下标的关系为:
-
元素下标=元素值-10,此时的示例 hashcode 就是和数组下标一致了,取得 hashcode 方法如下:
//取得 hashCode pubic int hashCode(int value) { return value – 10; }
2. HashSet
- HashSet中的数据是无序的不可重复的。
- 采用 Hashmap 的 key 来储存元素
3. hashCode()
覆盖了 equals 和 hashCode,当 hashCode 相同,它会调用 equals 进行比较,如果 equals 比较相等将不加把此元素加入到 Set 中
但 equals 比较不相等会重新根据 hashCode 换算位置仍然会将该元素加入进去的。
hashCode() 与 equals() 的相关规定:
- 如果两个对象相等,则 hashcode 一定也是相同的
- 两个对象相等,对两个对象分别调用 equals 方法都返回 true
- 两个对象有相同的 hashcode 值,它们也不一定是相等的
- 因此, equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
再次强调:特别是向 HashSet 或 HashMap 中加入数据时必须同时覆盖 equals 和 hashCode 方法,应该
养成一种习惯覆盖 equals 的同时最好同时覆盖 hashCode
Java 要求:
- 两个对象 equals 相等,那么它的 hashcode 相等
- 两个对象 equals 不相等,那么它的 hashcode 并不要求它不相等,但一般建议不相等hashcode 相等不代表两个对象相等(采用 equals 比较)
为什么重写 equals() 就必须要重写 hashCode()?
- 当key的hashCode()计算的数值相同时,就会出现hash冲突
处理 hash 冲突有哪些方法?Java 中用的哪一种?为什么?另一种方法你在工作中用过吗?在什么情况下用得多?
见:https://xzzz2020.gitee.io/post/d0otbdSTy
4. TreeSet
- TreeSet 可以对 Set 集合进行排序, 默认自然排序(即升序)
- 引用类型实现排序,需要实现Comparator或者Comparable
Comparable 和Comparator 的区别?
- Comparable是自然排序,需要实体类实现,需要修改源代码
- Comparator 是定制排序,不需要更改源代码,定义一个比较规则,传递给需要调用的方法中,将比较策略与数据分离,体现了策略模式
- Comparator 比Comparable 优先级更高
四、 Map
关于HashMap的全部面试题见:https://xzzz2020.gitee.io/post/hashmap
Map 中可以放置键值对,也就是每一个元素都包含键对象和值对象, Map 实现较常用的为 HashMap,HashMap 对键对象的存取和 HashSet 一样,仍然采用的是哈希算法,所以如果使用自定类作为 Map 的键对象,必须复写 equals 和 hashCode 方法
Map中常用的三个:
- HashMap: 与 HashSet 对应,也是无序的,O(1)。
- LinkedHashMap: 这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,所以既拥有 HashMap 的所有特性还能有顺序。
- TreeMap: 是有序的,本质是用二叉搜索树来实现的。
1. HashMap
实现原理
- 对于HashMap中的每个key,根据一个Hash函数,计算出一个Hash值,对应就是桶的编号,桶实际上是用数组实现的
- Hash函数跟HashMap的容量有关系
如果不同的元素的key算出了相同的哈希值,那么该怎么存放呢?
- 这就是哈希碰撞,即多个 key 对应了同一个桶。
HashMap 中是如何保证元素的唯一性的呢?即相同的元素会不会算出不同的哈希值呢?
- 通过 hashCode() 和 equals() 方法来保证元素的唯一性。
2. HashTable
是一个线程安全的,但是锁住了全部数据效率低下
HashMap 和 Hashtable 的区别 ?
- 线程是否安全: HashMap 是非线程安全的, HashTable 是线程安全的; HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
- 效率:因为线程安全的问题, HashMap 要比 HashTable 效率高一点。另外, HashTable 基本被淘汰,不要在代码中使用它;
- 对 Null key 和 Null value 的支持: HashMap 中, null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
- HashMap 的迭代器是 fail-fast 迭代器,HashTable是用的Enumeration(枚举)的迭代器
- 初始容量大小和每次扩充容量大小的不同:
①创建时如果不指定容量初始值, Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。
HashMap 默认的初始化大小为 16。 之后每次扩充,容量变为原来的 2 倍。
②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 - 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默
认为 8)时,将链表转化为红黑树,以减少搜索时间。 Hashtable 没有这样的机制。
3. LinkedHashMap
保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序 。
- 内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序
如何实现LRU 缓存 ?
构造方法提供了一个参数accessOrder,默认是false,表示按照插入顺序:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
而当accessOrder 为true时,表示按照访问顺序,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除, MAX_ENTRIES默认为3
需要继承LinkedHashMap,并且覆盖 removeEldestEntry() 方法实现 :
class LRUCache<k,v> extends LinkedHashMap<k,v> {
private final static int MAX_CACHE_SIZE = 100;
private final int limitCacheSize;
public LRUCache() {
this(MAX_CACHE_SIZE);
}
public LRUCache(int cacheSize){
super(cacheSize, 0.75f, true);
this.limitCacheSize = cacheSize;
}
/** * 判断什么时候删除缓存 */
@Override
protected boolean removeEldestEntry(Map.Entry<k, v> eldest) {
return this.size()>this.limitCacheSize;
}
}
五、 Collections常用方法
排序
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由 Comparator 控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当 distance 为正数时,将 list 后 distance 个元素整体移到前面。当 distance 为负数时,将 list 的前 distance 个元素整体移到后面。
查找,替换操作
int binarySearch(List list, Object key)//对 List 进行二分查找,返回索引,注意 List 必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。
int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由 Comparatator 类控制。
int min(Collection coll, Comparator c) void fill(List list, Object obj)//用指定的元素代替指定 list 中
所有元素。 int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计 target 在 list 中第一次出现的索引,找不到则返回-1,
int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素
同步控制
Collections 提供了多个 synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而解决多
线程并发访问集合时的线程安全问题。
我们知道 HashSet, TreeSet, ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。 Collections 提供了多个静态方法可以把他们包装成线程同步的集合
synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection。
synchronizedList(List<T> list) //返回指定列表支持的同步(线程安全的) List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的) Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的) set。
设置不可变集合
emptyXxx(): 返回一个空的、不可变的集合对象,此处的集合既可以是 List,也可以是 Set,还可以是 Map。
singletonXxx(): 返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以
是: List, Set, Map。
unmodifiableXxx(): 返回指定集合对象的不可变视图,此处的集合可以是: List, Set, Map。 上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本
六、Fail-Fast 和 Fail-Safe
-
Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。
-
java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。
-
快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。
快速失败原理
-
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
-
在进行序列化或者迭代或者remove()等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出
ConcurrentModificationException。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i = 0; i < size; i++) {
s.writeObject(elementData[i]);
}
if(modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
解决fail-fast的原理
在返回一个迭代器的时候,拷贝数据,因此,对容器内容的修改不影响遍历。
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
- 常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等。