先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
treeifyBin 链表树化
===============
如果 hashmap 的长度小于 64 会优先选择拓容,否则会当前冲突 key 所在的结构由链表转换为红黑树。 这个是 jdk 1.8 才有的新特征,hashmap 在 hash 冲突后可能由链表变化为红黑树结构。这样做的目的是为了提高读写效率。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 不一定树化还可能是拓容,需要看数组的长度是否小于 64 MIN_TREEIFY_CAPACITY
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// hd 头节点, tl 尾节点
TreeNode<K,V> hd = null, tl = null;
do {
// 将链表转换为树结构
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 转换红黑树操作,这里循环比较,染色、旋转等
hd.treeify(tab);
}
}
replacementTreeNode 方法
======================
replacementTreeNode 方法主要是将 Node 转换为 TreeNode
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
TreeNode#treeify 方法
===================
treeify 方法主要是将树结构转换为红黑树。
final void treeify(Node<K,V>[] tab) {
// 根节点默认为 null
TreeNode<K,V> root = null;
// 链表遍历,x 指向当前节点,next 指向下一个节点
for (TreeNode<K,V> x = this, next; x != null; x = next) {
// 下一个节点
next = (TreeNode<K,V>)x.next;
// 设置当前节点的 left, right 为 null
x.left = x.right = null;
// 如果没有根节点
if (root == null) {
// 当前父节点为 null
x.parent = null;
// 当前红色节点属性设置为 false (把当前节点设置为黑色)
x.red = false;
// 根节点指向当前节点
root = x;
}
// 如果已经存在根节点
else {
// 获取当前链表的 key
K k = x.key;
// 获取当前节点的 hash
int h = x.hash;
// 定义当前 key 所属类型
Class<?> kc = null;
// 从根节点开始遍历,此遍历设置边界,只能从内部跳出
for (TreeNode<K,V> p = root;😉 {
// dir 标识方向(左右)ph 标识当前节点的 hash 值
int dir, ph;
// 当前节点的 key
K pk = p.key;
// 如果当前节点 hash 值大于当前 链表节点的 hash 值
if ((ph = p.hash) > h)
// 标识当前节链表节点会放在当前红黑树节点的左侧
dir = -1;
else if (ph < h)
// 右侧
dir = 1;
// 如果两个节点的 key 的 hash 值相等,那么通过其他方式进行比较
// 如果当前链表节点的 key 实现了comparable 接口,并且当前树节点和链表节点是相同的 class 实例,那么通过 comparable 比较
// 如果还是相等再通过 tieBreakOrder 比较一次
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // 保存当前树节点
// 如果 dir 小于等于 0: 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左子节点,
// 也可能是左子节点或者右子节点或者更深层次的节点
// 如果dir 大于等于 0: 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右子节点,
// 也可能是右子节点或者左子节点或者更深层次的节点
// 如果当前树节点不是叶子,那么最终以当前树节点的左子节点或者右子节点为起始几点,然后再重新开始寻找自己当前链表节点的位置。
// 如果当前树节点就是叶子节点,那么更具 dir 的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
// 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp; // 当前链表节点作为当前树节点的子节点
if (dir <= 0)
xp.left = x; // 左子节点
else
xp.right = x; // 右子节点
root = balanceInsertion(root, x); // 重新平衡
break;
}
}
}
}
// 把所有的链表节点都遍历之后,最终构造出来的树可能是经历多个平衡操作,根节点目前到底是链表的那个节点是不确定的
// 因为我们需要基于树来做查找,所以就应该把 tab[N] 得到的对象一定是根节点对象,而且是链表的第一个节点对象,所以要做对应的调整。
// 把红黑树的节点设置为所在数组槽的第一个元素
// 说明: TreeNode 既是一个红黑树也是一个双向链表
// 这个方法做的事情是保证树根节点一定要成为链表的首节点
moveRootToFront(tab, root);
}
balanceInsertion 树平衡
====================
这个方法分析之前,我们可以先看看红黑树的规则:红黑树是每个结点都带有颜色属性的二叉查找树,颜色或红色或黑色。 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
-
性质1. 节点是红色或黑色。
-
性质2. 根节点是黑色。
-
性质3. 所有叶子都是黑色。(叶子是NIL结点)
-
性质4. 每个红色节点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
-
性质5. 从任一节节点其每个叶子的所有路径都包含相同数目的黑色节点。
// root 为根节点
// x 为需要插入的节点
// 最后返回的是一个平很后的根节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 查询节点标记为红色
x.red = true;
// 设置一个只可以内部退出的循环
// 变量说明:
// xp 当前节点, xpp 父节点的父节点, xppl 父节点的父节点的左节点, xppr 父节点的父节点的右节点
for (TreeNode<K,V> xp, xpp, xppl, xppr;😉 {
// 如果父节点为空, 说明当前节点就是根节点,那么直接把当前接待你标记为黑色返回当前节点。
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 如果当前节点为黑设色并且当前父节点为 null, 或者
// 父节点为红色,但是 xpp 节点为空
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 当前节点等于 xppl
if (xp == (xppl = xpp.left)) {
//xppr != null 并且 是红色
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp; // 当前节点等于 xpp, 进入下一次循环
}
else {
if (x == xp.right) { // 当前节点是父节点的右子节点
root = rotateLeft(root, x = xp); //父节点左旋
xpp = (xp = x.parent) == null ? null : xp.parent; // 获取 xpp
}
if (xp != null) { // 父节点不为空
xp.red = false; // 父节点设置为黑色
if (xpp != null) { // xpp 不为空
xpp.red = true; // xpp 为红色
root = rotateRight(root, xpp); // xpp 右旋转
}
}
}
}
else { // 如果 xp 是 xpp 的右节点
if (xppl != null && xppl.red) { // xppl 不为空,并且为红色
xppl.red = false; // xppl 设置为黑色
xp.red = false; // 父节点为黑色
xpp.red = true; // xpp 为红色
x = xpp; // x = xpp 进入下次循环
}
else {
if (x == xp.left) { // 当前节点为父节点的左子节点
root = rotateRight(root, x = xp); // 根节点你右旋转
xpp = (xp = x.parent) == null ? null : xp.parent; // xpp = xp.parent
}
if (xp != null) { // xp != null
xp.red = false; // xp 为黑色
if (xpp != null) { // xpp != null
xpp.red = true; // xpp 为红色
root = rotateLeft(root, xpp); // 左旋
}
}
}
}
}
}
// 节点左旋转
// root 当前根节点
// p 指定选装的节点
// 返回旋转后的根接待你(平衡涉及左旋右旋根根节点改变,所以需要返回最新的根节点)
// 示意图
// pp pp
// | |
// p —> r
// / \ / \
// l r p rr
// / \ / \
// rl rr l rl
// 旋转做了几件事情?
// 1. 将 rl 设置为 p 的子接待你,将 rl 设置为父节点 p
// 2. 将 r 的父节点设置 pp, 将 pp 的左子节点设或者右子接待你设置为 r
// 3. 将 r 的左子节点设置为 p, 将 p 的父节点设置为 r
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
// 左旋的节点以及需要左旋的节点的右节点不为空
if (p != null && (r = p.right) != null) {
// 要左旋转的右子节点 = rl ,
if ((rl = p.right = r.left) != null)
// 设置 rl 父亲节点设置为 p
rl.parent = p;
// 将 r 的父节点设置为 p 的父节点,如果 pp == null
if ((pp = r.parent = p.parent) == null)
// 染黑
(root = r).red = false;
else if (pp.left == p) // 判断父节点是在 pp 的左边还是右边
pp.left = r; // 如果是左子节点,把 pp.letf = r
else
pp.right = r; // 如果是右子节点, pp.reight = r
r.left = p; // 最后将 r的左子节点设置为 p
p.parent = r; // 最后将 p.parent 设置为 r
}
return root;
}
// 节点右旋转
// 右旋同理
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
moveRootToFront 方法
==================
把所有的链表节点都遍历之后,最终构造出来的树可能是经历多个平衡操作,根节点目前到底是链表的那个节点是不确定的。 因为我们需要基于树来做查找,所以就应该把 tab[N] 得到的对象一定是根节点对象,而且是链表的第一个节点对象,所以要做对应的调整。 把红黑树的节点设置为所在数组槽的第一个元素,这个方法做的事情是保证树根节点一定要成为链表的首节点。
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
// root 节点不为空, 并且表不为空, 并且数组长度大于 0
if (root != null && tab != null && (n = tab.length) > 0) {
// 当前 Node 所在槽位
int index = (n - 1) & root.hash;
// 获取当前槽所在接待你
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
// 如果当前槽位节点不是首节点
if (root != first) {
// 后驱节点
Node<K,V> rn;
// 修改为首节点
tab[index] = root;
// rp 前驱节点为 root 的前驱节点
TreeNode<K,V> rp = root.prev;
// 后驱节点不为空
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
// 原来的头节点前驱节点指向新的头节点 root 节点
first.prev = root;
// root 节点的后驱节点指向之前的头节点
root.next = first;
// root 由于是头节点所以前驱节点为 null
root.prev = null;
}
assert checkInvariants(root);
}
}
remove 方法
=========
remove 方法的本质是将 key 值所在的节点的值设置为 nu
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
removeNode 方法
=============
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// tab 不为空, 数组长度大于 0, 当前节点数据不为 null
// 不得不说 hashmap 源码的逻辑还是非常严谨的
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// node 用来存储当前节点信息
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 如果是树形结构
if (p instanceof TreeNode)
// 获取节点
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 链表查找
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果是红黑树,删除节点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) // 如果是头节点
// 那么头节点指针指向移除节点的后驱节点
tab[index] = node.next;
else
// 前驱节点的后驱指针,指向当前节点的后驱指针
p.next = node.next;
// 修改次数累加
++modCount;
// 数据长度减少
–size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
removeTreeNode 方法
=================
removeTreeNode 是删除节点的核心方法,删除的时候如果是一个普通节点就可以直接情况,如果是链表的话需要将当前节点删除。如果是红黑树的话,需要删除 TreeNode , 然后进行一次树平衡,或者将树转换为链表。
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
// 获取索引值
int index = (n - 1) & hash;
// 获取头节点,即树的根节点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// 当前节点的后驱节点,当前节点的前驱节点保存
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
// 前驱节点为 null
if (pred == null)
// 当前是头节点,删除之后,头节点直接指向了删除节点的后继节点
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
// 如果头节点(即根节点)为空,说明当前节点删除后,红黑树为空,直接返回
if (first == null)
return;
// 如果头接单不为空,直接调用 root() 方法获取根节点
if (root.parent != null)
root = root.root();
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
// 链表化,英文前面的链表节点完成删除操作,故这里直接返回,即可完成节点删除
tab[index] = first.untreeify(map); // too small
return;
}
// p 当前节点; pl 当前节点左节点,pr 当前节点右节点
// replacement p 节点删除后替代的节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
// p 节点删除后, 他的左右节点不为空时, 遍历他的右节点上的左子树
// (以下操作先让 p 节点和 s 节点交换位置,然后再找到 replacement 节点替换他 )
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
// 通过上述操作 s 节点是大于 p 节点的最小节点(替换它的节点)
// 将 s 节点和 p 节点的颜色交换
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
// sr s 节点的右节点
TreeNode<K,V> sr = s.right;
// pp p 节点的父节点
TreeNode<K,V> pp = p.parent;
// 如果 pr 就是 s 节点
if (s == pr) { // p was s’s direct parent
// 节点交换
p.parent = s;
s.right = p;
}
else {
// 获取 s 的父节点
TreeNode<K,V> sp = s.parent;
// 将 p 节点的父节点指向 sp, 且 sp 节点存在
if ((p.parent = sp) != null) {
// 判断 s 节点的 sp 节点在左侧还是右侧, 将 p 节点存放在 s 节点一侧
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
// 将 pr 节点编程 s 节点的右节点,并且 pr 节点存在
if ((s.right = pr) != null)
// 将 s 节点编程 pr 节点的父节点
pr.parent = s;
}
// 因为 s 节点的性质, s 节点没有左节点
// 当 p 节点和 s 节点交换了位置,所以将 p 节点的左几点指向空
p.left = null;
// 将 sr 节点编程 p 节点的左节点,并且 sr 节点存在
if ((p.right = sr) != null)
// 将 p 节点编程 sr 的父节点
sr.parent = p;
// 将 pl 节点编程 s 节点的左节点,并且存在 pl 节点
if ((s.left = pl) != null)
// 将 pl 父节点赋值为s
pl.parent = s;
// s 父节点设置为 pp 并且 pp 节点存在
if ((s.parent = pp) == null)
// root 节点为 s
root = s;
// p 节点等于 pp.left
else if (p == pp.left)
// pp 的左节点为 s
pp.left = s;
else
// p 节点等于 pp.right
// pp 右节点为 s
pp.right = s;
// sr 不为空
if (sr != null)
// 替换节点为 sr
replacement = sr;
else
// 否则替换节点为 p
replacement = p;
}
else if (pl != null)
// 如果 pl 节点存在, pr 节点不存在,不用交换位置, pl 节点为替换为 replacement 节点
replacement = pl;
else if (pr != null)
// 如果 pr 节点存在, pl 节点不存在, 不用交换位置, pr 节点为替换为 replacement 节点
replacement = pr;
else
// 如果都不存在 p 节点成为 replacement 节点
replacement = p;
// 以下判断根据上述逻辑查看,仅以p 节点的当前位置为性质, 对 replacement 节点进行操作
if (replacement != p) {
// 如果 replacement 不是 p 节点
// 将 p 节点的父节点 pp 变成 replacement 节点的父节点
TreeNode<K,V> pp = replacement.parent = p.parent;
// 如果 pp 节点不存在
if (pp == null)
// replacement 变成根节点
root = replacement;
else if (p == pp.left)
// 如果 pp 节点存在,根据 p 节点在 pp 节点的位置,设置 replacement 节点的位置
pp.left = replacement;
else
pp.right = replacement;
// 将 p 节点所有的引用关系设置为 null
p.left = p.right = p.parent = null;
}
// 如果 p 节点是红色,删除后不影响 root 节点,如果是黑色,找到平衡后的根节点,并且用 r 表示
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 如果 p 是 replacement 节点
if (replacement == p) { // detach
// 得到 pp
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
// pp 存在
// 根据 p 节点的位置,将 pp 节点的对应为位置设置为空
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
// 移动新的节点到数组上
if (movable)
moveRootToFront(tab, r);
}
最后
毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节
美团面试经验
字节面试经验
菜鸟面试经验
蚂蚁金服面试经验
唯品会面试经验
因篇幅有限,图文无法详细发出
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
p.left = p.right = p.parent = null;
}
// 如果 p 节点是红色,删除后不影响 root 节点,如果是黑色,找到平衡后的根节点,并且用 r 表示
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 如果 p 是 replacement 节点
if (replacement == p) { // detach
// 得到 pp
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
// pp 存在
// 根据 p 节点的位置,将 pp 节点的对应为位置设置为空
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
// 移动新的节点到数组上
if (movable)
moveRootToFront(tab, r);
}
最后
毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节
美团面试经验
[外链图片转存中…(img-L9W77Fqp-1713121890349)]
字节面试经验
[外链图片转存中…(img-j3Ox1Jzi-1713121890350)]
菜鸟面试经验
[外链图片转存中…(img-LlfNBm9s-1713121890350)]
蚂蚁金服面试经验
[外链图片转存中…(img-Abmrc416-1713121890351)]
唯品会面试经验
[外链图片转存中…(img-IPjlxe1B-1713121890351)]
因篇幅有限,图文无法详细发出
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-C37PEFhT-1713121890351)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!