Java集合常见面试题

Java集合

1.Java常见的集合类(容器)

在java中提供了大量的集合框架,主要分为两类:

第一个是Collection 属于单列集合,第二个是Map 属于双列集合。

  • 在Collection中有两个子接口List和Set。在我们平常开发的过程中用的比较多像list接口中的实现类ArrayList和LinkedList。 在Set接口中有实现类HashSet和TreeSet.
  • 在map接口中有很多的实现类,平时比较常见的是HashMap、TreeMap,还有一个线程安全的ConcurrentHashMap。

2.ArrayList底层如何实现?

       ArrayList类又称动态数组,同时实现了Collection和List接口,是List的主要实现类,底层使⽤ Object数组存储,适⽤于频繁的查找⼯作,线程不安全。Arraylist 底层使⽤的是 Object 数组,由于采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响,因为执行插入删除操作需要移动后面元素的位置。

       ArrayList底层数据结构是用动态的数组实现的;ArrayList的初始容量为0,当第一次添加数据的时候才会初始化容量为10;ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组。ArrayList的添加逻辑:

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

3. ArrayList list=new ArrayList(10)中的list扩容几次

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

4.如何实现数组和List之间的转换?

  1. 数组转list,可以使用jdk自动的一个工具类Arrars,里面有一个asList方法可以转换为数组; Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址。
  2. List 转数组,可以直接调用list中的toArray方法,需要给一个参数,指定数组的类型,需要指定数组的长度;list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。

5. ArrayList 和 LinkedList 的区别是什么?

它们两个主要是底层使用的数据结构不一样,ArrayList 是动态数组,LinkedList 是双向链表,这也导致了它们很多不同的特点。

(1)从操作数据效率来说

ArrayList按照下标查询的时间复杂度0(1)【内存是连续的,根据寻址公式】,LinkedList不支持下标查询;查找的时候ArrayList需要遍历,链表也需要遍历,时间复杂度都是0(n);新增和删除:ArrayList尾部插入和删除,时间复杂度是0(1);其他部分增删需要挪动数组,时间复杂度是0(n);LinkedList头尾节点增删时间复杂度是0(1),其他都需要遍历链表,时间复杂度是0(n);

(2)从内存空间占用来说

ArrayList底层是数组,内存连续,节省内存;LinkedList 是双向链表,需要存储数据,和两个指针,更占用内存

(3)从线程安全来说

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

6.如何解决ArrayList和 LinkedList的线程安全问题?

主要有两种解决方案:

  1. 第一:我们使用这个集合,优先在方法内使用,定义为局部变量,这样的话,就不会出现线程安全问题。
  2. 第二:如果非要在成员变量中使用的话,可以使用线程安全的集合来替代;ArrayList可以通过Collections 的synchronizedList 方法将 ArrayList 转换成线程安全的容器后再使用。LinkedList 换成ConcurrentLinkedQueue来使用。

7.HashMap的底层实现原理

(1)HashMap存的都是一个个键值对,我们通过键值对对其进行修改和访问。

(2)在JDK 1.8以前底层是一个数组+链表的结构,当发生哈希冲突时,就将节点采用头插法的方式放在链表头部。

(3)在JDK 1.8以后底层是一个数组+链表+红黑树的结构,同样的,发生哈希冲突时,依旧是插到链表里去,只不过现在是尾插法,这样做就是为了避免出现循环链表(多线程下对HashMap进行扩容操作会导致链表出现环),另外当链表节点大于8时,会转成红黑树进行存储,但这并不代表删除了链表结构,链表结构依然存在,当节点数量重新小于8后,红黑树又会重新变成链表结构。在将链表转化为红黑树之前,先检查数组的长度是否大于64,如果数组的长度小于64,那么不会把链表转化为红黑树,而是先对数组进行扩容。

8. HashMap的添加方法put原理

(1):HashMap只提供了put方法用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。

(2):当用户调用put方法时,底层会调用putVal方法。

(3):putVal方法解析:

①:在调用putVal方法之前,它会先调用hash()方法,来得到key所对应的哈希值。

②:调用key的hashcode方法计算哈希值hash,并据此hash和length - 1做一个与运算计算出索引下标index(数组位置)。例如,假设HashMap的数组长度是16(即length = 16),那么length - 1的值是15。现在,如果键的hashCode()方法返回的哈希值是30,那么30%15的结果是14。这意味着哈希值30对应于数组中的索引下标14。

③:如果定位到的数组位置(index)没有元素,说明没有发生哈希碰撞,就直接插入。

④:如果定位到的数组位置(index)有元素就和要插入的key比较,具体比较过程是:先比较当前key的哈希值和index位置上的元素的哈希值是否相等,如果不相等,说明当前key和index位置上的元素不相同;如果相等,再用equals()方法来比较当前key和index位置上的元素是否相等,如果返回True,说明当前key和index位置上的元素是真的相等,否则,就是不相等。

⑤:如果相等的话,我们就覆盖掉当前位置上的元素;不相等的话,就判断当前位置上的元素是否是一个树节点,如果是的话,就调用putTreeVal()方法将元素添加进入红黑树中;如果不是的话,就遍历链表插入,采用尾插法。

⑥:如果链表长度超过TREEIFY_THRESHOLD默认是8,则将链表转换为树结构。

⑦:put完成后,如果HashMap的当前元素个数超过threshold(默认12)就要resize。

9.HashMap的获取方法get原理

从hashmap中调用get方法获取value时,会先计算key的hash值,在链表或者红黑树中进行查找,查到以后与key进行equals比较看是否是同一个值,是的话就直接返回对应的value,不是的话就在冲突中遍历。如果没找到就会在数组中进行查找,找到了就返回,没找到就返回null。

10. HashMap并发put的时候会有什么问题?

(1)多线程put后可能导致get死循环:问题原因就是HashMap是非线程安全的,多个线程put的时候造成了某个key值所对应的链表出现环(多线程并发修改Map容量时出现的),当另外一个线程get这个key值所对应的链表的时候,就会在这个链表上一直无限循环,造成死循环。

补充:jdk1.7的情况下,hashmap的链表添加元素使用的是头插法,所以一直put当数组出现扩容的情况下,原来链表中的元素头插到新链表时会出现乱序的问题,而且当线程B对链表进行重新赋值后,线程A当前指针的元素卫视其实已经变了,这样头尾指针就可能出现环。而jdk1.8将插入链表的方式改成了尾插法,就不会改变原链表中元素的相对位置,也就不会出现成环的问题。

(2)多线程put的时候可能导致元素丢失:主要问题出在,如果两个线程都同时取得了要插入位置的前一个位置元素,则他们在插入元素时只能有一个线程能够将要插入的元素连接到前一个元素的下边,另一个线程要插入的元素会被覆盖。

(3)put非null元素后get出来的却是null。

11. HashMap jdk1.7、1.8 数据结构的差别

(1)1.7的HashMap底层的数据结构是数组+链表,1.8则是数组+链表+红黑树。JDK1.8之后当链表⻓度⼤于阈值(默认为 8)时,将链表转化为红⿊树,以减少搜索时间(将链表转换成红⿊树前会判断,只有数组长度也大于64才会进行转换,如果当前数组的⻓度小于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)。

(2)1.7中HashMap添加元素使用的是链表的头插法,1.8使用的是尾插法。

(3)1.8不区分键值为null的情况,而1.7会调用putForNullKey方法来对键值为null的元素进行插入,但最终两个版本键值为null的hash值都为0,都会被放到哈希表下标为0的位置。

(4)jdk1.8中resize方法在表为空的时候会创建表,不为空的时候才去扩容,1.7里面resize方法主要负责扩容,infateTable负责创建表。

12.HashMap线程安全怎么做?

(1)替换成Hashtable,Hashtable通过对整个表上锁实现线程安全,因此效率比较低.

(2)使用Collections类的synchronizedMap方法包装一下。它实际上也是对每个方法都加一个Synchronized关键字来进行修饰,效率也很低。

(3)使用ConcurrentHashMap,它使用分段锁来保证线程安全,效率比较高。

13.HashTable与HashMap的区别

  1. 数据结构不一样,hashtable是数组+链表,hashmap在1.8之后改为了数组+链表+红黑树
  2. hashtable存储数据的时候都不能为null,而hashmap是可以的
  3. hash算法不同,hashtable是用本地修饰的hashcode值,而hashmap经常了二次hash
  4. 扩容方式不同,hashtable是当前容量翻倍+1,hashmap是当前容量翻倍
  5. hashtable是线程安全的,操作数据的时候加了锁synchronized,hashmap不是线程安全的,效率更高一些

在实际开中不建议使用HashTable,在多线程环境下可以使用ConcurrentHashMap类

14.HashSet的底层实现原理

HashSet底层其实是用HashMap实现存储的,HashSet封装了一系列HashMap的方法。HashSet的值存放于HashMap的key上,而value值默认为0bject对象。所以HashSet也不允许出现重复值,判断标准和HashMap判断标准相同,两个元素的hashCode相等并且通过equals()方法返回true。

15. ConcurrentHashMap 线程安全的具体实现⽅式/底层具体实现

(1)首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问(减少锁粒度)。

(2)java7中一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁。

(jdk1.7里面,对数据进行分段segment,每一个段上加锁reentrantlock)

(3)Java8 的ConcurrentHashMap相对于Java7来说变化比较大,不再是之前的Segment数组 + HashEntry数组 + 链表,而是Node数组 + 链表/红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。也就是说,把锁粒度进一步减小,直接对Node节点进行加锁(实际上Node数组就是HashEntry数组)

(jdk1.8里面,不再进行分段,而是对每个slot进行加锁。在加入元素的时候,空闲区进行CAS争抢,不是空闲区加入元素的时候通过synchronized进行资源争抢。)   

读操作,1.7和1.8都是不加锁的,通过volatile关键字保证数据的可见性。1.7是对段加volatile,1.8是对槽加volatile。

16. ==和equals的区别

对于==,在基本数据类型比较时,比较的是对应的值,对引用数据类型比较时,比较的是其内存的存放地址。

对于equals方法,equals是Object中的方法,判断是this==o,可以看到也是从地址来进行判断的。因此在equals方法未被重写时,其效果和==一致。其次,用户可以根据对应需求对判断逻辑进行改写,比如直接比较对象某个属性值是否相同,相同则返回true,不同则返回false。需保证equals方法相同对应的对象hashCode也相同。例如,String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而 String的equals方法先比较对象是否相同,在比较的是对象的值。1.8采用的是两个字符数组进行逐个比较。

17. hashcode和equals的区别

equals和hashCode这两个方法都是从object类中继承过来的,equals主要用于判断对象的内存地址是否相同;hashCode根据定义的哈希规则将对象计算为一个哈希码。

比如HashSet中存储的元素是不能重复的,主要通过hashCode与equals两个方法来判断存储的对象是否相同:

  1. 如果两个对象的hashCode值不同,说明两个对象不相同。
  2. 如果两个对象的hashCode值相同,接着会调用对象的equals方法,如果equlas方法的返回结果为true,那么说明两个对象相同,否则不相同。

equals方法被覆盖过,则hashCode 方法也必须被覆盖。

hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

  • 12
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值