Java面试之集合

Java基础之集合

集合的整体框架

整体框架结构图

记忆对比

Collection与Collections的区别

在这里插入图片描述

List实现类的对比

在这里插入图片描述
具体简单的对比如下
在这里插入图片描述

ArrayList和LinkedList区别

ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问)。
扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后把老数组的数组拷贝到新数组,如果不是尾部插入数据还会设计到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚 至超过linkedList(需要创建大量的node对象)。
LinkedList:基于链表,可以存储在分散到内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历。遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需 要对list重新进行遍历,性能消耗极大另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结果为空时会遍历整个列表

Set集合实现类对比

在这里插入图片描述

Queue集合实现类对比(比较不重要)

在这里插入图片描述

Collection接口对比

在这里插入图片描述

Map接口的对比

在这里插入图片描述

HashMap 、 Hashtable 的区别

在这里插入图片描述
底层实现:数组+链表实现
jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在

计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,
如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,
如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链 表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
key为null,存在下标0的位置

HashMap 、 ConcurrentHashMap 的区别

在这里插入图片描述
###总结
在这里插入图片描述

集合中的快速失败机制

是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构
       (是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。
         每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;
         否则抛出异常,终止遍历。
解决办法:
		在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
		使用CopyOnWriteArrayList来替换ArrayList
 Collections. unmodifiableCollection(Collection c) 来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常

list接口

1:Iterator 和 ListIterator 有什么区别?
		Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
		Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
		ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
2:遍历方式有以下几种:
		for 循环遍历:基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
		迭代器遍历:iterator:Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。
							Java 在 Collections 中支持了 Iterator 模式。		
		foreach 循环遍历:foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;
						缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

Set接口

1:HashSet如何检查重复?HashSet是如何保证数据不可重复的?	
    当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,
    同时也会与其他已经加入的对象的 hashcode 值作比较,
    如果没有相符的hashcode,HashSet会假设对象没有重复出现。
    但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。
    如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。
    这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
 2: hashCode()与equals()的相关规定
	    1:如果两个对象相等,则hashcode一定也是相同的
	    2:两个对象相等,对两个对象分别调用equals方法都返回true
	    3:两个对象有相同的hashcode值,它们也不一定是相等的
	    4:综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
	    5:hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),
	      则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据

set接口和list接口的对比

	List , Set 都是继承自Collection 接口
	List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。
					常用的实现类有 ArrayList、LinkedList 和 Vector。
	Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。
					Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
	另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
	Set和List对比
	Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
	List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

Map接口

HashMap的底层实现

基本框架图

在这里插入图片描述

hashMap的存储流程

在这里插入图片描述

1:当调用put()方法传递键和值来存储时,先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象,也就是找到了该元素应该被存储的桶中(数组)。
  当两个键的hashCode值相同时,bucket位置发生了冲突,也就是发生了Hash冲突,这个时候,会在每一个bucket后边接上一个链表(JDK8及以后的版本中还会加上红黑树)来解决,
  将新存储的键值对放在表头(也就是bucket中)

2:当调用get方法获取存储的值时,首先根据键的hashCode找到对应的bucket,
  然后根据equals方法来在链表和红黑树中找到对应的值

HashMap中的重要参数(变量)

主要参数
容量 2的幂次方
加载因子 时间和空间的转换设置 默认为0.75
扩容阈值
在这里插入图片描述
在这里插入图片描述

分析源码

插入元素
在这里插入图片描述
计算数组table中的位置
在这里插入图片描述
衍生的问题

为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?
容易出现 哈希码 与 数组大小范围不匹配的情况,即 计算出来的哈希码可能 不在数组大小范围内,
从而导致无法匹配存储位置
解决方案
	HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
	在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样
	一来是比取余操作更加有效率,
	二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,
	三来解决了“哈希值与数组大小范围不匹配”的问题;

为什么数组长度要保证为2的幂次方呢?

1:只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;
	减少哈希碰撞的几率。
为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
根据HashMap的容量大小(数组长度),按需取 哈希码一定数量的低位 作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配
为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?
加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突,两次够两,达到了高低位同时参与运行的目的;

HashMap是怎么解决哈希冲突的?

哈希:Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值)。
			简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
哈希冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞).
具体的解决方法
	1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
	2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
	3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化

在这里插入图片描述

HashMap线程不安全一个重要原因:多线程下容易出现resize()死循环
本质 为 并发执行 put() 操作导致触发 扩容行为 ,从而导致 环形链表,使得在获取数据遍历时形成类死循环 。

为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

在这里插入图片描述

HashMap 中的 key若 Object类型, 则需实现哪些方法?

在这里插入图片描述

TreeMap的特性

如果Key存入的是字符串等类型,那么会按照字典默认顺序排序

如果传入的是自定义引用类型,比如说User,那么该对象必须实现Comparable接口,并且覆盖其compareTo方法;或者在创建TreeMap的时候,我们必须指定使用的比较器。如下所示
// 方式一:定义该类的时候,就指定比较规则
class User implements Comparable{
    @Override
    public int compareTo(Object o) {
        // 在这里边定义其比较规则
        return 0;
    }
}
public static void main(String[] args) {
    // 方式二:创建TreeMap的时候,可以指定比较规则
    new TreeMap<User, Integer>(new Comparator<User>() {
        @Override
        public int compare(User o1, User o2) {
            // 在这里边定义其比较规则
            return 0;
        }
    });
}

那么Comparable接口和Comparator接口有哪些区别呢?

Comparable实现比较简单,但是当需要重新定义比较规则的时候,必须修改源代码,即修改User类里边的compareTo方法

Comparator接口不需要修改源代码,只需要在创建TreeMap的时候重新传入一个具有指定规则的比较器即可。

如何决定使用 HashMap 还是 TreeMap?

	对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。
	然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。
	基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历

comparable 和 comparator的区别?

	comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
	comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序
	一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,
	我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,
	第二种代表我们只能使用两个参数版的Collections.sort().

Iterator 和 ListIterator 有什么区别?

Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。 
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等

HashMap与HashTable的区别?

HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;
HashMap允许K/V都为null;后者K/V都不允许为null;
HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

HashMap为啥是线程不安全的

在多线程环境下,HashMap扩容可能会导致死循环

ConcurrentHashMap和Hashtable的区别?

ConcurrentHashMap结合了HashMap和Hashtable二者的优势。HashMap没有考虑同步,Hashtable考虑了同步的问题。但是Hashtable在每次同步执行时都要锁住整个结构。

ConcurrentHashMap锁的方式是稍微细粒度的,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁上当前需要用到的桶

ConcurrentHashMap原理,jdk7和jdk8版本的区别

JDK7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个 HashEntry又是一个链表结构
元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部 锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment
get方法无需加锁,volatile保证

JDK8:
数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
查找,替换,赋值操作都使用CAS
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写 操作、并发扩容
读操作无锁: Node的val和next使用volatile修饰,读写线程对该变量互相可见 数组用volatile修饰,保证扩容时被读线程感知

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值