Java集合面试题

Java集合

本资料整理参考:https://www.bilibili.com/video/BV1yT411H7YK/?spm_id_from=333.337.search-card.all.click

  • 常用的集合类定义在 java.util 包中
  • 集合类只能存放对象,不能存放基本数据类型,且是对象的引用,而非对象本身。

1、说一说Java提供的常见集合?

在这里插入图片描述

在java中提供了量大类的集合框架,主要分为两类:Collection 单列集合和Map 双列集合

  • Collection中有两个子接口ListSet。在我们平常开发的过程中用的比较多像list 接口中的实现类ArrarListLinkedList。 在Set接口中有实现类HashSetTreeSet
  • 在map接口中有很多的实现类,平时比较常见的是HashMapTreeMap,还有一个 线程安全的map:ConcurrentHashMap

2、为什么数组索引从0开始呢?假如从1开始不行吗?

  • 在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址+索引乘以存储数据的类型大小。
  • 如果数组的索引从1开始,寻址公式中,就需要增加一次减法操作,对于CPU来说就多了一次指令,性能不高。

3、ArrayList底层是如何实现的?

  • ArrayList底层是动态数组

  • ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10

    // 3个构造函数
    /**
     *默认构造函数,使用初始容量10构造一个空列表(无参数构造)
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    /**
     * 带初始容量参数的构造函数。(用户自己指定容量)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//初始容量大于0
            //创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始容量等于0
            //创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    
    /**
    *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
    *如果指定的集合为null,throws NullPointerException。
    */
     public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    
    
  • ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组。

  • 通过****ensureCapacity****方法可以预分配空间,以减少增量重新分配的次数。

  • add方法的源码逻辑如下:

    • 确保数组已使用长度(size)加1之后足够存下下一个数据。ensureCapacityInternal(size + 1)
    • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用**grow方法**扩容(原来的1.5倍)
    • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
    • 返回添加成功布尔值。

4、ArrayList list=new ArrayList(10)中的list扩容几次?

  • 在ArrayList的源码中提供了一个带参数的构造方法,这个参数就是指定的集合初始长度,所以给了一个10的参数,就是指定了集合的初始长度是10,这里面并没有扩容。

5、如何实现数组和List之间的转换?

  • 数组转List ,使用JDK中java.util.Arrays工具类的asList方法。
  • List转数组,使用List的toArray方法。无参toArray方法返回 Object数组,传入初始化长度的数组对象,返回该对象数组。
String[] strs = {"aaa","bbb","ccc"};
// 数组 =>> list
List<String> list = Arrays.asList(strs);
// list =>> 数组
String[] array = list.toArray(new String[list.size()]);
  • 继续探究:用Arrays.asList转List后,如果修改了数组内容,list受影响吗?List用toArray转数组后,如果修改了List内容,数组受影响吗?
    • Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
    • list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。

6、ArrayList 和 LinkedList 的区别是什么?

  • 底层:ArrayList 是动态数组, LinkedList 是双向链表

  • 从操作数据效率来说

    • 查询(索引):ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公 式】, LinkedList不支持下标查询。
    • 查找(某个值): ArrayList需要遍历,链表也需要遍历,时间复杂度都是 O(n)。
    • 插入和删除:
      • ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)。
      • LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是 O(n)。
  • 内存空间:ArrayList底层是数组,内存连续,节省内存。LinkedList 是双向链表需要存储数据,和两个指针,更占用内存。

  • ArrayList和LinkedList都不是线程安全的。

  • 探究:那你如何解决这个的线程安全问题的?

    • 我们使用这个集合,优先在方法内使用,定义为局部变量,这样的话,就不会出现线程安全问题。
    • ArrayList可以通过Collections 的 synchronizedList 方法将 ArrayList 转换成 程安全的容器后再使用。 LinkedList 换成ConcurrentLinkedQueue来使用。
    List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>());
    List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());
    

7、说一下HashMap的实现原理?

  • 底层使用hash表数据结构,即数组+(链表 | 红黑树)
  • 添加数据时,计算key的值确定元素在数组中的下标
    • 如果key相同,则覆盖原始值;
    • 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
  • 获取数据通过key的hash计算数组下标获取元素

8、HashMap的jdk1.7和jdk1.8有什么区别?

  • jdk1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
  • jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。

9、你能说下HashMap的put方法的具体流程吗?

  • HashMap是懒惰加载,在创建对象时并没有初始化数组。判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)。
  • 根据键值key计算hash值得到数组索引
    • table[i]==null,条件成立,直接新建节点添加。
    • 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value。
    • 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对。
    • 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value。
    • 在无参的构造函数中,设置了默认的加载因子是0.75。插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。

10、能讲一讲HashMap的扩容机制吗?

在这里插入图片描述

  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75);
  • 每次扩容的时候,都是扩容之前容量的2倍
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中;
  • 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
  • 如果是红黑树,走红黑树的添加;
  • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为 0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这 个位置上。

11、你了解hashMap的寻址算法吗?

  • 计算对象的 hashCode()
  • 再进行调用 hash() 方法进行二次哈希, hashcode值右移16位再异或运算,让哈希分布更为均匀。
  • 为了性能更好,并没有直接采用取模的方式,而是使用了数组长度-1 得到一个值,用这个值按位与运算hash值,最终得到数组的位置。

12、为何HashMap的数组长度一定是2的次幂?

  • 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  • 扩容时重新计算索引效率更高:在进行扩容是会进行判断 hash值按位与运算旧数组长租是否 == 0。如果等于0,则把元素留在原来位置 ,否则新位置是等于旧位置的下标+旧数组长度。

13、你知道hashmap在jdk1.7下的多线程死循环问题吗?

  • 因为jdk7的的数据结构是:数组+链表。在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环。
  • 例子
    • 线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
    • 线程二也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
    • 当线程一再继续执行的时候就会出现死循环的问题。
  • JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。

14、如何解决hashmap的线程不安全问题?

  • 采用ConcurrentHashMap进行使用,它是一个线程安全的 HashMap

15、HashTable与HashMap的区别有哪些?

  • 数据结构:hashtable是数组+链表,hashmap在1.8之后改为了 数组+链表+红黑树
  • hashtable存储数据的时候都不能为null,而hashmap是可以的
  • hash算法不同,hashtable是用本地修饰的hashcode值,而hashmap使用了二次hash
  • 扩容方式不同,hashtable是当前容量翻倍+1,hashmap是当前容量翻倍
  • hashtable是线程安全的,操作数据的时候加了锁synchronized, hashmap不是线程安全的,效率更高一些
  • 在实际开中不建议使用HashTable,在多线程环境下可以使用 ConcurrentHashMap类

16、有哪些线程安全的集合类?

  • HashTable
  • ConcurrentHashMap
  • Vector

17、集合是否可以存储 NULL?

1)List 接口 ArrayList、LinkedList 以及 Vector 等都可以存储多个 null;

2)Set 接口中 HashSet、LinkedHashSet 可以存储一个 nullTreeSet 不能存储 null

3)Map 接口中 HashMap、LinkedHashMap 的 key 与 value 均可以为 null。Treemap 的 key 不可以为 null,value 可以为 null。HashTable、ConcurrentHashMap 的 key 与 value 均不能为 null。

18、ArrayList 和 Vector 的区别?

  • Vector 是线程安全的,它的方法之间都加了 synchronized 关键字修饰,而 ArrayList 是非线程安全的。因此在不考虑线程安全情况下使用 ArrayList 的效率更高。
  • ArrayList 和 Vector 都有一个初始的容量大小,并在必要时对数组进行扩容。但 Vector 每次增长两倍,而 ArrayList 增长 1.5 倍。

19、ArrayList 可以添加 null 值吗?

  • ArrayList 中可以存储任何类型的对象,包括 null 值。不过,不建议向ArrayList 中添加 null 值, null 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。

20、为什么使用transient修饰ArrayList里面的elementData?

  • 根据ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。
  • 所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值