JAVA学习笔记——集合
比较常用的集合一般有4种接口,每种接口可以由多种数据结构去实现。这样我们就可以在实现相同接口(功能)的情况下,根据具体场景选择不同的数据结构。
Interface | Hash Table | Resizable Array | Balanced Tree | Linked List | Hash Table + Linked List |
---|---|---|---|---|---|
Set | HashSet | TreeSet | LinkedHashSet | ||
List | ArrayList | LinkedList | |||
Deque | ArrayDeque | LinkedList | |||
Map | HashMap | TreeMap | LinkedHashMap |
1. List
ArrayList、LinkedList、Vector都实现了接口List。List接口是一个有顺序的集合,元素可以重复,可以通过索引去访问元素。
1.1 ArrayList
ArrayList类维护了一个数组,访问操作效率高,删除和插入操作效率低。
transient Object[] elementData; // non-private to simplify nested class access
当增加元素且ArrayList的容量不够时,需要对数组进行扩容,扩容使用Arrays.copyOf()
方法。
当删除元素时,会使用System.arraycopy()
方法。
以上两个copy方法都是蛮费时间的。
1.2 LinkedList
LinkedList类维护了一个双端链表,first和last分别指向链表的头尾Node。
1.3 Vector
Vector类与ArrayList类非常相像,主要的区别是:Vector是线程安全的,ArratList不是线程安全的。
protected Object[] elementData;
1.3.1 Stack
Stack类继承了Vector类,因为Vector类维护了一个数组,在数组基础上实现LIFO的功能还是很方便的。但这也会带来2个问题:
- Stack类是基于数组实现的,push和pop操作的开销会比较大。
- Stack可以使用Vector设置任意位置元素的方法
setElementAt(E obj, int index)
,那么Stack就不是纯粹的栈,栈只能操作栈顶的元素。
JDK文档建议,如果要使用栈这种数据结构,应该优先使用Deque<Integer> stack = new ArrayDeque<Integer>();
2. Deque
Deque是Double End Queue
的意思,该双端队列即可以用作FIFO的队列,也可以用作LIFO的栈。Stack类也是栈,但它有些缺点,应该优先使用Deque作为栈。
2.1 LinkedList
LinkedList类维护了一个双端链表,first和last分别指向链表的头尾Node。
2.2 ArrayDeque
ArrayDeque是在JDK1.6中才出现的。以数组的形式实现双端队列。
JDK文档建议:
- 当要使用栈时,ArrayDeque比Stack要高效;
- 当要使用队列时,ArrayDeque要比LinkedList要高效。
第1点的原因在上面说过解释过,第2点的原因有以下两点:
- LinkedList每次push一次需要new一个Node,每次pop一次会产生一个Node垃圾等待回收。ArrayDeque由于是数组,push不会new,pop不会产生垃圾,虽然在容量不够时,还是会做扩大一倍容量的操作。
- LinkedList在内存中是分散的,所以缓存命中率会比较低。
3. Map
3.1 HashMap
HashMap(散列表)是一个增删、查找都高效的数据结构。查找散列表一般分为2个步骤:
- 计算键的hash值;
- 找到相应的位置并解决冲突,放入该键值对。
解决冲突有2种方法:拉链法和开放地址法(线性探测、二次探测、随机探测)
java中的HashMap采用的是拉链法,这是数组和链表的结合形式。
HashMap中的put()方法:
- 计算哈希值。通过hashcode()计算出该对象key的hash值
- 哈希值取模。对hash值进行取模操作
hash%length
,length是数组的长度。该长度应该是2的幂方,这样我们就可以对取模操作进行优化hash&(length-1)
- 插入链表。得到模值之后,就将其作为索引找到相应的链表表头,该条链表中存储hash值一样的键值对,键值对存储在
静态内部类Entry
中。Entry保存键、值以及指向下一个键值对的next。
HashMap中的resize()方法:
当entry的数量大于阈值时,开始resize()。其中阈值是容量乘以加载因子。
resize()之后,table长度变为2倍,重新取模,重新链表插入。
负载因子:当hashmap中的有效容量除以总容量大于负载因子时,hashmap的容量就会翻倍。java的hashmap默认容量是16,负载因子时0.75。
容量:
Java8对HashMap的改进:当链表的长度大于一定值时,链表就转换为红黑树,实现了快速的增删改查,时间复杂度从顺序查找的O(n)优化到O(logn)。
HashTable与HashMap非常相似,他们有以下三点区别:
- HashTable是synchronized的,是线程安全的,多个线程可以共享;HashMap相反
- HashTable是synchronized的,在单线程下速度会比较慢;HashMap的速度比较快
- HashTable不能接受null的键和值;HashMap则可以
【Reference】