集合
1.ArrayList
- 底层是一个动态的对象数组,拥有数组的特性;
- 查询的时间复杂度是O(1),可以根据数组的index,直接获取到对应的值;增加和删除元素效率较低,因为数组元素可能会重排;
- 扩容:ArrayList在调用无参构造方法时创建的是一个长度为0的空数组,当调用add()方法添加元素时,ArrayList才会触发扩容机制,初始扩容到10,也可以在构造方法中指定初始长度,再次扩容会扩容到上一次容量的1.5倍。
2.LinkedList
- 底层结构是一个带头、尾指针的双向链表,可以快速的对头/尾节点 进行操作;
- 查询的时间复杂度近似O(N),需要逐个遍历;
- 指定位置插入和删除元素的效率较高,只需要修改两边元素的指向地址就行了。
3.HashMap
-
1.7之前是由 数组,链表组成的,jdk1.8后是由数组,链表,红黑树组成的;
-
添加元素:将key的hash值通过哈希算法/散列算法,转换成数组的下标,如果下标位置上没有元素,就把value添加到这个节点上,如果下标对应的位置上有值了,就将key的hash值与对应下标上的节点的hash值进行比较,如果hash值相同,就覆盖,如果逐个遍历后去全部不同,就将节点添加到末尾;
-
获取元素:通过上一步哈希算法转换成数组的下标之后。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value;
-
map容量的大小可以在构造方法时设置,默认大小为16,数组中每一个元素就是一个链表,jdk7之前链表中的元素采用头插法插入元素,jdk8之后采用尾插法插入元素;
-
链表转红黑树条件:红黑树是为了优化链表过长导致遍历链表的时间复杂度增加而引入的,链表长度大于8,且hashmap总大小大于64的时候链表会转化红黑树;
-
扩容触发条件:当添加某个元素后,map中的元素个数大于了map容量 * 0.75(负载因子,默认0.75,也可自己设定),容量为原来的两倍;
-
扩容步骤:创建一个新的entry空数组,长度是原来的两倍,然后遍历原数组,把所有的entry重新hash到新的数组;
-
哈希冲突:因为数据是无限的,而通过散列算法计算之后得到的结果的范围是有限的,所以总会出现不同的数据,hash值经过计算之后得到的值是一样的
三种解决办法:(1)线性探测法: threadlocal 该位置已经有值了,向右线性探测空闲位置,存入元素;
(2)链式寻址法 :将存在hash冲突的key通过单向链表的方式进行存储,如hashmap(链式寻址法+单向链表+红黑树),;
(3)再hash法 :多个hash函数计算。
4.HashSet
- HashSet 实现了Set接口,是由HashMap实现的
- 可以存放null值,但是只能有一个null,不能有重复的元素
- HashSet不能保证元素的存取顺序一致
5.ConcurrentHashMap
- JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构,当在链表长度达到 8 的时候,数组扩容或者将链表转换为红黑树;
- 存放元素:根据 key 计算出 hash 值,定位到 Node,拿到首节点 f,判断首节点 f,如果为 null ,则通过 CAS 的方式尝试添加,如果为
f.hash = MOVED = -1
,说明其他线程在扩容,参与一起扩容,如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入; - 获取元素:根据 key 计算出 hash 值,判断数组是否为空,如果是首节点,就直接返回,如果是红黑树结构,就从红黑树里面查询,如果是链表结构,循环遍历判断
- 因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。