1.1 说说常见的集合有哪些吧
Map接口: HashMap、TreeMap、Hashtable、ConcurrentHashMap、Properties
Collection接口的子接口:Set、List
Set接口的实现类:HashSet、TreeSet、LinkedHashSet
List接口的实现类:ArrayList、LinkedList、Stack、Vector
1.2 HashMap与HashTable的区别
1. HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;
2. HashMap允许K/V都为null;后者K/V都不允许为null;
3. HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;
1.3 HashMap的put方法的具体流程?
首先判断table是否为空,为空的话使用resize()方法扩容;判断table[i]是否为空,如果是则直接插入,插入之后判断键值对数量是否大于最大容量,大于的话需要使用resize()方法扩容;如果table[i]不为空,判断key是否存在,存在则直接进行覆盖;不存在需要则判断table[i]是否为树节点,是树节点插入红黑树;不是则遍历链表,若链表长度大于8,转换成红黑树插入;小于8则进行链表插入。
1.4 HashMap的扩容操作是怎么实现的?
如果数组未初始化,则(用指定或者使用默认值的方式进行初始化;若已经进行初始化,需要判断扩容前的数组容量超过最大值,超过则不再扩容;若没有超过最大值,就扩容为原来的两倍。计算出新数组长度和新数组扩容阈值,创建新数组;扩容前的数组元素迁移到扩容后的数组当中去。
1.5 HashMap是怎么解决哈希冲突的?
哈希:把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值)。根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
哈希冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
hash()函数:如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的。我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动。
JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);
1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
1.6 HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
如何解决呢?:
HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;
为什么是两次扰动呢?
这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;
1.7 HashMap在JDK1.7和JDK1.8中有哪些不同?
存储结构不同:1.7中是数组+链表;1.8中数组 + 链表 + 红黑树
初始化方式不同:1.7中采用单独函数:inflateTable(),1.8直接集成到了扩容函数resize()中。
hash值计算方式不同:1.7通过9次扰动(4次位运算+5次异或运算);1.8通过两次扰动(1次位运算+1次异或运算)
存放数据的规则不同:1.7无冲突时,存放数组;冲突时,存放链表;1.8链表长度大于8时,存放红黑树;
插入数据方式不同:1.7采用头插法,先把原位置的数据移到后1位,再插入数据到该位置);1.8采用尾插法,直接插入到链表尾部/红黑树。
扩容后存储位置的计算方式不同:1.8原位置 or 原位置 + 旧容量
1.8为什么HashMap中String、Integer这样的包装类适合作为Key
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率。1.都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况。2.内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范。
如果想用自己的Object作为K应该怎么办呢
重写hashCode()和equals()方法;重写hashCode()是因为需要计算存储数据的存储位置,重写equals()方法,需要遵守自反性、对称性、传递性、一致性,目的是为了保证key在哈希表中的唯一性。
1.9 ConcurrentHashMap和Hashtable的区别?
ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。
ConcurrentHashMap的具体实现?
该类包含两个静态内部类 HashEntry 和 Segment ;HashEntry用来封装映射表的键值对,Segment用来充当锁的角色;Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现。
1.10 Java集合的快速失败机制 “fail-fast”?
fail-fast是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
2. 使用CopyOnWriteArrayList来替换ArrayList。
1.11 ArrayList 和 Vector 的区别?
这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中的元素位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引来取出某个元素,并且其中的数据是允许重复的,这是与 HashSet 之类的集合的最大不同处,HashSet 之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。
ArrayList 与 Vector 的区别主要包括两个方面:
同步性:Vector 是线程安全的,也就是说它的方法之间是线程同步(加了synchronized 关键字)的,而 ArrayList 是线程不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线程安全的问题,所以效率会高一些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
数据增长:ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个人超过了容量时,就需要增加 ArrayList 和 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要去的一定的平衡。Vector 在数据满时(加载因子1)增长为原来的两倍(扩容增量:原容量的 2 倍),而 ArrayList 在数据量达到容量的一半时(加载因子 0.5)增长为原容量的 (0.5 倍 + 1) 个空间。
1.12 ArrayList和LinkedList的区别?
LinkedList 实现了 List 和 Deque 接口,一般称为双向链表;ArrayList 实现了 List 接口,动态数组;
LinkedList 在插入和删除数据时效率更高,ArrayList 在查找某个 index 的数据时效率更高;
LinkedList 比 ArrayList 需要更多的内存;
Array 和 ArrayList 有什么区别?什么时候该应 Array 而不是 ArrayList 呢?
Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
Array 大小是固定的,ArrayList 的大小是动态变化的。
ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。
1.13HashSet是如何保证数据不可重复的?
HashSet的底层其实就是HashMap,只不过我们HashSet是实现了Set接口并且把数据作为K值,而V值一直使用一个相同的虚值来保存。
由于HashMap的K值本身就不允许重复,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V,那么在HashSet中执行这一句话始终会返回一个false,导致插入失败,这样就保证了数据的不可重复性;
1.14 BlockingQueue是什么
是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
1.15 StringBuffer和StringBuilder的区别
StringBuffer、StringBuilder和String一样,也用来代表字符串。String类是不可变类,任何对String的改变都会引发新的String对象的生成;StringBuffer则是可变类,任何对它所指代的字符串的改变都不会产生新的对象。StringBuilder不支持并发操作,线性不安全的,不适合多线程中使用;StringBufferd支持并发操作,线性安全的,适合多线程中使用。