Java面试之集合总结

ArrayList

  • 存储方式:动态数组,连续内存存储,适合下标访问

  • 扩容机制:初始为10, 采用位运算进行1.5倍扩容;可以定义初始大小

  • null值:允许多个null值存在

  • 为什么ArrayList线程不安全?

    • 多线程向一个ArrayList对象添加数据,会报ConcurrentModification

    • 在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

    • 解决方案

      • new Vector<>()

      • Collections.synchronizedList(new ArrayList<>())

      • new CopyOnWriteArrayList()

        • 思想:写时复制
        • 缺点:内存占用;不能保证实时一致性,只能保证最终一致性。
        • 适用于读多写少的场景

LinkedList

  • 存储方式:双向链表,存储在分散的内存中,适合数据插入与删除

  • 遍历LinkedList 需要使用iterator,不能使用for(遍历链表)

  • 为什么LinkedList线程不安全?

    • 两个线程同时插入两个不同元素的时候,这两个元素的next指针都指向了相同的元素。
    • 一个线程通过Iterator修改LinkedList结构,另一个线程通过LinkedList对象修改其结构,前一个线程会抛出ConcurrentModificationException异常

HashSet

  • 实现方式:以HashMap的key作为Set集合存储元素,value是一个static final 类型的全局常量 PRESENT

    • 为什么value放对象却不放null值?

      • hashset的add方法调用的hashmap的put方法,put方法返回上次一存放的值;如果key不重复,则返回null,如果key重复,则返回被覆盖的value;如果将value设置成null,则put方法每一次都返回null,,add方法将返回结果和null作比较,每一次都返回true,即使插入相同值,也认为插入成功。
  • 线程不安全

    • 参考HashMap
    • 解决方案:new CopyOnWriteArraySet()

HashMap

  • 存储方式:数组+单向链表(红黑树)

  • 扩容机制:初始数组大小为16,装载因子0.75,数组大小达到阈值时以2的指数倍大小进行扩容

    • 为什么扩容时需要2的指数倍?

      • 在扩容的时候,原有数组里的数据迁移到新数组里不需要重新hash
  • Hash冲突:随着对象的增加,hash算法计算出的对象hashcode相同,此时需要用到对象的equal方法

  • 红黑树优化:如果链表的长度超过8,那么链表将转换为红黑树。(数组的大小必须大于64,小于64的时候只会扩容)

  • null值:最多只允许一条记录的键为null,允许多条记录的值为null

  • JDK 1.8加入红黑树的优点?

    • 提高HashMap插入和查询整体效率
  • 线程不安全

    • JDK 1.7 线程不安全问题?

      • 多线程头插法为会出现环形问题:进行扩容resize()时调用transfer()时多线程进入导致的。查找时会陷入死循环
    • JDK 1.8 线程不安全问题?

      • 数据覆盖:A,B线程同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null。线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全
      • size变小问题:假设两个线程A、B并发进行put操作,当前HashMap的size大小为10,当线程A从主内存中获得size的值为10后,但是由于时间片耗尽只好让出CPU,还未来得及进行+1操作;
        而线程B此时获得CPU资源,依然读出size的值为10,并完成+1操作将size=11写回主内存;
        之后线程A再次获得时间片并继续执行size+1的操作,将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1
    • 解决方案

      • HashTable
      • ConcurrentHashMap
      • Map map = Collections.synchronizedMap(new HashMap<>());
  • get操作

    • 先计算出key对应的hash值
    • 对超出数组范围的hash值进行处理
    • 根据正确的hash值(下标值)找到所在的链表的头结点
    • 遍历链表,如果key值相等,返回对应的value值,否则返回null
  • put操作

    • 先计算出key对应的hash值
    • 对超出数组范围的hash值进行处理
    • 根据正确的hash值(下标值)找到所在的链表的头结点
    • 如果头结点==null,直接将新结点赋值给数组的该位置
    • 否则,遍历链表,找到key相等的节点,并进行value值的替换,返回旧的value值
    • 如果没有找到,采用尾插法(1.8)/头插法(1.7)创建新结点并插入到链表中
    • 将存储元素数量+1
    • 校验是否需要扩容

HashTable

  • 线程安全:Synchronized关键字保证
  • null值:记录的key、value值均不可为null
  • 实现方式:数组+链表

LinkedHashMap

  • 基于hashmap实现
  • LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的
  • LinkedHashMap同样使用table存储元素,但元素不再是Node类,而是继承自Node类的Entry类。并新增了两个属性,before, after,表示前一个元素和后一个元素。

Vector

  • 存储方式:数组
  • 线程安全(Synchronized),效率低
  • 较为古老,基本不用

ArrayList的插入和删除操作一定慢于LinkedList吗?

  • 在集合里面插入元素速度比对结果是,首部插入,LinkedList更快;中间和尾部插入,ArrayList更快;
    在集合里面删除元素类似,首部删除,LinkedList更快;中间删除和尾部删除,ArrayList更快;
    由此建议,数据量不大的集合,主要进行插入、删除操作,建议使用LinkedList;
    数据量大的集合,使用ArrayList就可以了,不仅查询速度快,并且插入和删除效率也相对较高。

猜你喜欢

转载自blog.csdn.net/qq_36940806/article/details/121208912