集合框架接口
一般考察集合的内容主要有5个方面:
1.底层数据结构
2.增删改查方式
3.初始容量,扩容方式,扩容时机。
4.线程安全与否
5.是否允许空,是否允许重复,是否有序
1.集合结构图
1.collection类集合结构图
2.map类集合结构图
2.Iterable接口
1.作用
Iterable接口表示一组对象,Iterable接口需要实现的iterator方法的功能是“返回”一个迭代器,我们常用的实现了该接口的子接口有: Collection, Deque, List, Queue, Set 等,实现这个接口允许对象成为 Foreach 语句的目标,就可以通过Foreach语法遍历底层序列。 同时该对象还需要给定一个Iterator的实现类
public interface Iterator<E> {
// 判断是否有下一个元素
boolean hasNext();
// 获取下一个元素
E next();
// 将迭代器新返回的元素删除
default void remove() {
throw new UnsupportedOperationException("remove");
}
// lambda表达式循环迭代
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
2.结构
public interface Iterable<T> {
// 获取Iterator对象
Iterator<T> iterator();
// default是java8出现的关键字、在接口中也可以写方法实现
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
// forEach可使用lambda表达式快速消费
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
3.总结
(1) 对象要想用foreach必须要实现Iterable接口,并且自定义Iterator处理逻辑
(2) 增强for循环的底层是迭代器,任何实现了iterable接口的类都可以使用增强for循环来遍历
3.collection接口
1.作用
Collection接口作为集合的一个根接口,它提供了对集合对象进行基本操作的通用接口方法,接口在Java 类库中有很多具体的实现。其意义是为各种具体的集合提供了最大化的统一操作方式,JDK不提供此接口的任何直接实现,而是提供更具体的子接口,有Set接口、List接口、Queue接口。Set接口存放的元素是无序的且不包含重复元素(对象);List接口存放的元素是有序的且允许有重复的元素;Queue接口存放的元素顺序符合先入先出FIFO的规则
2.结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsNzqAPm-1617350261792)(\image\collection.png)]
- addAll:在集合中添加一个集合
- clear: 删除当前Collection中的所有元素
- containsAll: 判断此Collection是否包含指定Collection中所有元素
- parallelStream/stream:转为流对象
- removeAll:删除集合中的所有元素
- removeIf:条件删除集合中的元素
- rertainAll: 从当前Collection中删除collec中不包含的元素 (求交集)
- toArray:将集合转换为数组对象
3.重点补充
-
parallelStream提供了流的并行处理,它是Stream的另一重要特性,其底层使用Fork/Join框架实现。简单理解就是多线程异步任务的一种实现、Stream是普通的串行流、使用parallelStream要考虑线程安全问题
public static void main(String[] args) { List<Integer> list = new ArrayList<>(); for (int j = 0; j < 1000; j++) { list.add(j); } System.out.println("源数据长度:"+list.size()); for (int i = 0; i < 10 ; i++) { List<Integer> parseList = new ArrayList<>(); // List<Integer> synchronizedList = Collections.synchronizedList(parseList); list.parallelStream().forEach(integer -> { synchronizedList.add(integer); }); System.out.println("每次遍历的集合长度:"+ synchronizedList.size()); } } 结果:会出现集合长度减少、多次运行还会出现角标越界现象 串行流:适合存在线程安全问题、阻塞任务、重量级任务,以及需要使用同一事务的逻辑 并行流:适合没有线程安全问题、较单纯的数据处理任务
4.List接口
1.作用
- 有序的集合,存储元素和取出元素的顺序是一致的(存储123 取出123)
- 有索引,包含了一些带索引的方法(特有)
- 允许存储重复的元素
2.结构
public void add(int index,E element) // 将指定的元素,添加到该集合中的指定位置上。
public E get(int index) // 返回集合中指定位置的元素。
public E remove(int index) // 移除列表中指定位置的元素,返回的是被移除的元素。
public E set(int index,E element)// 用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
int indexOf(Object o); // 最开始出现元素的位置
int lastIndexOf(Object o); // 最后出现元素的位置
default void sort(Comparator<? super E> c) { // Comparator排序 先将集合转为数组、排序好之后再替换
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
5.Set接口
1.作用
由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。实现了Set接口的集合有:EnumSet、HashSet、TreeSet。
- Set是一种不包括重复元素的Collection
- 没有索引,通过对象的hashcode来维护自己的顺序,对外表现为无序
- 可以存null、但只能有一个
2.结构
里面的方法全部都是collect中的继承、仅仅为了维护自己的特性
6.Queue接口
1.作用
Queue接口是队列接口、在FIFO或LIFO情景下使用;主要的接口有
-
阻塞接口 ( BlockingQueue)
java.util.concurrent 中加入了 BlockingQueue 接口和五个阻塞队列类。它实质上就是一种带有一点扭曲的 FIFO 数据结构。不是立即从队列中添加或者删除元素,线程执行操作阻塞,直到有空间或者元素可用。
五个队列所提供的各有不同:* ArrayBlockingQueue :一个由数组支持的有界队列。
* LinkedBlockingQueue :一个由链接节点支持的可选有界队列。
* PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。
* DelayQueue :一个由优先级堆支持的、基于时间的调度队列。
* SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。 -
非阻塞接口 ( AbstractQueue)
内置的不阻塞队列: PriorityQueue 和 ConcurrentLinkedQueue、PriorityQueue 和 ConcurrentLinkedQueue 类在 Collection Framework 中加入两个具体集合实现,PriorityQueue 类实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(通过其 java.util.Comparable 实现)或者根据传递给构造函数的 java.util.Comparator 实现来定位,ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大 小,ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列
-
双端队列(Deque)
2.结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nj8s0OT2-1617350261794)(image\queue.png)]
add // 增加一个元索,如果队列已满,则抛出一个IIIegaISlabEepeplian异常
offer // 添加一个元素并返回true,如果队列已满,则返回false
remove // 移除并返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
element // 返回队列头部的元素,如果队列为空,则抛出一个NoSuchElementException异常
poll // 移除并返问队列头部的元素,如果队列为空,则返回null
peek // 返回队列头部的元素,如果队列为空,则返回null
--- 阻塞队列特殊方法
put // 添加一个元素,如果队列满,则阻塞
take // 移除并返回队列头部的元素,如果队列为空,则阻塞
集合框架核心类
1.ArrayList
1.特点
- 底层基于数组实现,容量大小动态变化
- 允许 null 的存在
- 基于索引查找元素速度快、插入和删除的效率比较低
- 每次扩容现有容量的50%
- 非线程安全
2.结构
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable{
private static final int DEFAULT_CAPACITY = 10; // 初始化容量
transient Object[] elementData; // 容器
private int size; // 记录数组大小
protected transient int modCount = 0; // 来源于abstractList 用于快速失败
}
3.总结
-
ArrayList是如何自动容量控制的?
jdk1.7是初始化就创建一个容量为10的数组(饿汉式),1.8后是初始化先创建一个空数组(懒汉式),第一次add时才扩容为10 ,每次在添加的时候会检查数组的容量、如果容量不够会新建一个数组、长度为将原来数组的1.5倍(1.7是取余、1.8是右移两位 >> 2),这样实现了自动扩容。ArrayList本身没有实现自动缩容机制、但有一个trimToSize方法可以看做是缩容
public void trimToSize() { // 修改次数自增 modCount++; // 判断当前是否需要缩容 if (size < elementData.length) { // 如果size为0,直接给elementData赋值内置的空数组 // 不为0则创建一个size长度的新数组 elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
-
ArrayList添加数据的过程?
public boolean add(E e) { ensureCapacityInternal(size + 1); // size + 1 代表本次最小需要的容量 elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { // calculateCapacity 计算出最小期望容量 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } // 计算容量最小值 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } // 扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 防止扩容了之后比最小需要容量小 if (newCapacity - MAX_ARRAY_SIZE > 0) // 溢出 newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } 1.计算最小容量值、如果elementData数组为空数组、则最小容量至少为10,如果不是空数组、最小容量为最小需要容量 2.校验扩容、如果最小需要容量减去数组的长度大于零,则扩容,并将elementData指向扩容的新数组 3.添加元素elementData[size++] = e并且返回true
-
ArrayList删除数据的过程?
public E remove(int index) { rangeCheck(index); checkForComodification(); E result = parent.remove(parentOffset + index); this.modCount = parent.modCount; this.size--; return result; } 1.校验索引 2.校验modCount、快速失败 3.最优化、删除、遍历移动 AccessRandom 4.modCount同步 5.size减一 、返回结果
-
快速失败作用和原理?
一、快速失败(fail—fast) 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。 注意:这里异常的抛出条件是检测到 modCount != expectedmodCount 这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。 场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。 二、安全失败(fail—safe) 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。 >缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。 场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
2.LinkedList
1.特点
- 底层基于 双向链表实现的,没有初始化大小,容量大小动态变化,也没有扩容的机制,可以包含重复的元素
- LinkedList实现了Queue、Deque接口
- LinkedList适合增加和删除操作,不会发生移位 ,在查找上要逐个遍历,性能不如ArrayList
- LinkedList是非线程安全的
- LinkedList维护了元素插入时的顺序
2.结构
同时实现了List、queue、Deque接口,所以LinkedList同时具备三种接口的共同特征,本身比较简单
3.总结
略…
3.Vector
1.特点
-
古老的集合since JDK1.0
-
底层基于数组实现,
-
vector底层数组不加transient,序列化时会全部复制
-
线程安全、全部都加上了 synchronized
2.结构
扩容机制、如果制定了capacityIncrement就每次增加多少,如果没有指定就翻倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
3.总结
代码结构跟ArrayList相识度90% , 略…
3.CopyOnWriteArrayList
1.特点
- 底层基于数组实现
- 增加和删除都是加了锁、查询基于索引、直接返回
- 初始容量为0、扩容方式为加一、扩容时机每次add的时候加一
- 线程安全、全部都加上了 ReentrantLock
- 允许为空、允许重复、有序
- 虽然Array元素数组上加了volatile保证了可见性,但是在新增删除的过程中都是copy了一个新数组进行操作,所以能保证最终一致性,但是不能保证实时性
2.结构
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
...
}
添加元素
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();
}
}
删除元素
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
3.总结
-
CopyOnWriteArrayList的缺点?
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求
-
ReentrantLock原理?
4.HashSet
1.特点
- 底层数据结构是HashMap,key为记录的值、value统一为同一个Object对象
- 增删改查都是间接用的HashMap对象的增删改查
- 初始容量、扩容方式、扩容时机和HashMap一样
- 线程不安全
- 允许null值
- 基于hash存储、存入的元素没有顺序
2.重点结构
1.基于集合构造初始化方式
public HashSet(Collection<? extends E> c) {
// 用集合的size除于0.75 + 1 来作为入参 目的就是得到最接近16倍数的容量值
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
2.default的构造方法为子类LinkedHashSet做铺垫
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
3.总结
一切基于HashMap(或 LinkedHashMap)、自己就是个壳子