Java-集合-八股文

  1. list、set
  2. list:有序,可重复,允许多个null,支持下标随机访问
    set:无序,不可重复,单一null,必须遍历访问
    
  3. arraylist、linkedlist
  4. arraylist:基于数组实现,占用连续空间,有利于查找、修改,不利于插入、删除[适用场景不同]
    linkedlist:基于链表实现,不要求占用空间连续,有利于插入、删除,不利于查找、修改[适用场景不同]
    
  5. concurrenthashmap扩容机理
  6. 1.7之前:基于segment分段hashmap存储实现的,segment分段存储部分不扩容,仅内部的hashmap进行扩容[哪个线程对应的内部hashmap需要扩容,哪个线程就负责做这个事情]
    1.8之后:不是基于segment分段存储实现的,所有的K-V对都包含在一个map中,只是扩容的时候,那就每个子线程都参与扩容
    
  7. JDK版本变迁,hashmap的主要变更
  8. 1.7:底层是数组+链表,哈希算法复杂 头插法
    1.8:底层是数组+链表+红黑树,由于引入红黑树,哈希算法得到简化,从而优化hashmap的插入、查询效率 尾插法
    
  9. hashmap的put方法
  10. 大体流程:
    1.依据key和哈希算法计算下标
    2.如果下标位置为空,则封装成对象(1.7:entry对象,1.8:node对象),放置该位置
    3.如果下标位置不空,
    3.1 JDK1.7:判断是否需要扩容,然后,头插法插入到对应位置的链表中
    3.2 JDK.8:判断是红黑树节点还是链表节点,然后插入到hashmap里面,最后判断扩容
    3.2.1 红黑树节点:将KV对封装成红黑树节点,加入到红黑树中
    3.2.2 链表节点:尾插法插入到对应位置的链表中,如果多于等于8个节点,将链表转为红黑树
    
  11. hashmap的get方法
  12. 大体流程:
    1.计算hashcode,其本质是将存储地址转换成一个整数得到
    2.hashcode高16位与低16位进行异或,得到一个整数值,将其对范围取模,从而限制下标的取值一定在范围之内
    3.查看对应的下标数据,是否为空
    3.1.哈希桶为空,直接返回空
    3.2.哈希桶不为空,节点结构是红黑树还是链表
    (1)、后续是红黑树,红黑树结构执行相应的 getTreeNode(hash,key) 查找操作。[hash 目标哈希值  key 目标键值]
    (2)、后续是链表,则循环遍历链表根据key获取value
    
  13. hashmap扩容机理
  14. 1.7:生成新数组,遍历原数组上的每个链表,将内部数据逐个转移至新数组
    1.8:生成新数组,遍历原数组上的每个链表与红黑树
    ①如果原数组上是链表,遍历每个元素,重新计算下标并转移
    ②如果原数组上是红黑树,遍历每个元素,重新计算下标并转移,有冲突再购建链表与红黑树
    
  15. copyonwriteArrayList
  16. 线程安全的arrayList,底层也是用数组实现的,主要集中在读与写操作上
    读:由于读写分别在老新数组上,因此,互相不干扰,也因此,读的性能不会受写的性能影响[适用于读多写少]
    写:写操作会生成新数组,在完成之前,其他线程无法进行写操作[上了锁,线程安全];在完成之前,读的是原数组,写的是新数组,两者是不会互相干扰的。
    
  17. concurrentHashMap为什么不能存储Null
  18. 因为concurrentHashMap一般的应用场景都是多线程的情况,
    如果允许存储Null值,就会出现歧义,不确定
    1.没有找到这个值
    2.这个值本身就是Null
    单线程不存在误判,因为逻辑都在一起,但是,多线程可能逻辑都是分开的。
    
  19. arraylist的扩容机制
  20. 触发扩容条件:初始默认情况下,arraylist容量为10,当10个容量已满,仍然需要往里添加时就会触发arraylist扩容。
    扩容流程:
    1.申请当前容量的 1.5倍的空间
    2.将当前数据拷贝至新空间
    3.将新数据存储至新空间,扩容完成
    
  21. 为什么hashmap会出现死循环
  22. 这个问题仅会出现在JDK7,JDK8以后 官方修复了
    主要原因:hashmap扩容机制+并发机制
    

    扩容机制:头插法

    由图可知,头插法会导致链表元素反转

    在并发机制下,指针的指向会变成

    由图可知,两个线程的指针指向,此时都是一样的,且是“正确”的
    由于并发在微观上是串行的,因此,极端情况下,一个线程扩容完毕以后,另一个线程可能都没有开始扩容,就会导致如下情况

    此时,线程2的指针逻辑与真实情况相反,如果继续扩容,就会形成死循环



    扩容基本逻辑:

    next = T2.next
    i = indexFor(T2.hash,newCapcity)
    T2.next = newTable[i]
    newTable[i] = T2
    T2 = next
    //循环上述 直至T2为 null
    //这段逻辑带入T2情况 即可得到死循环
    

    可能1.8 从头插法改成尾插法 也有这方面的考虑 毕竟尾插法是不会改变链表顺序的

  23. 为什么链表转红黑树的阈值是8,而红黑树转链表的阈值是6
  24. 1.当数量小于8的时候,链表性能并不太低
    2.在默认的负载因子0.75的情况下,通过计算泊松分布,哈希冲突达到8次的概率已经小于百万分之一了,所以没必要再大了
    3.红黑树本身单个节点的空间占用是链表单个节点的两倍,且红黑树本身的自平衡特性也会带来插入的开销[左旋右旋],因此,也不能单纯的替换掉链表,否则会带来空间上的浪费
    
    之所以,链表转红黑树设置阈值为6
    1.因为,如果两种转换设置成一样的,会造成两种数据结构不停的相互激荡转换
    
    ps:当容量大于等于64时,也会将链表转换为红黑树
    
  25. arraylist的fail-fast机制
  26. fail-fast:迭代的时候,发现了其他线程在修改,立即抛异常
    
    成员变量:
    迭代器:expectedModCount
    集合:modCount
    
    每次迭代会去检查两个成员变量是否一致 不一致就抛异常
    
  27. copyOnWriteArrayList的fail-safe机制
  28. fail-safe:迭代的时候,发现了其他线程在修改,可以牺牲部分一致性,使得迭代可以继续(类似copyOnWriteArrayList那种读写分离的方式)
    
    区别在于
    迭代器的内部数据 与 集合的数据 并不是同一份
    
    集合每次写操作 都会在原数组上 拷贝一份 长度+1 的数组
    因为是拷贝 因此地址与迭代器内的数据地址不是同一个
    
    从而实现读写分离
    读:old
    写:new
    

参考:
1.https://zhuanlan.zhihu.com/p/458785543
2.https://blog.csdn.net/m0_68006260/article/details/125028368
3.https://blog.csdn.net/Liu_Wd/article/details/108052428

posted @ 2023-07-16 17:25  356a  阅读(13)  评论(0编辑  收藏  举报