Java 容器都有哪些?
Java 容器分为 Collection 和 Map 两大类
Collection 和 Collections 有什么区别?
Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法:Collections. sort(list)。
如何决定使用 HashMap 还是 TreeMap?
对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,
但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择。
说一下 HashSet 的实现原理?
HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
ArrayList 和 LinkedList 的区别是什么?
相同点:
ArrayList 和 LinkedList:有序(存取元素顺序一致)、有索引、可重复
不同点:
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
随机访问效率:ArrayList 比 LinkedList 在随机访问(查询)的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
如何实现数组和 List 之间的转换?
数组转 List:使用 Arrays. asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。
// list to array
List<String> list = new ArrayList<String>();
list. add("ben");
list. add("的博客");
Object[] objects = list.toArray();
for (int i = 0; i < objects.length; i++) {
System.out.println(objects[i]);//ben 的博客
}
// array to list
String[] array = new String[]{"ben","的博客"};
System.out.println(Arrays.asList(array));//[ben, 的博客]
Array 和 ArrayList 有何区别?
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
哪些集合类是线程安全的?
Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 JDK 1.5 之后随着 Java. util. concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。
迭代器 Iterator 是什么?
Iterator是Collection下的,所以list,set都可以拿来遍历。在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
ConcurrentModificationException并发修改异常
ConcurrentModificationException并发修改异常
怎么确保一个集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
示例代码如下:
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错UnsupportedOperationException
System. out. println(list. size());
HashMap的扩容机制?
HashMap初始容量:
HashMap默认容量大小是16
当我们设置HashMap初始化容量时,实际上HashMap会采用第一个>=该数字的2幂作为初始容量(比如设置初始容量为7,那么HashMap会采用2三次幂8作为初始容量)
HashMap默认容量大小是16,装载因子是0.75,所以扩容的边界值就是0.75*16=12,当HashMap大小超过12 的时候,HashMap就会自动增加一倍,变成32
HashMap 的初始化容量大小 = 需要存储的元素个数 / 负载因子0.75 + 1.0F
比如:需要存储11个元素,通过按位与运算,11/0.75+1=15.66,比这个数大的2的几次幂是16
HashMap的扩容机制
1.7 数组+链表 数组先扩容,遍历老数组每个位置链表的每个元素,取每个元素的key,基于数组下标,将元素添加到新数组中
1.8 数组+链表+红黑树,数组先扩容,遍历老数组中每个位置上的链表或红黑树,
当链表长度>=8时,且数组长度>64时,链表数据将树化,以红黑树形式存储。
当红黑树个数<6时,红黑树将退化为链表
HashMap put()?
1,根据hash算法与取余得出数组下标,
2,如果数组下标位置元素为空,1.7 将key,value封装成entry对象 。1.8 将key,value封装成node对象
3,如果数组下标位置不为空,1.7判断数组是否需要扩容,如果需要就先扩容,如果不需要,就头插法将entry对象插入到链表中。1.8先判断当前位置node类型,链表node,还是红黑树node,如果是红黑树node,就需要将key,value封装成红黑树node,添加到红黑树中。
4,也会判断是否已经存在这个key,如果存在只需要更新value
5,如果该位置是个链表node,就需要将key、value封装成链表node,通过尾插法添加到链表最后。
6,插入后如果链表的长度超过8的话看是否需要树化(如果数组容量小于64的话,就只会进行扩容,大于64才会转换为红黑树)
7,再插入链表或者红黑树之后,在判断是否需要扩容,如果需要扩容就扩容,如果不需要就结束put方法
总之,1.7是先扩容,再插入节点;1.8是先插入节点,再扩容
HashMap中的put方法工作原理
1:当传入一个k-v的时候,首先会根据hash()方法计算一个hash值(这个值不决定在数组的位置)
2:在put的时候才会进行数组的初始化,判断数组是否存在,如果不存在就调用resize()方法创建默认数组容量为16的数组
3:通过key的hash值与(数组最大索引-1)进行位运算,确定在数组中的位置
4:获取该位置是否有元素,如果没有元素就创建一个新的Node节点元素存放在该数组中
5:如果该位置有元素的话就判断put进来的key与当前数组Node节点的key是否相同,如果相同的话就就进行值的替换。
6:当前的条件没有成立的话,判断该位置是红黑树还是链表
7:如果是红黑树,那么就把当前节点放在红黑树上面
8:如果是链表的话,就遍历该链表,把Node追加在链表中
9:这个时候也会去判断链表的长度,如果链表的长度超过8的话看是否需要树化(
会调用treeifyBin方法,如果数组容量小于64的话,就只会进行扩容,大于64才会转换为红黑树
)
10:返回覆盖的值
HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?
对于key的hashCode方法的值,结合数组长度进行无符号右移然后做异或运算、与运算计算出索引。还有平方取中法,伪随机数法或取余数法。这三种效率都比较低。而位运算效率是最高的。
如果两个键的 hashCode 相同,如何存储键值对?
会产生哈希冲突。
此时结合equals()方法比较key值内容是否相同
如果相同(比如key都是"重地")则替换旧的 value;
key值不相等(比如key是"重地",“通话”)继续向下和其他的数据的key进行比较,如果都不相等,则划出一个结点存储数据,这种方式称为拉链法。链表长度超过8并且数组长度超过64,就转换为红黑树存储。
什么是哈希碰撞,如何解决哈希碰撞?
只要两个元素的 key计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。
为什么数组长度必须是 2 的 n 次幂?如果输入值不是 2 的幂比如 10 会怎么样?
当向 HashMap 中添加一个元素的时候,需要根据 key 的 hash 值,去确定其在数组中的具体位置。HashMap 为了存取高效,减少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现的关键就在把数据存到哪个链表中的算法。
这个算法实际就是取模,hash % length,计算机中直接求余效率不如位移运算。所以源码中做了优化,使用 hash & (length - 1)按位与运算,而实际上 hash % length 等于 hash & ( length - 1) 的前提是 length 是 2 的 n 次幂。
例如长度为 8 的时候,3 & (8 - 1) = 3,2 & (8 - 1) = 2,不同位置上,不碰撞。
按位与运算:相同的二进制数位上,都是1的时候,结果为1,;否则为0。
按位或运算:相同的二进制数位上,都是0的时候,结果为0;否则为1。
如果输入值不是 2 的幂比如 10,底层会通过右移运算、按位或运算将10转化为2的4次幂=16。数组长度最大是2的30次幂。
为什么 Map 桶中结点个数超过 8 才转为红黑树?
TreeNodes 占用空间是普通 Nodes 的两倍,所以只有当桶(数组上)包含足够多的结点时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESH〇LD的值决定的。当 bin 中结点数变少时,又会转成普通的 bin。并且我们查看源码的时候发现,链表长度达到 8 就转成红黑树,当长度降到 6 就转成普通 bin。
这样就解释了为什么不是一开始就将其转换为 TreeNodes,而是需要一定结点数才转为 TreeNodes,说白了就是权衡空间和时间。
红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8) = 3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长虔为 8/2 = 4,这才有转换成树的必要;链表长度如果是小于等于 6, 6/2 = 3,而 log(6) = 2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
// 当桶(bucket)上的结点数大于这个值时会转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值,树转为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的数组长度最小的值
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap实现逻辑?
数组加链表实现的,
数组中每个元素都是链表,链表中每个元素又是一个entry对象
entry对象用来存储真正的K-V
里面有两个比较重要的方法get(),put()
put()在存储K-V时,首先会调用hash(),可以得到一个key的hash值,hash值与(length - 1)按位与运算,得到数组下标
找到相应下标下插入的每一个key进行equals()比较,如果相等就把value进行更新。如果不相等就把新的K-V值put到链表中去
put过程中如果存放的K-V超过了,数组元素16*负载因子0.75时,就进行扩容
如果链表上存储的K-V超过阈值8时,并且数组长度超过64时,就会自动转化为红黑树
get()方法和put()比较类似,同样也会先去调用hash()与(length - 1)按位与运算,得到数组下标
遍历下标对应的元素,进行equals()比较,如果key相同就把value取出返回给用户
红黑树?
红黑树是二叉查找树的一种,查找算法相当于二分查找
红黑树时间复杂度O(log n),如果长度为 8,平均查找长度为 log(8) = 3,在数据比较多时,会比链表的时间复杂度O(n),链表的平均查找长度为 n/2,当长度为 8 时,平均查找长虔为 8/2 = 4要好很多
红黑树结构是“小中大,左中右”,某个节点上数据,比它小的都在左边,比它大的都在右边
所以红黑树查找快,但是插入时因为需要维护红黑树结构,所以相对慢
HashMap可不可以不使用链表?
HashMap之所以没有一开始就使用红黑树,应该是时间和空间的折中考虑。
在hash冲突比较小时,即使转化为红黑树,在时间复杂中所产生的效果,也并不是特别大
put时效率会降低,因为每次都要进行比较复杂的红黑树这种旋转算法和旋转操作,
空间上每个节点需要来维护更多的指针。
HashMap之所以选择红黑树而不是二叉搜索树,二叉树在一些极端情况下,会变成倾斜结构,查找效率就退化成链表差不多
红黑树是一种平衡树,可以防止这种退化
红黑树又不像其他完全的平衡二叉树有着严格的平衡条件,
所以,红黑树插入效率要比完全的平衡二叉树要高。
所以说,HashMap选择红黑树,既可以避免极端情况下的退化,也可以兼顾查询和插入的效率
HashMap是线程安全的么?
HashMap是针对单线程设计的,所以是线程不安全的
在多线程并发环境下,可以使用ConcurrentHashMap
ConcurrentHashMap?
Java7 | Java8 | |
---|---|---|
数据结构 | 采用Segment分段锁来实现 | 数组+链表+红黑树 |
并发度 | 每个Segment独立加锁,最大并发个数就是Segment的个数 | 数组长度 |
并发原理 | Segment分段锁来保证现成安全,Segment继承自ReenreantLock | 采用Node+CAS+synchronized保证线程安全 |
hash碰撞 | 链表 | 链表+红黑树 |
查询时间复杂度 | O(n),n为链表长度,比如链表长度为8,那么时间复杂度为4 | O(log(n)),n为树的节点个数,比如节点个数=8,那么时间复杂度为3 |
JDK1.7底层使用数组+链表实现的,使用了分段锁来保证线程安全,
将数组分成了16段,给每个Segment来配一把锁,在读每个Segment时,就要先获取对应的锁
它是最多能有16个线程去并发操作
JDK1.8和HashMap一样引入了红黑树,在并发处理方面采用CAS+synchronized关键字来实现一种更加细粒度的锁
JDK1.7中ConcurrentHashMap中利用ReenreantLock tryLock()去尝试加锁,加锁过程中,同时遍历链表,创建node对象,如果一切准备工作都做完了,tryLock()还没有加到锁,就再重试64次(一半再来查看链表头部、尾部是否有新的元素插入),还没加到就使用lock()阻塞加锁,如果没有拿到锁,后边的代码不会执行。
Segment继承了ReentrantLock,
1,先根据key,算出对应的Segment数组的下标index
2,获取index位置上的锁,segments[index].lock();
3,segments[index].put(key,value)–entry–数组或链表
4,释放index位置上的锁,segments[index].unlock();
创建Segment线程中,用到了乐观锁,seg = UNSAFE.getObjectValitie()
ConcurrentHashMap扩容是Segment内部的数组的扩容,Segment本身不扩容
CAS
保证多线程环境下,对于共享变量修改的一个原子性
Unsafe类中方法全称是compare and swap比较相同再交换
一种乐观锁的实现机制,预期值和希望更改之后的值
线程1,希望把0改成1,cpu会比较原本值是不是0,如果已经被别的线程改成1,线程1的修改操作就会失败,然后再自旋重新修改
CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共
享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧
的预估值X等于内存中的值V,就将新的值B保存到内存中。
适合竞争不激烈,多核cpu的场景下。1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。