0.前言
java集合实现两大接口,coliection与map,其中list与set接口实现coliection接口,而hashmap等具体类实现map接口,除此之外,coliections为集合的工具类,提供了更多的方法
参考博文:集合复习
整体结构:
collection:
map:
1.Collection
1.List
Java的List是非常常用的数据类型。List是有序的Conllection。List主要有三个实现类:ArrayList,Vector和LinkedList。
1. ArrayList(数组)
- 内部是通过数组实现的,使用无参构造生成时,默认容量为0,第一次添加扩容为10,随后再次扩容机制为新List长度=原List长度*1.5(原list长度1.5倍)


- 允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组的大小不满足时需要增加存储能力,就是要将已经有的数据复制到新的存储空间中。
- 当从 ArrayList 的中间位置插入或删除元素时,需要对数组进行复制,移动,代价比较高。因此,它更适合随机查找和遍历,不适合插入和删除。
- 内部使用的是transient Object[] elementData; 来存储数组,transient 瞬间修饰,表示属性不会被序列化
1.扩容机制
当添加新元素时,会先判断当前数组中的元素个数是否==数组大小,如果满足条件,则对数组扩容
调用数组扩容函数,随后将原数组数据复制到新数组中
对数组扩容时将原数组长度右移1位(原值/(2*n)),并加上原数组长度,使得新数组长度位原数组长度的1.5倍,如果调用的是无参构造生成的list,数组为初始数组,第一次就就将其扩大到10,否则返回新数组的长度
2. Vector(数组实现,线程安全)
- 和ArrayList 一样也是通过数组实现的,默认容量为10,扩容机制为新Vector长度=原Vector长度*2,源码与ArrayList类似
- 支持线程的同步,即某一刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费
-
3. LinkedList(链表)
LinkedList 使用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。
LinkedList 还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾的元素,可以当作堆栈、队列和双向队列使用。
2.Set
存储的元素无序且不可重复,所以最多只允许存在一个null
对象的相等性本质是对象的 hashCode 值(Java是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象是为相等的,就必须覆盖 Object 的 hashCode() 方法和 equals() 方法。
1. HashSet(Hash表)
哈希表存放的是哈希值,底层是HashMap(使用key值存数据,value为空)
- HashSet 存储元素的顺序并不是按照存入的顺序(和 List 显然不同)而是按照哈希值来对所有数据进行存取
- 元素的哈希值是通过元素的 hashCode()方法来获取的,HashSet 首先判断两个元素的哈希值,如果哈希值相同,接着会比较 equals() 方法,如果 equals() 结果为true,HashSet 就会将这两个元素视为同一个元素。如果 equals() 为false就不是同一个元素。
- HashSet 会通过 hashCode 值来确定元素的位置。当元素的 hashCode 相同,但 equals 不同,则会在这一个hashCode 的位置上存放多个元素。(就是在同样的哈希值下顺延,也就是哈希值一样的存一列。)
- 创建Set实际上就是创建HashMap
添加与扩容机制
因为HashSet底层是HashMap,所以其使用的方法与逻辑与Map是一致的
整体流程:
Set添加方法时,调用的时Map的添加方法,因为map是键值对形式,所以还需要传入一个空的对象占位
(hashMap部分源码)
由源码可知,根据hashcode()计算出的hash并不是最终的hash
代码太长,所以直接复制
:::danger
ps: equals()不一定是判断内容,每个类equals()标准都可能不一样
ps: 此处size指的是map中存放元素的个数,临界值则是节点数组的临界值,即不是占用了75%的数组索引才扩容,而是只要总数据超过就扩容(添加完第13个才扩容);size并不是占用新的数组索引才增加,而是每存放一个元素就增加
:::
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i; //创建辅助变量//1.如果节点数组为空,就初始化数组,大小为16if ((tab = table) == null || (n = tab.length) == 0) //table为节点数组n = (tab = resize()).length;//2.根据hash值计算在数组中的索引,且如果计算出的索引为空(意味着节点没有值),就直接存入if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//3.如果数组中索引位置已有节点,就执行以下代码//3.1先判断该处索引位置的第一个节点的hash值是否与要插入的hash值相等//然后再满足以下条件之一://1.两个节点的地址是否相等(==)//2.两个节点的equals对比结果是否相等(如不重写,则仍然是地址)//如果满足,则证明是相同的,就不插入if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//3.2判断节点是否为一颗树(即map是否已经转变为红黑树),是就调用树的添加节点方法else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//3.3如果不是相同节点且不是一颗树,则循环找到该数组索引位置的最后一个节点,向其next//添加一个新节点else {for (int binCount = 0; ; ++binCount) {//3.3.1当遍历到最后节点时if ((e = p.next) == null) {//将最后节点的next指向新节点p.next = newNode(hash, key, value, null);//判断该数组中索引位置链表节点个数是否大于等于8(从0开始计算,所以要-1),//还必须满足节点数组>=64,就将链表转换为红黑树,否则对数组扩容//TREEIFY_THRESHOLD:8if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//3.3.2在循环中依次判断每个节点是否和要插入的节点相同,相同就结束循环//(意味着已经存在要插入的值),退出if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;//将p指向下个节点(因为在开始使e=p.next了),即e=p.next,p=e,两条语句交替执行//就会遍历节点p = e;}}//4.当相同时,不会执行添加,返回不为空,在map中这里是替换相同key的值//即当key相同时,e存放的是链表中相同key的旧值,e.value会被新值替代if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)//新值替代旧值e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//5.判断size是否超过了临界值,true就扩容(添加完第13个才扩容)//注意:此处size指的是map中存放元素的个数,临界值则是节点数组的临界值//即不是占用了75%的数组索引才扩容,而是只要总数据超过就扩容if (++size > threshold)resize();//这是留给子类去实现的空方法afterNodeInsertion(evict);return null;}
扩容的方法
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;//1.当数组长度>0时,扩容if (oldCap > 0) {//如果过超出了允许的最大值,不扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//扩容为原来的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//2.长度=0时,即第一次扩容时else { // zero initial threshold signifies using defaults//第一次调用时,将table初始化为16newCap = DEFAULT_INITIAL_CAPACITY;//计算临界值,当数据达到临界值时就提前扩容,而不是满了才扩容//0.75*16,0.75是负载因子newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
转换为红黑树的方法
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;//如果数组小于64,不会转树,而是扩容if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}}
2. TreeSet(二叉树)
- TreeSet()是使用二叉树的原理对新的add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入二叉树指定的位置。
- Integer 和String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自定义的类必须实现Comparable 接口,并覆写相应的compareTo()函数(或则向set构造器传参匿名内部类/lambda),才可以正常使用排序。
- 在覆写compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序。
- 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数,零或正整数。
-
3. LinkHashSet(HashSet+LinkedHashMap)
对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。
- LinkedHashSet 底层使用数组+双向链表, LinkedHashMap 来保存所有元素,它继承于HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。
- 对链表的链接操作在LinkedHashMap的newNode()中,Entry是继承了HashMap.Node的内部类
2.Map
1. HashMap(数组+链表+红黑树)
- HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。
- 初始化大小为0,第一次扩容为16,加载因子为0.75,当map存放的元素数量超过当前数组长度的75%时,或者链表长度>=8而数组长度<64时,会触发扩容,扩容机制为原长度的2倍,只有当链表长度>=8且数组长度>=64时,才将链表转换为红黑树(源码参照set部分的笔记)
- HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。
- HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections() 的 synchronizedMap() 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap()。
- 线程不安全的原因:
- 多个线程并发执行put(),当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(不然每插入一次就要遍历一次)。如果条件竞争发生了,那么就会产生死循环了(称为循环链表)。
:::danger
此处元素数量指的是map中存放元素的个数,75%则是节点数组的临界值,即不是占用了75%的数组索引才扩容,而是只要总数据量超过就扩容;元素数量并不是占用新的数组索引才增加,而是每存放一个元素就增加
:::
例:
在节点数组长度16的map中,扩容临界值为12,如果索引为1的位置存放了12个节点的链表,当在其他索引存放新的数据时,就会触发扩容机制(13>12)。而此时索引数组中只占用了2个索引位置,所以扩容机制判断的是map中总数据量>节点数组长度*75%,而不是节点数组内索引的占用个数。
- 多个线程并发执行put(),当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(不然每插入一次就要遍历一次)。如果条件竞争发生了,那么就会产生死循环了(称为循环链表)。
:::danger
此处元素数量指的是map中存放元素的个数,75%则是节点数组的临界值,即不是占用了75%的数组索引才扩容,而是只要总数据量超过就扩容;元素数量并不是占用新的数组索引才增加,而是每存放一个元素就增加
:::
例:
map底层数据实际是存放在table数组的Node中的,但是因为map没有迭代器,所以为了方便遍历集合,会创建一个EntrySet

Java 7 实现:大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。
JAVA 8 实现:Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。

在 Java 7 HashMap 中,查找的时候,根据 hash 值能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java 8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
2. ConcurrentHashMap
1.Segment 段
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一
些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的
意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了 “槽” 来代表一个
segment。
2.线程安全(Segment 继承 ReentrantLock 加锁)
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
3.并行度(默认 16)
concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
4.Java8 实现 (引入了红黑树)
Java8 对 ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树。
3. HashTable(线程安全)
- Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。
- Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
- 不允许键和值存在Null
-
4. TreeMap(可排序)
TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。
- 如果使用排序的映射,建议使用 TreeMap。
在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。
5. LinkHashMap(记录插入顺序)
LinkedHashMap继承于HashMap,它在HashMap的基础上,通过维护一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上,LinkedHashMap很多方法直接继承自HashMap,仅为维护双向链表重写了部分方法。
6. Properties
继承自HashTable类并实现了map接口,特点和HashTable类似
可以用于从xxx.properties配置文件中读取属性放入properties对象,进行读取和修改
3.Collections
collection的工具类,提供了更多的操作方法
reverse(list):反转list集合
- shuffle(list):对list随机排序
- sort():自然排序,也有重载的指定比较器排序
- swap(List,int,int):将集合中指定位置元素交换
- frequency(collection i):返回集合指定元素的出现次数
- synchronizedxxxxx():将指定的集合类型包装为线程安全


