一篇详尽的HashMap整理(面试看这一篇就够了)

Node<K,V> candidate = getNode(hash(key), key);

return candidate != null && candidate.equals(e);

}

public final boolean remove(Object o) {

if (o instanceof Map.Entry) {

Map.Entry<?,?> e = (Map.Entry<?,?>) o;

Object key = e.getKey();

Object value = e.getValue();

return removeNode(hash(key), key, value, true, true) != null;

}

return false;

}

public final Spliterator<Map.Entry<K,V>> spliterator() {

return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);

}

public final void forEach(Consumer<? super Map.Entry<K,V>> action) {

Node<K,V>[] tab;

if (action == null)

throw new NullPointerException();

if (size > 0 && (tab = table) != null) {

int mc = modCount;

for (int i = 0; i < tab.length; ++i) {

for (Node<K,V> e = tab[i]; e != null; e = e.next)

action.accept(e);

}

if (modCount != mc)

throw new ConcurrentModificationException();

}

}

}

2.3 HashMap 1.7 底层结构


JDK1.7 中,HashMap 采用数组 + 链表的实现,链表被用来处理冲突。当位于一个桶中的元素较多时,即 hash 值相等的元素较多时,通过 key 依次查找的效率较低

HashMap的桶其实就是一个 Node 数组,链表的每一个节点都是 Node 的一个具体对象,存储了hash,key,value,next属性

所以 HashMap 的整体结构如下:

2.4 HashMap 1.8 底层结构


与 JDK 1.7 相比,1.8 在底层结构方面做了一些改变,当每个桶中元素大于 8 的时候,会转变为红黑树,目的就是优化查询效率,JDK 1.8 重写了 resize() 方法,将在多线程访问下头插法可能导致环的问题修改为使用尾插法。

2.4.1 jdk1.8 的其他改进

  • 优化hash()方法

采用高16位异或低16位,避免低位相同高位不同的哈希值产生严重的碰撞。

  • 扩容时插入顺序的改进

  • 其他

  • 函数方法:forEach, compute系列

  • Map的新API:merge, replace

2.5 HashMap 重要属性


2.5.1 初始容量

HashMap 的默认初始容量是由 DEFAULT_INITIAL_CAPACITY 属性确定的。默认值是16。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

2.5.2 最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

由于桶的容量要求是2的n次幂(具体原因放在下面讲),int型的数据最大是2^31 - 1不满足需求,所以就取了个2的30次幂。

2.5.3 默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

扩容机制的原则是:当

HashMap 中存储的数量 > HashMap 容量 * 负载因子

时,就会把 HashMap 的容量扩大为原来的二倍。扩容的时机:1.7在添加数据之前进行扩容判断,1.8在添加数据之后或判断树化时会进行扩容判断。

2.5.4 树化阈值

static final int TREEIFY_THRESHOLD = 8;

在进行添加元素时,当一个桶中存储元素的数量 > 8 时,会自动转换为红黑树。

2.5.5 链表阈值

static final int UNTREEIFY_THRESHOLD = 6;

在进行删除元素时,如果一个桶中存储元素数量小于该阈值,则会自动转换为链表

2.5.6 扩容临界值

static final int MIN_TREEIFY_CAPACITY = 64;

当桶数组容量小于该值时,优先进行扩容,而不是树化。

2.5.7 扩容阈值

在 HashMap 中,使用 threshold 表示扩容的阈值,也就是 初始容量 * 负载因子的值。

其中通过 tableSizeFor 方法来计算扩容之后的容量。

static final int tableSizeFor(int cap) {

int n = cap - 1;

n |= n >>> 1;

n |= n >>> 2;

n |= n >>> 4;

n |= n >>> 8;

n |= n >>> 16;

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

比如针对输入cap = 2 ^ 29,这段代码的实现过程如下:

由上图可知 2^29 次方的数组经过一系列的或操作后,会算出来结果是 2^30 次方。所以扩容后的数组长度是原来的 2 倍。

2.6 HashMap构造函数


  • 传入初始容量 initialCapacity 和负载因子 loadFactor 的构造方法

public HashMap(int initialCapacity, float loadFactor) {

if (initialCapacity < 0)

throw new IllegalArgumentException("Illegal initial capacity: " +

initialCapacity);

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

if (loadFactor <= 0 || Float.isNaN(loadFactor))

throw new IllegalArgumentException("Illegal load factor: " +

loadFactor);

this.loadFactor = loadFactor;

this.threshold = tableSizeFor(initialCapacity);

}

  • 只带有 initialCapacity 的构造函数

public HashMap(int initialCapacity) {

this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

最终也会调用到上面的构造函数,不过这个默认的负载因子就是 HashMap 的默认负载因子,即0.75

  • 无参构造函数

public HashMap() {

this.loadFactor = DEFAULT_LOAD_FACTOR;

}

初始化默认负载因子0.75

  • 带有map的构造函数

public HashMap(Map<? extends K, ? extends V> m) {

this.loadFactor = DEFAULT_LOAD_FACTOR;

putMapEntries(m, false);

}

直接把外部元素批量放入 HashMap 中。

2.7 put全过程


以1.8为基准。为了更直观的查看,将put方法提取出来流程图如下:

具体的源代码如下,可以结合注释以及上面的示意图来看

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node<K,V>[] tab; Node<K,V> p; int n, i;

// 如果table 为null 或者没有为 table 分配内存,就resize一次

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

// 指定hash值节点为空则直接插入,这个(n - 1) & hash才是表中真正的哈希

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

// 如果不为空

else {

Node<K,V> e; K k;

// 计算表中的这个真正的哈希值与要插入的key.hash相比

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

// 若不同的话,并且当前节点已经在 TreeNode 上了

else if (p instanceof TreeNode)

// 采用红黑树存储方式

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

// key.hash 不同并且也不再 TreeNode 上,在链表上找到 p.next==null

else {

for (int binCount = 0; ; ++binCount) {

if ((e = p.next) == null) {

// 在表尾插入

p.next = newNode(hash, key, value, null);

// 新增节点后如果节点个数到达阈值,则进入 treeifyBin() 进行再次判断

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

treeifyBin(tab, hash);

break;

}

// 如果找到了同 hash、key 的节点,那么直接退出循环

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

// 更新 p 指向下一节点

p = e;

}

}

// map中含有旧值,返回旧值

if (e != null) { // existing mapping for key

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

// map调整次数 + 1

++modCount;

// 键值对的数量达到阈值,需要扩容

if (++size > threshold)

resize();

afterNodeInsertion(evict);

return null;

}

这个方法涉及五个参数

  • hash : put 放在桶中的位置,在 put 之前,会进行 hash 函数的计算。

  • key : 参数的 key 值

  • value : 参数的 value 值

  • onlyIfAbsent : 是否改变已经存在的值,也就是是否进行 value 值的替换标志

  • evict : 是否是刚创建 HashMap 的标志

2.8 扩容机制


HashMap 中的扩容机制是由 resize() 方法来实现的,程序流程图大致如下:

对应的源码如下,大家可以结合示意图与注释来理解。

final Node<K,V>[] resize() {

Node<K,V>[] oldTab = table;

// 存储old table 的大小

int oldCap = (oldTab == null) ? 0 : oldTab.length;

// 存储扩容阈值

int oldThr = threshold;

int newCap, newThr = 0;

if (oldCap > 0) {

// 如果old table数据已达最大,那么threshold也被设置成最大

if (oldCap >= MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return oldTab;

}

// 左移扩大二倍,

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

oldCap >= DEFAULT_INITIAL_CAPACITY)

// 扩容成原来二倍

newThr = oldThr << 1; // double threshold

}

// 如果oldThr > 0

else if (oldThr > 0) // initial capacity was placed in threshold

newCap = oldThr;

// 如果old table <= 0 并且 存储的阈值 <= 0

else { // zero initial threshold signifies using defaults

newCap = DEFAULT_INITIAL_CAPACITY;

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

}

// 如果扩充阈值为0

if (newThr == 0) {

// 扩容阈值为 初始容量*负载因子

float ft = (float)newCap * loadFactor;

newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

(int)ft : Integer.MAX_VALUE);

}

// 重新给负载因子赋值

threshold = newThr;

// 获取扩容后的数组

@SuppressWarnings({“rawtypes”,“unchecked”})

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

table = newTab;

// 如果第一次进行table 初始化不会走下面的代码

// 扩容之后需要重新把节点放在新扩容的数组中

if (oldTab != null) {

for (int j = 0; j < oldCap; ++j) {

Node<K,V> e;

if ((e = oldTab[j]) != null) {

oldTab[j] = null;

if (e.next == null)

newTab[e.hash & (newCap - 1)] = e;

else if (e instanceof TreeNode)

// 重新映射时,需要对红黑树进行拆分

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

else { // preserve order

Node<K,V> loHead = null, loTail = null;

Node<K,V> hiHead = null, hiTail = null;

Node<K,V> next;

// 遍历链表,并将链表节点按原顺序进行分组

do {

next = e.next;

if ((e.hash & oldCap) == 0) {

if (loTail == null)

loHead = e;

else

loTail.next = e;

loTail = e;

}

else {

if (hiTail == null)

hiHead = e;

else

hiTail.next = e;

hiTail = e;

}

} while ((e = next) != null);

// 将分组后的链表映射到新桶中

if (loTail != null) {

loTail.next = null;

newTab[j] = loHead;

}

if (hiTail != null) {

hiTail.next = null;

newTab[j + oldCap] = hiHead;

}

}

}

}

}

return newTab;

}

上面代码主要做了这几个事情:

1. 判断 HashMap 中的数组的长度,也就是 (Node<K,V>[])oldTab.length() ,再判断数组的长度是否比最大的的长度也就是 2^30 次幂要大,大的话直接取最大长度,否则利用位运算扩容为原来的两倍

if (oldCap > 0) {

// 如果old table数据已达最大,那么threshold也被设置成最大

if (oldCap >= MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return oldTab;

}

// 左移扩大二倍,

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

oldCap >= DEFAULT_INITIAL_CAPACITY)

// 扩容成原来二倍

newThr = oldThr << 1; // double threshold

}

2. 如果数组长度不大于0 ,再判断扩容阈值 threshold 是否大于 0 ,也就是看有无外部指定的扩容阈值,若有则使用,因为 HashMap 中的每个构造方法都会调用 HashMap(initCapacity,loadFactor) 这个构造方法,所以如果没有外部指定 initialCapacity时,初始容量是 16,然后根据 this.threshold = tableSizeFor(initialCapacity); 求得 threshold 的值。

// 如果oldThr > 0

else if (oldThr > 0) // initial capacity was placed in threshold

newCap = oldThr;

3. 否则,直接使用默认的初始容量和扩容阈值(在 table 刚刚初始化的时候执行)

// 如果old table <= 0 并且 存储的阈值 <= 0

else { // zero initial threshold signifies using defaults

newCap = DEFAULT_INITIAL_CAPACITY;

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

}

然后会判断 newThr 是否为 0,为0的情况在于上面逻辑判断中的扩容操作,可能会导致位溢出

在扩容后需要把节点放在新扩容的数组中,这里也涉及到三个步骤:

  • 循环桶中的每个 Node 节点,判断 Node[i] 是否为空,为空直接返回,不为空则遍历桶数组,并将键值对映射到新的桶数组中。

  • 如果不为空,再判断是否是树形结构,如果是树形结构则按照树形结构进行拆分,拆分方法在 split 方法中。

  • 如果不是树形结构,则遍历链表,并将链表节点按原顺序进行分组。

if (oldTab != null) {

for (int j = 0; j < oldCap; ++j) {

Node<K,V> e;

if ((e = oldTab[j]) != null) {

oldTab[j] = null;

if (e.next == null)

newTab[e.hash & (newCap - 1)] = e;

else if (e instanceof TreeNode)

// 重新映射时,需要对红黑树进行拆分

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

else { // preserve order

Node<K,V> loHead = null, loTail = null;

Node<K,V> hiHead = null, hiTail = null;

Node<K,V> next;

// 遍历链表,并将链表节点按原顺序进行分组

do {

next = e.next;

if ((e.hash & oldCap) == 0) {

if (loTail == null)

loHead = e;

else

loTail.next = e;

loTail = e;

}

else {

if (hiTail == null)

hiHead = e;

else

hiTail.next = e;

hiTail = e;

}

} while ((e = next) != null);

// 将分组后的链表映射到新桶中

if (loTail != null) {

loTail.next = null;

newTab[j] = loHead;

}

if (hiTail != null) {

hiTail.next = null;

newTab[j + oldCap] = hiHead;

}

}

2.9 get全过程


对应的源码如下,大家可以结合示意图与注释来理解。

public V get(Object key) {

Node<K,V> e;

return (e = getNode(hash(key), key)) == null ? null : e.value;

}

final Node<K,V> getNode(int hash, Object key) {

Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

// 找到真实的元素位置

if ((tab = table) != null && (n = tab.length) > 0 &&

(first = tab[(n - 1) & hash]) != null) {

// 总是会check 一下第一个元素

if (first.hash == hash && // always check first node

((k = first.key) == key || (key != null && key.equals(k))))

return first;

// 如果不是第一个元素,并且下一个元素不是空的

if ((e = first.next) != null) {

// 判断是否属于 TreeNode,如果是 TreeNode 实例,直接从 TreeNode.getTreeNode 取

if (first instanceof TreeNode)

return ((TreeNode<K,V>)first).getTreeNode(hash, key);

// 如果还不是 TreeNode 实例,就直接循环数组元素,直到找到指定元素位置

do {

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

return e;

} while ((e = e.next) != null);

}

}

return null;

}

3 HashMap面试细节

=============

3.1 两个对象的 hashCode 相同会发生什么?

因为 hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。

3.2 你知道 hash 的实现吗?为什么要这样实现?

JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。

3.3 为什么要用异或运算符?

保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。

3.4 为什么要保证 capacity 是2的次幂呢?

key在tab中索引位置是由(n-1)&hash计算得到的。

如果 n 是2的次幂,所以 n-1 的二进制都是尾端以连续1的形式表示(00001111,00000011),当(n - 1) 和 hash 做与运算时,会保留hash中 后 x 位的 1。

这样做有3个好处:

  • &运算速度快,至少比%取模运算块

  • 能保证 索引值 肯定在 capacity 中,不会超出数组长度

  • (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n

3.5 capacity 都是 2 次幂,如果我们指定 initialCapacity 不为 2次幂时,是不是就破坏了这个规则?

不会的,HashMap的tableSizeFor方法做了处理,能保证n永远都是2次幂

/**

  • Returns a power of two size for the given target capacity.

*/

static final int tableSizeFor(int cap) {

//cap-1后,n的二进制最右一位肯定和cap的最右一位不同,即一个为0,一个为1,例如cap=17(00010001),n=cap-1=16(00010000)

int n = cap - 1;

//n = (00010000 | 00001000) = 00011000

n |= n >>> 1;

//n = (00011000 | 00000110) = 00011110

n |= n >>> 2;

//n = (00011110 | 00000001) = 00011111

n |= n >>> 4;

//n = (00011111 | 00000000) = 00011111

n |= n >>> 8;

//n = (00011111 | 00000000) = 00011111

n |= n >>> 16;

//n = 00011111 = 31

//n = 31 + 1 = 32, 即最终的cap = 32 = 2 的 (n=5)次方

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

3.6 链表法导致链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

二叉查找树在特殊情况下会变成一条线性结构,红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

3.7 HashMap,LinkedHashMap,TreeMap 有什么区别?

LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;

TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)

3.8 HashMap & TreeMap & LinkedHashMap 使用场景?

HashMap:在 Map 中插入、删除和定位元素时;

TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;

LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。

3.9 HashMap 和 HashTable 有什么区别?

1 HashMap 是线程不安全的,HashTable 是线程安全的;

2 由于线程安全,所以 HashTable 的效率比不上 HashMap;

3 HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;

4 HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;

5 HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

3.10 ava 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?

ConcurrentHashMap 类(是 Java并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。

HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);

ConcurrentHashMap:

  • JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。

  • JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。

3.11 针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?

JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。

1 Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;

2 HashEntry 用来封装映射表的键-值对;

3 每个桶是由若干个 HashEntry 对象链接起来的链表

JDK 1.8 中,采用Node + CAS + Synchronized来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。

3.12 ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?

1 粒度降低了;

2 JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。

3 在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

3.13 ConcurrentHashMap 的并发度是什么?

程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后我们该如何学习?

1、看视频进行系统学习

这几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。

另外,我自己也珍藏了好几套视频资料躺在网盘里,有需要的我也可以分享给你:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

2、读源码,看实战笔记,学习大神思路

“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 + 理解事物,更重要的:看到源码背后的想法;程序员说:读万行源码,行万种实践。

Spring源码深度解析:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Mybatis 3源码深度解析:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Redis学习笔记:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Spring Boot核心技术-笔记:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

3、面试前夕,刷题冲刺

面试的前一周时间内,就可以开始刷题冲刺了。请记住,刷题的时候,技术的优先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。

关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

只有技术过硬,在哪儿都不愁就业,“万般带不去,唯有业随身”学习本来就不是在课堂那几年说了算,而是在人生的旅途中不间断的事情。

人生短暂,别稀里糊涂的活一辈子,不要将就。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后我们该如何学习?

1、看视频进行系统学习

这几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。

另外,我自己也珍藏了好几套视频资料躺在网盘里,有需要的我也可以分享给你:

[外链图片转存中…(img-sXiNo39N-1713493839626)]

2、读源码,看实战笔记,学习大神思路

“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 + 理解事物,更重要的:看到源码背后的想法;程序员说:读万行源码,行万种实践。

Spring源码深度解析:

[外链图片转存中…(img-NESHKEW9-1713493839626)]

Mybatis 3源码深度解析:

[外链图片转存中…(img-O09D8SIv-1713493839626)]

Redis学习笔记:

[外链图片转存中…(img-bS0QjL8N-1713493839626)]

Spring Boot核心技术-笔记:

[外链图片转存中…(img-u9ZWaZXm-1713493839626)]

3、面试前夕,刷题冲刺

面试的前一周时间内,就可以开始刷题冲刺了。请记住,刷题的时候,技术的优先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。

关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三:

[外链图片转存中…(img-uzkpnj9x-1713493839627)]

只有技术过硬,在哪儿都不愁就业,“万般带不去,唯有业随身”学习本来就不是在课堂那几年说了算,而是在人生的旅途中不间断的事情。

人生短暂,别稀里糊涂的活一辈子,不要将就。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 27
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值