Skip to content

Commit b012b6f

Browse files
添加Java结合面试题
1 parent de13edb commit b012b6f

File tree

1 file changed

+114
-0
lines changed

1 file changed

+114
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
### 集合面试题
2+
3+
> ArrayList、LinkedList和Vector的区别和实现原理
4+
5+
#### 数据结构实现
6+
7+
ArrayList和Vector都是基于可改变大小的数据实现的,而LinkedList是基于双链表实现的。
8+
9+
#### 增删改查效率对比
10+
11+
ArrayList和Vector都是基于可改变大小的数据实现的,因此,从指定的位置检索对象时,或在集合的末尾插入对象、删除一个对象的时间都是O(1),但是如果在其他位置增加或者删除对象,花费的时间是O(n);
12+
13+
而LinkedList是基于双链表实现的,因此,在插入、删除集合中的任何位置上的对象,所花费的时间都是O(1),但基于链表的数据结构在查找元素时的效率是更低的,花费的时间为O(n)。
14+
15+
因此,从以上分析我们可以知道,查找特定的对象或者在集合末端增加或者删除对象,ArrayList和Vector的效率是ok的,如果在指定的位置删除或者插入,LinkedList的效率则更高。
16+
17+
#### 线程安全
18+
19+
ArrayList、LinkedList不具有线程安全性,在多线程的问题下是不能使用的,如果想要在多线程的环境下使用怎么办呢?我们可以采用Collections的静态方法synchronizedList包装一下,就可以保证线程安全了,但是在实际情况下,并不会使用这种方式,而是会采用更高级的集合进行线程安全的操作。
20+
21+
Vector是线程安全的,其保证线程安全的机制是采用synchronized关键字,我们都知道,这个关键字的效率是不高的,在后续的很多版本中,线程安全的机制都不会采用这种方式,因此,Vector的效率是比ArrayList、LinkedList更低效的。
22+
23+
#### 扩容机制
24+
25+
ArrayList和Vector都是基于数据这种数据结构实现的,因此,在集合的容量满了时,是需要进行扩容操作的。
26+
27+
在扩容时,ArrayList扩容后的容量是原先的1.5倍,扩容后,再将原先的数组中的数据拷贝到新建的数组中。
28+
29+
Vector默认情况下,扩容后的容量是原先的2倍,除此之外,Vector还有一种可以设置**容量增量**的机制,在Vector中有capacityIncrement变量用于控制扩容时的增量,具体的规则是:当capacityIncrement大于0时,扩容时增加的大小就是capacityIncrement的大小,如果capacityIncrement小于等于0时,则将容量增加为之前的2倍。
30+
31+
> HashMap原理分析
32+
33+
在分析HashMap的原理之前,先说明一下,大家应该都知道HashMap在JDK1.7和1.8的实现上是有较大的区别的,而面试官也是非常喜欢考察这一个点,因此,这里也是采用这两个JDK版本对比来进行分析,这样也可以印象更加深刻一些。
34+
35+
#### 数据结构
36+
37+
在数据结构的实现上,大家应该都知道,JDK1.7是数组+单链表的形式,而1.8采用的是数组+单链表+红黑树,具体的表现如下:
38+
39+
|版本|数据结构|数组+链表的实现形式|红黑树实现形式|
40+
|-|-|-|-|
41+
|JDK1.8|数组+单链表+红黑树|Node|TreeNode|
42+
|JDK1.7|数组+单链表|Entry|-|
43+
44+
为了更好的让大家理解后续的讲解,这里先讲解一下HashMap中实现的一些重要参数。
45+
46+
- 容量(capacity): HashMap中数组的长度
47+
- 容量范围:必须是 2 的幂
48+
- 初始容量 = 哈希表创建时的容量
49+
- 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的 2^4 = 16
50+
`static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;`
51+
- 最大容量 = 2的30次方
52+
`static final int MAXIMUM_CAPACITY = 1 << 30;`
53+
54+
- 加载因子(Load factor):HashMap在其容量自动增加时,会设置加载因子,当达到设置的值时,就会触发自动扩容。
55+
- 加载因子越大、填满的元素越多,也就是说,空间利用率高、但冲突的机会加大、查找效率变低
56+
- 加载因子越小、填满的元素越少,也就是说,空间利用率小、冲突的机会减小、查找效率高
57+
// 实际加载因子
58+
`final float loadFactor;`
59+
// 默认加载因子 = 0.75
60+
`static final float DEFAULT_LOAD_FACTOR = 0.75f;`
61+
62+
- 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)。
63+
- 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
64+
- 扩容阈值 = 容量 x 加载因子
65+
66+
#### 获取数据(get)
67+
68+
HashMap的获取数据的过程大致如下:
69+
70+
- 首先,根据key判断是否为空值;
71+
- 如果为空,则到hashmap数组的第1个位置,寻找对应key为null的键;
72+
- 如果不为空,则根据key计算hash值;
73+
- 根据得到的hash值采用`hash & (length - 1)`的计算方式得到key在数组中的位置;
74+
- 结束。
75+
76+
以上就是大致的数据获取流程,接下来,我们再对JDK1.7和1.8获取数据的细节做一个对比。
77+
78+
|版本|hash值的计算方式|
79+
|-|-|
80+
|JDK1.8|1、hash = (key == null) ? 0 : hash(key); <br> 2、扰动处理 = 2次扰动 = 1次位运算+1次异或运算|
81+
|JDK1.7|1、hash = (key == null) ? 0 : hash(key); <br> 2、扰动处理 = 9次扰动 = 4次位运算+5次异或运算|
82+
83+
#### 保存数据(put)
84+
85+
HashMap的保存数据的过程大致如下:
86+
87+
- 判读HashMap是否初始化,如果没有则进行初始化;
88+
- 判断key是否为null,如果为null,则将key-value的数据存储在数组的第1个位置,这里与获取数据时对应的;否则,进行后续操作;
89+
- 根据key计算数据存放的位置;
90+
- 根据位置判断key是否存在,如果存在,则用新值替换旧值;如果不存在,则直接设置;
91+
92+
这里也对保存数据的过程进行一个更加细致的对比。
93+
94+
|版本|hash值的计算方式|存放数据方式|插入数据方式|
95+
|-|-|-|-|
96+
|JDK1.8|1. hash = (key == null) ? 0 : hash(key); <br> 2. 扰动处理 = 2次扰动 = 1次位运算+1次异或运算|数组+单链表+红黑树 <br> - 无冲突,直接保存数据 <br> - 冲突时,当链表长度小于8时,存放到单链表,当长度大于8时,存到到红黑树|尾插法|
97+
|JDK1.7|1、hash = (key == null) ? 0 : hash(key); <br> 2、扰动处理 = 9次扰动 = 4次位运算+5次异或运算|数组+单链表 <br> - 无冲突,直接保存数据 <br> - 冲突时,存放到单链表|头插法|
98+
99+
#### 扩容机制
100+
101+
HashMap的扩容的过程大致如下:
102+
103+
- 当发现容量不足时,开始扩容机制;
104+
- 首先,保存旧数组,再根据旧容量的2倍新建数组;
105+
- 遍历旧数组的每个元素,采用头插法的方式,将每个元素保存到新数组;
106+
- 将新数组引用到hashmap的table属性上;
107+
- 重新设置扩容阀值,完成扩容操作。
108+
109+
最后,也对扩容的过程进行一个更加细致的对比。
110+
111+
|版本|扩容后的位置计算方式|数据转移方式|
112+
|-|-|-|
113+
|JDK1.8|扩容后的位置 = 原位置 or 原位置+旧容量|尾插法|
114+
|JDK1.7|扩容后的位置 = hashCode() -> 扰动处理 -> h & (length - 1)|头插法|

0 commit comments

Comments
 (0)