先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
// 数组转 List
String[] array = new String[]{“挽周出品”,“必属精品”};
Arrays.asList(array);
八. ArrayList 和 LinkedList 的区别是什么
-
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
-
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
-
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标。
-
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
-
线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多 时,更推荐使用 LinkedList。LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
九. ArrayList 和 Vector 的区别是什么?
这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合
-
线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
-
性能:ArrayList 在性能方面要优于 Vector。
-
扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问 Vector的话代码要在同步操作上耗费大量的时间。
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
十. 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述ArrayList、Vector、LinkedList 的存储性能和特性?
- ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操
作,所以索引数据快而插入数据慢。
-
Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。
-
LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需
要记录当前项的前后项即可,所以 LinkedList 插入速度较快。
十一. 多线程场景下如何使用 ArrayList?
- ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。
例如像下面这样:
List synchronizedList = Collections.synchronizedList(list);
synchronizedList.add(“aaa”);
synchronizedList.add(“bbb”);
for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}
十二. 为什么 ArrayList 的 elementData 加上 transient 修饰?
- ArrayList 中的数组定义如下:
private transient Object[] elementData
- 再看一下 ArrayList 的定义:
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
- 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out array length
s.writeInt(elementData.length);
// Write out all elements in the proper order.
for (int i=0; i<size; i++){
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
- 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后
遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后
的文件大小。
十三. List 和 Set 的区别
-
List , Set 都是继承自Collection 接口
-
List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
-
Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及TreeSet。
-
另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
Set和List对比
-
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
-
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变
接下来是我们的Set,
一. 说一下 HashSet 的实现原理?
- HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet 不允许重复的值。
二. HashSet如何检查重复?HashSet是如何保证数据不可重复的?
-
向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
-
HashSet 中的add ()方法会使用HashMap 的put()方法。
-
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap 比较key是否相等是先比较hashcode 再比较equals )。
以下是HashSet 部分源码:
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}
以下是拓展问题 😊
hashCode()与equals()的相关规定:
-
如果两个对象相等,则hashcode一定也是相同的hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
-
两个对象相等,对两个equals方法返回true
-
两个对象有相同的hashcode值,它们也不一定是相等的
-
综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
-
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==与equals的区别
-
==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
-
==是指对内存地址进行比较 equals()是对字符串的内容进行比较
三. HashSet与HashMap的区别
| HashMap | HashSet |
| — | — |
| 实现了Map接口 | 实现Set接口 |
| 存储键值对 | 仅存储对象 |
| 调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |
| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false |
| HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |
========================================================================
- 哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
- 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。
链表大致分为单链表和双向链表
- 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针
- 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针
链表的优点
-
插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素)
-
内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间)
-
大小没有固定,拓展很灵活。
链表的缺点
- 不能随机查找,必须从第一个开始遍历,查找效率低
-
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
-
HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。
HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap 基于 Hash 算法实现的
-
当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
-
存储时,如果出现hash值相同的key,此时有两种情况。
- 如果key相同,则覆盖原始值;
- 如果key不同(出现冲突),则将当前的key-value放入链表中
-
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
-
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
四. HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
- 在Java中,保存数据有两种比较简单的数据结构:数组和链表。**数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;**所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。
HashMap JDK1.8之前
- JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
HashMap JDK1.8之后
- 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
JDK1.7 VS JDK1.8 比较,JDK1.8主要解决或优化了一下问题:
-
resize 扩容优化
-
引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
-
解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题
说道红黑树先讲什么是二叉树
- 二叉树简单来说就是 每一个节上可以关联俩个子节点
红黑树
-
红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。
-
红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色
注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!
。 -
如果一个结点是红色的,则它的子结点必须是黑色的。
-
每个结点到叶子结点NIL所经过的黑色结点的个数一样的。
确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!
-
红黑树的基本操作是 添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。
- 当我们put的时候,首先计算
key
的hash
值,这里调用了hash
方法, hash 方法实际是让key.hashCode()
与key.hashCode()>>>16
进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。 按照函数注释,因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) & hash ,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。
putVal方法执行流程图
源码阅读
- HashMap的put方法,翻看源码
/**
-
Associates the specified value with the specified key in this map.
-
If the map previously contained a mapping for the key, the old
-
value is replaced.
-
@param key key with which the specified value is to be associated
-
@param value value to be associated with the specified key
-
@return the previous value associated with key, or
-
<tt>null</tt> if there was no mapping for <tt>key</tt>.
-
(A <tt>null</tt> return can also indicate that the map
-
previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- 紧接着调用内部方法putVal
/**
-
Implements Map.put and related methods
-
@param hash hash for key
-
@param key the key
-
@param value the value to put
-
@param onlyIfAbsent if true, don’t change existing value
-
@param evict if false, the table is in creation mode.
-
@return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
-
判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
-
根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向
6
,如果table[i]不为空,转向3
; -
判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
-
判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向
5
; -
遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
-
插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
-
在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
-
每次扩展的时候,都是扩展2倍;
-
扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
- 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上。
- 答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;
8-1. 什么是哈希?
- Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
8-2. 什么是哈希冲突?
- 当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
8-3. HashMap的数据结构
在Java中,保存数据有两种比较简单的数据结构:
数组和链表
。
-
数组的特点是:寻址容易,插入和删除困难;
-
链表的特点是:寻址困难,但插入和删除容易;
所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放地址法可以解决哈希冲突:
-
链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
-
开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
-
但相比于hashCode返回的int类型,我们HashMap初始的容量大小
DEFAULT_INITIAL_CAPACITY = 1 << 4 (即2的四次方16)
要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化
8-4. hash()函数
- 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:
static final int hash(Object key) {
int h;
// 与自己右移16位进行异或运算(高低位异或)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);
总结
简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
-
链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
-
开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:
-
如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
-
类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
-
如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
-
用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。
十. 为什么HashMap中String、Integer这样的包装类适合作为K?
-
答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
-
都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
-
内部已重写了 equals() 、 hashCode() 等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
十一. 如果使用Object作为HashMap的Key,应该怎么办呢?
- 答:重写 hashCode() 和 equals() 方法
-
重写 hashCode() 是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
-
重写 equals() 方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;
十二. HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
- 答: hashCode() 方法返回的是int整数类型,其范围为
-(2 ^ 31)~(2 ^ 31 - 1)
,约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30
,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过 hashCode() 计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
那怎么解决呢?
-
HashMap自己实现了自己的 hash() 方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
-
在保证数组长度为2的幂次方的时候,使用 hash() 运算之后的值与运算(&)(数组长度 -1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;
- 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
这个算法应该如何设计呢?
- 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
那为什么是两次扰动呢?
- 答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;
十四. HashMap 与 HashTable 有什么区别?
-
线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap );
-
效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap );
-
对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
-
初始容量大小和每次扩充容量大小的不同;
-
创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
-
创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
-
底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
-
推荐使用: 在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
-
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
-
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
-
TreeMap是线程非同步的。
十六. 如何决定使用 HashMap 还是 TreeMap?
- 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
十七. HashMap 和 ConcurrentHashMap 的区别
-
ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
-
HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
十八. ConcurrentHashMap 和 Hashtable 的区别?
- ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构:
- JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式:
-
在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
-
Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图:
- HashTable:
- JDK1.7的ConcurrentHashMap:
- JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
- 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。
十九. ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?
JDK1.7
- 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。在JDK1.7中,ConcurrentHashMap采用
Segment + HashEntry
的方式进行实现。
结构如下
- 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁
-
该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
-
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
JDK1.8
- 在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
结构如下
(附加源码,有需要的可以看看)
- 插入元素过程(建议去看看源码):如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
- 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent){
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
最后
即使是面试跳槽,那也是一个学习的过程。只有全面的复习,才能让我们更好的充实自己,武装自己,为自己的面试之路不再坎坷!今天就给大家分享一个Github上全面的Java面试题大全,就是这份面试大全助我拿下大厂Offer,月薪提至30K!
我也是第一时间分享出来给大家,希望可以帮助大家都能去往自己心仪的大厂!为金三银四做准备!
一共有20个知识点专题,分别是:
Dubbo面试专题
JVM面试专题
Java并发面试专题
Kafka面试专题
MongDB面试专题
MyBatis面试专题
MySQL面试专题
Netty面试专题
RabbitMQ面试专题
Redis面试专题
Spring Cloud面试专题
SpringBoot面试专题
zookeeper面试专题
常见面试算法题汇总专题
计算机网络基础专题
设计模式专题
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
共有20个知识点专题,分别是:
Dubbo面试专题
[外链图片转存中…(img-bW3c1MXL-1713468542097)]
JVM面试专题
[外链图片转存中…(img-ZPgknzxL-1713468542098)]
Java并发面试专题
[外链图片转存中…(img-fCDmZ2wH-1713468542098)]
Kafka面试专题
[外链图片转存中…(img-XdX8olla-1713468542099)]
MongDB面试专题
[外链图片转存中…(img-h3Dm4DW5-1713468542100)]
MyBatis面试专题
[外链图片转存中…(img-hqR8i49d-1713468542101)]
MySQL面试专题
[外链图片转存中…(img-95yTYmP4-1713468542101)]
Netty面试专题
[外链图片转存中…(img-YFuw6Ynt-1713468542102)]
RabbitMQ面试专题
[外链图片转存中…(img-X80YV5P4-1713468542103)]
Redis面试专题
[外链图片转存中…(img-fCopEieb-1713468542104)]
Spring Cloud面试专题
[外链图片转存中…(img-v6skpLGm-1713468542104)]
SpringBoot面试专题
[外链图片转存中…(img-SEKua3s6-1713468542105)]
zookeeper面试专题
[外链图片转存中…(img-ynR9uiIJ-1713468542106)]
常见面试算法题汇总专题
[外链图片转存中…(img-tdyFPe8q-1713468542107)]
计算机网络基础专题
[外链图片转存中…(img-JZ7LmxjQ-1713468542107)]
设计模式专题
[外链图片转存中…(img-Gxysi0zW-1713468542108)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-8i6kfigS-1713468542108)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!