
很多人一搜“Java基础知识”搜到的多半是那种“超详细总结”“全覆盖思维导图”动辄上万字看到最后反而不知道重点在哪。我在这个行业写了近十年代码也带过不少新人越来越觉得所谓“基础”真正值钱的不是那些背下来的名词而是你在写每一行业务代码时能不能下意识地做出正确的选择——变量用对了吗这个对象能不能复用HashMap 和 TreeMap 换着用会不会出事异常到底是吞掉还是抛出去线程该加什么锁这些问题没有标准答案但一定有一套经得起追问的底层判断逻辑。这篇文章不打算按教科书顺序给你罗列语法而是从我实际的开发经历出发把 Java 基础里最容易踩坑、面试最容易追问、日常代码最高频出现的几个大块拆开讲透。适合两类人看一类是刚开始学 Java、被术语和框架绕晕的新手另一类是写过一阵子业务代码、想回头把基础补扎实的开发者。我会尽量说人话也会把一些只有实际写代码才会碰到的细节讲出来。1. 从 JDK 到第一个能跑的程序环境里藏着第一道坎很多人觉得 Java 基础就是从变量、循环学起环境搭建这种东西“一会儿就搞定”不值得花时间。实际上我带过的实习生里至少有三分之一卡在环境问题上——不是 JDK 装不上而是装了之后完全不知道自己装的是什么出了错也看不懂日志。环境这块不花五分钟搞清楚后面学什么都像在沙子上盖楼。1.1 JDK、JRE、JVM 到底是什么关系先把这个最基础也最容易被问懵的关系说清楚。JDKJava Development Kit是开发工具包给写代码的人用的里面包含编译器javac、调试工具、文档生成工具以及一个完整的 JRE。JREJava Runtime Environment是运行环境给跑 Java 程序的人用的里面装着类库和 JVM。JVMJava Virtual Machine是 Java 虚拟机真正执行字节码的那个“引擎”。你可以这样理解JDK 是“厨房全套设备带菜刀砧板”JRE 是“能加热半成品饭菜的微波炉”JVM 是“微波炉里的加热管”。你写代码需要 JDK用户跑你的程序只需要 JRE而 JVM 是整个机制的核心——Java 之所以能“一次编写到处运行”靠的就是 JVM 把字节码翻译成当前机器的指令。面试里常问的“JDK 和 JRE 的区别”其实就一句话JDK 包含 JREJRE 包含 JVMJDK 是给开发者用的JRE 是给普通用户用的。这里有个实操层面的建议如果你只是在学习装 JDK 就够了不用单独装 JRE因为 JDK 自带 JRE但如果你想在服务器上跑一个打包好的 jar 包只装 JRE 会更轻量攻击面也小。1.2 环境变量配置的细节Path 和 JAVA_HOME 不是一回事Windows 上装完 JDK教程都会让你配两个环境变量JAVA_HOME和Path。但很少有人解释为什么两个都要配。JAVA_HOME是告诉系统“JDK 装在哪个目录”很多开发工具比如 Maven、Tomcat、IDEA启动时会去读取这个变量找 JDK 位置。Path则是告诉系统“可执行文件在哪里”这样你在命令行里直接敲java或javac就能找到对应的命令不用每次写全路径。有的教程还会让你配一个CLASSPATH这个在 JDK 1.5 之后其实已经不需要手动配了。早期的 Java 版本需要靠它指定类搜索路径现在编译器会从当前目录和依赖库中自动加载。如果你在网上看到老教程还在教配CLASSPATH可以直接跳过那属于历史遗留写法。再说一个常见坑装完 JDK 之后在命令行里输java -version提示“不是内部或外部命令”绝大多数是因为Path配的是 JDK 的根目录而不是bin目录。正确写法是%JAVA_HOME%\bin。也别把bin之前的一级配错比如配成C:\Program Files\Java\jdk-17\bin才能生效如果你只配到C:\Program Files\Java\jdk-17命令同样找不到。1.3 命令行编译运行不用 IDE 你也得会这一套现在很多人从 IDEA 或其他集成开发环境入门点一下运行按钮就能出结果这是好事但也导致一个隐性短板对编译运行过程缺乏直觉。IDE 帮你掩盖了所有中间步骤一旦出现类路径异常、打包失败你连排查方向都没有。至少一次请打开命令行手动走一遍完整的 Java 编译运行流程。新建一个Hello.java内容就写最简单的main方法。然后执行javac Hello.java java Hello第一行javac会把.java源码编译成.class字节码文件第二行java会启动 JVM 加载这个字节码并运行。这里经常有人犯的错是运行java Hello.class这不对。java后面跟的是类名不是文件名。如果你写的类带了包名比如package com.demo;那运行命令就得写java com.demo.Hello而且要保证从包结构的最外层目录往下执行。这个细节我见过不少工作两三年的同事偶尔也会犯迷糊。还有一点值得知道命令行里的中文编码问题。如果你的源码里有中文javac默认按平台编码读取Windows 上通常是 GBK而你的文件是用 UTF-8 存的就会报“编码 GBK 的不可映射字符”。解决办法是编译时显式指定编码javac -encoding UTF-8 Hello.java这个参数在你以后用脚本批量编译或者写 CI 流程时非常实用别等到中文乱码才想起来。2. 数据类型与控制结构变量的“边界感”比概念重要数据类型这一节几乎所有教材都会先让你背八大基本类型byte、short、int、long、float、double、char、boolean。背这个没错但实际开发中真正需要用到的是对“边界”和“转换”的敏感。2.1 基本类型和引用类型的分水岭在哪基本类型直接存值引用类型存的是“指向对象的地址”。这句话如果只是背诵那价值不大它的价值体现在两个具体场景里。第一个场景是比较。整型用比较没问题因为比较的是值但如果你用比较两个Integer对象行为就有点绕。看这段代码Integer a 127; Integer b 127; System.out.println(a b); // true Integer c 128; Integer d 128; System.out.println(c d); // false同样是用结果却不一样。原因在于Integer内部有一个缓存池默认缓存了-128到127之间的对象。a和b都在缓存范围内直接指向同一个对象所以为 truec和d超出缓存范围各自 new 了对象比较的是地址自然为 false。这段逻辑面试考烂了但实际写代码时千万不要依赖这个缓存行为Integer之间比较统一用equals()或者拆成基本类型intValue()再比较就不会踩这种玄学坑。第二个场景是参数传递。Java 只有值传递没有引用传递——这个说法我在无数面试里问过能答对的不到一半。具体来说传基本类型时传递的是值的副本你在方法里改参数不会影响外部变量传引用类型时传递的是“地址”的副本你用这个地址去修改对象内容外部能看到变化但你重新给参数赋值新的对象外部不会受影响。换句话说你拿到的是一把钥匙的复制品你用钥匙开门里面的东西能变但你换一把钥匙原来的锁一点都不会变。2.2 String、StringBuilder、StringBuffer 的选择逻辑String 是引用类型里最特殊的一个它在 Java 里的出场率极高相关的“为什么 String 是不可变的”也是面试高频题。我的理解是String 不可变首先是为了安全比如你作为 key 放进 HashMap如果 String 可变hashCode 就得跟着变整个 Map 就乱了其次是为了字面量池的复用比如String s1 abc; String s2 abc;这两个变量指向同一个常量池对象省内存最后是不可变对象天然线程安全不需要加锁。但不可变带来一个性能问题大量拼接时会生成很多中间对象。比如在循环里不断用拼接字符串每次拼接都会新建一个 String效率很低。这个时候应该用StringBuilder。StringBuffer则是线程安全的版本方法加了synchronized但代价是性能差一些。我个人的选择标准很简单单线程环境一律用StringBuilder多线程环境下如果真需要共享可变字符串才考虑StringBuffer但实际情况里多线程共享可变字符串的频率极低大部分场景用线程封闭或者干脆用不可变的String就够了。如果用了 Java 8 以上的版本也可以用StringJoiner或 Stream 的joining代码可读性更好我自己很常用这个。2.3 循环和分支里最容易影响性能的写法控制结构看起来人人都会写但细节处很见功底。举个例子switch和if-else的选择分支数量多的时候switch在编译期会生成跳转表比一串if-else更高效代码也更整洁。但要注意switch的穿透问题——每个case后面不写break就会一直往下执行这既是历史包袱也是一把双刃剑有时候你故意利用穿透合并逻辑比如多个 case 处理同一段代码但大多数时候它是 bug 源头。另一个细节是循环里的条件判断。假设你要遍历一个List最忌讳的写法是for (int i 0; i list.size(); i) { // ... }每次循环都会调用一次size()方法。虽然现代 JIT 可能会优化但对于很大的集合明确地把size存到局部变量里是更好的习惯。更推荐的写法是使用增强 for 循环或者Iterator也顺便解决了并发修改时抛出ConcurrentModificationException的一些困扰。切记不要在增强 for 循环里直接list.remove()这个坑后面讲到容器时再细说。3. 面向对象的核心不是用继承强行套关系而是用接口划定契约面向对象OOP是 Java 的根但很多教材把它讲成了“抽象、封装、继承、多态”四个名词解释。我这些年写下来的感悟是面向对象的关键不是你会不会背这四个词而是你在设计类结构时有没有把“变化”和“稳定”分开把“做什么”和“怎么做”分开。3.1 封装私有化只是手段隐藏变化才是目的private字段加 getter/setter 是封装但封装的真正意义是“隐藏实现细节把变化关在门内”。举个例子你写了一个订单金额计算的类里面金额字段用BigDecimal将来如果改成long存储分外部调用方不应该感知到任何变化——这是封装的收益。但我也见过很多“过度封装”的代码类里所有字段都提供 getter 和 setter连一个内部计数器都要暴露出去改。这样写让类的内部状态完全裸露跟直接把字段设成public没什么区别还多了几行模板代码。正确的做法是尽量少暴露可修改的入口能只读就只读能不可变就不可变。这一点上recordJava 16 正式引入帮了大忙定义数据载体类型的类时可以用 record 一行搞定不可变语义代码干净很多。3.2 继承组合优先于继承这是实战逼出来的规矩“组合优于继承”是 OOP 里非常经典的一条设计原则。它的意思很直白能用“包含一个对象”实现的复用就不要用“继承一个类”实现。继承是一种非常强的耦合关系子类会继承父类的全部实现细节父类改动一个不相关的方法可能悄悄影响子类行为。组合则是你持有另一个对象只调用你关心的接口耦合面小得多。举一个我自己重构过的例子之前有个类叫BaseExporter里面既做了数据读取、格式转换、加密又做了日志上报。新来一个需求要导出的数据要额外脱敏开发直接继承了BaseExporter重写了读取方法结果发现父类构造时机、加密顺序等被全盘打乱。后来我用组合重写一个Exporter类持有DataFetcher、DataMasker、DataEncryptor几个组件按需调用改动彼此隔离。从此再也没因为“改一个导出格式”而连带弄坏加密逻辑。所以我的建议是继承关系一定要满足严格的”is-a”比如Dog extends Animal这种拿不准的时候一律选组合。想让多实现多个能力怎么办用接口组合。3.3 多态重写和重载面试必问但总有人卡壳这两个名字相近实际含义完全不同。重写Override发生在父子类之间子类用相同的签名重新实现父类的方法运行时由 JVM 根据实际对象类型决定调用哪个版本这是多态的核心机制。重载Overload发生在同一个类里方法名相同但参数列表不同编译期就能决定调用哪个版本这事严格来说跟多态关系不大。实际编码里最有用的多态场景是方法参数声明为父类或接口类型调用时传入具体的子类或实现类。比如方法接收一个ListString你传ArrayList还是LinkedList都能跑方法本身不需要知道具体实现。这种写法是解耦的基础。3.4 接口与抽象类Java 8 之后边界变模糊了Java 8 之前接口里只能有抽象方法和常量抽象类里可以有成员变量、构造方法、普通方法。Java 8 给接口加了default方法和static方法正好是“接口从纯契约变成含实现的契约”的分水岭。Java 9 又加了私有方法接口甚至可以在内部复用一些逻辑了。那场景上怎么选我说说自己的判断逻辑如果你想描述“能做什么”的能力契约允许一个类同时实现多个不同能力用接口。如果你想沉淀部分公共实现、并放置共享状态比如公有的成员变量用抽象类。接口的default方法是用来做平滑升级的比如给一个已发布的接口加新方法时加上 default 可以避免所有实现类被迫改动。但新设计的接口我不太建议用 default 大段堆业务逻辑那样接口的“契约感”会被稀释。4. 容器是“选型题”不是“背诵题”Java 容器集合框架大概是面试里占比最大的基础模块也是实际开发中用得最密集的工具集。但我不建议一口气背完所有容器的原理再去用顺序应该是反过来先知道这个东西能解决什么问题在业务里遇到场景了再用用出错题了再回头查原理。4.1 从 ArrayList 和 LinkedList 的差异看“根据场景选容器”ArrayList 底层是数组LinkedList 底层是双向链表。教科书会告诉你数组随机访问快链表插入删除快。这句话出过太多误导因为“插入删除快”是有前提的——在已知节点位置的前提下链表插入确实只需要改变指针但如果你靠索引去定位比如list.add(1, element)LinkedList 需要从头或从尾遍历到索引位置代价同样很大。实际开发中 99% 的列表场景我都用 ArrayList原因是它对 CPU 缓存友好内存连续遍历能力极强。LinkedList 的典型优势场景其实很少比如你有一个队列需要频繁在头部插入尾部移除那它确实合适但这种情况很多人会直接选择ArrayDeque后者的综合性能更好。记住一句话选择容器不是看单个操作的上限而是看你的整体访问模式。频繁按索引随机访问选 ArrayList频繁在两端操作选 ArrayDeque只有当你需要频繁在链表中间插入且已经持有节点引用时LinkedList 才有真正的用武之地。4.2 HashMap 的底层逻辑从数组链表到红黑树的演变HashMap 是 Java 容器里的“天王级话题”面试几乎没有不问的工作里最容易出 bug 的也集中在这。JDK 1.8 之后HashMap 的底层结构是“数组 链表 红黑树”。先说数组每个桶位置通过key.hashCode()再经过一次扰动函数计算出的 hash 决定。hash 相同或者碰撞时同一个桶里的元素以链表形式挂起来查找退化为链表的线性遍历。当链表长度超过阈值 8同时数组长度大于 64 时链表会树化成红黑树把查找复杂度从 O(n) 降到 O(log n)。这个设计的本质是防止 hash 碰撞极端情况下的性能恶化。注意阈值 8 不是“一超过就立刻树化”它还要求数组长度先达到 64否则优先扩容而不是树化。这些细节如果只看别人总结的“HashMap 八股文”很容易记混建议自己打开源码把putVal方法过一遍。OpenJDK 的源码不复杂配合断点跑一跑比死记硬背强十倍。再说几个实战里最容易踩的坑第一HashMap 不是线程安全的。多线程同时 put 导致扩容时JDK 1.7 在某些条件下可能形成环形链表get 时死循环JDK 1.8 修复了这个死循环问题但数据丢失、覆盖、size 不对的问题依然存在。并发场景用ConcurrentHashMap不要自己给 HashMap 加裸锁。第二自定义对象做 key 时一定要重写equals()和hashCode()。不重写的话即使两个对象内容完全相同hashCode 不同get还是找不到值。重写时要保证equals 相等的两个对象hashCode 必须相等反过来不要求。这是约定违反了他你的 Map 就会表现得很诡异。第三不要在遍历 HashMap 时直接做 remove 操作会抛ConcurrentModificationException。要用迭代器的remove()方法或者 Java 8 的removeIf。4.3 其他常用容器Set、Queue 也有自己的脾气HashSet 底层实际上是 HashMap 的壳只是只用 keyvalue 存一个固定的空对象。所以 HashSet 的去重逻辑本质上依赖你元素的equals和hashCode。TreeSet 底层是红黑树元素会按自然顺序或自定义比较器排序但插入和删除时间复杂度是 O(log n)比 HashSet 的 O(1) 慢。你确需顺序访问时用 TreeSet否则优先 HashSet。队列里PriorityQueue是优先级队列底层用堆实现每次取出的都是“当前最小或按比较器最大”的元素。它可以用来做任务调度、TopK 问题但要注意它只保证出队顺序迭代遍历时不保证顺序。日常开发中如果只是先进先出ArrayDeque比LinkedList更适合。一句话总结容器这块搞懂“数组、链表、树、哈希表”这四种底层结构再看任何容器都像看透明的盒子而不是背说明书。5. 异常处理与 IO 流框架帮你兜底但你得知道底在哪Java 的异常处理是很优秀的机制但实际项目里我看得最多的“坏味道”就是 catch 了异常之后默默吞掉或者打一行e.printStackTrace()完事。这会给线上排查埋很多雷。5.1 异常体系与分类检查型和非检查型怎么分界先梳理一下体系Throwable 是所有错误和异常的祖先下面分为 Error 和 ExceptionError 代表 JVM 层面的严重问题比如 OutOfMemoryError、StackOverflowError一般是程序无法合理恢复的不要试图去 catch应该让程序尽早暴露。Exception 下面又分两类受检异常checked和运行时异常unchecked。受检异常是编译器强制要求你处理的比如 IOException、SQLException要么 throws 显式声明要么 try-catch。运行时异常则不需要强制声明最常见的比如 NullPointerException、IllegalArgumentException。我个人的看法是受检异常的设计初衷是逼开发者考虑异常路径但过度使用会让代码变得碎比如有些方法每一行都要 catch。现在的趋势反而是对业务异常使用运行时异常让异常向上抛到统一处理层比如 Spring 的ControllerAdvice代码会清爽很多。受检异常则保留在必须由调用方感知的场景比如文件不存在、网络断开这类。5.2 try-with-resources关闭资源不再靠 finally 硬写处理 IO 流最经典的教科书写法是用 finally 来关流FileInputStream in null; try { in new FileInputStream(file.txt); // ... } finally { if (in ! null) { try { in.close(); } catch (IOException ignored) { } } }这段代码写起来极其啰嗦而且很容易漏掉一些流。Java 7 之后有了 try-with-resources只要你的资源实现了AutoCloseable接口就可以这样写try (FileInputStream in new FileInputStream(file.txt); ByteArrayOutputStream out new ByteArrayOutputStream()) { // ... }编译器会自动生成关闭逻辑而且如果关闭时又抛异常它会处理成抑制异常不会把主异常遮盖掉。Java 9 之后你甚至可以用已有的 final 变量来声明资源不用每次新起名字。这里给一条经验凡是打开了一个可关闭的资源立刻放进 try 的小括号里别等最后再说。流式 API 里比如 Files.lines 这样一行生成 Stream后面用完也应该 close或者直接用 try-with-resources 包住。5.3 IO/NIO 的理解框架阻塞、非阻塞、多路复用Java 的 IO 体系内容很多但真正需要建立的框架认知是传统java.io是字节流 字符流、输入 输出的组合上面叠着 BufferedInputStream、InputStreamReader 这样的装饰器。它的问题是阻塞式每条连接都要一个线程等着连接一多资源就不够用。NIONew IO引入了 Channel、Buffer、Selector 的概念。Selector 可以让一个线程管理多个 Channel监听哪些通道有事件就绪这就是多路复用的思路。Netty 这类高性能网络框架的底层都是基于 NIO 演进出来的。对做业务开发的人来说你大概率不会直接写 NIO 代码但理解“一个线程管多个连接”的模型至关重要否则看网络框架的线程模型会一头雾水。打个比方传统 IO 像开了一家餐厅一个服务员只盯一张桌子客人多了就得加服务员NIO 多路复用是眼观六路的领班一个人盯着整个大堂哪个桌有需求就过去处理哪个桌。关于 IO 这块我觉得学习顺序建议是先把传统的 FileInputStream、FileOutputStream、InputStreamReader、BufferedReader 搞熟会正确地用 try-with-resources 关闭资源再去看 NIO 的 Buffer 和 Channel。只要理解到位后面看 Netty 的高性能模型就是水到渠成的事。6. 并发与数据一致性基础里的“分水岭”知识有多少人学完 Java 基础碰到的第一个“感觉自己还没入门”的时刻是看不懂多线程代码上一份工作是维护一套库存扣减系统第一次线上出现超卖才真正意识到并发和数据一致性远不是背几个关键词能搞定的。6.1 JMM 和线程安全为什么会有“看不见”的变量先扯一个根本问题为什么多线程下会出现数据不一致答案是你的变量值不是单纯存在“共享内存”这一个地方。JVM 内存模型Java Memory Model里每个线程有一个自己的工作内存里面的值是主内存变量的副本。线程操作的是副本操作完再刷回主内存。如果多个线程同时操作同一个变量彼此之间看不到对方在工作内存中的修改就会出现“明明改了另一个线程看不到”的诡异现象。解决手段第一层是volatile。它的作用是每次读都从主内存读每次写立即刷回主内存并且禁止指令重排序。注意volatile 只保证可见性不保证原子性。比如count这种操作本质是“读-改-写”三步volatile 无法保证这三步不被并发打断结果仍然可能丢更新。要保证原子性基本手段是锁。Java 里最朴素的锁是synchronized它保证了互斥和可见性还对代码块做了内存屏障处理。另一个方向是java.util.concurrent.atomic包里的原子类它们是靠 CASCompare And Swap实现的无锁但并发很高时可能出现自旋消耗。更高并发场景还要考虑LongAdder它牺牲了一致性把热点分散到多个 cell 上适合统计场景。6.2 一致性问题的场景化理解从超卖说起我自己印象最深的一个案例是做一个秒杀库存系统。最开始代码很简单if (stock 0) { stock--; }在高并发下两个线程同时读到 stock1都走过了判断都执行了减一最后 stock 可能变成 0 而不是预期中的 0不对两个线程都把库存从 1 减到 0但实际是两次操作同时生效造成了逻辑判断重复执行最终结果是库存扣成负数或者被覆盖为错误值。真实场景里库存是数据库里的一个字段如果不用原子化的 UPDATE比如UPDATE stock SET count count - 1 WHERE count 0或者不用分布式锁控制就必然出现超卖。这个案例说明一件事数据一致性问题出现在“检查-操作”之间存在时间窗口。解决思路要么是缩小窗口加锁、CAS要么是让检查成为原子操作的一部分数据库条件更新、乐观锁。正好搜索词里有一条是“java怎么保证数据一致性”这里我展开说几句。单机多线程场景有synchronized、ReentrantLock、Atomic系列可以用分布式场景就要引入分布式锁常见的有基于 Redis 的 SET NX EX 和基于数据库的乐观锁版本号。你应该先分清场景单 JVM 还是多 JVM如果是多 JVM本地锁再复杂也拦不住其他机器的线程。6.3 线程池与并发工具基础里的进阶题Java 并发这块java.util.concurrent包几乎是另一个世界内容非常多。对基础阶段的人我建议盯住三个点穷追猛打第一个是线程池。Executors 框架的newFixedThreadPool、newCachedThreadPool用起来方便但有一个大坑newFixedThreadPool底层用的是无界队列任务无限堆积时会导致 OOMnewCachedThreadPool则允许最大线程数到Integer.MAX_VALUE极端情况下也会 OOM。生产环境建议手动指定ThreadPoolExecutor的各个参数包括核心线程数、最大线程数、队列类型和容量、拒绝策略。这里参数不是随便填要结合业务的耗时和 QPS 估算比如 IO 密集型任务核心线程数可以设成 CPU 核数两倍左右计算密集型则接近 CPU 核数。这块知识不是“基础语法”层面但它确实是 Java 开发者的起点级必修。第二个是并发容器。前面已经提过 ConcurrentHashMap 替代 HashMap类库里还有 CopyOnWriteArrayList、BlockingQueue 系列。BlockingQueue 和线程池深度绑定是实现生产者-消费者模式的基础组件。面试时被问“线程池里怎么用队列”如果脑子里没有 BlockingQueue 的概念基本到这就卡住了。第三个是 JUC 几个同步工具CountDownLatch、Semaphore、CyclicBarrier。它们解决的问题各不相同CountDownLatch 是“多个线程都完成后主线程再往下走”Semaphore 是“限制同时访问的线程数量”CyclicBarrier 是“多个线程互相等待到齐后才开始执行”。每个都能找到实际场景哪怕只是用来控制测试并发度。7. 学习路径与面试“八股文”的正确打开方式最后想聊一点可能不算纯技术、但对学习 Java 基础的人来说很实用的话题怎么学、怎么查、怎么准备面试。我见过那种啃完一遍《Java 核心技术》两卷就开始刷面试题的人也见过只在 IDEA 里点运行、一开命令行就懵的人。这两种偏科都不太推荐。7.1 从“能跑”到“懂为什么跑”的三步走我的建议是把基础分成三个阶段每阶段有明确目标。第一阶段目标是把语法和环境用顺。变量、循环、数组、方法、类、对象、String 操作配合 IDE 能写出小型控制台程序。这个阶段不要深抠 JVM 原理遇到异常报错能看明白 stack trace 就行。第二阶段目标是建立内存模型和容器意识。开始接触集合框架、泛型、常用工具类并刻意练习读源码尤其是ArrayList、HashMap的源码。同时可以开始自己做文件读写、异常处理的练习学习怎么写出可复用的方法结构。第三阶段目标是理解并发和设计。理解线程怎么创建、怎么同步、怎么用线程池然后横向对比几种锁和原子类理解接口和抽象类的区别尝试用组合替代继承看一些基本的设计原则在实战里尝试重构一段旧代码。三个阶段没法完全割裂但顺序别乱。没建立内存意识就去硬啃 JVM 调优效果很差没写过几个控制台程序就去看 Spring学到的全是空中楼阁的配置。7.2 “八股文”怎么背才有用网上的 Java 面试题、八股文漫天飞很多人拿到手就开始背背完第二天就忘。我的建议是把每一个面试题当成一条“源码阅读线索”。比如看到“HashMap 为什么线程不安全”你最优的做法不是背三段结论而是去打开源码找到 put 方法里“没有锁”这个事实再看并发下可能的覆盖场景最后自己总结结论。这一套流程下来知识点长在你脑子里而不是只存在笔记里。当然不是说结论完全不重要面试表达也需要有层次感。我一般建议的回答结构是先说结论再给原理最后给一个场景佐证。比如“HashMap 线程不安全因为在 put 时没有同步机制并发场景可能发生数据覆盖JDK 1.7 的扩容还可能形成环形链表所以并发应该用 ConcurrentHashMap。”这一句话里既有结论、有原理、有场景也有不同版本的对比比干巴巴背“HashMap 不安全”好得多。7.3 给新人后来人的几句实在话如果让我只给三条建议我会说第一条多用命令行别把 IDE 当掩体。不要求你脱离 IDE 开发但至少要会手动编译运行一个带包的类会看异常栈。第二条看源码不是看天书先把核心类的结构摸清。比如打开HashMap你有不有第一时间找到它的字段理解 table 是干什么的看到内部类 Node 的定义。能做到这一步很多源码类都能自己看下去。第三条遇到不确定性一定要查证不要凭感觉下结论。写 Java 的人最忌讳“我在别的地方看到过类似写法应该差不多吧”。基础阶段的任何一个疑问都建议用一分钟在官方文档或源码里验证一下长期积累下来的准确率会非常恐怖。我到现在写代码遇到集合选择、异常策略、并发方案还是会先回到这些基础问题上过一遍。基础的收益就是这样它不会让你第一天就写出惊艳的代码但能保证你在第五年的时候不写出让人崩溃的代码。