Collection
List
最大的特点是有序,可重复。
ArrayList:
底层用数组实现,默认的大小为10,每次扩容为原来容量的1.5倍;
int newCapacity = oldCapacity + (oldCapacity >> 1);
查找的过程为O(1),但是增加和删除元素均摊复杂度都是O(n),且要移动元素,如果在尾部操作,则效果较好;
LinkList:
底层链表实现,查找和增删的复杂度都是O(n),只有增删的操作较多,且都不在尾部才考虑使用;
Vector:
线程安全,底层数组实现,使用了太多的Synchronized,因此目前已被弃用;和ArrayList一样需要扩容,但扩容的大小是默认是原有的2倍
Queue & Deque
Queue 是一端进另一端出的线性数据结构;而 Deque 是两端都可以进出的;
Set
HashSet:采用HashMap的key来存储元素,无序,基本操作都是O(1)的复杂度。
LinkedHashSet:是一个HashSet+LinkedList的结构,既拥有O(1)的复杂度,又能够保留插入的顺序。
TreeSet:采用红黑树结构,可以用自然排序或者自定义比较器排序,查询速度没有HashSet快
每个set的底层实现都是对应的map,value上放persent,是一个静态的object,每个key都指向这个object;
HashMap:
由数组和链表构成的数据结构,每个数组都存放了key-value这样的实例,在Java7叫entry,在Java8叫node,本身所有的位置都是null,在put是会根据key的hash去计算一个index的值,即插入的位置。由于数组的长度是有限的,而且hash本身存在概率性,所以可能有两个对象hash到一个值上,就形成了链表;
头插与尾插:
Java8之前是头插法,新来的值会取代原有的值,原有的值推到链表中去;好处是解决循环链表的问题,头插法在resize扩容时,如果是多线程操作,可能会导致循环链表的形成。即便解决了循环链表的问题,在多线程环境下还是不建议使用HashMap,因为无法保证上一秒put的值下一秒get还是原值,总归是线程不安全的。
HashMap扩容:
由于数组容量是有限的,达到一定数量就会扩容(resize),由两个因素决定,capacity::当前长度,默认16;LoadFactor:负载因子,默认值0.75F。
扩容步骤分为两步,首先创建一个新的Entry或Node数组,长度是原来的两倍;然后便利数组,把所有的节点重新Hash到新数组;由于长度扩大后,Hash的规则也会改变
Java1.8中capacity默认大小为1<<4的原因:
在初始化Map的时候,最好指定原始大小,而且最好是2的幂数,为了方便位运算,位与运算比算数计算效率高很多,之所以选择2的幂数,是为了实现均匀分布,length-1所有的二进制位全为1,只要HashCode本身分布均匀,拿hash算法的结果就是均匀的。
元素定位:
get()方法通过hash计算找到元素所在的链表,然后通过equals方法进行比较确认,因此在重写equals方法时,最好重写hashcode方法,以保证相同的对象返回相同的hash值。
ConcurrentHashMap:
在多线程的场景下,有Collections.synchronizedMap(Map),HashTable,ConcurrentHashMap三种方式线程安全;
Collections.synchronizedMap(Map):内部维护了一个普通的Map对象和排斥锁mutex:
在调用方法时传入一个Map和mutex,在操作map的时候,就会对方法上锁;
HashTable:在操作数据的时候都会上锁,因此效率很低;
CurrentHashMap:采用分段锁技术,内部维护了一个Segment数组,segment继承于ReentrantLock,每当一个线程占用锁访问Segment时,不会影响到其他的segnent;put数据时,先定位到Segment,再进行put操作,先尝试自旋,自旋次数达到MAX_SCAN_RETRIE则改为阻塞锁获取,保证能获取成功,JDK1.8之后,采用CAS+synchronized保证并发安全性;get定位到segment之后直接通过hash获取数据,不用加锁,因此效率很高;
Java8之后引入红黑树,当链表大于一定值之后转换为红黑树,默认是8;
size方法总体流程:
先采用乐观的方式统计,如果在两次记录的数量相同,则结束,如果不同再次尝试,再次不同则对所有的segment枷锁统计;