java集合-Collection和Map
一、概述
数组的弊端
一旦初始化以后,其长度就不可修改。
数组中提供的方法非常限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
集合概述
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表
Collection接口:单列数据,定义了存取一组对象的方法的集合
List:元素有序、可重复的集合
Set:元素无序、不可重复的集合
Map接口:双列数据,保存具有映射关系“key-value对”的集合
Collection

Collection:单列集合
常用方法
1 | add(Object obj),addAll(Collection coll),size(),isEmpty(),clear(); |
使用Collection集合存储对象,要求对象所属的类满足:
向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals().
集合复制
1 | List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"}); |
1. Set
Set接口:
无序: 不等于随机性。存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的。
不可重复:保证添加的元素照equals()判断时,不能返回true.同时他们的hashCode方法返回的值不能相等 。即:相同的元素只能添加一个。必须重写这2个方法。
- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。可以存null
- LinkedHashSet:HashSet子类,具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。LinkedHashSet插入性能略低于 HashSet
2. List
List:无序的、可重复的数据。
- ArrayList:基于动态数组实现,支持随机访问。
- Vector:和 ArrayList 类似,但它是线程安全的。
- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
3. Queue
- LinkedList:可以用它来实现双向队列。
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
Map
双列数据,存储key-value对的数据
- TreeMap:基于红黑树实现。
- HashMap:基于哈希表实现。可以存储null的key和value
- HashTable:不能存储null ,和 HashMap 类似,但它是线程安全的,就是在方法上加入synchronize这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
- Properties:HashTable的子类常用来处理配置文件。key和value都是String类型
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。频繁的遍历操作。
二、容器中的设计模式
迭代器模式
iterator
Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
1 | Iterator iter = coll.iterator();//回到起点 |
Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方 法,不是集合对象的remove方法。
如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。
从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。
遍历集合的底层调用Iterator完成操作
1 | List<String> list = new ArrayList<>(); |
ListIterator
ListIterator是一个功能更加强大的, 它继承于Iterator接口,只能用于各种List类型的访问。可以通过调用listIterator()方法产生一个指向List开始处的ListIterator, 还可以调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。
(1)双向移动(向前/向后遍历).
(2)产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引.
(3)可以使用set()方法替换它访问过的最后一个元素.
(4)可以使用add()方法在next()方法返回的元素之前或previous()方法返回的元素之后插入一个元素.
1 | import java.util.*; |
结果
1 | Before iterate : [aaa, bbb, ccc] |
适配器模式
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
1 |
|
应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
1 | Integer[] arr = {1, 2, 3}; |
也可以使用以下方式调用 asList():
1 | List list = Arrays.asList(1, 2, 3); |
三、源码分析
iterator
ArrayList中的实现
1 | private class Itr implements Iterator<E> { |
下一个重点,modCount
modCount顾名思义就是修改次数,每次对ArrayList内容的修改都将增加这个值
Fail-Fast 机制
modCount主要是为了防止在迭代过程中通过List的方法(非迭代器)改变了原集合,导致出现不可预料的情况,从而提前抛出并发修改异常,注意是“提前“,这可能也是Fail-Fast机制命名的由来。在可能出现错误的情况下提前抛出异常终止操作,如下:
1 | ArrayList<Integer> arrayList = new ArrayList<>(); |
这段代码最终会抛出ConcurrentModificationException
原因是因为,在迭代器进行遍历的时候,如果 iterator.next()选择了需要遍历的下一个目标时(假设这个目标为坐标3的数),
我却调用了arrayList.remove(1)将坐标1给删了,那么这时候它就会遍历成原本坐标为4的数字4,却没有遍历数字3了,如果是LinkedList,会直接找不到目标
为了防止这种情况,在迭代器初始化过程中会将modCount赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示通过其他方法修改了 ArrayList的结构
1 | final void checkForComodification() { |
next
1 |
|
remove
1 | public void remove() { |
它在每一次删除之后都会将cursor(下一项)的位置设置为当前位置,也就是将cursor往前移动了一位,之后再将modCount赋值给expectedModCount使它们保持相等。
这里把lastRet设为-1的目的是为了阻止程序员在没有进行next()的情况下连续去remove(),只有再次执行next()之后,lastRet完成刷新,remove()方法才知道应该删除哪个元素。
1 |
|
ListIterator
ArrayList中的实现
1 | public ListIterator<E> listIterator(int index) { |
add
1 | public void add(E e) { |
他在添加完成后将cursor的位置向后移动,不会遍历到已经添加的那个值
ArrayList
ArrayList的JDK1.8之前与之后的实现区别?
JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组
Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合
概述
因为 ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。
1 | ublic class ArrayList<E> extends AbstractList<E> |
数组的默认大小为 10。
1 | private static final int DEFAULT_CAPACITY = 10; |
构造器
1 | //有长度的构造器 |
扩容
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1)
,也就是旧容量的 1.5 倍。
扩容操作需要调用 Arrays.copyOf()
把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
1 | public boolean add(E e) { |
删除元素
需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。
1 | public E remove(int index) { |
序列化
1 | transient Object[] elementData; |
transient用来表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient修饰的变量的值是不包括在序行化的表示中的。但是ArrayList又是可序行化的类,elementData是ArrayList具体存放元素的成员,用transient来修饰elementData,岂不是反序列化后的ArrayList丢失了原先的元素?
这段主要是这2个方法
1 | private void writeObject(java.io.ObjectOutputStream s) |
ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。
为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
Fail-Fast
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。代码参考上节序列化中的 writeObject() 方法。
问题
1 | public void testListRemove(){ |
Vector
jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。
在扩容方面,默认扩容为原来的数组长度的2倍。
他有一个子类Stack
同步
它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。
1 | public synchronized boolean add(E e) { |
加了synchronized保证同步
扩容
1 | public Vector(int initialCapacity, int capacityIncrement) { |
Vector 的构造函数可以传入 capacityIncrement 参数,它的作用是在扩容时使容量 capacity 增长 capacityIncrement。如果这个参数的值小于等于 0,扩容时每次都令 capacity 为原来的两倍。
与 ArrayList 的比较
- Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
- Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍。
替代方案
可以使用 Collections.synchronizedList();
得到一个线程安全的 ArrayList。
1 | List<String> list = new ArrayList<>(); |
也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
1 | List<String> list = new CopyOnWriteArrayList<>(); |
CopyOnWriteArrayList
1 | public class CopyOnWriteArrayList<E> |
读写分离
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
1 | public boolean add(E e) { |
适用场景
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是 CopyOnWriteArrayList 有其缺陷:
- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
LinkedList
1 | public class LinkedList<E> |
概览
基于双向链表实现,使用 Node 存储链表节点信息。
1 | private static class Node<E> { |
每个链表存储了 first 和 last 指针:
1 | transient Node<E> first; |
操作
1 | public boolean add(E e) { |
与 ArrayList 的比较
ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:
- 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
- 链表不支持随机访问,但插入删除只需要改变指针。
但在少量元素的情况下,链表需要定位元素,在插入,ArrayList可以直接插入。效率其实没有什么不同。
速度不能根据底层决定。
HashSet
1 | public class HashSet<E> |
1 | public HashSet() { |
本质是一个HashMap 只不过是一个value相同的
元素添加过程
哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置,判断(存放位置一样但哈希值不一样)
数组此位置上是否已经元素:
- 如果此位置上没其他元素,则元素a添加成功。 —>情况1
- 如果此位置上其他元素b(或以链表形式存在的多个元素,则比较元素a与元素b的hash值:
- 如果hash值不相同,则元素a添加成功。—>情况2
- 如果hash值相同,进而需要调用元素a所在类的equals()方法:
- quals()返回true,元素a添加失败
- equals()返回false,则元素a添加成功。—>情况2
对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
jdk 7 :元素a放到数组中,指向原来的元素。
jdk 8 :原来的元素在数组中,指向元素a
总结:七上八下
hashCode和equals的重写
(01) 若某个类没有覆盖hashCode()方法,当它的通过hashCode()比较两个对象时,实际上是比较两个对象是不是同一个对象。这时,等价于通过“==”去比较这两个对象,即两个对象的内存地址是否相同。
(02) 我们可以覆盖类的hashCode()方法,来让hashCode()通过其它方式比较两个对象是否相等。通常的做法是:若两个对象的内容相等,则hashCode()方法返回true;否则,返回fasle。
1 |
|
为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
Objects.hash()底层
1 | public static int hash(Object... values) { |
选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
并且31只占用5bits,相乘造成数据溢出的概率较小
31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)
TreeSet
1 | public class TreeSet<E> extends AbstractSet<E> |
不能添加不同类的对象
自然排序(实现Comparable接口 )和 定制排序(Comparator)
1.自然排序中,比较两个对象是否相同的标准为:compareTo()返回0.不再是equals().
2.定制排序中,比较两个对象是否相同的标准为:compare()返回0.不再是equals().
1 | public TreeSet(Comparator<? super E> comparator) { |
可以传入一个比较器,完成定制排序
HashMap
单独分出
ConcurrentHashMap
单独分出