概述:Java集合,也叫做容器,主要是由两大接口派生而来:一个是Collection接口,主要用于存放单一元素;另一个是Map接口,主要用于存放键值对。对于Collection接口,下面又有三个主要的子接口:List、Set、Queue。
总结:
List:
1.ArrayList:Object[]数组。
2.Vector:Object[]数组。
3.LinkedList:双向链表。
Set:
HashSet(无序,唯一):基于HashMap实现的,底层采用HashMap来保存元素;
LinkedHashSet:LinkedHashSet是HashSet的子类,并且其内部是通过LinkedHashMap来实现的。
TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)
Queue:
PriorityQueue:Object[]数组来实现小顶堆;
DelayQueue:PriorityQueue;
ArrayDeque:可扩容动态双向数组。
Map:
HashMap:JDK1.8之前HashMap由数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(拉链法解决冲突)。JDK1.8之后再解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
LinkedHashMap:LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。
1.ArrayList和Array(数组)的区别?
ArrayList内部基于动态数组实现,比Array更加灵活;
1.ArrayList会根据实际存储的元素动态地扩容或者缩容,而Array被创建出来后就无法改变长度;
2.ArrayList允许使用泛型来确保类型安全,Array则不可以;
3.ArrayList中只能存储对象,对于基本数据类型,需要使用其对应的包装类(如Integer、Double)。Array可以直接存储基本数据类型,也可以存储对象。
4.ArrayList支持插入、删除、遍历等常见操作,并且提供了丰富的API操作方法,比如add()、remove()等。Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
5.ArrayList初始化时不需要指定数组大小,Array初始化时必须指定大小。
2.ConcurrentHashMap为什么不能存null,而HashMap可以?
ConcurrentHashMap的key和value不能为null主要是为了避免二义性。null是一个特殊的置,表示没有对象或者没有引用。如果用null作为键,就无法区分这个键是否存在于ConcurrentHashMap中,还是根本没有这个键。同样,作为值也无法区分是否真正存在于其中,还是因为找不到对应的键而返回的。
与此形成对比的是,HashMap可以存储null的key和value,但null作为键只能由一个,null作为值可以有多个。如果传入null作为参数,就会返回hash值为0的位置的值。单线程环境下,不存在一个线程操作,其他线程也将该HashMap修改的情况,所以可以通过contains(key)来判断是否存在这个键值对。
3.ConcurrentHashMap能保证复合操作的原子性吗?
ConcurrentHashMap是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况。但是并不能保证所有的复合操作都是原子性的。
复合操作是指由多个基本操作(如put、get、remove、containsKey)组成的操作,例如:
// 线程 A
if (!map.containsKey(key)) {
map.put(key, value);
}
// 线程 B
if (!map.containsKey(key)) {
map.put(key, anotherValue);
}
如果A和B的执行顺序是:1.A判断map中不存在key;2.B判断map中不存在Key;3.B将键值对插入;4.A将键值对插入;那么最终的结果是(key,value),而不是预期的(key,anotherValue)
如何保证ConcurrentHashMap的原子性的?可以使用提供的原子性的复合操作,如putIfAbsent,compute,computeIfAbsent、merge等。
4.数组和链表的区别是什么?
数组的内存是连续的,且存储的元素大小是固定的,实现上是基于一个内存地址,然后由于元素大小固定,支持利用下标直接访问,随机访问的效率很高,是O(1)。而由于要保证连续性,删除元素是就需要搬迁元素,进行内存拷贝,效率不高。
链表的内存不需要连续,它们是通过指针相连,不过链表需要额外存储指针,占用的内存会更大一些;无法随机访问元素,必须从头开始遍历,所以随机查找的效率不高,是O(n);删除的效率较高,改变指针的指向即可;
5.HashMap和HashTable的区别?
(1)线程安全性:HashMap不是线程安全的,HashTable是线程安全的,所有的方法都加了锁,可以在多线程环境中使用;
(2)性能:HashMap由于没有同步开销,所以它的性能一般比HashTable好,尤其是在单线程环境中,而HashTable由于每个方法都得同步,因此性能比HashMap差;
(3)null值的处理:HashMap允许一个null键和多个null值;HashTable不允许null键和null值。如果将null键或者值放入其中,会抛出NullPointerException;
(4)其他:HashMap初始默认容量为16,负载因子0.75,使用Iterator遍历键值对,支持fail-fast机制,如果遍历过程中结构发生变化,会抛出ConcurrentModificationException;HashTable默认初始容量为11,负载因子为0.75,使用Enumeration遍历键值对,不支持fail-fast机制。
6.HashMap的扩容机制是怎样的?
扩容的时机:在HashMap中有阈值的概念,比如设置一个16大小的map,那么默认的阈值就是16 * 0.75 = 12,也就是说如果map中的元素超过12,就会触发扩容;
扩容的过程:扩容的时候,默认会新建一个数组,新数组的大小是老数组的两倍,然后将老数组中的元素重新hash映射到新的数组中;1.7时每个元素是一个一个搬迁过去,1.8时做了优化,关键点在于数组的长度是2的次方,且扩容为两倍。以16为例,以前的数组长度是010000,新数组的长度32是100000,而通过key的hash值定位其在数组位置采用的方法是(数组长度 - 1)& hash,16 - 1 = 15,其二进制001111,32 - 1 = 31,其二进制011111,所以重点在key的hash值从右往左数第五位是否为1,如果是1说明其需要搬迁到新位置,且新位置的下标就是原下标 + 16(原数组的大小),如果是0说明可以保留其原下标,不需要迁移。
7.为什么HashMap在扩容时要采用2的n次方倍?
主要基于i = (n - 1) & hash这个公式。
哈希分布均匀性:如果数组容量是2的n次方,那么n - 1后低位都是1,此时进行&运算,可以确保哈希码的最低几位均匀分布;
性能:正常情况下,如果基于哈希码来计算数组下标,常用的都是 %(取余)计算,但相比于位运算来说,效率比较低,所以推荐用位运算,而要满足i = (n - 1) & hash这个公式,n的大小就必须是2的n次幂,即当b等于2的n次幂时,a % b = a & ( b - 1)
8.为什么HashMap的默认负载因子是0.75?
设置0.75主要是为了时间和空间上的平衡;较低的负载因子,如0.5,会导致HashMap频繁扩容,了空间利用率低,不过因为冲突少,查询效率较高,但是因为扩容频繁会增加rehashing的开销;较高的负载因子如1.0,会减少扩容次数,空间利用率高了,但会增加哈希冲突的概率,从而降低查找效率;经过大量实践证明0.75是大多数场景下比较合适的值;
9.为什么jdk1.8对hashmap做了红黑树的改动?
主要是避免hash冲突导致链表的长度过长,这样get的时间复杂度严格来说就不是O(1)了,因为可能需要遍历链表来查找命中的键值对;
为什么定义链表长度为8且数组大小大于等于64才转红黑树?直接用不行吗?
答:因为红黑树的大小是普通节点大小的两倍,所以为了节省内存空间不会直接只用红黑树,只有当节点到达一定数量才会转化为红黑树,这里定义的是8;选择8的原因是因为在默认0.75的负载因子下,冲突节点长度为8的概率为0.00000006,也就是说概率非常小(毕竟红黑树耗内存,而且链表长度较短时遍历速度还比较快速)。
为什么节点少于6要从红黑树转成链表?
答:也是为了平衡时间和空间,节点太少遍历链表速度也很快,没必要变成红黑树,变成链表节约内存;为什么定为6而不是8是为了留下缓冲余地,避免反复横跳,例如一个节点反复添加从8变为9,链表变为红黑树,又将其删除,从9变为8,从红黑树又变回链表。所以留有一些余量,毕竟树化和反树化都是有开销的;
10.JDK1.8对HashMap做了哪些改动?
主要有以下几个方面:
1.hash函数的优化;2.扩容rehash的优化;3.头插法和尾插法;4.插入与扩容时机的变更;
5108

被折叠的 条评论
为什么被折叠?



