Java集合是日常开发中经常使用到的功能。不同的集合中封装了不同的数据结构,开发人员只需要知道集合类中的方法是做什么用的就好了,如果愿意完全可以忽略其具体实现。但是作为有追求的程序员,还是有必要了解的。
Java中集合可以分为两大类:
- 线性集合:Collection
- 线性集合是由多个元素组成的单列集合。
- Collection是所有线性集合的基本接口
- 映射集合:Map
- 映射集合的每个单元由一组键-值对组成,键和值之间存在映射关系。比如通过键可以检索到对应的值
- Map是所有映射集合的顶级接口
Collection
现在已经知道Collection是一个接口,那么它的哪些实现类是我们经常用到的呢?
List->ArrayList
ArrayList指数组列表,它的数据结构是可变长度的数组。优点是可以直接通过索引查找和修改列表中的元素,并且由于数组在内存中是一段连续的存储空间,所以使用for循环遍历元素的速度也很快。但是它的缺点也很明显,由于是一段连续的内存空间,所以在做添加和删除操作的时候,所有相关的元素都要前移或后移,因此添加和删除元素的效率相对较低;元素数量越多,增、删效率越低。
ArrayList的添加和删除操作:
添加
删除
ArrayList如何实现可变长度的数组:
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
Arrays.copyOf()第一个参数表示原数组,第二参数表示扩容后的新数组。copyOf方法的作用则是将老数组中的数据拷贝到新数组中。这样就完成了一次数组扩容。
ArrayList采用归并排序算法对数组进行排序,平均时间复杂度线性对数阶O(nlogn),空间复杂度常数阶O(1),稳定性好。ArrayList有序可重复。
List->LinkedList
LinkedList,链式列表,简称链表。这种集合采用双向链表的结构,链表分为单向链表和双向链表。
- 单向链表:有多个节点,从头结点开始直到尾节点每一个节点都会有一个指针指向下一个节点,这样便形成了一个单向链表。可以把链表比作为一种游戏:老鹰捉小鸡,表演小鸡的小朋友们就是单向链表。
- 双向链表:同样有多个节点,并且同样上一个节点会有一个指针指向下一个节点,但是不同的是,双向链表中,后面的节点也会有一个指针去指向前面一个节点。可以把双向链表也比作为一种游戏:丢手绢,那一圈小朋友就是双向链表。
链表的查询操作没有数组列表的效率高,但是在数据量较大的情况下,它的添加和删除操作效率要优于数组。
链表的添加和删除操作
添加
删除
对于链表来说,添加和删除操作只需要改变相关节点的指针指向,而不需要关心节点的位置。链表的每个节点可以说都独立存在于内存中某个位置,只是它有两个指针分别指向了前置节点和后置节点的引用,正式这种引用关系组成了一个完整的链表。
链表的排序算法为归并排序,但是需要注意的是,Java并没有直接对链表本身做归并排序,而是将链表先转为数组再对数组进行归并排序,最后再将数组转为链表。LinkedList列表有序可重复。
Set->HashSet
HashSet是一个散列集,散列集是一种由数组+链表组成的数据结构。数组中存放存储了元素的链表,数组的下标为 (链表中元素数据的HashCode % 数组的长度) ,当添加一个元素的时候就通过这种方式计算下标,然后将元素添加到指定位置存储的链表中去,如果当前位置还没有数据则直接添加,否则还需要跟链表中的每个节点中的元素做对比,如果不存在相同的元素才会添加。根据以上描述可以推断出,散列表是一种无序且没有重复元素的数据结构。
散列表
相较于LinkedList而言,在数据量大的情况下,散列表有比链表更好的查询、添加、删除性能,因为可以快速计算出元素所在的数组下标。
还有一个细节问题,如果数组中不为空的单元达到了75%(默认),散列表就会对数组进行扩容并对当前存储的元素进行再散列(重新计算元素存储到新数组的下标,并存储)。
Set->TreeSet
TreeSet指树集,所以它是一个树结构,并且是红黑树,红黑树是二叉排序树的一种。这种树结构在进行添加删除操作时效率比二叉平衡树(AVL树)要高,因为红黑树可以做到任意的添加删除操作都在三次旋转内解决不平衡问题。红黑树查询效率略低于AVL树,因为红黑树并不是一种绝对的平衡二叉树,并不保证绝对平衡,所以它的树的高度会高于AVL树,查询效率自然低一些。TreeSet是一种有序且无重复元素的数据结构。
红黑树
树结构的添加元素操作要比散列表慢,因为散列表是不需要考虑元素的存储顺序。但是可以运用红黑树查询重复数据且效率比数组和链表高很多。
Queue->Deque->PriorityQueue
简单来说,Deque扩展了Queue的接口,允许队列在头部和尾部都能够添加或删除元素,形成了双端队列。PriorityQueue是优先队列,它是通过小顶堆来实现的,优先队列的典型用法是任务调度,可以随机的将任务添加到队列中,每添加一个新任务,都会将队列中优先级最高(值越小,优先级越高)的任务删除。做完以上操作后再重新组织堆。始终保持最小值在堆顶。
小顶堆
堆是以数组的形式存储的,所以在直接遍历的时候并不是有序的,但是调用poll()方法让元素出队列的是有序的,所以优先队列有序且可以有重复元素,但元素不能为null。
@Test
public void ceshi(){
PriorityQueue<Integer> p = new PriorityQueue<>();
p.add(3);
p.add(2);
p.add(6);
p.add(1);
System.out.println(p);
while (!p.isEmpty()){
System.out.print(p.remove()+",");
}
}
运行结果:
[1, 2, 6, 3]
1,2,3,6,
Map
HashMap
HashMap实现了Map接口,它是以一组键值对的形式存储数据的。实际上HashMap本质上与HashSet是一样的,其实确切的说应该是HashSet是依赖于HashMap来实现的。
HashSet代码片段
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing {@code HashMap} instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
...
}
可以看到当HashSet去调用add方法时,实际上是在使用HashMap。HashSet其实就是使用HashMap中的key作为自身存储的值。所以与其说HashSet是使用数组+链表实现,不如说HashMap是由数组+链表实现的。HashMap是无序并且键不可以重复。
其实还有一个细节:如果数组中有一条链表的节点数超过8,并且数组的长度>=64,那么这个链表就会变成一颗红黑树。
TreeMap
TreeMap是一种数映射,它是TreeSet的底层实现,同样的TreeSet只是使用了TreeMap中的key。
TreeSet部分代码
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
private static final Object PRESENT = new Object();
public TreeSet() {
this(new TreeMap<>());
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
所以TreeMap它的底层数据结构是红黑树,他是有序集合并且键不可重复。