Java刷题错题笔记-day05-集合(CopyOnWriterArrayList、HashMap)

1.CopyOnWriterArrayList是强一致性列表吗?

不是

CopyOnWriteArrayList 不提供强一致性主要是因为它的修改操作是在一个新的拷贝上进行的,而不是直接在原始数据结构上。这种设计决策带来了一些影响:

  1. 读取操作不阻塞: CopyOnWriteArrayList 的读取操作是在原始数组上进行的,无锁,而写入在原数组的拷贝上进行。因此,写入操作期间,读取操作不会被阻塞,允许并发读取。但这也意味着在写入操作完成之前,读取操作可能会看到旧的数据。
  2. 写入操作的延迟: 当有写入操作发生时,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恒为0
   return (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值赋予e
           e = p;
       else if (p instanceof TreeNode)
           e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
       else {
           //...省略代码
       }
       if (e != null) { // existing mapping for key
           V oldValue = e.value;
           if (!onlyIfAbsent || oldValue == null)
               //新value替换旧value
               e.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 {
   // 用于存储元素的 HashMap
   private 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 方法实际上是调用 HashMapput 方法来将元素作为键存储在 HashMap 中,将 PRESENT 作为相应的值。

因此,可以说 HashSet 是通过在 HashMap 的基础上添加一些包装来实现的。这种基于哈希表的实现提供了快速的插入和查询操作,并确保集合中的元素是唯一的。

4.JDK8 HashMap为啥不直接用红黑树?

1.红黑树(TreeNode)占用更大的内存,大约是常规节点(Node )的两倍内存大小
2.红黑树查询更快,当链表达到一定长度时链表查询变慢
所以不直接使用红黑树,等链表达到一定长度后再转换为红黑树结构

TreeNodeNode 分别是 HashMap 中两种不同的节点类型,用于表示哈希表中的元素。

  1. 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;
        }
        // ...
    }
    
  2. 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 会在链表长度超过一定阈值时,将链表转换为红黑树,以提高在大规模哈希冲突情况下的性能。

你可能感兴趣的:(Java刷题笔记,java,面试)