先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
| Constructor | Constructor描述 |
| :-: | :-: |
| ArrayList() | 构造一个初始容量为10的空列表 |
| ArrayList(int initialCapacity) | 构造具有指定初始容量的空列表 |
| ArrayList(Collection<? extends E> c) | 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序 |
===================================================================================
/**
- Constructs an empty list with an initial capacity of ten.
*/
//构造一个初始容量为10的数组
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认的空容量的数组
//elementData:集合真正存储数据的容器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
👋2.2 ArrayList(int initialCapacity)构造方法
======================================================================================================
/**
-
Constructs an empty list with the specified initial capacity.
-
@param initialCapacity the initial capacity of the list
-
@throws IllegalArgumentException if the specified initial capacity
-
is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果传进来的变量大于0,则初始化一个指定容量的空数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//传进来的变量=0,则不去创建新的数组,直接将已创建的EMPTY_ELEMENTDATA空数组传给
//ArrayList
this.elementData = EMPTY_ELEMENTDATA;
} else {
//传进来的指定数组容量不能<0
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
👋2.3 ArrayList(Collection<? extends E> c)构造方法
============================================================================================================
/**
-
Constructs a list containing the elements of the specified
-
collection, in the order they are returned by the collection’s
-
iterator.
-
@param c the collection whose elements are to be placed into this list
-
@throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
//将构造方法中的参数转换成数组形式,其底层是调用了System.arraycopy()
elementData = c.toArray();
//将数组的长度赋值给size
if ((size = elementData.length) != 0) {
//检查elementData是不是Object[]类型,不是的话将其转换成Object[].class类型
if (elementData.getClass() != Object[].class)
//数组的创建与拷贝
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//size为0,则把已创建好的空数组直接给它
this.elementData = EMPTY_ELEMENTDATA;
}
}
===============================================================================
====================================================================================
//长度为0的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认容量为0的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//集合存元素的数组
transient Object[] elementData;
//集合的长度
private int size;
//集合默认容量
private static final int DEFAULT_CAPACITY = 10;
=======================================================================
| 方法名 | 描述 |
| :-: | :-: |
| add(E e) | 将指定的元素追加到此列表的末尾 |
| add(int index, E element) | 在此列表中的指定位置插入指定的元素 |
| addAll(Collection<? extends E> c) | 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾 |
| addAll(int index, Collection<? extends E> c) | 将指定集合中的所有元素插入到此列表中,从指定的位置开始 |
==============================================================================
/**
-
Appends the specified element to the end of this list.
-
@param e element to be appended to this list
-
@return true (as specified by {@link Collection#add})
*/
//将指定的元素追加到此列表的末尾
public boolean add(E e) {
//每增加1个元素,数组所需容量+1,并检查增加数组容量后是否要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//添加元素
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//如果是默认的DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//将两者之中的最大值最为新的容量
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//检查是否需要扩容
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//记录操作次数
modCount++;
/*
elementData.length:原集合中实际的元素数量
minCapacity:集合的最小容量
*/
if (minCapacity - elementData.length > 0)
//扩容的核心方法
grow(minCapacity);
}
private void grow(int minCapacity) {
//获取为添加元素之前的集合元素数量,设置为oldCapacity
int oldCapacity = elementData.length;
//右移位运算符:oldCapacity>>1=oldCapacity/2
//新容量为原有容量的1.5倍
//扩容的核心代码
int newCapacity = oldCapacity + (oldCapacity >> 1);
//第一次add时newCapacity为0,故newCapacity - minCapacity会小于0
//如果新容量比所需要的容量还小
if (newCapacity - minCapacity < 0)
//新容量就等于所需要的容量
newCapacity = minCapacity;
//如果新容量比规定的最大容量还大,最新容量等于最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//将原数组拷贝到一个更大容量的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
我们总结一下流程:
-
每增加一个元素,都要判断增加后容量是否达到了规定的
最大容量值
,如果达到就触发扩容操作grow
-
在扩容操作中,设置
新容量为老容量的1.5倍
(int newCapacity = oldCapacity + (oldCapacity >> 1)),如果新容量小于所需容量,则新容量等于所需容量
(newCapacity = minCapacity),如果新容量比规定的最大值还要大,则新容量等于最大容量newCapacity = hugeCapacity(minCapacity); -
将原数组拷贝进长度为新容量的新数组中
👋3.1.2 add(int index, E element)
===============================================================================================
在指定索引处添加元素
public void add(int index, E element) {
//判断所传入的索引是否符合规则(既不能<0,也不能大于原数组容量)
rangeCheckForAdd(index);
//数组所需容量+1,并判断增加元素之后是否需要扩容
ensureCapacityInternal(size + 1);
//数组元素拷贝,index之后的元素向后移一位,把index的位置空了出来
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//在index位置上填充要插入的元素
elementData[index] = element;
//元素个数+1
size++;
}
//判断index是否符合规则
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
我们总结一下流程:
-
检查传入index是否符合规则
-
判断数组是否需要进行扩容
-
创建新数组,并将原数组元素进行拷贝,拷贝原理是使index之后的元素向后移动一位,将index位置空出,再在该位置填充元素
-
元素个数+1
👋3.1.3 addAll(Collection<? extends E> c)
=======================================================================================================
按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾
public boolean addAll(Collection<? extends E> c) {
//将有数据的集合c转换成数组形式
Object[] a = c.toArray();
//将数据集合长度赋值给numNew
int numNew = a.length;
//判断增加元素后是否需要扩容
ensureCapacityInternal(size + numNew);
//将a拷贝进elementData最后
System.arraycopy(a, 0, elementData, size, numNew);
//数组中元素个数=原数组中元素个数+新数组中元素个数
size += numNew;
//c容量为0则返回false,容量不为0则返回true
return numNew != 0;
}
我们总结一下流程:
-
将原集合转换为数组形式
-
判断数组是否需要扩容
-
将传进来的集合元素拷贝到原集合的末尾
-
元素个数变化
👋3.1.4 addAll(int index, Collection<? extends E> c)
==================================================================================================================
将指定集合中的所有元素插入到此列表中,从指定的位置开始
public boolean addAll(int index, Collection<? extends E> c) {
//判断index是否符合规则
rangeCheckForAdd(index);
//将要添加的集合转为数组形式
Object[] a = c.toArray();
//将数组长度赋给numNew
int numNew = a.length;
//判断增加元素之后是否需要扩容
ensureCapacityInternal(size + numNew); \
//记录有多少位元素需要向后移动
int numMoved = size - index;
//如果有元素需要移动
if (numMoved > 0)
//进行数组拷贝,这一步只是进行移动操作,并没有添加数据,将数组中index之后的所有元素向后移动numNew位
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//进行数组填充,将集合c中的元素从数组index处开始填充
System.arraycopy(a, 0, elementData, index, numNew);
//数组中元素个数=原数组中元素个数+新数组中元素个数
size += numNew;
//返回c集合是否为空的布尔值
return numNew != 0;
}
我们总结一下流程:
-
将原集合转换为数组形式
-
判断数组是否需要扩容
-
记录下需要有多少元素进行移动
-
如果有元素需要移动,新数组中index之后的所有元素向后移动numNew位
-
对数组进行填充
-
数组元素个数变化
👋3.1.5 set(int index, E element)
===============================================================================================
根据索引修改集合元素
public E set(int index, E element) {
//判断索引是否符合规则,索引不能超过数组长度
rangeCheck(index);
//获得下标处的元素
E oldValue = elementData(index);
//修改索引处的元素值
elementData[index] = element;
//将旧的元素值返回
return oldValue;
}
//校验索引
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//获得下标处的元素
E elementData(int index) {
return (E) elementData[index];
}
====================================================================================
获得索引处的元素值
/**
-
Returns the element at the specified position in this list.
-
@param index index of the element to return
-
@return the element at the specified position in this list
-
@throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
//校验索引范围
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//返回索引处元素
E elementData(int index) {
return (E) elementData[index];
}
======================================================================================
移除指定元素值的元素
public boolean remove(Object o) {
//如果要删除的元素是否为空
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//遍历集合
for (int index = 0; index < size; index++)
//将集合中的每一个元素进行比对,比对成功则删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*集合真正删除元素的方法
-
Private remove method that skips bounds checking and does not
-
return the value removed.
*/
private void fastRemove(int index) {
//对集合的实际修改次数+1
modCount++;
//计算要移动的元素的个数
int numMoved = size - index - 1;
//如果移动的元素的个数>0
if (numMoved > 0)
//移动元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将要删除的元素置为null,就是为了尽快被垃圾回收机制回收
elementData[–size] = null;
}
ArrayList中使用迭代器遍历时删除元素会爆ConcurrentModificationException()并发修改异常的原因:这种情况只出现在要删除的元素位于集合尾部的时候
迭代器在遍历时会调用next()方法,next方法会在checkForComodification()方法中对expectedModCount
(期待修改次数)和modCount(实际修改次数)进行比对
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
expectedModCount值的变化只与第一次创建迭代器时的modCount有关,而modCount值的变化与增加和删除
(不考虑其他情况)都有关,如果在迭代器遍历时删除元素,会导致modCount与expectedModCount不一致,故
抛出ConcurrentModificationException异常
结论
-
当要删除的元素在集合的倒数第二个位置的时候,不会产生并发修改异常,因为在调用
hashNext方法
的时候,光标的值和集合的长度一样,那么就会返回false,因此就不会再去调用next方法
获取集合的元素,既然不会调用next方法,那么底层就不会产生并发修改异常 -
集合每次调用add方法的时候,实际修改次数变量的值都会
自增一次
-
在获取迭代器的时候,集合只会执行一次将时机修改集合的次数赋值给预期修改集合的次数
-
集合在删除元素的时候也会针对实际修改次数进行自增的操作
=============================================================================
清空集合
public void clear() {
//修改次数
modCount++;
//将数组中的每个元素都设为null
for (int i = 0; i < size; i++)
elementData[i] = null;
//数组元素个数清空
size = 0;
}
====================================================================
==================================================================================
public String toString()把集合中所有元素转换成字符串
ArrayList集合本身并没有toString,我们需要到它的父类AbstractList
的父类AbstractCollection
中去寻找
/**
-
Returns a string representation of this collection. The string
-
representation consists of a list of the collection’s elements in the
-
order they are returned by its iterator, enclosed in square brackets
-
(“[]”). Adjacent elements are separated by the characters
-
", " (comma and space). Elements are converted to strings as
-
by {@link String#valueOf(Object)}.
-
@return a string representation of this collection
*/
public String toString() {
//获取集合元素的迭代器
Iterator it = iterator();
//判断迭代器是否为空
if (! it.hasNext())
return “[]”;//为空直接返回空
//创建StringBuilder对象便于拼接字符串,StringBuilder线程安全
StringBuilder sb = new StringBuilder();
sb.append(‘[’);
for (;😉 {
E e = it.next();//迭代器向后移动
sb.append(e == this ? “(this Collection)” : e);
if (! it.hasNext())//迭代器执行完毕,将整个缓冲区对象转换为String对象
return sb.append(‘]’).toString();
sb.append(‘,’).append(’ ‘);//否则拼接’,‘’ ’
}
}
==========================================================================================
ArrayList对迭代器方法进行了重写
//获取迭代器对象
public Iterator iterator() {
return new Itr();
}
//迭代器源码,不同集合实现的迭代器底层源码都是不一样的
private class Itr implements Iterator {
int cursor; //光标,默认值就是0
int lastRet = -1; //记录-1这个位置
//将集合实际修改次数赋值给预期修改次数
int expectedModCount = modCount;
//集合是否还有元素
public boolean hasNext() {
//判断光标是否不等于集合元素总量
return cursor != size;
}
public E next() {
checkForComodification();
//将光标赋值给i 第一次i=0
int i = cursor;
//如果光标大于等于集合size就说明集合中已经没有元素
if (i >= size)
throw new NoSuchElementException();
//将集合存储数据的地址赋值给该方法的局部变量
Object[] elementData = ArrayList.this.elementData;
//针对多线程环境,如果成立,触发并发修改异常
if (i >= elementData.length)
throw new ConcurrentModificationException();
//光标向前移动1位
cursor = i + 1;
//将光标位的元素返回
return (E) elementData[lastRet = i];
}
//校验预期修改次数是否和实际修改次数一样
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
**if (i >= elementData.length)
throw new ConcurrentModificationException();的作用:
防止多并发修改异常,如果多个线程同时调用next(),i的值会增加,如果在容量之内会返回0,但如果增加的i的值大于容量,证明并发数量过多,对i的准确性造成破坏性影响,抛出ConcurrentModificationException()异常**
================================================================================
===================================================================================
第一次扩容10 以后每次都是原容量的1.5倍
👋5.2 ArrayList频繁扩容导致添加性能急剧下降,如何处理?
=================================================================================================
我们通过一个案例解答
public class Test04 {
public static void main(String[] args) {
//未指定容量的list
ArrayList list = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
list.add(i+“”);
}
long endTime = System.currentTimeMillis();
System.out.println(“未指定容量时添加100W条数据用时:”+(endTime-startTime));
//指定容量的list1
ArrayList list1 = new ArrayList<>(1000000);
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
list.add(i+“”);
}
long endTime1 = System.currentTimeMillis();
System.out.println(“指定容量时添加100W条数据用时:”+(endTime1-startTime1));
}
}
未指定容量时添加100W条数据用时:95
指定容量时添加100W条数据用时:69
由此我们可得,在数据量较大时,为了避免扩容,我们最好创建ArrayList时便指定容量
👋5.3 ArrayList插入或删除元素一定比LinkedList慢吗
===================================================================================================
首先我们要确定ArrayList和LinkedList的底层数据结构
-
ArrayList数据结构是·动态数组·
-
LinkedList数据结构是·链表·
我们从源码层面去分析
观察LinkedList的remove源码
public E remove(int index) {
//对索引的校验,我们这里不做讨论
checkElementIndex(index);
//主要看node(int index)方法
return unlink(node(index));
}
//根据索引虽然可以直接去除一半的元素参与判断,但是效率依然很低,需要不断遍历
Node node(int index) {
//这里是判断index在链表的前半部分还是后半部分,因为是双向的
//判断index是否小于集合长度的一半
if (index < (size >> 1)) {
//在前半部分的话,第一个节点就为x,从前往后找
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//在后半部分的话,最后一个节点为x,从后往前找
Node x = last;
for (int i = size - 1; i > index; i–)
x = x.prev;
return x;
}
}
//通过node(int index)方法找到了index所在节点的位置
E unlink(Node x) {
// assert x != null;
final E element = x.item;
//获取下一个节点
final Node next = x.next;
//获取上一个节点
final Node prev = x.prev;
//如果x在链表的开头
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//如果x在链表的末尾
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size–;
modCount++;
return element;
}
由此我们可以得出结论:
-
要删除的节点位于集合中间位置,则LinkedList的速度慢
-
要删除的节点位于集合的头部和尾部,则LinkedList的速度快(如果不从添加元素时就开始计算时间的话是这样,但是如果要把添加元素的时间计算在内那么ArrayList快,因为LinkedList中的last节点每次在add元素时都会向后移动一位,也是需要花费时间的)
void linkLast(E e) {
final Node l = last;
final Node newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
====================================================================================
先回答问题:ArrayList不是线程安全的,我们使用一个案例来演示
package ArrayListProject.CloneTest;
import java.util.ArrayList;
import java.util.List;
public class CollectionTask implements Runnable {
private List list;
public CollectionTask(List list) {
this.list = list;
}
@Override
public void run() {
try {
Thread.sleep(50);
}catch (Exception e){
e.printStackTrace();
}
list.add(Thread.currentThread().getName());
}
}
class MyTest{
public static void main(String[] args) throws Exception {
List list = new ArrayList<>();
CollectionTask ct = new CollectionTask(list);
//开启50条线程
for (int i = 0; i < 50; i++) {
new Thread(ct).start();
}
//确保子线程执行完毕
Thread.sleep(3000);
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println(“集合长度:”+list.size());
}
}
Thread-47
Thread-45
Thread-44
Thread-41
Thread-34
Thread-35
Thread-39
Thread-33
Thread-27
Thread-26
Thread-40
Thread-32
Thread-20
Thread-21
Thread-15
Thread-29
Thread-14
Thread-9
Thread-8
Thread-28
Thread-23
Thread-3
null
null
null
null
null
null
null
Thread-22
Thread-38
Thread-49
Thread-48
null
null
null
null
null
null
null
null
null
null
null
null
null
null
null
Thread-43
总结
面试难免让人焦虑不安。经历过的人都懂的。但是如果你提前预测面试官要问你的问题并想出得体的回答方式,就会容易很多。
此外,都说“面试造火箭,工作拧螺丝”,那对于准备面试的朋友,你只需懂一个字:刷!
给我刷刷刷刷,使劲儿刷刷刷刷刷!今天既是来谈面试的,那就必须得来整点面试真题,这不花了我整28天,做了份“Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法等”
且除了单纯的刷题,也得需准备一本【JAVA进阶核心知识手册】:JVM、JAVA集合、JAVA多线程并发、JAVA基础、Spring 原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB、Cassandra、设计模式、负载均衡、数据库、一致性算法、JAVA算法、数据结构、加密算法、分布式缓存、Hadoop、Spark、Storm、YARN、机器学习、云计算,用来查漏补缺最好不过。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
onTask(list);
//开启50条线程
for (int i = 0; i < 50; i++) {
new Thread(ct).start();
}
//确保子线程执行完毕
Thread.sleep(3000);
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println(“集合长度:”+list.size());
}
}
Thread-47
Thread-45
Thread-44
Thread-41
Thread-34
Thread-35
Thread-39
Thread-33
Thread-27
Thread-26
Thread-40
Thread-32
Thread-20
Thread-21
Thread-15
Thread-29
Thread-14
Thread-9
Thread-8
Thread-28
Thread-23
Thread-3
null
null
null
null
null
null
null
Thread-22
Thread-38
Thread-49
Thread-48
null
null
null
null
null
null
null
null
null
null
null
null
null
null
null
Thread-43
总结
面试难免让人焦虑不安。经历过的人都懂的。但是如果你提前预测面试官要问你的问题并想出得体的回答方式,就会容易很多。
此外,都说“面试造火箭,工作拧螺丝”,那对于准备面试的朋友,你只需懂一个字:刷!
给我刷刷刷刷,使劲儿刷刷刷刷刷!今天既是来谈面试的,那就必须得来整点面试真题,这不花了我整28天,做了份“Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法等”
[外链图片转存中…(img-MLaiO4Te-1713440363020)]
且除了单纯的刷题,也得需准备一本【JAVA进阶核心知识手册】:JVM、JAVA集合、JAVA多线程并发、JAVA基础、Spring 原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB、Cassandra、设计模式、负载均衡、数据库、一致性算法、JAVA算法、数据结构、加密算法、分布式缓存、Hadoop、Spark、Storm、YARN、机器学习、云计算,用来查漏补缺最好不过。
[外链图片转存中…(img-b3QREakP-1713440363020)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-EMaAHkRZ-1713440363021)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!