1.CopyOnWriterArrayList是强一致性列表吗?
不是
CopyOnWriteArrayList
不提供强一致性主要是因为它的修改操作是在一个新的拷贝上进行的,而不是直接在原始数据结构上。这种设计决策带来了一些影响:
- 读取操作不阻塞:
CopyOnWriteArrayList
的读取操作是在原始数组上进行的,无锁,而写入在原数组的拷贝上进行。因此,写入操作期间,读取操作不会被阻塞,允许并发读取。但这也意味着在写入操作完成之前,读取操作可能会看到旧的数据。- 写入操作的延迟: 当有写入操作发生时,
CopyOnWriteArrayList
会创建一个新的数组,并在上面执行修改。在这个过程中,其他线程可能仍然在引用旧的数组。因此,在写入操作完成之前,其他线程可能无法感知到最新的修改。下面是简化的
CopyOnWriteArrayList
的部分关键代码,以便更好地理解:public class CopyOnWriteArrayList<E> {private transient volatile Object[] array;// .../***写入操作/public boolean add(E element) {synchronized (this) {Object[] currentArray = array;//1.拷贝原数组Object[] newArray = Arrays.copyOf(currentArray, currentArray.length + 1);//2.在新副本上执行添加操作newArray[currentArray.length] = element;//3.将原数组引用指向新副本array = newArray;return true;}}// ...public E get(int index) {return (E) array[index];}// ... }
在
add
方法中,修改是在一个新的数组上进行的。而get
方法只是直接访问当前数组,没有加锁,因此可能在写入操作进行时看到旧的数组。这就是导致不强一致性的主要原因之一。
2.HashMap允许key为null吗?
允许
key为null时,key的hash值恒为0,元素将被存储在数组的第一个位置
public V put(K key, V value) {return putVal(hash(key), key, value, false, true); }static final int hash(Object key) {int h;//key为null,hash恒为0return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)//hash为0,`tab[i = (n - 1) & hash])`为tab[0])tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//新key也为null时,走到这。p=tab[0],然后将p值赋予ee = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//...省略代码}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)//新value替换旧valuee.value = value;afterNodeAccess(e);return oldValue;}} }
3.HashSet允许有null值吗?
允许
因为
HashSet
是基于HashMap
实现的,HashMap
允许key为null
HashSet
是基于哈希表的集合,它不允许重复元素。- 当你向
HashSet
中添加元素时,实际上是将这个元素作为键存储在一个HashMap
实例中,而值则是一个常量PRESENT
。以下是简化的
HashSet
类的一部分关键代码,以说明其是如何基于HashMap
实现的:public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {// 用于存储元素的 HashMapprivate transient HashMap<E, Object> map;// 一个常量对象,作为所有元素的值private static final Object PRESENT = new Object();// 构造方法public HashSet() {map = new HashMap<>();}// 添加元素的方法public boolean add(E e) {return map.put(e, PRESENT) == null;}// 其他方法... }
在上述代码中,
HashSet
的构造方法初始化了一个HashMap
实例,add
方法实际上是调用HashMap
的put
方法来将元素作为键存储在HashMap
中,将PRESENT
作为相应的值。因此,可以说
HashSet
是通过在HashMap
的基础上添加一些包装来实现的。这种基于哈希表的实现提供了快速的插入和查询操作,并确保集合中的元素是唯一的。
4.JDK8 HashMap为啥不直接用红黑树?
1.红黑树(TreeNode
)占用更大的内存,大约是常规节点(Node
)的两倍内存大小
2.红黑树查询更快,当链表达到一定长度时链表查询变慢
所以不直接使用红黑树,等链表达到一定长度后再转换为红黑树结构
TreeNode
和Node
分别是HashMap
中两种不同的节点类型,用于表示哈希表中的元素。
Node 节点:
Node
是基本的链表节点,用于处理哈希冲突时形成的链表。Node
的结构相对简单,包含了键、值、哈希码和指向下一个节点的引用。static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}// ... }
TreeNode 节点:
TreeNode
是红黑树节点,用于处理链表转化为红黑树时的节点。TreeNode
的结构相对复杂,包含了键、值、哈希码、指向父节点、左子节点、右子节点的引用,以及颜色信息用于红黑树平衡。static final class TreeNode<K,V> extends Node<K,V> {TreeNode<K,V> parent; // 父节点TreeNode<K,V> left; // 左子节点TreeNode<K,V> right; // 右子节点TreeNode<K,V> prev; // 用于双向链表的前一个节点boolean red; // 红黑树中的颜色标记// 构造方法TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}// ... }
为什么
TreeNode
占用的内存更多但查询更快呢?
占用内存更多:
TreeNode
占用的内存更多主要是由于其包含了额外的红黑树结构信息,如父节点、左子节点、右子节点等。这些额外的信息使得每个节点的内存占用更大。查询更快: 红黑树的查询性能相对较好,因为红黑树是一种平衡二叉搜索树,保持了相对平衡的树结构。在红黑树中,查询操作的时间复杂度为 O(log N),而链表的查询操作的时间复杂度为 O(N)。所以,当链表转化为红黑树后,在具有大量哈希冲突的情况下,查询性能更好。红黑树的平衡性质确保了在最坏情况下的查询性能。
总的来说,
TreeNode
占用更多内存但查询更快是通过引入红黑树结构来平衡在大型哈希冲突情况下的性能。在一般情况下,链表结构可能更为简单且更省内存。因此,HashMap
会在链表长度超过一定阈值时,将链表转换为红黑树,以提高在大规模哈希冲突情况下的性能。