Java集合
Set和list的区别是list允许有重复值,而set不允许重复值;Map是基于键值对(key-value)形式的;
一、 List:
1. ArrayList:
数组实现,采用一块连续内存存放数据,其声明如下:
private transient Object[] elementData;
当调用无参数的构造函数时,默认创建大小为10个元素的数组,否则创建指定大小元素的数组:
this.elementData = new Object[initialCapacity];
Add:
添加元素时,如果elementData没有空间存放数据,则按照新的数组长度生成一个新数组,然后将原数组的内容拷贝到新数组里面,并且给新元素赋值;
remove:
如果要删除的元素不是最后一个元素,则将删除的元素后面的元素进行整体拷贝往前移动一个元素位置,并将最后一个元素位置赋值为null;
Indexof:
查找元素时采用循环遍历的方式逐个遍历,直至找到要查找的元素为止(该算法效率较低);
Iterator:
迭代器遍历元素时创建一个Itr对象,该对象保存数组的下标位置变量cursor,其next和prev使用这个cursor变量来获取前一个和后一个元素;ArrayList类的modcount保存了修改的次数,当迭代器遍历元素过程中如果对数组进行了修改会导致modcount值变化,导致modcount与迭代器的exceptedmodcount的值不一致而抛出ConcurrentModificationException异常;
性能:
ArrayList在根据下标序号索引时效率是非常高的,插入和删除元素时由于要内存拷贝,效率相对较低,查找元素时要从头到尾的遍历元素,效率相对比链表(LinkedList)要高,要提高查找效率可以采用排序后的数组进行查找;
由于数组要申请一整块连续内存,当这块内存比较大时容易出现内存溢出现象;
ArrayList是非线程安全的;
2. Vector:
与ArrayList完全相同,也是基于连续内存空间的数组,但他与ArrayList的不同在于:
1) 容量不够时的扩大容量的策略:如果capacityIncrement大于0,则将数组容量扩大capacityIncrement个元素,如果capacityIncrement小于等于0,则将数组容量扩大2倍:
newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
2) Vector是线程安全的:在add, remove, addall, indexof, ListIterator等方法前面都加了synchronized关键字;
3. CopyOnWriteArrayList:
是一个线程安全,并且在读时无锁的数组实现类;初始创建时数组大小为0;其加锁控制使用是重入锁ReentrantLock来控制的,如下:
transient final ReentrantLock lock = new ReentrantLock();
private volatile transient Object[] array;
add:
使用ReentrantLock.lock进行加锁控制,然后申请一块原数组长度+1的长度的新数组,将原来的数据拷贝过去,并更新类的数组引用;从其名字copyOnWrite就可以看出该类是在数据发生变化时就要拷贝;
remove:
判断要删除的数据如果是最尾端的数据,则拷贝前面所有数据到一个新的数组中并将新数组更新类的数组引用,否则创建一个新的数组,然后分别将删除元素前面和后面的元素拷贝过去,并将新数组更新类的数组引用;
get:
既然是数组,那么就直接使用下标来索引,且无加锁;
性能:
线程安全;
添加删除会加锁,但是查找不加锁,因此读取性能非常高,但是添加删除性能会比ArrayList稍微低,但是读可能会读取到脏数据,但是这种概率非常低,因此对于写少读多且对脏数据影响不大的场景,CopyOnWriteArrayList是个不错的选择;
4. CopyOnWriteArraySet:
CopyOnWriteArraySet与CopyOnWriteArrayList的区别是CopyOnWriteArrayList允许重复元素,而CopyOnWriteArraySet不允许重复元素;
其实现依赖于CopyOnWriteArrayList来实现,所不同的是在add时调用的是addIfAbsent方法,这个方法保证了元素不会重复:
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
public boolean add(E e) {
return al.addIfAbsent(e);
}
5. LinkedList:
双向链表实现类,每个元素有三个值:prev, value和next,表示为Node类,声明如下:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
链表中保存了首尾节点引用,其余节点均位于首尾节点的中间,首尾节点形成双向链表的闭环:
transient Node<E> first;
transient Node<E> last;
add:
创建一个Node对象,并改变prev和next指针以形成双向链表,因此其内存空间利用率则比ArrayList要高得多;
remove:
改变前后两个节点的prev和next指针,使得被删除的节点孤立出来;
indexof:
循环遍历链表,直至找到要查找的节点;
ListIterator:
通过当前节点指针位置来遍历整个链表,在遍历过程中如果修改了链表会导致modcount改变,引发listiterator的exceptedmodcount和链表的modcount不一致而抛出ConcurrentModificationException异常;
性能:
空间利用率上比ArrayList高得多,而且单纯的添加删除元素(指的是不做元素位置的查找)要比ArrayList效率高,但是针对下标索引和查找元素的效率则比ArrayList低;
LinkedList是非线程安全的;
6. Stack:
基于数组的栈操作(push, pop),操作特点是LIFO(先进后出),继承自Vector,表示他是一个线程安全的数组,数组的第一个元素是栈底,最后一个元素是栈顶;
Push:
调用Vector的addElement来完成;
Pop:
调用peek获取数组最后一个元素(即栈顶元素),并删除;
Peek:
获取数组最后一个元素;
性能:
是线程安全的;
支持LIFO操作;
二、 Set:
1. HashSet:
实现了set接口,基于HashMap实现:
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
由此可以看出HashSet的所有接口实现都是依赖于HashMap的实现(也就是对HashMap的一层包装),其中HashMap的key为HashSet的值,value为一个固定无意义的对象,如下代码:
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
备注:
HashSet不支持下标索引(也即不支持get(int)的方式获取元素);
HashSet是非线程安全的;
2. TreeSet:
与Hashset的区别是对排序的支持,其实现基于TreeMap实现:
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
public TreeSet() {
this(new TreeMap<E,Object>());
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
备注:
基于TreeMap实现,支持排序;
非线程安全;
三、 Map:
1. HashMap:
基于键值对的存储,实现了map接口,其元素存储不是顺序的,而是离散无序的,对于每个元素,可以通过其Key计算Hash值,然后将hash值与Hashmap数组的长度取余来计算元素应该存储在什么位置,hash算法的选取很关键,决定了数据存储是否均匀分布以及访问的效率;HashMap的存储可以认为是数组和链表的结合存储,同一个Hash值的元素以链表存储,所有的Hash值以数组存储;
冲突处理:当多个元素经过计算后的下标相同,则冲突发生,处理冲突常用的方法是采取链表的方法存储,因此查找元素时首先计算下标,然后遍历该下标下的链表,直至查找到元素为止;
扩容处理:当数组存储空间不够需要扩容时,需要根据扩容后的大小重新申请一块数组空间,原来存储的数据需要根据key的Hash值重新计算下标并存储,因此扩容时的效率是非常低的,使用时如果能预知大小就一次分配,避免后续多次分配导致的效率下降;
源码分析:
几个重要的成员变量:
transient Entry[] table;
存储数据的数组空间,每个元素的key值通过hash值计算出在这个数组中的下标,然后将数据存储进来,存储时每个元素是一个链表,数据也即存储到这个链表中;
transient int size;
数组实际存储数据的大小;
int threshold;
数据阀值,当数组存储的数据的大小超过这个指时,数组会扩容;
final float loadFactor;
加载因子,threshold=size*loadfactor,取值范围是0~1,如果值大了,内存空间利用率较高,但是数据访问的效率也下降了,如果值小了,数据访问的效率上升,但是内存空间的利用率也下降了;一般取系统默认值0.75;
transient int modCount;
数组数据修改的次数,创建时值为0,每次调用add/remove等修改数组数据时,modcount就加一,一般用在迭代时如果修改数据,会导致modcount和迭代器的exceptedmodcount的值不相同而抛出异常;
默认的构造函数会创建加载因子为0.75,数据阀值为12,大小为16的数组;
put:
分key为null和不为null的处理,不为null时先计算key的hash值,将hash值与数组长度取余得到下标,如果该下标对应的链表中有相同的key则替换value ,如果没有则将元素添加到链表的头部(也即table[i] = newEntry);
get:
根据key的hash值计算下标,然后在该下标的链表中查找指定元素;
remove:
根据key的hash值计算下标,然后在该下标的链表中查找指定元素,如果找到则从链表中删除;
keyset:
创建一个keySet对象,包装了对HashMap的访问;
性能:
扩容时要重新计算hash并复制对象到新数组中,因此效率会比较低,使用时要尽量避免让HashMap做扩容处理;
HashMap是非线程安全的,如果要线程安全就应该使用ConcurrentHashMap;
2. TreeMap:
实现了Map接口,并且支持排序;
private final Comparator<? super K> comparator;
支持排序的比较器;
private transient Entry<K,V> root = null;
数据以红黑树结构存储,root存储根节点;
private transient int size = 0;
数据元素个数;
private transient int modCount = 0;
修改次数;
put:
如果root为null,则将添加的元素存储到root中,否则则以红黑树的方式遍历左子树和右子树,并决定将数据存放到合适的位置;
treeMap是一个典型的基于红黑树的实现,因此要求key一定要有比较的方法,要么key实现了comparable接口,要么传入comparable实现;
get:
基于红黑树遍历算法从root开始找,直至找到指定的值并返回;
remove:
基于红黑树遍历算法从root开始找,直至找到指定的值并删除,如果未找到则直接返回;
备注:
TreeMap基于红黑树实现;
非线程安全;