文章目录
1.集合类简介
Interface Iterable
迭代器接口,这是Collection类的父接口。实现这个Iterable接口的对象允许使用foreach进行遍历,也就是说,所有的Collection集合对象都具有"foreach可遍历性"。这个Iterable接口只有一个方法: iterator()。它返回一个代表当前集合对象的泛型迭代器,用于之后的遍历操作
Collection
Collection是最基本的集合接口,一个Collection代表一组Object的集合,这些Object被称作Collection的元素。Collection是一个接口,用以提供规范定义,不能被实例化使用
1.1 Set
Set集合类似于一个罐子,“丢进"Set集合里的多个对象之间没有明显的顺序。Set继承自Collection接口,不能包含有重复元素(记住,这是整个Set类层次的共有属性)。
Set判断两个对象相同不是使用”=="运算符,而是根据equals方法。也就是说,我们在加入一个新元素的时候,如果这个新元素对象和Set中已有对象进行注意equals比较都返回false,
则Set就会接受这个新元素对象,否则拒绝。
因为Set的这个制约,在使用Set集合的时候,应该注意两点:
- 为Set集合里的元素的实现类实现一个有效的equals(Object)方法、
- 对Set的构造函数,传入的Collection参数不能包含重复的元素
HashSet
HashSet是Set接口的典型实现,HashSet使用HASH算法来存储集合中的元素,因此具有良好的存取和查找性能。当向HashSet集合中存入一个元素时,HashSet会调用该对象的 hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值决定该对象在HashSet中的存储位置。
值得注意的是,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法的返回值相等
LinkedHashSet
LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但和HashSet不同的是,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。
当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历)
SortedSet
此接口主要用于排序操作,即实现此接口的子类都属于排序的子类
1.2.1) TreeSet
TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态
1.3) EnumSet
EnumSet是一个专门为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式、或隐式地指定。EnumSet的集合元素也是有序的,它们以枚举值在Enum类内的定义顺序来决定集合元素的顺序
1.2 List
List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许加入重复元素,因为它可以通过索引来访问指定位置的集合元素。List集合默认按元素的添加顺序设置元素的索引
ArrayList
ArrayList是基于数组实现的List类,它封装了一个动态的增长的、允许再分配的Object[]数组。
Vector
Vector和ArrayList在用法上几乎完全相同,但由于Vector是一个古老的集合,所以Vector提供了一些方法名很长的方法,但随着JDK1.2以后,java提供了系统的集合框架,就将Vector改为实现List接口,统一归入集合框架体系中
Stack
Stack是Vector提供的一个子类,用于模拟"栈"这种数据结构(LIFO后进先出)
LinkedList
implements List, Deque。实现List接口,能对它进行队列操作,即可以根据索引来随机访问集合中的元素。同时它还实现Deque接口,即能将LinkedList当作双端队列使用。自然也可以被当作"栈来使用"
1.3 Queue
Queue用于模拟"队列"这种数据结构(先进先出 FIFO)。队列的头部保存着队列中存放时间最长的元素,队列的尾部保存着队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,
访问元素(poll)操作会返回队列头部的元素,队列不允许随机访问队列中的元素。结合生活中常见的排队就会很好理解这个概念
PriorityQueue
PriorityQueue并不是一个比较标准的队列实现,PriorityQueue保存队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行重新排序,这点从它的类名也可以看出来
Deque
Deque接口代表一个"双端队列",双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可以当成队列使用、也可以当成栈使用
ArrayDeque
是一个基于数组的双端队列,和ArrayList类似,它们的底层都采用一个动态的、可重分配的Object[]数组来存储集合元素,当集合元素超出该数组的容量时,系统会在底层重新分配一个Object[]数组来存储集合元素
LinkedList
带有首尾节点的一个双向链表
1.1.2 Map
Map用于保存具有"映射关系"的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value。key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较结果总是返回false。
关于Map,我们要从代码复用的角度去理解,java是先实现了Map,然后通过包装了一个所有value都为null的Map就实现了Set集合
Map的这些实现类和子接口中key集的存储形式和Set集合完全相同(即key不能重复)
Map的这些实现类和子接口中value集的存储形式和List非常类似(即value可以重复、根据索引来查找)
HashMap
和HashSet集合不能保证元素的顺序一样,HashMap也不能保证key-value对的顺序。并且类似于HashSet判断两个key是否相等的标准也是: 两个key通过equals()方法比较返回true、 同时两个key的hashCode值也必须相等
LinkedHashMap
LinkedHashMap也使用双向链表来维护key-value对的次序,该链表负责维护Map的迭代顺序,与key-value对的插入顺序一致(注意和TreeMap对所有的key-value进行排序进行区分)
Hashtable
是一个古老的Map实现类,且是一个线程安全类
Properties
Properties对象在处理属性文件时特别方便(windows平台上的.ini文件),Properties类可以把Map对象和属性文件关联起来,从而可以把Map对象中的key-value对写入到属性文件中,也可以把属性文件中的"属性名-属性值"加载到Map对象中
SortedMap
正如Set接口派生出SortedSet子接口,SortedSet接口有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap接口也有一个TreeMap实现类
TreeMap
TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态。同样,TreeMap也有两种排序方式: 自然排序、定制排序
WeakHashMap
WeakHashMap与HashMap的用法基本相似。区别在于,HashMap的key保留了对实际对象的"强引用",这意味着只要该HashMap对象不被销毁,该HashMap所引用的对象就不会被垃圾回收。
但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,当垃圾回收了该key所对应的实际对象之后,WeakHashMap也可能自动删除这些key所对应的key-value对
IdentityHashMap
IdentityHashMap的实现机制与HashMap基本相似,在IdentityHashMap中,当且仅当两个key严格相等(key1 == key2)时,IdentityHashMap才认为两个key相等
EnumMap
EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)
2.ArrayList相关深入
可调整大小的数组的实现List接口。 实现所有可选列表操作,并允许所有元素,包括null 。 除了实现List 接口之外,该类还提供了一些方法来操纵内部使用的存储列表的数组的大小。 (这个类是大致相当于Vector,不同之处在于它是不同步的)。
该size,isEmpty,get,set,iterator和listIterator操作在固定时间内运行。 add操作以摊余常数运行 ,即添加n个元素需要O(n)个时间。 所有其他操作都以线性时间运行(粗略地说)。
与LinkedList实施相比,常数因子较低。
应用程序可以添加大量使用ensureCapacity操作元件的前增大ArrayList实例的容量。 这可能会减少增量重新分配的数量。
概括:
- ArrayList是可以动态扩容和动态删除冗余容量的索引序列,基于数组实现的集合。
- ArrayList支持随机访问、克隆、序列化,元素有序且可以重复。
- ArrayList初始默认长度10,超出扩容1.5倍,使用Object[]存储各种数据类型。
2.1 ArrayList源码
字段:
抽象父类AbstractList字段:
注意这个字段哦:
//并发修改异常需要用到的变量,记录集合结构变化的次数,即add,remove操作
protected transient int modCount = 0;
//默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
//动态数组的大小,默认0
private int size;
//存储ArrayList元素的数组缓冲区。
transient Object[] elementData;
/**
*用于空实例的共享空数组实例。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
*用于默认大小的空实例的共享空数组实例。我们
*将其与EMPTY_ELEMENTDATA区分开来,以了解何时扩容多少
*添加第一个元素。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
构造方法:
//带初始化参数的构造方法
public ArrayList(int initialCapacity) {
//初始化一个动态数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//否则就直接复制一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 构造一个初始容量为10的空列表。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//构造一个包含指定元素的列表集合,按照集合返回它们的顺序迭代器。
public ArrayList(Collection<? extends E> c) {
//得到c的数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray可能(不正确地)不返回Object[
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 用空数组替换。
this.elementData = EMPTY_ELEMENTDATA;
}
}
核心方法:
2.1.1 add()
public boolean add(E e) {
//当前size+1 是否需要扩容
ensureCapacityInternal(size + 1);
//把当前添加元素放到数组尾部
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果是空空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//返回默认容量10,和size+1的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
//增加变化计数值
modCount++;
// 如果 所需最小容量 大于 当前容器的大小
if (minCapacity - elementData.length > 0)
//说明需要扩容了
grow(minCapacity);
}
private void grow(int minCapacity) {
//旧容量
int oldCapacity = elementData.length;
// 新容量= 旧容量的1.5倍 ( 右移一位 等于 处以2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 确保 新得到的容量是大于 size+1的 否则就还是旧容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
//确保新容量不大于 Integer.MAX_VALUE - 8
newCapacity = hugeCapacity(minCapacity);
//数组赋值 ,扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
插入指定位置的add()
public void add(int index, E element) {
//索引位置检查
rangeCheckForAdd(index);
//确保内部容量足够,同时modcount ++
ensureCapacityInternal(size + 1);
//拷贝 当前数组 从index起 拷贝到当前数组的index+1 位置
//即index 位置腾出来
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//赋值
elementData[index] = element;
size++;
}
来看看 System.arraycopy的四个参数:
* @param src the source array. 源数组
* @param srcPos starting position in the source array. 源数组中的起始位置。
* @param dest the destination array.目标数组。
* @param destPos starting position in the destination data. 目标数据中的起始位置。
* @param length the number of array elements to be copied.要复制的数组元素的数目。
2.1.2 get()
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
2.1.3 set()
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
2.1.4 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;
return oldValue;
}
2.1.5 SubList
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
//也有并发修改异常检查
this.modCount = ArrayList.this.modCount;
}
2.2 并发修改异常检查
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("123");
list.add("124");
list.add("125");
list.add("126");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
if (iterator.next().equals("123")){
list.add("jia");
}
}
System.out.println(list.toString());
}
探究为什么会发生并发修改异常?
初始化后,就把当前list的modcount赋值给了Itr的expectedModCount
public Iterator<E> iterator() {
return new Itr();
}
int expectedModCount = modCount;
因为我们执行了4次add操作:
我们再来看看next()做了些什么?
itr.hasNext():通过判断 cursor(游标) != size(长度)来决定是否结束循环,cursor(游标) 初始是0 每次经过 itr.next() +1;当cursor==size时 会跳出循环,这也是为什么倒数第2个不会出错的主要原因;
itr.next(): 看源代码可以发现每次在next()调用后,都会先调用checkForComodification()这个方法;
checkForComodification(): 主要作用是判断itr迭代器数据是否和list一致
有两个参数:
第一个 modCount 集合结构变动次数,如:一开始你add调用了7次,那么这个数就是7,
第二个 expectedModCount 在调用iterator()方法时,初始化值等于modCount
public E next() {
//哦哦哦,检查修改
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
// 如果 游标不小于 当前的容器大小
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
所以我们来看,第一次next,由于没有add或者remove ,next能正常通过
而当前又equals了"123",我们往下执行一步
可见,list的modcount是肯定会加了1的,变成了5
继续往下:
我们进去看看:
所以就抛出了并发修改异常
这里总结一下就是,迭代器生成后的modcount是不会跟着 ArrayList变的
还有一点也得注意下:
public boolean hasNext() {
return cursor != size;
}
我们知道,我们是在加了新元素之后的next发生并发修改移除
那么我们是否可以,在移除最后2个之前,我们给list 加上一个新的呢?
即修改过后,不再产生itreator.hasNext的判断。
来看:
欧mygod
知道这个玩意儿有多么危险了吧,假如我们一个线程在迭代,另一个线程在add,或者remove,可能查这个动作都做不了,但是删除这个动作却有可能成功;
那么怎么能够避免这个严重的问题呢?
只能够说:
remove是可以的,但是单线程下可以,多线程下,我估计仍然不能保证线程安全
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
建议大家还是使用线程安全的集合类,也要注意迭代器的用法
来贴几个alibaba开发者手册注意的地方:
3. ArrayList和LinkedList的区别?分别用在什么场景?
①ArrayList和LinkedList可想从名字分析,它们一个是Array(动态数组)的数据结构,一个是Link(链表)的数据结构,此外,它们两个都是对List接口的实现。
前者是数组队列,相当于动态数组;后者为双向链表结构,也可当作堆栈、队列、双端队列
②当随机访问List时(get和set操作),ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
③当对数据进行增加和删除的操作时(add和remove操作),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
④从利用效率来看,ArrayList自由性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
⑤ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。
场景:
链表,插入删除快,查找修改慢。 适用于频繁增删的场景。
数组,查找快,插入删除慢。 适用于频繁查找和修改的场景。
4. ArrayList插入或删除元素一定比LinkedList慢么?
- 数组删除元素确实要比链表慢,慢在需要创建新数组,还有比较麻烦的数据拷贝,但是在ArrayList底层不是每次删除元素都需要扩容,因此在这个方面相对于链表来说数组的性能更好
- LinkedList删除元素之所以效率并不高,其原理在于底层先需要对整个集合进行折半的动作,然后又需要对集合进行遍历一次,这些操作导致效率变低
5. 已知成员变量集合存储N多用户名称,在多线程的环境下,使用迭代器在读取集合数据的同时如何保证还可以正常的写入数据到集合?
使用CopyOnWriteArrayList。