Java集合框架
https://juejin.cn/post/6845166891397300238#heading-13
说一说 List Set Map的区别
List 存储的元素是有序的,是可以重复的
Set 无序的,不可重复的
Map 一对一对的,双列集合,key是无序的,不可重复的,value是无序的,可重复的
List
存储有序的,可重复的数据,动态数组,替换原来的数组
https://juejin.cn/post/6845166891397300238
- ArrayList 线程不安全,底层是Object数组
- LinkedList 线程不安全,底层是双向链表
- Vector 线程安全,底层也是Object数组
ArrayList 底层原理
new 一个arrayList时,底层数组初始化为{},当第一次add的时候才创建了长度为10的数组,并将数据添加到数组中,当数组容量不够时,就扩容,创建了一个长度为原来的1.5倍的新数组,并将原数组的数据复制到新数组中
ArrayList和LinkedList的时间复杂度的比较
增
add(E e) 都是在末尾添加元素,时间复制度都是O(1)
//对于LinkedList的源码,保存了LinkedList的first和last属性
transient Node<E> first;
transient Node<E> last;
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;
}
}
add(int index,E e) 在指定位置添加元素
arraylist 找到这个元素的时间复杂度是O(1) 但是它添加元素的话,要将这之后的元素后移,时间复制度是O(n)
LinkedList 添加元素的时间复杂度是O(1) 但是找到一个元素的时间复杂度是O(n)
删
remove(int index)
ArrayList 找到这个元素的过程是 O(1),但是 remove 之后,后续元素都要往前移动一位,所以均摊复杂度是 O(n);
LinkedList 也是要先找到这个 index,这个过程是 O(n) 的,所以整体也是 O(n)。
改和查
因为数组支持随机访问,所以ArrayList的效率高
Vector和ArrayList的区别
首先是Vector是线程安全的,其次时Vector扩容时是扩容为原来的两倍
Queue 队列
队头出,队尾进的一种数据结构,可分为普通队列,双向队列Deque,优先队列PriorityQueue
比如队列空了,那 remove() 就会抛异常,但是 poll() 就返回 null;element() 就会抛异常,而 peek() 就返回 null 就好了。
add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,则返回false
poll 移除并返问队列头部的元素 如果队列为空,则返回null
peek 返回队列头部的元素 如果队列为空,则返回null
put 添加一个元素 如果队列满,则阻塞
take 移除并返回队列头部的元素 如果队列为空,则阻塞
Set
无序的,不重复的
HashSet 采用HashMap的key来存储元素
LinedHashSet 这是一个HashSet+LinkedList的结构,特点就是既拥有O(1)的时间复杂度,又能够保留插入的顺序
TreeSet 采用红黑树结构,可以有序,可以用自然排序或者自定义比较器来排序
Map
HashMap(重点)
底层数据结构是数组(哈希桶数组)+链表+红黑树
首先new 一个hashmap的时候,并没有创建一个长度为16的数组,首次调用put方法的时候才创建的,调用key所在类的hashcode去计算哈希值,得到在数组中的存放位置,如果位置上有数据的话,就比较hash值,如果hash值相同的就通过key所在类的equals进行比较,如果相同就插入失败。发送hash冲突,通过链地址法解决,当链的长度大于8时,就将链表转换成红黑树,之后链的长度小于6时,又退化为链表
数组里面存储的是 Node对象
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node<K,V> next; //链表的下一个node
Node(int hash, K key, V value, Node<K,V> next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
扩容机制
当超过临界值时,扩容,而不是满了再扩容,默认是将长度扩容为原来的2倍,并将原来的数组重新计算hash值,插入到新数组
扩容的临界值 = 容量*填充因子 : 16 * 0.75 = 12
Hashtable
就是线程安全的hashmap,当加锁的时候,对整个hashmap进行加锁
HashMap和HashTable的区别
HashMap是线程不安全的,HashTable是线程安全的
HashMap允许key和value为null,而hashtable不允许为null
这是因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。
如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。
初始容量大小和每次扩容的大小不同:hashtable的初始大小为11,之后每次扩容为原来的2n+1,,负载因子都是0.75
ConcurrentHashMap
直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和cas来操作
跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性
put操作是如何保证线程安全的?
- 根据key计算出hashcode
- 判断是否需要进行初始化
- 找到当前key定位出的node,如果为空表示当前位置可以写入数据,利用cas尝试写入,失败则自旋保证成功
- 如果当前位置的hashcode == moved == -1 则需要扩容
- 如果都不满足,则利用synchronzied锁写入数据