Java的集合主要分为三大块:List、Map、Set
List
常用的List类有:ArrayList、LinkedList
List接口需要实现一下方法:
ArrayList
ArrayList的默认容量为10
每次扩容会变为之前的1.5倍
ArrayList中的数据减少时,容量不会减少
ArrayList存储元素使用数组:
transient Object[] elementData;
ArrayList中的size指的是集合中元素的个数:
private int size;
ArrayList通过索引访问元素很快,时间复杂度为O(1):
public E get(int index)
ArrayList在尾部插入元素,时间复杂度为O(1):
public boolean add(E e)
ArrayList在尾部删除元素,时间复杂度为O(1):
public E remove(int index)
index为size-1时ArrayList从中间插入元素,需要搬移元素,平均时间复杂度为O(n):
public void add(int index, E element)
ArrayList从中间删除元素,需要搬移元素,平均时间复杂度为O(n):
public E remove(int index)
ArrayList可以求并集、交集、单向差集
- public boolean addAll(Collection<? extends E> c)
public boolean retainAll(Collection<?> c)
public boolean removeAll(Collection<?> c)
为什么ArrayList中存储元素的数组elementData是被transient的?
因为ArrayList中的elementData是可以扩容的,但是它的长度可能大于或等于实际元素的个数,如果,在序列化的时候将elementData自动序列化,那么会有多个空元素被序列化到磁盘中,所有源码中使用transient禁止序列化,并且在
private void writeObject(java.io.ObjectOutputStream s)
中通过size的个数对元素进行序列化
LinkedList
- LinkedList继承了Queue接口,实现队列的功能
- LinkedList继承了Deque接口,实现双端队列的功能(队列、栈)
- LinkedList内部使用双向链表实现List的功能
- LinkedList没有实现RandomAccess接口,所以访问非队列首尾的元素比较低效
- LinkedList中用size记录元素个数,并有链表首节点first和链表尾节点last
- LinkedList在队列首尾添加、删除元素效率高,时间复杂度为O(1):
private void linkFirst(E e)
void linkLast(E e)
private E unlinkFirst(Node<E> f)
private E unlinkLast(Node<E> l)
- LinkedList在中间添加、删除元素效率低,时间复杂度为O(n):
public void add(int index, E element)
public E remove(int index)
Map
常用的Map有:
- HashMap
- LinkedHashMap
- ConcurrentHashMap/HashTable
- TreeMap
HashMap
HashMap的内部结构是数组+链表(jdk1.7)。链表长度大于8时,且扩容达到64长度数组时,链表会转化为红黑树(jdk1.8新增)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16transient Node<K,V>[] table;//数组
//链表的结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
//红黑树结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}为什么用数组+链表
当新插入的元素放入数组中,数组当前位置为空时,直接插入;如何当前位置已经存在元素,则对当前链表进行搜索,找到最后一个链表元素,将新插入的元素放在最后一个链表元素的后面。这样的做法是为了解决Hash冲突的问题。
HashMap的hash()如何设计,为什么?
将Key的hashCode与hashCode的高16位进行异或得到。
因为在插入数据的时候是通过2的幂次进行散列操作,如果不与高16位进行异或,那么会有更高的hash冲突。
hash冲突你还知道哪些解决办法?
比较出名的有四种(1)开放定址法(2)链地址法(3)再哈希法(4)公共溢出区域法
HashMap在什么条件下扩容?
数组中所有元素大于数组长度*扩容阈值
为什么数组长度/扩容的容量为2的幂次?
为了存取高效、减少碰撞、让数据均匀分布在每个链表中,所以元素对于数组的位置计数使用元素key的hash对数组长度进行取模操作,并使用位移操作进行计算。
1
2hash%length
hash&(length-1)当length为2的幂次($2^n$)时,length的二进制位1000…00,那么length-1位111…11,这样在&hash的时候可以减小碰撞。
为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?
因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。
当元素小于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
ConcurrentHashMap
- 是HashMap的线程安全版本,内部结构相同
- 效率高于HashTable