Java集合
1.索引为什么从0开始?
计算机这种底层在获取数组中的元素的时候,是通过索引和寻址公式来查找的,共识是基础地址再加上索引乘以数据类型的大小。如果索引为1的话,在寻址的时候,就要对索引减去1再乘以数据类型的大小,多了一个减法操作,由于计算机所有的操作本质上都是二进制的加法操作,这种减法操作本质上也会进行转化为补码的加法运算,会加大cpu的内存开销
2.ArrayList的底层原理了解吗
ArrayList底层基于这个动态数组实现,初始化一个arraylist后,如果不手动传入参数,会创建一个空的数组,然后这个arrayList还会设置一个初始容量,默认的无参构造器是10。再第一次添加元素的时候回去初始化初始容量的一个数组。稍有不同的是如果是手动传入参数,比如16,他会直接在另一个有参构造器中直接初始化参数大小的数组。后续当往这个数组中去添加元素的时候,他回去判断添加了元素后的数组元素数量是否超过了数组的容量大小,如果没超过直接放入数组后面,数量计数器加1,否则的话就去调用grow扩容函数,将数组的长度扩容为数组大小的1.5倍数,然后计数器+1。
3.ArrayList list = new ArrayList(10)中的list扩容几次?
答案是0次,这个在创建的时候会调用它的有参构造器,直接初始化一个大小为10的数组。
4.如何实现List和Array的转换?
主要有两个API,普通数组可以直接调用asLists()的方法,而对于list可以调用toArray的方法。其中数组转换为list,底层是通过引用实现的,所以修改了原来的数组,会联动影响到arraylist。而后者底层是重新从arraylist中复制了一份数组到新数组中,不是通过引用实现的,因此修改了原来的list不会对新的数组产生影响
5.ArrayList和LinkedList的区别说一说
arraylist和linkedlist的区别主要可以从四个方面来说:第一个就是底层的数据结构,前者是动态数组,后者是一个双向链表,第二个就是从一下使用效率来说一说,对于按照索引查抄arraylist是O1,但是linkedlist是On,对于未知元素的查都则都是On,新增元素的话,如果是添加在最后就都是O1,但是如果添加在中间的话,就都是On,如果是删除元素的话,如果是最后一个就都是O1,否则的话都是On,第三点就是从存储效率上来看,linkedlist由于要村两个指针,占用空间大一些,最后从第四点来看,他们都不属于线程安全的类,如果需要使用线程安全的需要通过syconlized加锁。
6.什么是散列表?散列冲突是什么?
散列表就是哈希表,它可以根据key来直接访问key对应的value值,其主要是由数组演化而来,利用了数组直接访问数组下标元素的特性,散列冲突是指多个key都被映射到了同一个key下,造成了冲突,这个时候一般会在当前key下采用链表法(拉链法)来解决,放到一个链表或者红黑树中
7.HashMap的实现原理?jdk1.7和jdk1.8有什么实现的区别?
HashMap主要是采用了哈希表来实现,也就是这个数组+链表和红黑树,他在去存储元素的时候会根据元素的这个key值进行哈希运算,计算出在数组中的对应映射下标的链表或者这个红黑树中,其中红黑树是jdk1.8与1.7的最大不同,解决了一个链表中元素过多,查询效率过低的问题,其中由链表转化为红黑树的条件是当数组的长度大于等于64,并且链表的元素大于等于8.
8.HashMap的put的流程说一下?
第一步:首先put之前,会判断这个HashMap的table数组是否空,空的话首先回去执行resize()方法进行扩容成16大小的数组。第二步:会通过哈希函数计算key值,得到对应的数组下标索引,第三步:会去执行插入操作,这个时候由多种情况,第一种情况就是这个table[i]直接为null,相当于什么东西都没有,那么直接执行插入就好了,如果不为null说明就有元素,这个时候回去判断一下是否为红黑树还是链表,如果是红黑树直接去执行这个插入的操作,如果是链表,则会去遍历这个链表,放到最后一个位置,当然如果过程中发现key已经存在了只会执行这个更新覆盖的操作,key不存在才会插入,然后他还会判断插入后的链表的长度,如果大于8还会去执行这个转化为红黑树的操作。第四步:在插入操作执行完毕之后,会去检查一下当前的容量是否超过了总容量乘以负载因子,如果超过了,再去执行这个扩容的操作。
9.HashMap的扩容机制?
首先HashMap会设置初始的大小为16,负载因子是0.75,也就是阈值为12,后续添加元素超过这个12的话,就会触发这个扩容机制,默认为两倍扩容,然后把这个哈希表上的值迁移到这个新的哈希表上,然后这里面有三种情况,第一种情况就是假如这个时候no.next==null也就是只有一个元素,那么只需要拿到这个node中的hash值和新的扩容后的长度-1再进行一个与操作得到新的索引,放到新的哈希表中,第二种情况就是如果这个是一个链表的结构,就要进行重构,判断当前节点的hash值和就数组长度-1进行与运算,看下是否等于0,等于零的话,直接放在原始位置,否则就放在原始位置加上增加的元素大小的位置上去。第三种情况就是红黑树的情况,这个就会按照红黑树的机制去做复制。
10.hashmap的寻址算法说一下?
首先回去直接计算对象的hashcode,紧接着会进行二次哈希计算,把第一次得到的哈希值右移16位再与原来的哈希值进行抑或运算,得到最终的hash值,最周会通过这个hash值与这个长度减去1进行一个与运算,这个与运算的结果就是这个索引的映射位置。
11.为什么要按照两倍去扩容?
第一个就是如果按照二倍去扩容,那么所有的数组二点长度他一定是这个二的指数,这个指数有一个特点就是只有一位是1,其他都是0,比如16是2的四次方,他的二进制表示就是 1 加上三个 0,而数组的长度减去一,又会有一个特点,比如15 的二进制表示是0 + 三个1 ,那这个特点到底是起什么作用的呢,我们知道我们的hashmap再去寻址的时候,要去做一个映射,就是比如数组大小为6,就要包所有阿元素映射到这个0-15索引的的位置,这个时候,可以通过 mod模运算来实现的,但是这个效率其实比较低,所以我们会采用hash和这个长度减去1进行一个与运算,比如 000000111 和这个hash值进行与运算,前5位一定是0,后面111都是1映射为1,正好可以映射出来0-15的索引,这样就达成了原来这个mod模运算,加快了效率,第二个就是扩容的时候也更方便,我们可以通过hash与原来的这个长度减去1进行与运算,看下是否等于0 ,如果等于0 就位置保持不变,否则直接就是原来的位置加上这个数组增加长度的偏移量,计算非常的高效和方便
12.jdk1.7的多线程死循环问题(HashMap采用的是尾插法还是头插法?)
HashMap再jdk1.7的版本采用了头插法,这种方法再多线程并发的情况下,会造成这种死循环的问题。举个例子
有一个线程正在进行HashMap的扩容,并且正在做数据迁移,这个时候发现当前数组存放的是链表,,他们的顺寻分别是A,B,这个时候刚准备进行扩容,突然又有一个线程2进来了,也来扩容,这个时候完成了扩容,现在新的数组中,这个链表的元素反过来了,变成了BA,因为是采用的头插法把,注意,这个时候线程2已经执行结束了,这个时候B的指针指向了A,这个时候继续恢复执行线程1的时候,就会去继续采用头插法,链表的顺便又变回去了AB,注意这个时候A指针指向了B。最后一切都结束的时候,我们发下一个问题,就是A指向了B,同时B又指向了A,这个时候假如有人去访问这个hashmap,访问到了这个链表的元素,机会走向死循环,因为是一个循环链表了,jdk1.8采取了尾插法修复了这个问题,使得在进行元素迁移的时候,保持相对的元素位置。