Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列),因此Java集合大致也可分成List、Set、Queue、Map四种接口体系。
注意:Collection是一个接口,Collections是一个工具类,Map不是Collection的子接口。
线程安全的集合有哪些?线程不安全的呢?
线程安全的:
- Hashtable:比HashMap多了个线程安全。
- ConcurrentHashMap:是一种高效但是线程安全的集合。
- Vector:比Arraylist多了个同步化机制。
- Stack:栈,也是线程安全的,继承于Vector。
线性不安全的:
- HashMap
- Arraylist
- LinkedList
- HashSet
- TreeSet
- TreeMap
Arraylist与 LinkedList 异同点?
- 数据结构:
-
- ArrayList:底层基于Object动态数组实现,使用数组来存储元素,支持随机访问。
- LinkedList:底层基于双向链表实现,每个节点包含数据元素和前后两个节点的引用,支持快速的插入和删除操作。
- 访问效率:
-
- ArrayList:由于是数组结构,随机访问元素非常高效,时间复杂度为 O(1)。
- LinkedList:由于是链表结构,随机访问元素效率较低,时间复杂度为 O(n),需要遍历链表才能找到指定位置的元素。
- 插入和删除效率:
-
- ArrayList:在末尾插入和删除元素的效率较高,时间复杂度为 O(1)。但在中间或开头插入和删除元素需要移动后续元素,时间复杂度为 O(n)。
- LinkedList:插入和删除元素非常高效,只需要修改节点的引用,时间复杂度为 O(1),即使在中间或开头插入和删除也是如此。
- 空间占用:
-
- ArrayList:由于是动态数组,可能会分配更多的内存,尽管实际元素数量较少。
- LinkedList:由于额外存储了节点之间的引用,可能会占用更多的内存。
- 迭代效率:
-
- ArrayList:通过索引进行迭代时效率很高,但在删除和插入元素时可能导致迭代器失效。
- LinkedList:迭代效率较高,不受删除和插入元素的影响,可以在迭代过程中进行元素的增删。
综上所述,ArrayList 和 LinkedList 在访问效率、插入和删除效率、空间占用等方面有所不同。ArrayList 适合频繁随机访问元素的场景,而 LinkedList 适合频繁插入和删除元素的场景。在选择使用时,需要根据具体的需求和使用场景来进行合理的选择。
ArrayList的扩容机制
-
ArrayList是一个动态数组,实现了List接口,可以根据需要自动增加容量。12
-
ArrayList的底层是一个Object[]数组,初始容量为10,当添加元素时,会检查是否超过当前容量,如果超过,就会进行扩容。34
-
ArrayList的扩容机制是调用grow()方法,创建一个新的数组,容量为原来的1.5倍,然后将原来的元素复制到新的数组中。
-
ArrayList是非线程安全的,如果需要线程安全,可以使Collections.synchronizedList()方法包装一下。
HashMap的数据结构和扩容机制
在JDK1.7 和JDK1.8 中有所差别:
在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:
- 当链表超过 8 且数据总量超过 64 才会转红黑树。
- 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
HashMap解决hash冲突
- 链地址法(拉链法),将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。
为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?
因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
HashMap 中 key 的存储索引是怎么计算的?
- 调用key的hashCode()方法,得到key的hash值
- 将hash值的高16位与低16位进行异或运算,得到一个新的hash值
- 将新的hash值与HashMap容量-1,进行位于运算,得到下标
异或:两个不同的值为 1
位于:只有1&1 为1
HashMap的put流程
- 首先,通过传入的键对象 key,调用 hashCode() 方法计算其哈希值。如果 key 为 null,则哈希值为 0。
- 使用哈希值和当前桶数组长度进行取模运算(hash & (table.length - 1)),计算得到插入的桶索引 index。这个桶索引表示键值对将被存放在桶数组的哪个位置。
- 检查哈希表中的该桶位置是否为空(即是否存在冲突)。如果该桶位置为空,直接将键值对插入该位置,完成插入操作。
- 如果该桶位置已经存在元素,则需要进行冲突处理。
-
- 如果当前桶位置的元素的 key 与插入的 key 相同(即发生键重复),则更新对应的 value 值为新的值。
- 否则,如果当前桶位置的元素是一个链表或红黑树(发生哈希冲突),则将新的键值对插入到链表或红黑树中。
- 如果当前桶位置的元素数量超过了阈值(默认值为 8),并且当前桶位置的元素是链表,则将链表转换为红黑树以提高查找性能。
- 检查哈希表中的元素数量是否超过了负载因子阈值(默认值为 0.75)。如果超过了负载因子阈值,则进行扩容操作。扩容操作会重新计算每个元素的桶索引,以适应新的桶数组长度。
- 插入完成后,HashMap 的元素数量会增加,操作结束。
HashMap的扩容方式
- 当元素大于负载因子乘以容量,触发扩容
- 扩容新建新的桶数组,为原来两倍
- 遍历桶数组,每个元素重新计算hash值,并放在新的桶中
- 将HashMap的引用指向,新的桶数组
ConcurrentHashMap 的实现原理是什么?
JDK 1.7 中的 ConcurrentHashMap 实现原理:
- ConcurrentHashMap 把哈希桶切分成小数组(Segment )分段锁
- 每个段都有自己的锁,不同段之间的数据修改可以并发进行,从而提高并发性能。
- 每个段组有 n 个 HashEntry 组成
JDK 1.8 中的 ConcurrentHashMap 实现原理:
- 数组,链表,红黑树
- 锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加低粒度的锁。
- 将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。
- 锁哈希桶元素,即锁链表表头结点
ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?
- 不明确性问题:
-
- 在哈希表中,通常使用 null 值表示某个键不存在或者某个键对应的值为空。
- 在多线程环境下,一个线程可能在读取操作时得到 null 值,而另一个线程在稍后的操作中可能会修改或删除这个键。
- 这样会导致无法确定 null 到底是表示键不存在还是对应的值为空,造成了不明确性。
- 安全性问题:
-
- 允许 key 或 value 为 null,在进行插入操作时,如果出现键值对中任意一个为 null 的情况,可能破坏哈希表的数据结构。
- 这样会导致不稳定和不一致的结果,增加了并发环境下的错误风险。
- 实现复杂性:
-
- 处理 null 值需要额外的逻辑判断,增加了代码的复杂性和维护难度。
- 替代方案:
-
- 如果需要在 ConcurrentHashMap 中存储 null 值,可以使用特定的占位值来表示空值,比如使用 Optional 类或其他适当的占位对象来代替 null。
- 这样可以保持哈希表的稳定性和可靠性,避免了上述不明确性和安全性问题。
ConcurrentHashMap 的并发度是多少?
ConcurrentHashMap的默认容量为16,所以并发度也是16。
如果您想修改并发度,可以在构造函数中传入一个参数,但是实际的并发度会取大于等于该值的最小2幂指数。
例如,如果您传入17,实际的并发度就是32。
这样做是为了保证数组的长度是2的幂次方,方便计算哈希值和索引。
说一下Hashtable的锁机制 ?
Hashtable是使用Synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差!
Iterator 和 ListIterator 有什么区别?
- Iterator:
-
- Iterator 是最基本的遍历方式,可以用于遍历所有实现了 Iterable 接口的集合类(如 List, Set, Queue, 等)。
- 它只能向前遍历集合,并且只能在遍历过程中删除当前位置的元素,不能添加或替换元素。
- 它提供了三个方法:hasNext() 判断是否有下一个元素,next() 获取下一个元素,remove() 删除当前位置的元素。
- ListIterator:
-
- ListIterator 是 Iterator 的子接口,只能用于遍历实现了 List 接口的集合类。
- 它除了支持向前遍历外,还可以向后遍历,即在列表中的任意位置进行双向遍历。
- 它比 Iterator 多了一些额外的方法,例如:hasPrevious() 判断是否有前一个元素,previous() 获取前一个元素,add(E e) 在当前位置前插入一个元素,set(E e) 替换当前位置的元素。
总结:
- Iterator 适用于所有实现了 Iterable 接口的集合类,只能单向遍历,只能删除元素。
- ListIterator 适用于实现了 List 接口的集合类,可以双向遍历,并且支持在遍历过程中添加、替换和删除元素。
讲一讲快速失败(fail-fast)和安全失败(fail-safe)
- 快速失败 (Fail-Fast):
-
- 特点:迭代器在遍历集合过程中,如果发现其他线程对集合的结构进行了修改(增加、删除元素等),立即抛出 ConcurrentModificationException 异常,中止当前的迭代。
- 目的:快速失败机制的目的是在出现并发修改时尽早检测到问题,并防止程序在不一致的状态下继续执行,避免潜在的数据不一致问题。
- 使用场景:适用于迭代器并发修改的情况,例如在多线程环境下,一个线程在遍历集合,而另一个线程对集合进行修改。
- 安全失败 (Fail-Safe):
-
- 特点:迭代器在遍历集合过程中,即使其他线程对集合的结构进行了修改,也不会抛出异常,而是继续完成遍历。
- 目的:安全失败机制的目标是保证遍历的安全性,即使在迭代过程中有其他线程修改了集合,也不会影响当前迭代器的操作。
- 使用场景:适用于那些需要在迭代过程中允许对集合进行修改的情况。Java 集合框架中的一些类,如 ConcurrentHashMap 和 CopyOnWriteArrayList,就采用了安全失败机制。
总结:快速失败和安全失败是针对集合迭代器在并发环境中的两种不同的错误处理策略。快速失败能够迅速检测并报告并发修改,确保数据的一致性,但可能会导致遍历中断;而安全失败则允许并发修改,保证迭代的安全性,但可能在遍历时看到部分修改。选择哪种策略取决于具体的应用场景和需求。