集合分为两大能力块
Conllection阵营和Map阵营
Collection阵营的数据是一个一个的存储,Map阵营的数据是一对一对存储的。
Collection阵营
Collection有两个儿子,一个叫List,一个叫Set,他们都继承了Collection的能力,但是自身的发展方向不同。
List:
- 他是有序的,按照元素进入的顺序存储数据。
- 可重复,进入List的数据可重复,且可以为null
- 可以使用Iterator迭代器取出所有的元素,再逐一遍历,也可以使用get(int index)进行随机访问。
Set:
- 他是无序的,不是按照元素进入的顺序进行存储的
- 存入的元素不可重复
- 只能使用Iterator迭代器取出所有的元素。
- 对于元素查询的效率底下,但是对元素的插入和删除的效率及高。
List接口有三个能力者获得了他的技能
分别是ArrayList、LinkedList、Vector
Array List:
- 底层是Object类型数组,一个挨着一个紧密排列。
- 在Array List初始化的时候,就在内存空间中开辟出来一块指定大小的空间来存储元素,即使元素未填满这个空间,他所占用的空间也不会随元素的数量很少而变小。
- 底层的代码不同步执行,因此线程并不安全。
- 在JDK1.7以前:在调用构造器的时候,数组的初始化长度为10,在元素填满之后自动扩容,扩容的长度为原来数组的1.5倍。
- 在JDK1.8以后:在调用构造器的时候,底层初始化为{},只有在调用Add方法后底层数组才重新赋值为新数组,新数组的长度为10,扩容长度也是为原来的1.5倍。
- 数组存储的优点是:查询效率高,允许有重复元素。缺点是:插入和删除的效率低。
调用add方法
当数组中的10个位置都满了之后,对数组进行扩容,扩容长度为原数组的1.6倍
Vector:
- 底层是Object类型的数组,int属性表示数组中的有效长度。
- 底层代码同步实现,因此线程安全。
- 初始化长度为10,扩容长度为原来数组的2倍。
构造方法:
扩容方法:
Linked List:
- 底层采用的是双向链表(JDK1.6以前是循环链表,1.7之后取消了循环)
- 底层实现不同步,因此线程不安全。
- 他因为是链表存储,所以在头尾位置插入元素速度极快,但是在指定位置插入元素,需要先遍历数组,找到n位置再插入元素,因此时间复杂度为O(N)。
- 不支持快速的随机访问,不管是通过值查找还是通过索引查找,都需要遍历整个数组找到元素所在位置。
底层原理图:
LinkedList源码解析:
public class LinkedList<E>{//E是一个泛型,具体的类型要在实例化的时候才会最终确定
transient int size = 0;//集合中元素的数量
//Node的内部类
private static class Node<E> {
E item;//当前元素
Node<E> next;//指向下一个元素地址
Node<E> prev;//上一个元素地址
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
transient Node<E> first;//链表的首节点
transient Node<E> last;//链表的尾节点
//空构造器:
public LinkedList() {
}
//添加元素操作:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {//添加的元素e
final Node<E> l = last;//将链表中的last节点给l 如果是第一个元素的话 l为null
//将元素封装为一个Node具体的对象:
final Node<E> newNode = new Node<>(l, e, null);
//将链表的last节点指向新的创建的对象:
last = newNode;
if (l == null)//如果添加的是第一个节点
first = newNode;//将链表的first节点指向为新节点
else//如果添加的不是第一个节点
l.next = newNode;//将l的下一个指向为新的节点
size++;//集合中元素数量加1操作
modCount++;
}
//获取集合中元素数量
public int size() {
return size;
}
//通过索引得到元素:
public E get(int index) {
checkElementIndex(index);//健壮性考虑
return node(index).item;
}
Node<E> node(int index) {
//如果index在链表的前半段,那么从前往后找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//如果index在链表的后半段,那么从后往前找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
}
Array List与Linked List的区别?
- Array List底层采用的是数组,属于紧密结构。Linked List底层采用的是双向链表,属于跳转结构。
- Linked List不支持快速随机访问,因为不管是值查询还是索引查询都需要先遍历整个数组找出元素的所在位置;Array List支持快速随机访问,可以通过元素的序号快速获取元素(对应get(index) 方法)。
- 插入删除元素位置的影响
- Array List底层采用数组,调用add方法时默认在数组的尾部添加元素,这时时间复杂度为O(1),但是如果在数组的指定的i位置添加或者删除元素,就要把数组第i位置之后的所有元素都要向前或者向后移一位,因此时间复杂度为O(N)。
- Linked List底层采用链表存储,因此对比ArrayList,他在数组的首尾两端插入元素就很快,但是如果要在指定的i位置插入或者删除元素,时间复杂度也为O(N),因为他要把元素移动到指定的地方再进行插入操作
- Array List对于空间的浪费在于,他的末尾必须预留一定的存储空间,而Linked List对于空间的花费则在于他的每一个元素都要存储上一个元素的位置和下一个元素的位置,因此每一个元素都比Array List的要更加大。
Set接口有两个能力者获得了他的技能
分别是Tree Set和Hash Set,其中Hash Set还有一位能力者学习了他的技能,那就是Linked Hash Set。
Tree Set
- 插入的元素唯一,无序(不会按照元素输入的顺序输出),有序(按照升序进行遍历)
- 底层使用的是红黑树
- 通过内部比较器或者外部比较器对元素进行存储。
- 在向Tree Set中放入自定义类的时候,自定义类必须实现比较器
底层原理图:
放入自定义类的时候,自定义类必须实现比较器接口,并且定义比较方法:
如定义学生类,我想通过学生id对学生进行排序(内部比较器):
@Data
public class Student implements Comparable<Student> {
private int id;
private int age;
private String name;
@Override
public int compareTo(Student o) {
return this.getId()-o.getId();
}
}
如定义学生类,我想通过学生id对学生进行排序(外部比较器):
@Data
public class Student {
private int id;
private int age;
private String name;
}
class BiJiao implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.getId().compareTo(o2.getId());
}
}
Ps:实际开发利用外部比较器比较多,因为扩展性好(多态的实现)
Hash Set:
- 元素不重复且无序。
- 底层采用哈希表,通过hashCode()和equals()对元素进行比较。
- 线程不安全
底层原理图:
Linked Hash Set:
Linked Hash Set就是在HashSet上添加了一个总链表,将所有的元素串联起来,方便对元素有序的遍历。
底层原理图:
Map阵营
Map有三个能力者实现了他的功能:Hash Map、HashTable、Tree Map
都是按照key-value的形式存储数据
Hash Map:
- 元素无序且唯一
- 按照key进行总结,因为底层key遵循哈希表的结构(数组+链表)
HashMap在初始化的时候,底层默认长度为16,其中定义了一个float类型的变量,其值为0.75,这个变量的作用是默认装填因子,加载因子是标是Hash表中的元素填满程度,意思就是:当Hash表中的元素超过16*0.75这么多时Hash表就会进行扩容,扩容的大小为原来的2倍。装填因子0.75这个值是为了避免哈希碰撞和空间浪费寻找到的一个平衡的折中值。
HashMap中的元素对象图:
这个对象为单向链表。在插入元素时,底层先根据key值计算出哈希值,再把哈希值放在主数组对应的位置上。
因为Hash Map的底层遵循哈希表结构,因此对于元素的检索插入删除的操作很快。
Tree Map:
- 底层元素唯一且有序。
- key遵循二叉树的特点,放入集合的key数据对应的类型内部一定要实现比较器(内部比较器,外部比较器)
HashTable和Hash Map的区别:
- HashMap是非线程安全的,Hash Table线程安全,是因为其内部的方法基本都经过了synchronized修饰。
- 因为线程安全问题,Hash Map的效率比Hash Table要高。另外Hash Table基本被淘汰,可以了解Concurrent Hash Map。
- Hash Map可以存储null的key和value,但是null的key只能有一个,null的value可以存在多个。而Hash Table不允许有null的键和值。
- 在创建时:Hash Table的默认长度为11,之后的每次扩容的容量变为原来的2n+1,Hash Map的初始默认长度为16,之后的每次扩容长度为原来的2倍。
- 在JDK1.8以后,Hash Map在遇到链表长度超过阈值时(默认为8),会将链表转换成红黑树,从而减少搜索时间,(在转换成红黑树之前会进行判断,如果当前数组的长度小于64,则会优先选择扩容,否则转换成红黑树),而Hash Table没有这样的机制。
Hash Map和Concurrent Hash Map的区别:
Concurrent Hash Map对整个桶数组进行了分割分段,然后在每一个分区上加上了lock锁进行保护,这样就相对于Hash Table的synchronized锁的粒度更精细了,并发性能更好,而HashMap没有锁机制,因此线程不安全。
Hash Map的键值对允许有Null,但是ConcurrentHashMap不允许。
本文出于本人对所学知识的自己现阶段所能理解的总结,其中的图和部分知识点来源于马士兵教育的赵姗姗老师讲解。如有不足或者错误希望能有补全。