容器类
容器类其实就是一种用来存储数据的数据结构,在JAVA中容器可分为“集合(Set)、列表(List)、映射(Map)”。有时我们又把容器类叫做集合框架,这里的集合和set集合是有区别的。
框架图:

主要类介绍:所有集合类都位于java.util包下。Java的集合类主要是由两个接口派生而出:Collection和Map,这两个是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。
1. Collection是一个接口,是高度抽象出来的集合,包含了集合的基本操作和属性,包含了List和Set两大分支。
- List是一个有序的队列,每一个元素都有它的索引。List的实现类有LinkedList,ArrayList,Vector,Stack。
- Set是一个不允许有重复元素且无序的集合。Set的实现类有HashSet和TreeSet。HashSet依赖于HashMap,它实际上是通过HashMap实现的;TreeSet依赖于TreeMap,是通过TreeMap实现的。
2. Map是一个映射接口,即key-value键值对。AbstractMap 是个抽象类,它实现了 Map 接口中的大部分 API。而 HashMap,TreeMap,WeakHashMap 都是继承于 AbstractMap。Hashtable 虽然继承于 Dictionary,但它实现了 Map 接口。
3. Iterator。它是遍历集合的工具。我们说 Collection 依赖于 Iterator,是因为 Collection 的实现类都要实现 iterator () 函数,返回一个 Iterator 对象。ListIterator 是专门为遍历 List 而存在的。
4. Arrays 和 Collections。它们是操作数组、集合的两个工具类。
实现类的对比
List实现类:
1. Vector和ArrayList
- vector是线程安全 的, arraylist 是线程不安全的。Vector 由于使用了 synchronized 方法(线程安全)所以性能上比 ArrayList 要差
- 如果集合中的元素的数目大于目前集合数组的长度时,vector 容量*2,而 arraylist容量*1.5。如果在集合中使用数据量比较大的数据,用 vector 有一定的优势。
- ArrayList 和 Vector 是采用数组方式存储数据,都是通过索引访问数据,如果移动一个指定位置都会导致后面的元素都发生移动。索引数据快,插入数据慢。
2. ArrayList和LinkedList
- ArrayList和Vector都是基于Array的,其实就是封装了Array所不具备的一些功能方便我们使用,它不可能走入Array的限制。性能也就不可能超越Array。所以,在可能的情况下,我们要多运用Array。而LinkedList是基于链表的数据结构。
- 对于随机访问 get,ArrayList 优于 LinkedList,因为 LinkedList 要移动指针。所以ArrayList适合查询。
- 对于新增和删除操作 add 和 remove,LinedList 比较占优势,因为 ArrayList 要移动数据。这一点要看实际情况的。若只对单条数据插入或删除,ArrayList 的速度反而优于 LinkedList。但若是批量随机的插入删除数据,LinkedList 的速度大大优于 ArrayList.
3. ArrayList的扩容机制


当第一次插入元素时才分配10(默认)个对象空间。假如有20个数据需要添加,那么会分别在第一次的时候,将ArrayList的容量变为10;之后扩容会按照1.5倍增长。也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15(如下图二);当添加第16个数据时,继续扩容变为15 * 1.5 =22个
4. ArrayList频繁扩容导致性能急剧下降,如何处理?
创建集合的时候指定足够大容量。注意,这种优化方式只针对特定的场景,如果添加的元素是少量的、未知的,不推荐使用。

Map实现类和Set实现类:
1.HashMap 可实现快速存储和检索,但其缺点是其包含的元素是无序的,这导致它在存在大量迭代的情况下表现不佳。
2.LinkedHashMap 保留了 HashMap 的优势,且其包含的元素是有序的。它在有大量迭代的情况下表现更好。
3.TreeMap 能便捷的实现对其内部元素的各种排序,但其一般性能比前两种 map 差。
LinkedHashMap 映射减少了 HashMap 排序中的混乱,且不会导致 TreeMap 的性能损失。HashMap、LinkedHashMap、TreeMap 三者均线程不安全
Set 的实现类有 HastSet 和 TreeSet。HashSet是通过 HashMap 实现的;TreeSet是通过 TreeMap 实现的。比起 map 所有的 set 都是带有 去重功能的,同时三者均是非线程安全。
-
HashSet:元素无序。比如存入 a、e、c、d、b,输出 d、e、b、c、a。
-
LinkedHashSet:怎么存进去,怎么出来。比如存入 a、e、c、d、b,输出 a、e、c、d、b。
-
TreeSet:排好序的输出。比如存入 a、e、c、d、b,输出 a、b、c、d、e。
有去重需求的考虑 set 实现类,如果没有去重需求,则考虑 map, map 的性能整体比 set 要好,因为 set 的底层实现是 map。
补充:
Set<Integer> set = new TreeSet<>(Comparator.reverseOrder()); Map<String, Integer> map = new TreeMap<>(Comparator.reverseOrder());
在 Tree 对象声明的时候,可以调用 comparator 类的 reverseOrder 方法实现倒序存储。
注意:TreeSet和TreeMap的树形结构都是红黑树。
迭代器
1. 介绍
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为 “轻量级” 对象,因为创建它的代价小。
只有 Collection 接口继承 Iterator 接口,而 Map 接口并没有继承 Iterator,所以只有 List 和 Set 的实现类实现了 Iterator 接口的 iterator 方法,而 Map 的实现类是无法直接使用 iterator 方法,之前的章节讲了 Map 遍历的方式(keyset () 和 entryset ()),是通过返回一个 set 集合再调用 iterator 方法的。
2. Iterator的 fail-fast 和 fail-safe 属性
结论:对 JAVA 集合进行遍历删除时务必要用迭代器
三种遍历方法在删除元素时带来的问题https://blog.csdn.net/weixin_39892619/article/details/80936280
Java.util 包中的所有集合类都被设计为 fail-fast 的,而 java.util.concurrent 中的集合类都为 fail-safe 的。Fail-fast 迭代器抛出 ConcurrentModificationException,而 fail-safe 迭代器从不抛出 ConcurrentModificationException。
Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。
所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象, Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。
集合中有两个字段:
-
modCount:集合的修改次数
-
expectedModCount:迭代器对集合进行修改的次数
当集合结构发生变化而导致这两个字段不相同就会报 ConcurrentModificationException 异常。如果不使用Iterator的remove方法删除,集合的变化迭代器是收不到的,因此字段不一致,便会报错。
源码分析:
每次使用迭代器遍历元素的时候,都会在调用iterator函数的时候将modCount赋值给expectedModCount,在每次调用next函数时都会检查这两个变量值是否相等,不相等就会报异常。
- 当使用集合的remove()进行元素删除的时候,只是把modCount++了,所以两个变量值会不一样。
- 当使用迭代器的remove()的时候,在删除元素之后还会再把modCount赋值给expectedModCount,所以不会报异常。


3. Iterator和ListIterator区别
- 我们可以使用 Iterator 来遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
- Iterator 只可以单向单向遍历,而 ListIterator 可以双向遍历。
- ListIterator 从 Iterator 接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
HashMap的使用
1. HashMap如何线程安全
Java HashMap 是非线程安全的。在多线程条件下,容易导致死循环,具体表现为 CPU 使用率 100%。因此多线程环境下保证 HashMap 的线程安全性,主要有如下几种方法:
-
java.util.Hashtable 类,此类是线程安全的。
-
java.util.concurrent.ConcurrentHashMap类,此类是线程安全的。同 Hashtable 相比,ConcurrentHashMap 不仅保证了访问的线程安全性,而且在效率上有较大的提高。
-
java.util.Collections.synchronizedMap () 方法包装 HashMap object,得到线程安全的 Map,并在此 Map 上进行操作。
SynchronizedMap 是一个实现了 Map 接口的代理类,该类中对 Map 接口中的方法使用 synchronized 同步关键字来保证对 Map 的操作是线程安全的。
除了对 Map 进行封装,Collections 工具类还提供了对 Collection(比如 Set,List)的线程安全实现封装方法。

synchronizedMap使用方式:
Map<String, Long> user = Collections.synchronizedMap(new HashMap<>()); user.put("hj", 123L); System.out.println(user.get("hj"));
注:hashmap的key和value可以为null,但是concurrentHashMap的key和value不能为null。
2. Hashtable与HashMap的区别
(1)继承的父类不同:
-
public class Hashtable extends Dictionary implements Map
- public class HashMap extends AbstractMap implements Map
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
(2)线程安全性不同:
Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。
(3)是否提供contains方法:
HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。
Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
(4)key和value是否允许null值
- Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。
- HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
(5)hash值的使用不同
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。
HashMap计算hash值的函数以及计算索引的方法:


Hashtable的hash值以及计算索引的方法:

- Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值。
- Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用hash与table长度-1的与运算。
&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。
(6)内部实现使用的数组初始化和扩容方式不同
- Hashtable中hash数组默认大小是11,增加的方式是old*2 + 1。
- HashMap中hash数组的默认大小是16,将容量变为原来的2倍,一定是2的指数。
HashMap的底层实现
HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap的底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。每个Map.Entry是一个key-value对,也是数组中的元素,它持有指向下一个元素的引用,就构成了链表。HashMap的存取实现:
1)存储
当我们put的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置,如果数组该位置上已经存有其他元素了,那么在这个位置上的元素将以链表的形式存储,新加入的放在表头,最先加入的在表尾。JDK1.8中,当链表长度太长时,链表就转换为红黑树,这样会大大提高查找效率。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
2)读取
当get元素时,首先计算key的hashCode,找到数组中对应位置,然后通过key的equals方法在对应位置的链表中找到需要的元素。
3)Fail-Fast机制
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
hashCode和equals方法的重要性:
由上可知,HashMap的存取,对元素的索引都是通过hashCode和equals来实现的,所以这两个方法的实现对HashMap的精确性和正确性是至关重要的。
HashMap 可以存 null 键和 null 值,不保证元素的顺序恒久不变,通过 hashCode () 方法和 equals 方法保证键的唯一性。
如果key是自定义的类,就必须重写hashCode()和equals()。自定义的类的 hashcode () 方法继承于 Object 类,其 hashcode 码为默认的内存地址,这样即便有相同含义的两个对象,比较也是不相等的,例如,生成了两个 Student A对象,正常理解这两个对象应该是相等的,但如果你不重写 hashcode()方法的话,比较是不相等的!
如果只重写 hashcode () 不重写 equals () 方法,当比较 equals () 时只是看他们是否为同一对象(即进行内存地址的比较,因为equal方法默认实现还是==,而==是比较内存地址的),两个对象还是不同的,因为地址不同。
树形化:
链表长度大于8 的时候就会调用 treeifyBin方法转化为红黑树,但是在treeifyBin方法内部却有一个判断,当只有数组长度大于64 的时候,才会进行树形化,否则就只是resize扩容。
因为链表过长而数组过短,会经常发生 hash碰撞,这个时候树形化其实是治标不治本,因为引起链表过长的根本原因是数组过短。所以执行树形化之前,会先检查数组长度,如果长度小于 64,则对数组进行扩容,而不是进行树形化。
因此,真正发生树形化的时候,是数组长度大于64并且链表长度大于8 的时候才会发生的。
JDK1.8对HashMap的优化
1. jdk1.8优化
-
扩容时不需要重新计算 hash 值;
-
扩容后的链表不会逆序;
-
当数组长度大于64且链表长度达到 8 的时候,此时会将其链式结构转换成一颗红黑树,加快效率。
2. 传统HashMap的缺点(为什么引入红黑树)
JDK 1.8 以前 HashMap 的实现是 数组 + 链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。JDK1.8中的实现是数组+链表+红黑树(处理hash冲突)。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O (n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O (logn))来优化这个问题。
红黑树
红黑树是高效查找算法
首先说二分查找算法,条件是有序。 如果二分查找转换成数据结构,就是二叉查找树。
时间复杂度是数的深度,如果节点数为n,高度为h,则n=2^h-1, h = log(n+1),所以时间复杂度为O(logn)。
二叉查找树在最坏的情况下就是一个单链表,时间复杂度变为了O(n)。如果不要变成一个链表,就出现了平衡二叉树(AVL树,追求极致的平衡,理想状态)。
红黑树的底层结构:特殊的二叉查找树(自平衡的二叉查找树)
红黑树的性质(重点):
1. 每个结点不是红色就是黑色
2. 不可能有连在一起的红色结点
3. 根节点都是黑色
4. 每个红色结点的两个子节点都是黑色(满足第2点就会满足这一点)。叶子节点都是黑色
满足了这几条性质就可以近似的平衡了,就不会有退化成链表的情况。为了满足这些性质,就要进行很多的操作
为了完成一个红黑树,有3种变换规则:
1. 改变颜色:最简单,红变黑,黑变红
2. 左旋:3步



3. 右旋



旋转和颜色变换规则:所有插入的点默认为红色,只有是红色数据结构才会出现变换,如果默认是黑色,则整棵树都是黑色也会满足红黑树性质,不会引起树结构的变化。
1. 变颜色的情况:当前结点的父亲是红色,且它的叔叔节点(祖父节点的另一个子节点)也是红色:
1)把父节点设为黑色
2)把叔叔节点也设为黑色
3)把祖父(父亲的父亲)设为红色
4)把指针定义到祖父节点设为当前要操作的
2. 左旋:当前父节点是红色,叔叔是黑色的时候,且当前的结点是右子树。左旋
以父节点作为旋转结点
3. 右旋:当前父节点是红色,叔叔是黑色的时候,且当前的结点是左子树。右旋
1)把父节点变为黑色
2)把祖父节点变为红色
3)以祖父节点旋转



常见面试题:
1. HashMap为什么用红黑树而不用B+树?
众所周知,B+树是数据库索引的底层实现,为什么数据库索引用B+树呢,因为B+树一个节点可以有多个索引值,所以适用于存储大数据量,更加的扁胖,可以大大减少磁盘IO的次数。
而对于HashMap来说,一般没有大数据量的需求,且插入效率要比B+树高,而且查询效率又比普通二叉树高。
2. 为什么用ConcurrentHashMap而不用HashTable或syncornizedMap?https://cloud.tencent.com/developer/article/1543475
HashTable是对整个map加了一个对象锁,而ConcurrentHashMap1.8之后变成了同样的数组+链表+红黑树的结构,只是锁住目前获取到的那个Entry所在的那个节点,是基于CAS乐观锁+Synchronized实现的。且在jdk1.6之后对Synchronized进行了一个优化锁升级的过程。相比之下ConcurrentHashMap的更新效率要比其他两个高,并发度更高。
3. 锁升级的过程
浙公网安备 33010602011771号