最后
答应大伙的备战金三银四,大厂面试真题来啦!
这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
《960全网最全Android开发笔记》
《379页Android开发面试宝典》
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析
资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
Java 中的引用可以分为四类,强引用、软引用、弱引用和虚引用。强引用在程序中普遍存在,类似 new 的这种操作,只要有强引用存在,即使 OOM JVM 也不会回收该对象。软引用是在内存不够用时,才会去回收,JDK 提供了 SoftReference 类来实现软引用。弱引用是在 GC 时不管内存够不够用都会去回收的,可以使用 WeakReference 类来实现弱引用。虚引用对对象的生命周期没有影响,只是为了能在对象回收时收到一个系统通知,可以使用 PhantomReference 类来实现虚引用。
接下来就是要讲垃圾回收算法了。
垃圾回收算法
垃圾回收算法主要有标记清除、复制算法、标记整理。标记清除是先通过 GC Roots 标记所存活的对象,然后再统一清除未被标记的对象,它的主要问题是会产生内存碎片。老年代使用的 CMS 收集器就是基于标记清除算法。复制算法是把内存空间划分为两块,每次分配对象只在一块内存上进行分配,在这一块内存使用完时,就直接把存活的对象复制到另外一块上,然后把已使用的那块空间一次清理掉,但是这种算法的代价就是内存的使用量缩小了一半。现代虚拟机都采用复制算法回收新生代,不过是把内存划分为了一个 Eden 区和两个 Survivor 区,比例是 8:1:1,每次使用 Eden 和其中一块 Survivor 区,也就是只有 10% 的内存会浪费掉。如果 Survivor 空间不够用,需要依赖其他内存比如老年代进行分配担保。复制算法在对象存活率比较高时效率是比较低下的,所以老年代一般不使用复制算法。标记整理算法即是在标记清除之后,把所有存活的对象都向一端移动,然后清理掉边界以外的内存区域。
最后就是讲垃圾回收算法的具体应用了,也就是垃圾收集器。
G1 及 ZGC
Garbage First(G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。它和 CMS 同样是一款主要面向服务端应用的垃圾收集器,不过在 JDK9 之后,CMS 就被标记为废弃了,G1 作为默认的垃圾收集器,在 JDK 14 已经正式移除 CMS 了。在 G1 收集器出现之前的所有其他收集器,包括 CMS 在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),在要么就是整个 Java 堆(Full GC)。而 G1 是基于 Region 堆内存布局,虽然 G1 也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间或者老年代。收集器根据 Region 的不同角色采用不同的策略去处理。G1 会根据用户设定允许的收集停顿时间去优先处理回收价值收益最大的那些 Region 区,也就是垃圾最多的 Region 区,这就是 Garbage First 名字的由来。
G1 收集器的运作过程大致可划分为以下四个步骤:
1.初始标记
仅仅只是标记一下 GC Roots 能直接关联到的对象,这个阶段需要停顿线程,但耗时很短。
2.并发标记
从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但是可与用户程序并发执行。
3.最终标记
对用户线程做另一个短暂的暂停,用于处理在并发标记阶段新产生的对象引用链变化。
4.筛选回收
负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。
G1 的目标是在可控的停顿时间内完成垃圾回收,所以进行了分区设计,但是 G1 也存在一些问题,比如停顿时间过长,通常 G1 的停顿时间在几十到几百毫秒之间,虽然这个数字其实已经非常小了,但是在用户体验有较高要求的情况下还是不能满足实际需求,而且 G1 支持的内存空间有限,不适用于超大内存的系统,特别是在内存容量高于 100GB 的系统上,会因内存过大而导致停顿时间增长。
ZGC 在 JDK11 被引入,作为新一代的垃圾回收器,在设计之初就定义了三大目标:支持 TB 级内存,停顿时间控制在 10ms 之内,对程序吞吐量影响小于 15%。
类加载机制
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 对象,这就是虚拟机的类加载机制。
类加载流程分为五个阶段,分别是加载、验证、准备、解析和初始化。
加载阶段,就是通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。加载阶段是开发人员可控性最强的阶段,因为开发人员可以自定义类加载器。对于数组而言,情况有所不同,数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建。
验证是链接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。它包括文件格式校验、元数据校验、字节码校验等。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。需要注意的是,这时候进行内存分配的仅仅包含类变量,不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆上。其次,这里所说的变量初始值是该数据类型的零值。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用以一组符号来描述所引用的目标,直接引用可以是直接指向目标的指针。
初始化阶段是执行类构造器 () 方法的过程。() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。虚拟机会保证一个类的 () 方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 () 方法,其他线程都需要阻塞等待,这也是静态内部类能实现单例的主要原因之一。
双亲委派模型
双亲委派模型的工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的类加载请求最终都应该传送给顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。比如 Object 类,无论哪个类加载器去加载,应用程序各种加载器环境中都是同一个类,同时也避免了重复加载。而且,双亲委派模型也保证了 Java 程序的稳定运作。比如在应用程序中你是不能直接使用 UnSafe 这一不安全操作的类的。
双亲委派模型的实现相对简单,代码都集中在 ClassLoader 的 loadClass 方法中先检查是否已经被加载过了,如果没加载则先调用父加载器的 loadClass 方法,若父加载器为空则使用默认的启动类加载器作为父加载器。如果父加载器加载失败,抛出 ClassNotFoundException 异常,然后调用自己的 findClass 方法进行加载。
编译器优化
在公司内部,我是分享过一次关于编译优化的相关知识。课题是 “从 final ‘能够’ 提升性能,谈编译优化”。
对于 Java 代码的编译,分为前端编译和后端编译。前端编译是指通过 javac 工具,将 Java 代码转化为字节码的过程。既然 javac 负责字节码的生成,那肯定就会有一些通用的优化手段。比如常量折叠、自动装拆箱、条件编译等,其次还有 JDK9 使用 StringContactFactory 对 “+” 的重载提供的统一入口等。后端编译则指 JVM 内置的解释器和即时编译器(C1、C2)。JVM 在对代码执行的优化可以分为运行时优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如 TLAB)等。除此之外,还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。JVM 的即时编译器优化是指将热点代码以方法为单位转化成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序的运行 profile 的投机性优化。
下面我就主要讲一下方法内联和逃逸分析。
方法内联,它指的是在编译的过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化。因此,它可以算是编译优化里最为重要的一环。以 getter/setter 为例,如果没有方法内联,在调用 getter/setter 时,程序需要保存当前方法的执行位置,创建并压入用于 getter/setter 的栈桢、访问字段、弹出栈桢,最后再恢复当前方法的执行。而当内联了对 getter/setter 的方法调用后,上述操作就只剩下字段访问了。但是即时编译器不会无限制的进行方法内联,它会根据方法的调用次数、方法体大小、Code cache 的空间等去决定是否要进行内联。比如即使是热点代码,如果方法体太大,也不会进行内联,因为会占用更多内存空间。所以平时编码中,尽可能使用小方法体。对于需要动态绑定的虚方法调用来说,即时编译器则需要先对虚方法调用进行去虚化,即转化为一个或多个直接调用,然后才能进行方法内联。说到这,你应该就明白 final/static 的好处了。所以尽量使用 final、private、static 关键字修饰方法,虚方法因为继承,会需要额外的类型检查才能知道实际上调用的是哪个方法。
逃逸分析是判断一个对象是否被外部方法引用或外部线程访问的分析技术,即时编译器可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。我们先看一下锁消除,如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没有任何意义,因为其他线程并不能获得该锁对象,在这种情况下,即时编译器就可以消除对该不逃逸对象的加锁、解锁操作。比如 synchronized(new Object) 这种操作会被完全优化掉。不过一般不会有人这么写,事实上,逃逸分析的结果更多被用于将新建对象操作转换成栈上分配或者标量替换。我们知道,Java 虚拟机中对象都是在堆上进行分配的,而堆上的内容对任何线程可见,与此同时,JVM 需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存。如果逃逸分析能够证明某些新建的对象不逃逸,那么 JVM 完全可以将其分配至栈上,并且在方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。不过,由于实现起来需要更改大量假设了 “对象只能堆分配” 的代码,因此 HotSpot 虚拟机并没有采用栈上分配,而是使用了标量替换这么一项技术。所谓的标量,就是仅能存储一个值的变量,比如 Java 代码中的局部变量。标量替换这项优化技术,可以看成将原本对对象的字段的访问,替换成一个个的局部变量的访问。
虚拟机相关
先说 HotSpot 虚拟机。
从硬件视角来看呢,Java 字节码是无法直接运行的,因此 JVM 需要将字节码翻译成机器码。在 HotSpot 里面,翻译过程有两种,一种是解释执行,即逐条将字节码翻译成机器码并执行,第二种是即时编译执行,即以方法为单位整体编译为机器码后再执行。前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和编译执行两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行编译执行。
HotSpot 内置了多个 JIT 即时编译器,C1 和 C2,之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。Java 7 引入了分层编译,分层编译将 JVM 的执行状态分为 5 个层次。第 0 层是解释执行,默认开启性能监控;第 1 层到第 3 层都是称为 C1 编译,将字节码编译成本地代码,进行简单、可靠的优化;第 4 层是 C2 编译,也是将字节码编译成本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
至此,HotSpot 及 JIT 就讲完了。
再说 Dalvik 和 ART。
HotSpot 是基于栈结构的,而 Dalvik 是基于寄存器结构。在官方文档上,已经没有 Dalvik 相关的信息了,Android 5 后,ART 全面取代了 Dalvik。Dalvik 使用 JIT 而 ART 使用 AOT。AOT 和 JIT 的不同之处在于,JIT 是在运行时进行编译,是动态编译,并且每次运行程序的时候都需要对 odex 重新进行编译;而 AOT 是静态编译,应用在安装的时候会启动 dex2oat 过程把 dex 预编译成 oat 文件,每次运行程序的时候不用重新编译。另外,相比于 Dalvik,ART 对 GC 过程也进行了改进,只有一次 GC 暂停,而 Dalvik 需要两次,而且在 GC 保持暂停状态期间并行处理。AOT 解决了应用启动和运行速度问题的同时也带来了另外两个问题,一个是应用安装和系统升级之后的应用安装时间比较长,二是优化后的文件会占用额外的存储空间。在 Android 7 之后,JIT 回归,形成了 AOT/JIT 混合编译模式,这种混合编译模式的特点是:应用在安装的时候 dex 不会被编译,应用在运行时 dex 文件先通过解释器执行,热点代码会被识别并被 JIT 编译后存储在 Code cache 中生成 profile 文件,再手机进入 IDLE(空闲)或者 Charging(充电)状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。这样一说,其实是和 HotSpot 有点内味。
面试问的关于JVM问题
JVM 是如何执行方法调用的?
其实呢就是了解 Java 编译器和 JVM 是如何区分方法的。方法重载在编译阶段就能确定下来,而方法重写则需要运行时才能确定。
Java 编译器会根据所传入的参数的声明类型来选取重载方法,而 JVM 识别方法依赖于方法描述符,它是由方法的参数类型以及返回类型所构成。JVM 内置了五个与方法调用相关的指令,分别是 invokestatic 调用静态方法、invokespecial 调用私有实例方法、invokevirtual 调用非私有实例方法、invokeinterface 调用接口方法以及 invokedynamic 调用动态方法。对于 invokestatic 以及 invokespecial 而言,JVM 能够直接识别具体的目标方法,而对于 invokevirtual 和 invokeinterface 而言,在绝大多数情况下,JVM 需要在执行过程中,根据调用者的动态类型来确定具体的目标方法。唯一的例外在于,如果虚拟机能够确定目标方法有且只有一个,比如方法被 final 修饰,那么它就可以不通过动态类型,直接确定目标方法。
上面所说的 invokespecial、invokeinterface 也被称为虚方法调用或者说动态绑定,相比于直接能定位方法的静态绑定而言,虚方法调用更加耗时。JVM 采用了一种空间换时间的策略来实现动态绑定。它为每个类生成一张方法表,用于快速定位目标方法,这个发生在类加载的准备阶段。方法表本质上是一个数组,它有两个特性,首先是子类方法表中包含父类方法表中所有的方法,其次是子类方法在方法表中的索引,与它所重写的父类方法的索引值相同。我们知道,方法调用指令中的符号引用会在执行之前解析为实际引用。对于静态绑定的方法调用而言,实际引用将指向具体的方法,对于动态绑定而言,实际引用则是方法表的索引值。
JVM 也提供了内联缓存来加快动态绑定,它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。
JVM 是如何实现反射的?
反射呢是 Java 语言中一个相当重要的特性,它允许正在运行的 Java 程序观测,甚至是修改程序的动态行为。表现为两点,一是对于任意一个类,都能知道这个类的所有属性和方法,二是对于任意一个对象,都能调用它的任意属性和方法。
反射的使用还是比较简单的,涉及的 API 分为三类,Class、Member(Filed、Method、Constructor)、Array and Enumerated。我当时是直接扒 Oracle 官方文档看的,讲的很详细。
我对反射的好奇是来源于,经常会听说反射影响性能,那么性能开销在哪以及如何优化?
在此之前,我先讲讲 JVM 是如何实现反射的。
我们可以直接 new Exception 来查看方法调用的栈轨迹,在调用 Method.invoke() 时,是去调用 DelegatingMethodAccessorImpl 的 invoke,它的实际调用的是 NativeMethodAccessorImpl 的 invoke 方法。前者称为委派实现,后者称为本地实现。既然委派实现的具体实现是一个本地实现,那么为啥还需要委派实现这个中间层呢?其实,Java 反射调用机制还设立了另一种动态生成字节码的实现,成为动态实现,直接使用 invoke 指令来调用目标方法。之所以采用委派实现,是在本地实现和动态实现直接做切换。依据注释信息,动态实现比本地实现相比,其运行效率要快上 20 倍。这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换,但由于生产字节码比较耗时,仅调用一次的话,反而是本地实现要快上三四倍。考虑到很多反射调用仅会执行一次,JVM 设置了阈值 15,在 15 之下使用本地实现,高于 15 时便开始动态生成字节码采用动态实现。这也被称为 Inflation 机制。
在反手说一下反射的性能开销在哪呢?平时我们会调用 Class.forName、Class.getMethod、以及 Method.invoke 这三个操作。其中,Class.forName 会调用本地方法,Class.getMethod 则会遍历该类的公有方法,如果没有匹配到,它还将遍历父类的公有方法,可想而知,这两个操作都非常耗时。下面就是 Method.invoke 调用本身的开销了,首先是 invoke 方法的参数是一个可变长参数,也就是构建一个 Object 数组存参数,这也同时带来了基本数据类型的装箱操作,在 invoke 内部会进行运行时权限检查,这也是一个损耗点。普通方法调用可能有一系列优化手段,比如方法内联、逃逸分析,而这又是反射调用所不能做的,性能差距再一次被放大。
优化反射调用,可以尽量避免反射调用虚方法、关闭运行时权限检查、可能需要增大基本数据类型对应的包装类缓存、如果调用次数可知可以关闭 Inflation 机制,以及增加内联缓存记录的类型数目。
JVM 是如何实现泛型的?
Java 中的泛型不过是一个语法糖,在编译时还会将实际类型给擦除掉,不过会新增一个 checkcast 指令来做编译时检查,如果类型不匹配就抛出 ClassCastException。
不过呢,字节码中仍然存在泛型参数的信息,如方法声明里的 T foo(T),以及方法签名 Signature 中的 “(TT;)TT”,这些信息可以通过反射 Api getGenericXxx 拿到。
除此之外,需要注意的是,泛型结合数组会有一些容易忽视的问题。数组是协变且具体化的,数组会在运行时才知道并检查它们的元素类型约束,可能出现编译时正常但运行时抛出 ArrayStoreException,所以尽可能的使用列表,这就是 Effective Java 中推荐的列表优先于数组的建议。这在我们看集合源码时也能发现的到,比如 ArrayList,它里面存数据是一个 Object[],而不是 E[],只不过在取的时候进行了强转。还有就是利用通配符来提升 API 的灵活性,简而言之即 PECS 原则,上取下存。典型的案例即 Collections.copy 方法了:
Collections.copy(List<? super T> dest, List<? extends T> src);
JVM 是如何实现异常的?
在 Java 中,所有的异常都是 Throwable 类或其子类,它有两大子类 Error 和 Exception。 当程序触发 Error 时,它的执行状态已经无法恢复,需要终止线程或者终止虚拟机,常见的比如内存溢出、堆栈溢出等;Exception 又分为两类,一类是受检异常,比如 IOException,一类是运行时异常 RuntimeException,比如空指针、数组越界等。
接下来我会从三个方面阐述这个问题。
首先是,异常实例的构造十分昂贵。这是由于在构造异常实例时,JVM 需要生成该异常的栈轨迹,该操作逐一访问当前线程的 Java 栈桢,并且记录下各种调试信息,包括栈桢所指向方法的名字、方法所在的类名以及方法在源代码中的位置等信息。
其次是,JVM 捕获异常需要异常表。每个方法都有一个异常表,异常表中的每一个条目都代表一个异常处理器,并且由 from、to、target 指针及其异常类型所构成。form-to 其实就是 try 块,而 target 就是 catch 的起始位置。当程序触发异常时,JVM 会检测触发异常的字节码的索引值落到哪个异常表的 from-to 范围内,然后再判断异常类型是否匹配,匹配就开始执行 target 处字节码处理该异常。
最后是 finally代码块的编译。我们知道 finally 代码块一定会运行的(除非虚拟机退出了)。那么它是如何实现的呢?其实是一个比较笨的办法,当前 JVM 的做法是,复制 finally 代码块的内容,分别放在所有可能的执行路径的出口中。
JVM 是如何实现注解的?
其实也没啥银弹,主要就是要知道注解信息是存放在哪的?在 Java 字节码中呢是通过 RuntimeInvisibleAnnotations 结构来存储的,它是一个 Annotations 数组,毕竟类、方法、属性是可以加多个注解的嘛。在数组中的每一个元素又是一个 ElementValuePair 数组,这个里面存储的就是注解的参数信息。
运行时注解可以通过反射去拿这些信息,编译时注解可通过 APT 去拿,基本上就没啥东西了。
Android及JVM学习资源
其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
这里再分享一下我面试期间的复习路线:(以下体系的复习资料是我从各路大佬收集整理好的)
《Android开发七大模块核心知识笔记》
《JVM核心知识点》
最后的最后
对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的
最后,互联网不存在所谓的寒冬,只是你没有努力罢了!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
都是我们去适应环境,而不是环境来适应我们!**
当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的
[外链图片转存中…(img-CTrMhsj2-1715446289454)]
最后,互联网不存在所谓的寒冬,只是你没有努力罢了!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!