1.Java容器有哪些?
java容器主要有Collection和Map两大类,还有他们的子类和实现类
- Collection
- List
- ArrayList
- LinkedList
- Vector
- Stack
- Set
- HashSet
- LinkedHashSet
- TreeSet
- List
- Map
- HashMap
- LinkedHashMap
- TreeMap
- ConcurrentHashMap
- HahsTable
- HashMap
2.HashMap的数据结构是什么样的?
HashMap本质是一个定长的数组,数组中存放链表,HashMap在jdk1.8的源码如下图
当向HashMap中put值时,会首先通过hash函数计算出数组的位置,比如索引值为i,将其放到entry[i]的位置,如果当前位置有元素了,会插在这个元素的前面(jdk1.7头插法,jdk1.8尾插法),最先加入的元素在链表尾部。比如第一个键值对a通过hash得到数组的索引为index=0,键值对b也计算index=0,则b.next=a,entry[0]=b;这样index=0的位置存放了b,a2个键值对,是用链表关联的。也就是说数组中存储的是最后插入的元素。
3.HashMap的put方法实现原理?
jdk1.7采用的是位桶+链表的方式
jdk1.8采用的是位桶+链表/红黑树
4.HashMap的put方法的hash函数是如何计算的?
- key==null
- key等于null时,值为0,所以HashMap的key可以为null,
- 对比HashTable,如果key为null,会抛出异常;HashTable的key不可为null
- key!=null
- 首先计算HashCode的值,再HashCode的值右移16位再和HashCode的值进行异或运算
- 此过程就是扰动函数
- 扰动函数原理,如果是小于32位,则右移16位后补零,进行异或运算后还是0
- 如果是32位的,则右移16位后,高位补0,原来的高位变成了低位,进行亦或运算,增加随机,均匀分布
5.HashMap是如何计算数组下标的?
实际计算公式是hash&(length-1)
步骤 | 代码 | 说明 |
---|---|---|
1.计算hashCode | h=key.hashCode() | 计算key的hashCode值 |
2.二次处理计算hash值 | h^(h>>>16) | 扰动函数 |
3.计算index | hash&(length-1) | 二次处理的hash值 & (数组长度-1) |
6.为什么hash要进行右移16位的异或运算?
由于最终要和(length-1)进行与运算,数组的长度大多都是小于2的16次方的,高16位是用不到的。所以始终是hashCode的低16位参与运算,如何让高16位也参与运算呢,会让下标更加散列。右移16位后,高16位和低16位进行异或运算,增加随机性。
7.为什么用^不用&或者|?
增加随机性
如果用&操作符,得到75%的0,25%的1
如果用|操作符,得到75%的1,25%的0
如果用^操作符,得到50%的1,50%的0,异或运算对均匀分布非常有用
8.jdk1.8的HashMap为什么引入红黑树?
在jdk1.7以前,HashMap用的是数组+链表,如果链表越来越长,查询的时间复杂度最坏时O(n)
为了提高查询效率,jdk1.8使用了红黑树,查询的平均时间复杂度为O(logn)
9.jdk1.8的HashMap为什么在链表长度为8的时候判断红黑树?
在jdk1.8及以后的版本,HashMap采用的数据结构是,数组+链表,更改为在链表长度为8时,开始由链表转化为红黑树。
链表的时间复杂度是O(n),红黑树的时间复杂度是o(logn),红黑树的时间复杂度是优于链表的。因为树节点所占空间是普通节点的2倍。所以当节点足够多时选择使用红黑树。也就是说,当节点比较少的时候,尽管红黑树的时间复杂度表现比链表好一些,但红黑树所占空间比链表大,综合考虑,在节点较多时,红黑树所占空间劣势相比查询性能的提升不那么明显时,转化为红黑时。
为什么选择8作为临界值呢?
在理想状况下,受随机分布hashCode的影响,链表中的节点遵循泊松分布。据统计链表中节点数是8的概率大概是千分之一,并且此时链表的性能很差了。在这种情况下,转化为红黑树,优化查询性能。
10.什么是泊松分布?
泊松分布的参数λ是单位时间(或单位面积)内随机事件的平均发生次数。
泊松分布适合于描述单位时间内随机 事件发生的次数。
11.如果链表的长度大于8,一定会转化为红黑树吗?
先判断table数组的长度是否小于64,小于64则扩容,避免红黑树结构化
大于等于64,才转化为红黑树
12.HashMap为什么选红黑树,能不用avl树?
avl树和红黑树有以下区别:
- AVL树更加平衡,提供更快的查询速度,一般读取密集型任务,用avl树。
- 红黑树更适合插入和修改密集型任务。
- 通常,avl树的旋转比红黑树更加复杂。
- AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于 在任何添加/删除操作时完成的旋转操作次数。
- 两种实现都缩放为O(logN),其中N是叶子的数量,但实际上AVL树在查找 密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方 面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。
- 在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在 红黑树中,差异可以是2倍。
- 两个都是O(logn)查找,但平衡AVL树可能需要O(logn)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(logn)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。
13.HashMap的为什么不直接采用hashCode的值作为下标?
hashCode的值范围在-(231)到231-1,HashMap的容量范围是16~230,HashMap通常取不到最大值,且机器设备也无法提供这么大的数组空间,hashCode的值可能不在HashMap的index范围内,导致无法匹配。
解决方法是右移16位进行异或运算。
14.为什么HashMap的数组长度要保持为2的幂次方呢?
- 只有是2的幂次方,hash&(length-1)才等价于hash%length,实现key的定位。
- 2的幂次方可以减少hash冲突,提高查询效率。
- 如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111…的形式,在与 h 的 二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在与 h 与操作,最后一位 都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存 放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度 小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空 间的浪费。
15.jdk1.7和jdk1.8的HashMap有什么区别?
jdk1.7
- 底层采用数组+链表
- 数组长度默认是16,加载因子是0.75,阈值是0.17*16=12,当发生冲突时,会以链表的形式存储新的数据,新的数据插入到链表的头部,将新来的值赋值给当前位置的数组。
- 当数组中的12个位置被占据时,同时新插入的数据位置不为空,需要进行2倍扩容。
- 并发环境会产生死锁。
jdk1.8
- 底层采用数组+链表/红黑树
- 数组长度默认是16,加载因子是0.75,阈值是0.17*16=12,当发生冲突时,会以链表的形式存储新的数据,新的数据插入到链表的尾部,将新来的值赋值给当前位置的数组。插入尾部也是为转换红黑树做准备,如果链表上的元素超过8个,则转换为红黑树。提高增删查的效率。
- 达到阈值值,直接扩容,2倍扩容。不需要判断新元素的位置是否为空。
- 并发下不会产生思死锁,但是会出现数据覆盖。
16.HashMap的主要成员变量有哪些?
- **transient Node<K,V>[] table:**这是一个Node类型的数组(也有称作Hash桶),可以从下面源码中看 到静态内部类Node在这边可以看做就是一个节点,多个Node节点构成链表,当链表长度大于8的时候并且 table长度大于64的时候转换为红黑树。
- **transient int size:**表示当前HashMap包含的键值对数量
- **transient int modCount:**表示当前HashMap修改次数
- **int threshold:**表示当前HashMap能够承受的最多的键值对数量,一旦超过这个数量HashMap就会进行扩 容
- **final float loadFactor:**负载因子,用于扩容
- **static final int DEFAULT_INITIAL_CAPACITY = 1 << 4:**默认的table初始容量16
- **static final float DEFAULT_LOAD_FACTOR = 0.75f:**默认的负载因子
- static final int TREEIFY_THRESHOLD = 8: 链表长度大于该参数转红黑树
- static final int UNTREEIFY_THRESHOLD = 6: 当树的节点数小于该参数转成链表
17.jdk1.7和jdk1.8的扩容原理有什么不同?
jdk1.7
扩容前和扩容后都是使用的公式hash&(length-1)
整体扩容过程是取出数组元素,遍历以该数组元素为头节点的链表元素,然后计算在新数组中的下标,然后进行交换,即原来hash冲突的单向链表的尾部变成了扩容后单向链表的头部。
jdk1.8
对公式进行判断(hash&newTable)==0
jdk1.8的扩容更加的优雅,由于扩容数组长度是2倍关系。假设原数组size是4,扩容后为8,左移一位是2倍,二进制表示为0100 》1000的变化。扩容时只需要判断原来的hash值和newtable进行与运算的结果,如果为0,则位置保持不变,如果为1,则在原来的位置加上原数组的长度。
如果为true,则放在原位置
如果为false,则放在原位置+oldCap处
18.HashMap jdk1.8扩容源码分析
HashMap的扩容原理也挺值得说的,其中把链表分割成一条高位链表和一条低位链表分别插入到新的2倍空间数组中,并且不需要重新对每个key重新取模,低位链表还是放在原来相同的下标桶位,高位链表放在原来下标桶位+n的下标位置
在jdk1.7源码中HashMap进行扩容时,hash冲突的数组索引处的旧链表元素扩容到新数组时,如果扩容后的数组元素的位置与原数组的索引位置相同,则链表会发生倒置,在jdk1.8中不会出现倒置。
在jdk1.7中,扩容时紧紧只是重新计算了数组的下标,整体的数据结构还是数组+链表
在jdk1.8中,扩容时,不仅重新计算了下标,在链表长度达到8时,会转换为红黑树。且当前结构为红黑树,元素个数小于6时,会转换为链表结构。
19.HashMap jdk1.8红黑树扩容情况的split方法?
20.String类适合做HashMap的key的原因?
在《Java 编程思想》中有这么一句话:设计 hashCode() 时最重要的因素就是对同一个对象调用 hashCode() 都应该产生相同的值。
String 类型的对象对这个条件有着很好的支持,因为 String 对象的 hashCode() 值是根据 String 对象的 内容计算的,并不是根据对象的地址计算。下面是 String 类源码中的 hashCode() 方法:String 对象底 层是一个 final 修饰的 char 类型的数组,hashCode() 的计算是根据字符数组的每个元素进行计算的,所 以内容相同的 String 对象会产生相同的散列码
HashMap内部实现是通过key的hashCode来确定value的位置的。
第一个原因
string天生复写了hashCode方法,根据string的内容来计算hashCode。
第二个原因
因为字符串是不可变的,当创建字符串时,它的hashCode被缓存下来,不需要再次计算,所以相比于其他对象更快。
21.如果用自定义的对象作为key,需要注意什么问题?
需要重写hashCode方法和equals方法。
当HashMap存入k1的时候,会执行hashCode方法,因为没有重写hashCode方法,回去object类找hashCode方法,而object类的hashCode方法返回的时对象的地址。这时候用k2去获取,用相同的方式去获取hashCode方法,因为内存地址不一样,所以hashCode不一样。即使hashCode一致,在hash冲突的情况下,需要调用equals方法进行对比,因为没有重写equals方法,会调用object的equals方法,object的equals方法会比较内存地址是否一样,因为k1和k2时new出来的,内存地址是不一样的,所以k2获取不到k1的值。因为没有重写hashCode方法和equals方法。
22.什么是hash,什么是hash冲突?
hash,一般翻译为散列,也音译为哈希。
通俗来讲就是将任意长度的字符通过散列算法,输出为固定长度的散列值,也叫hash值。这种散列是一种压缩映射,散列值的空间远小于原值的空间,不同的输入可能会有相同的散列值输出。所以不能仅仅通过散列值来做字符相等的判断。简单来说就是将任意长度的消息,压缩到某一固定长度消息的函数。
根据同一散列函数计算出的hash值不通,则输入值肯定不同,hash值相同,输入值也可能不同。也就是hash冲突的情况。
23.解决hash冲突的方式有哪些?
开放定址法:使用探测的方式在数组中找到另一个可以存储值的位置。
链地址法:也叫拉链法,HashMap和HashSet都是使用的这种方式,在存在hash冲突的时候,使用链表或者红黑树的形式存储数据。
再散列法:hash冲突时,通过再次散列的方式确定插入的位置,缺点是每次冲突都要计算散列,时间复杂度增加。
24.开发寻址法的探索方式?
线性探测
按顺序决定哈希值时,如果某数据的哈希值已经存在,则在原来哈希值的基础上往后加一个
单位,直至不发生哈希冲突。
再平方探测
按顺序决定哈希值时,如果某数据的哈希值已经存在,则在原来哈希值的基础上先加1的平方
个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。
伪随机探测
按顺序决定哈希值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来哈希值
的基础上加上随机数,直至不发生哈希冲突。
25.开放地址法和拉链法的优缺点?
**开放地址法:**会产生堆积问题,不适合大规模的数据存储,插入时,可能会出现多次冲突的情况,删除数据时,其他数据也有影响,实现相对较为复杂。且节点规模大时,再平方探测会浪费空间。
**拉链法:**处理冲突简单,且无堆积现象。平均查找长度短,时间复杂度低。链表中的节点是动态申请的,适合构造表不能确定的情况。相对而言,指针域可以忽略不计,所以更节省空间。尾插法简单,只需要修改指针,不需要对其他冲突做处理。
26.HashMap的查询效率?
- 理想情况o(1),没有冲突
- jdk1.7最坏为o(n)
- jdk1.8最坏为o(logn)
27.HashMap和HashTable的区别?
-
继承的父类不同
- HashTable继承自Diconary类,HashMap继承自AbtractMap类
- 两者都实现了map接口
-
线程安全不同
-
HashMap的实现不是同步的,如果多线程同时访问同一hash映射,至少有一个线程从结构上修改了该映射,则应该实现外部同步。结构上的修改是指添加或删除一个或多个映射关系的任
何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。
-
HashTable的方法是synchronize修饰的。在多线程环境下可以直接使用HashTable,不需要额外增加同步,使用HashMap需要额外处理同步问题。一般使用封装好的工具类。或则加上synchronize修饰。
-
-
是否提供contains方法
- HashMap去掉了contains方法,保留了containValue和containsKey方法。因为contains方法容易让人产生误解。
- HashTable保留了contains方法,contains方法和containsValue方法功能相同。
-
key和value是否允许为null
- HashTable中,key和value都不能为null。put(null,null)编译可以通过,因为key和value都是object类型,但是执行会报错。会报空指针异常。
- 在HashMap中key可以为null,并且只能有一个null键。值可以有多个null值。当get方法获取到的值为null时,可能是HashMap中不存在该key,也可能是key对应的值为null,所以不能通过get方法判断key是否存在,应该用containsKey方法。
-
迭代方式不同
- 两者都使用了terator迭代器。由于历史原因,HashTable还使用了enumeration的方式。
-
hash值不同
- HashTable直接使用了hashCode,HashMap还进行了扰动函数的计算和与函数运算。
-
内部实现(初始化+扩容)
- HashTable默认不指定长度为11,HashMap默认为16。HashTable不要求数组的容量为2的幂次方,HashMap要求数组的容量为2的幂次方。
- HashTable扩容时为2n+1,HashMap为2n。
28.为什么HashTable的扩容方式选择为2n+1?
为了均匀分布,降低冲突率。
首先,Hashtable的初始容量为11。Index的计算方式为: int index = (hash & 0x7FFFFFFF) % tab.length;
常用的hash函数是选一个数m取模(余数),这个数在课本中推荐m是素数,但是经常见到选择
m=2n,因为对2n求余数更快,并认为在key分布均匀的情况下,key%m也是在[0,m-1]区间均匀分
布的。但实际上,key%m的分布同m是有关的。
证明如下:key%m = key - xm,即key减掉m的某个倍数x,剩下比m小的部分就是key除以m的余数。
显然,x等于key/m的整数部分,以floor(key/m)表示。假设key和m有公约数g,即key=ag, m=bg, 则
key - xm = key - floor(key/m)*m = key - floor(a/b)*m。由于0 <= a/b <= a,所以floor(a/b)只有a+1中
取值可能,从而推导出key%m也只有a+1中取值可能。a+1个球放在m个盒子里面,显然不可能做到
均匀。
由此可知,一组均匀分布的key,其中同m公约数为1的那部分,余数后在[0,m-1]上还是均匀分布的,
但同m公约数不为1的那部分,余数在[0, m-1]上就不是均匀分布的了。把m选为素数,正是为了让
所有key同m的公约数都为1,从而保证余数的均匀分布,降低冲突率。
鉴于此,在HashTable中,初始化容量是11,是个素数,后面扩容时也是按照2N+1的方式进行扩容,
确保扩容之后仍是素数。
29.简述jdk1.7和jdk1.8中HashMap的改动?
不同 | jdk1.7 | jdk1.8 |
---|---|---|
存储结构 | 数组+链表 | 数组+链表+红黑树 |
初始化方式 | 单独函数inflatetable()函数 | 集成在resize()方法 |
hash值计算方式 | 扰动函数=4次位运算+5次异或运算 | 扰动函数=1次位运算+1次异或运算 |
存放数据规则 | 无冲突时,存放数组,有冲突时,存放链表 | 无冲突时,存放数组,有冲突时,存放链表,链表大于8时,变为红黑树 |
插入方式 | 头插法(原数据后移一位) | 尾插法,直接插入到链表尾部 |
扩容后index的计算方式 | 全部按照hash&(length-1) | 扩容后index=原index/原index+旧容量 |
30.负载因子为什么会影响HashMap的性能?
负载因子代表了一个散列表的空间使用程度。
initailCapacity*loadFactor=HashMap的容量
负载因子越大,元素越多,导致扩容时机越晚,导致hash冲突的机会变多,从而链表变长,查询的时间复杂度增大,性能下降。
负载因子越小,元素稀疏,空间利用率低。查找效率高。
31.介绍下jdk1.7中ConcurrentHashMap的数据结构?
ConcurrentHashMap和HashMap结构差不多,不过ConcurrentHashMap支持并发操作。所以结构更加复杂一些。
整个ConcurrentHashMap右一个个segment组成。segment代表一段的意思。所以ConcurrentHashMap也叫分段锁。简单理解,ConcurrentHashMap是由segment数组组成。segment继承自reentrantlock来进行加锁,所以每个segment是线程安全的,整个ConcurrentHashMap就是线程安全的。
32.ConcurrentHashMap的构造函数有几个?
concurrencylevel:并发级别,并发数,segment数。默认是16,也就是说默认是16个segments。
理论上来说,最多支持16个线程并发写。操作分布在不同的segment上,对单独的segment进行加锁处理,可以做到线程安全,可以在初始化的时候设置此值,设置之后不支持扩容。
33.简述下jdk1.7的ConcurrentHashMap是如何进行锁操作的?
数据结构:reentrantlock+segment+hashentry组成,写的时候对单个segment加锁
34.jdk1.8的ConcurrentHashMap是如何保证并发的?
结构数组+链表+红黑树,内部大量采用cas来实现。
DK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来
设计,内部大量采用CAS操作,这里我简要介绍下CAS。
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观
锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,
下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比
如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。
Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的
可见性。
35.jdk1.8的ConcurrentHashMap和jdk1.7的区别?
数据结构,在jdk1.7中使用的是reentrantlock+segment+hashentry,在jdk1.8中使用的是node+cas+synchronized+红黑树。
- 取消了segment分段锁,采用数组+链表+红黑树。
- 1.7中使用reentrantlock+segement加锁,在1.8中使用的是cas+synchronized加锁
- 1.7是对需要进行数据操作的segment加锁,1.8是对数组元素node加锁。
- 在链表节点大于8时,会转为红黑树存储。数据量大时,hash冲突加剧,性能下降。
- 查询时间复杂度,1.7最坏时是单链表o(n),1.8是红黑树o(logn)
36.HashTable和concurrenttHashMap在多线程下有什么区别?
多线程环境下都是线程安全的,ConcurrentHashMap的效率更高。
HashTable使用一把锁处理并发问题,在多线程情况下,多个线程竞争同一个锁,效率较低,导致阻塞。
1.7使用分段锁,相当于把HashMap分成多段,每一段都拥有各自的锁,这样可以实现多线程访问。
1.8采用了cas和synchronized加锁,锁粒度细化到元素本身,理论上是最高级别的并发。
37.LinkedHashMap和HashMap有什么区别?
- LinkedHashMap继承自HashMap,是基于HashMap和双向链表实现的。
- HashMap是无序的,LinkedHashMap是有序的,分为插入顺序和访问顺序。如果是访问顺序,使用put和get时,都会把entry移动到双向链表的表尾。
- LinkedHashMap存取数据还是和HashMap一样,使用entry[]数组的形式,双向链表只是为了保证顺序。
- LinkedHashMap也是线程不安全的。
38.LinkedHashMap 是如何维护双向链表的?
数据结构:数组+单向链表+双向链表
每一个节点都是双向链表的节点,维持插入顺序。head指向第一个插入的节点,tail指向最后一个节点。
数组+单向链表是HashMap的结构,用于记录数据。
双向链表保存的是插入顺序,顺序访问。
next是用于维护数据位置的,before和after是用于维护插入顺序的。
遍历分为插入方式和访问方式:
插入方式:遍历时和插入时位置固定
访问方式:put和get方法都会将当前元素移到双向链表的最后
是否使用访问顺序遍历,是通过**LinkedHashMap 的accessOrder参数控制的,true为访问顺序遍历,false为插入顺序遍历。**默认是false,插入方式遍历。如果是true,注意并发修改异常。因为get方法会修改LinkedHashMap的结构。
39.使用map时,要求按照put的顺序进行遍历,选择什么map?
LinkedHashMap
分为访问遍历和插入遍历,初始化时可以指定。相对于访问顺序,插入顺序使用的场景更多一些,所以默认是插入顺序进行编排。
40.介绍LinkedHashMap的2种遍历顺序?
插入顺序和访问顺序
插入顺序就是put插入的顺序。
访问顺序是put和get操作都是把当前元素移至到双向链表的末尾。
41.LinkedHashMap访问顺序的源码原理?
关键点是accessOrder参数,默认为false,插入方式,true为访问方式。
当调用get方法时,会判断accessOrder的值,如果为true,会执行afterNodeAccess方法,就是放到node的后面。
42.LinkedHashMap的put方法的原理?
LinkedHashMap没有put方法,使用的是HashMap的put方法,并且复写了newNode方法和afterNodeAccess方法。
新增的节点放到双向链表末尾。
将新增的节点添加至链表尾部
43.LinkedHashMap的get方法原理?
会判断是否是访问顺序,如果是,放到双向链表末尾。
JDK1.8 的HashMap的get方法
1)计算数据在桶中的位置 (tab.length- 1) & hash(key)
2)通过hash值和key值判断待查找的数据是否在对应桶的首节点, 如果在,则返回对应节点 据;否则判断桶首节点的类型。如果节点 为红黑树,从红黑树中获取对应数据;如果节点为链表节点,则遍历 链表,从中获取对应数据
44.用LinkedHashMap实现lru算法?
主要考察2个点
- accessOrder实现lru的逻辑
- removeEldestEntry的复写
在插入之后,会调用LinkedHashMap的afternodeinsertion方法,需要重写removeeldestentry方法
import java.util.LinkedHashMap;
import java.util.Map;
class Scratch<K, V> extends LinkedHashMap<K, V> {
private int capacity;
public Scratch(int capacity) {
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
45.HashSet的底层原理?
底层是依赖HashMap实现的,通过HashSet的构造参数可以知道,用HashMap来初始化成员变量。所以HashSet的初始容量也是1<<4即16,加载因子为0.75f。
46.HashSet是如何判断元素重复的?
HashSet通过元素的hashCode和equals方法判断元素重复。
HashSet的add方法,其实是HashMap的put方法,第一个参数e代表了存入的元素,第二个参数present代表初始化好的new object对象,这样HashSet在存入一个值的时候就可以很好的利用HashMap的put方法key-value结构。
47.LinkedHashSet源码说一下?
HashSet是依赖HashMap,但是不是继承关系,是构造器时new的HashMap。
分析HashSet的构造函数可以知道 只有一个default修饰的走了LinkedHashMap,也就是专门为LinkedHashSet准备的。default修饰的同包才能访问。其他的构造函数都是new的HashMap的构造函数。
48.jdk1.8中set的类继承结构?
49.TreeSet和treemap的关系?
与HashSet和LinkedHashSet一个套路,TreeSet其实是使用了TreeMap的结构
treemap也是key-value的结构,TreeSet和treemap并没有继承关系,只是构造的时候使用了treemap构造函数。
set和map是不能继承的。
50.set集合的特点,常见的set集合有哪些?
- 元素不允许重复,相同元素add,会返回false。
- 判断重复使用equals方法,不是==运算法。2个对象用equals返回true,set就不会同时接受。
HashSet和TreeSet都是基于set的实现类。其中TreeSet是set接口的子接口SortedSet接口的实现类。
set
- sortedset
- TreeSet
- HashSet
- LinkedHashSet
51.说说HashSet的特点?
- 不能保证元素的排列顺序,顺序可能发生变化。
- 不是同步的。
- 集合元素可以是null,但只能有一个。
当向HashSet存入一个值时,需要计算key的hashCode,并通过hashCode得到的结果再进行(length-1)&hash得到index的位置,判断是否重复是通过hashCode和equals方法。存入数据是通过key-value方式,value是初始化好的new object。
52.说说LinkedHashSet的特点?
HashSet集合通过hashCode确定index的位置,并通过LinkedHashMap确定顺序,顺序也是插入顺序,LinkedHashSet会保持元素的插入顺序。将以元素添加的顺序访问set集合。
53.讨论下map的性能,大数据量下哪个更好?
前提:IdentityHashMap不在讨论范围内。数据 100W,机器指标I3处理区,4G内存。单线程环境。
1.看着hashTable和HashMap性能差不多, 因为是单线程环境,其实table和map最显著的区别就是
同步问题,没有同步问题,两者的性能区别不是很大, 就像我们经常所说的, hashTable会慢一点,因为是同步
的.
2.LinkedHashSet,不多说,维护双向顺序链表,肯定会累一点,慢一点。其中插入操作慢的更加显著。
3.TreeMap,红黑树结构的有序map。 如果对存入的数据顺序没有要求的话,TreeMap的性能慢的比较显著,因为红黑树需要进行平衡的旋转变色 。况且,多数情况下,HashMap的存取时间复杂度都是O(1),红黑树是O(logn),所以treemap慢一点,正常。
54.说说jdk1.8中ArrayList的特点?
- ArrayList底层是动态数组,实现了list,RandomAccess, Cloneable, java.io.Serializable接口, 并允许包含null元素,实现了 RandomAccess表示支持快速访问,底层是数组实现,访问时间复杂度是o(1),实现了cloneable接口,表示可以被复制,且是浅复制。实现了java.io.Serializable接口,支持序列化传输。
- 底层是数组实现,默认容量是10,当超出默认容量后,会扩容1.5倍,即自动扩容机制。数组的扩容是新建一个大数组,将原数组元素拷贝到新数组,此操作代价很高,我们应该减少这种操作。
- 该集合是可变长度的数组,扩容时,扩容为1.5倍,将原数组的元素拷贝到新数组,Arrays.copyOf浅复制的方式进行拷贝。
- 采用了fail-fast的机制,面对并发修改时,迭代器很快就会完全失败,报异常concurrentModificationException并发修改错误。
- remove方法会将下标到末尾的元素向前移动一位,并把最后一位置空,为了gc。
- 数组扩容代价很高,我们在使用时尽量指定好容量。以避免数组扩容发生,或者根据实际需求,通过调用ensureCapacity方法手动增加ArrayList实例的容量。
- ArrayList不是线程安全的,只能在单线程下使用,多线程下,尽量使用Collections.synchronizedList(List l)返回一个安全的ArrayList类,或者使用并发包下面的CopyOnWriteArrayList类。
- 如果是删除指定元素,可能会挪动大量的数组元素,如果是末尾元素,那么代价是最小的。
55.ArrayList有几个构造方法,简单说下?
三个构造方法
第一个:
无参构造方法,初始容量为10.
第二个:
构造一个包含指定元素的列表。
第三个:
构造一个具有初始化容量的空列表。
我们看到代码逻辑不复杂,从代码逻辑中,依稀可以看到, 会有new Object[] 的操作,从这里就能印证,ArrayList就是 以数组为底层的。 更直接的,我们通过看源码可以发现:
图中有两个fianl的静态变量引用:
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
ArrayList( int initialCapcity ):构造一个具有初始容量值得空列表
Object[]这是什么?数组。
56.在ArrayList中,为什么有2个静态final修饰的object数组?
,EMPTY_ELEMENTDATA & DEFAULTCAPACITY_EMPTY_ELEMENTDATA?
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
从代码可以看到,这2个object的数组基本是一样的,那么为什么要用2个呢?从源码可以看到只有无参构造器使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,其他2个构造器使用的是EMPTY_ELEMENTDATA,先说结论,这里是为了初始化容量不同而设定的。
先看一道面试题
==和equals是啥区别?
- ==是判断两个变量或实例是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是不是相同
- ==是指对内存地址进行比较 , equals()是对字符串的内容进行比较
- ==指引用是否相同, equals()指的是值是否相同
回到正题,在使用add方法时,
从源码可以看到,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则容量为默认的10,如果是EMPTY_ELEMENTDATA,则容量为1。调用add方法时才为10,不调用为0.
57.ArrayList中的elementData用transient修饰,序列化后数据会丢失吗?
源码中的全局变量,transient Object[] elementData;
隐含面试题:
1.序列化是什么?
我们知道对象是不能直接进行网络传输的,必须将对象转为二进制字节流进行传输。序列化就是将对象转为二进制字节流的过程。同理,反序列化就是将字节流构建对象的过程
- 对java对象来说,如果使用jdk的序列化实现,只需要实现java.io.Serializable接口。
- 可以使用ObjectOutputStream和ObjectInputStream对对象进行序列化和反序列化。序列化的时候会调用writeObject方法,把对象转为字节流。反序列化会调用readObject方法,把字节流转为对象。
- java在反序列化的时候会校验serialVersionUid与对象的serialVersionUid是否一致,如果不一致,会抛出InvalidClassException异常
- 官方强烈推荐序列化时指定一个serialVersionUid,否则虚拟机会根据类的相关信息通过一个摘要算法生成,所以当我们修改类的参数的时候,虚拟机生成的serialVersionUid时变化的。
- transient关键字修饰的变量不会被序列化为字节流。
2.transient关键字的具体含义?
transient关键字修饰的变量不会被序列化为字节流。
进入正题:
从源码可以看到elementData就是ArrayList的底层数组,如果不能被序列化,那ArrayList就是不可用的。
我们在进行对象序列化的时候,只需要实现java.io.Serializable接口,ArrayList实现了该接口,说明ArrayList是可以被序列化的。所有用户数据,都保存在elementData中,如果序列化后数据丢失,那ArrayList肯定是有问题的。
arraylsit用什么巧妙的方式,既防止了elementData的序列化,又保证存入的元素不丢失呢?
答案很简单,不对elementData序列化,对elementData里面的元素进行循环,取出的元素单独进行序列化。
通过查看ArrayList源码中的2个方法,可以看到具体的实现 writeObject和readObject
58.为什么不直接序列化elementData,这样设计有什么好处吗?
elementData是一个对象数组,不直接序列化这个对象,是因为绝大多数的情况下,存在没有存储任何元素的空间,这样序列化会存在空间浪费,全部序列化效率更低。
比如容量为10,但只有一个元素,浪费了9个容量。
每次扩容都是原来的1.5倍,如果在大容量空间下比如10万,扩容到15万,将有5万的空间浪费。
59.说说你对transient的理解,列举下使用场景?
- 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量的内容在序列化后无法获得访问。
- transient关键字只能修饰变量,不能修饰类和方法。
- 本地变量不能被transient关键字修饰。
- 自定义的类需要序列化,只需要实现java.io.Serializable接口。
- 被transient关键字修饰的变量不能再被序列化,静态变量不管是否被transient修饰,都不能被序列化。
- 使用场景,密码和银行卡不想被序列化,可以加上transient关键字。这个字段的生命周期仅存在于调用者的内存中,不会写到磁盘持久化。
60.ArrayList的fail-fast机制是什么原理?
采用了fail-fast机制,面对并发修改时,会立即失败,报concurrentModificationException并发修改异常。
ArrayList的父类abstractlist中有一个类属性,protected transient int modCount = 0; 这个属性代表了list被结构性修改的次数。
结构性修改是指:改变了list的size大小。
这个字段用于迭代器和列表迭代器的实现类中,由迭代器和列表迭代器的方法返回。如果这个值被意外修改,就会抛出ConcurrentModificationException异常。
在迭代过程中,它提供了fail-fast机制,而不是不确定的行为来处理并发修改。子类使用这个字段是可选的, 如果子类希望提供fail-fast迭代器,它仅仅需要在add(int, E),remove(int)方法(或者它重写的其他任何 会结构性修改这个列表的方法)中添加这个字段。调用一次add(int,E)或者remove(int)方法时必须且仅仅给这个字段加1,否则迭代器会抛出伪装的ConcurrentModificationExceptions错误。如果一个实现类 不希望提供fail-fast迭代器,则可以忽略这个字段。
- expectedModCount初始值是modCount。
- hasnext的判断条件时cursor!=size,当前迭代位置不是数组的最大容量值就返回true。
- next和remove操作之前都会调用checkForComodification来检查expectedModCount和modCount是否相等。
如果没checkForComodification去检查expectedModCount与modCount相等,这个程序肯定会报越界异常
ArrayIndexOutOfBoundsException 因为有modCount的存在,在使用多线程对非线程安全的集合进行操作时,使用迭代器循环会产生modCount != expectedModCount的情况,会抛出异常。
61.ArrayList中add(E e)方法的原理?
add方法主要执行以下逻辑:
- 确保数组已经使用的长度size+1之后足够存下下一个元素。
- 修改次数modCount自动加1,如果当前数组的长度size加1后的长度大于当前数组的长度,则调用grow方法,增长数组,grow方法会将当前数组的长度变为原数组的1.5倍。
- 确保新增的元素有地方存储后,新元素存储在size处。
- 返回添加成功的布尔值。
方法入口
62.ArrayList中add(int index,E element)有了解过吗?这个方法的优劣
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-igkt4pvw-1647515759897)(…/…/…/…/Library/Application%20Support/typora-user-images/image-20220311191559923.png)]
该方法可以按照元素的位置,指定元素的插入位置,具体流程如下:
- 确保插入的位置小于等于当前数组的长度,并且不小于0,否则抛出异常。
- 确保数组已经使用的长度size加1后足够存下一个数据。
- 修改标识自动加1,如果当前数组已经使用的长度size加1后大于当前数组的长度,则调用grow方法,增长数组。
- grow方法会将当前数组的长度变为原来容量的1.5倍。
- 确保有足够的容量之后,调用System.arraycopy方法,将需要插入位置index后面的元素统统后移一位。
- 将新的数据存放到新的数组的指定位置index处。
好处:因为存在index,可以存在指定的位置。只要index符合要求。
坏处:调用System.arraycopy方法,插入的时候需要移动其他元素,频繁移动,速率会打折扣。
63.ArrayList的扩容原理?
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE – 8 = 2^31-1-8 ;
Integer.MAX_VALUE = 0x7fffffff = 2^31-1;
- 老的长度等于当前elementData的长度。
- 新数组的长度=原数组的长度+原数组长度>>1,右移1是除以2.
- 若扩容1.5倍后仍不够用,则newCapacity=minCapacity
- 如果newCapacity比MAX_ARRAY_SIZE还大,则调用hugeCapacity方法。
- 老数据拷贝到新数组中。
如果MAX_ARRAY_SIZE达不到要求,则赋值Integer.MAX_VALUE,理论上ArrayList的最大容量为Integer.MAX_VALUE
64.为什么MAX_ARRAY_SIZE是Integer.MAX_VALUE减去8,而不是别的数字?
- 数组在java中是一种特殊的数据类型,既不是基本类型也不是引用类型。不是类,没有class文件,数组是jvm从元素类型中合成出来的元素。
- 在jvm中获取数组的长度使用arraylength这个专门的字节码指令,在数组的对象头中有一个_length字段,记录数组的长度,只需要去读_length字段就可以了
- 所以这个8就是存了数组_length字段
65.ArrayList的remove方法有了解过吗?如果长度为1的ArrayList,移除后是如果进行垃圾回收的?
- 移除元素后,会改变modCount,并且是++操作
- 判断是否是移除最后一个元素,如果不是,则进行拷贝操作,如果是最后一个,则将最后一个元素设置为null,为gc做准备。这个设计非常细节。
66.ArrayList中的contains方法的时间复杂度?
67.ArrayList如果在循环中删除一个元素,有什么办法避开fail-fast机制?
使用迭代器和普通for循环都是可行的,使用增强for循环不行。
迭代器的内部实现
cursor为游标,指向下一个元素的索引,默认初始化为0
lastRet 也为游标,指向已被迭代过的元素,默认初始化为-1.
expectedModCount,赋值为modCount,删除元素后重新赋值
每调用一次next方法,cursor=i+1 ,指向下一个元素。
lastRet 指向刚刚被迭代过的元素 ,lastRet=i。
我们可以看到,多数情况下,lastRet与cursor的角标是连续的,只差1。
lastRet<0. 代表lastRet没有被i赋值,说明是初始值-1. 说明没有被迭代过,没有被迭代过就删除,这是不允许的。也就是说,iterator是靠lastRet的值来判断是否可以进行remove操作的。
如果lastRet > 0,说明已经被迭代过,可以删除,这时候cursor的角标需要减去 1,cursor-1 = lastRet,所以对cursor 进行lastRet的赋值操作
lastRet的位置被成功的remove了,自己的位置被cursor替代了。把自己置成 初始值-1,等待下次的赋值删除操作。
68.ArrayList和LinkedList的区别?
1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于双向链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
4.Arraylist的额外空间占用是1.5倍扩容导致的空间资源预留,LinkedList是需要 对前后指针进行保存,单个元素比ArrayList占用更大的空间。
69.描述下LinkedList的数据结构?
如图所示,LinkedList底层使用的双向链表结构,有一个头结点和一个尾结点,双向链表意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。
70.ArrayList和vector的区别?
1、同步性:
Vector 是线程安全的,也就是说是它的方法之间是线程同步的,而 ArrayList 是线程是不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。 备注:对于 Vector&ArrayList、Hashtable&HashMap,要记住线程安全的问题,记住Vector 与 Hashtable 是旧的,是 java 一诞生就提供了的,它们是线程安全的,ArrayList与 HashMap 是 java2 时才提供的,它们是线程不安全的。
2、数据增长:
ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加 ArrayList 与 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector 默认增长为原来两倍,而 ArrayList 的增长为原来的 1.5 倍。ArrayList 与 Vector 都可以设置初始的空间大小,Vector 还可以设置增长的空间大小,而 ArrayList 没有提供设置增长空间的方法。
71.说说List,Map,Set三者之间的区别?
List,主要是为顺序存储诞生的,List接口是为了存储一组不唯一的(允许重复)有序的对象。
Set,主要特性是不允许重复的集合。对象存储不可重复性,且无序。
Map,主要特征是Key-value。Map会维护与Key对应的值。