Java集合八股

103 阅读5分钟

常见集合框架


Java集合框架分为两大块:

  1. Collection,主要由List、Set、Queue组成:
    • List代表有序、可重复的集合
    • Set代表无序不可重复的集合
    • Queue代表队列
  2. Map,代表键值对的集合

List


ArrayList扩容机制

创建一个1.5倍的新数组,然后把原数组的值拷贝过去

ArrayList线程安全方案

  • 用于Vector
  • 使用Collections.synchronizedList包装ArrayList,然后操作包装后的list
  • 使用CopyOnWriteArrayList
  • 通过同步机制控制ArrayList读写

CopyOnWriteArrayList

允许并发读,读操作无锁

写操作,先复制一份,然后在新副本上执行写操作,结束后将原容器的引用指向新容器

CopyOnWriteArrayList原理

Map


HashMap底层数据结构

JDK8中为数组+链表+红黑树

三分恶面渣逆袭:JDK 8 HashMap 数据结构示意图

核心是一个动态数组(Node[]table),用于存储键值对。数组中的每个元素称为“桶”,每个桶的索引是通过对键的哈希值进行哈希函数处理得到的。

哈希冲突:多个键经哈希处理后得到相同的索引

通过链表解决哈希冲突---将具有相同索引的键值对通过链表连接起来

向HashMap中添加一个键值对时,会使用哈希函数计算键的哈希码,确定其在数组中的位置,哈希函数目标是尽量减少哈希冲突

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

  • 添加元素时,若该位置已有元素(发生哈希冲突),则新元素将被添加到链表末尾或红黑树。若键存在,将覆盖原来的值
  • 获取元素,使用哈希函数计算键的位置,然后根据位置在数组、链表、红黑树中查找元素
  • 初始容量为16,当达到原容量的0.75倍时就会扩容,扩容后的数组长度大小为2倍,然后重新计算哈希值,放入新数组

红黑树

是自平衡的二叉查找树:

  1. 每个节点要么是红色,要么是黑色
  2. 根节点永远是黑色
  3. 所有叶子节点都是黑色的
  4. 红色节点的子节点一定是黑色的
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点

HashMap的put流程

  1. 通过hash方法计算key的哈希值

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    
  2. 数组进行第一次扩容

    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    
  3. 根据哈希值计算key在数组中的下标,如果对应下标正好没存放数据,则直接插入

    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    

如果对应下标已有数据,就要判断key是否相同咯,相同覆盖,不相同判断是否为树节点

HashMap查找流程

  1. 使用扰动函数,获取新的哈希值
  2. 计算数组下标,获取节点
  3. 当前节点和key匹配,直接返回
  4. 否则,当前节点是否为树节点,查找红黑树
  5. 否则遍历链表查找

HashMap的hash函数是怎么设计的

对key的hashcCode,进行高16位与低16位进行异或操作,因为该值是一个32位的int数值

目的是为了降低哈希碰撞的概率

static final int hash(Object key) {
    int h;
    // key的hashCode和key的hashCode右移16位做异或运算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

初始化HashMap,传入一个17容量,会怎么处理

会将这个值转成大于等于17的最小的2的幂。因为哈希表的大小最好是2的幂,优化哈希值的计算,减少哈希冲突,所以或转乘32

哈希函数构造方法

HashMap中的构造方法是

  • 除留取余法H(key)=key%p(p<=N)
  • 直接定址法:根据key映射到对应的位置
  • 数字分析法:取key的某些数字作为映射位置
  • 平方取中法:取key平方的中间几位作为映射位置
  • 折叠法:把key分割成位数相同的几段,然后把它们的叠加和作为映射位置

解决哈希冲突的方法

  1. 再哈希法

    准备两套哈希算法,发生冲突了就用另一个

  2. 开放地址法

    遇到哈希冲突就找下一个空槽

    • 线性探测:从冲突位置开始往后找,直到空槽
    • 二次探测:从冲突位置开始,第一次增加1^2^12个位置
    • 双重哈希:类似再哈希法类似
  3. 拉链法

    当发生冲突就用链表将冲突的元素串起来(HashMap)

判断key相等

equals hashCode ==

解决HashMap线程不安全

  1. Hashtable是直接在方法上加synchronized关键字

  2. Collections.synchronizedMap返回的是Collections工具类的内部类

    内部通过synchronized对象锁来保证线程安全

  3. ConcurrentHashMap,JDK8中使用了CAS(乐观锁)+synchronized关键字

HashMap、LinkedHashMap、TreeMap

1.HashMap无序

2.LinkedHashMap维护了一个双向链表,有头尾节点,可以按插入顺序和访问顺序排序Entry节点

  1. TreeMap通过key的比较器来决定元素的顺序,如果没有指定比较器,则key必须实现Comparable接口,底层是红黑树

Set


HashSet底层

底层由HashMap实现,值由固定的Object的对象来填充,键用于操作

实际开发中并不常用,主要作用也就是去重,底层通过调用HashMap的put方法实现的

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}