
我不太确定你指的是哪方面的“整理语言后重新输出”。如果你是想让我基于“JavaSE大总结礼包”这个标题按前面设定的博主身份和格式要求生成一篇完整博文那我直接开工。如果你是指其他内容需要调整也麻烦你说明一下具体方向。先按项目要求输出这篇JavaSE总结博文。1. 为什么JavaSE值得做一次系统性大总结直接说结论JavaSE是Java所有技术栈的地基无论是SSM、Spring Boot还是微服务底层都跑在JavaSE的机制上。我接触过不少工作两三年的开发写业务代码很溜但一问HashMap的扩容机制、JVM内存模型、线程池的核心参数就开始含糊。原因很简单——当初学JavaSE的时候是零散记忆没有形成体系后来也一直没回头补课。这篇总结的定位是“礼包”意思不是罗列API文档而是把JavaSE里那些面试高频、实战常用、容易踩坑的知识点按体系重新过一遍。适合三类人准备校招或跳槽面试的Java开发、学完JavaSE但感觉知识不成体系的初学者、以及想快速回顾核心知识的在职人员。我在整理这套内容时尽量把每个知识点都落到“为什么”上比如为什么要重写equals就一定要重写hashCode、为什么HashMap的默认负载因子是0.75、为什么线程池的队列要分有界无界。理解了这些背后的逻辑比死记硬背强得多。2. JavaSE知识体系整体拆解从语法到原理的分层结构很多人学JavaSE的误区是把语法、集合、JVM、并发当成独立的知识点来背结果就是越学越乱。实际上JavaSE是一个分层体系每一层都建立在前一层之上。2.1 基础语法层语言特性的底层逻辑基础语法不只是if/else和for循环。这里最关键的是要理解Java作为一门“面向对象语言”的设计初衷。比如“一切皆对象”那为什么还有int、double这些基本类型因为性能。对象要存储在堆上访问需要通过引用而基本类型直接存储在栈上访问速度更快。JVM对基本类型的处理是做了性能妥协的——用空间换时间。还有一个经常被忽略的点是自动装箱与拆箱。我见过好多人在循环里用Integer做累加导致频繁装箱拆箱性能下降明显。正确的做法是能用基本类型就用基本类型集合里必须用包装类时再包装。2.2 面向对象层封装、继承、多态的真正含义封装的意义不只是private修饰符而是“隐藏实现细节暴露稳定接口”。这在团队协作里尤其重要——别人只需要知道你提供的方法怎么用不需要知道内部怎么实现的。我在实际项目中见过最典型的封装问题一个类里所有字段都直接public到处被修改最后出了问题根本没法排查。这不算封装这给自己埋雷。继承的核心是“is-a”关系子类确实是父类的一种。但继承也带来了耦合所以Effective Java里有一条建议优先使用组合而非继承。比如一个鸭子类需要飞行能力不一定要继承一个飞鸟类而可以组合一个飞行行为接口。多态是Java面向对象中最难理解也最重要的概念。它的本质是“父类引用指向子类对象调用方法时动态绑定到子类实现”。实现多态有三个必要条件继承、重写、父类引用指向子类对象。很多初学者分不清重载和重写——重载是同一个类中方法名相同参数列表不同编译期决定重写是子类重新实现父类的方法运行期决定。2.3 核心类库层String、包装类、日期时间String是JavaSE里最特殊的类特殊到面试官总爱问。String为什么设计成不可变我个人的理解是不可变性带来了线程安全、字符串常量池复用、以及hashCode缓存这几个好处。比如String被多个线程共享时因为不可变天然就是线程安全的。String、StringBuilder、StringBuffer这三者的区别是高频考点。简单说String不可变适合少量字符串操作StringBuilder线程不安全但性能好适合单线程大量拼接StringBuffer线程安全但性能稍差因为方法加了synchronized。实际开发中循环拼接字符串要用StringBuilder这是基本功。日期时间API也要重点看。Java 8之前的Date和Calendar设计得很难用比如月份从0开始而Java 8的LocalDate、LocalTime、LocalDateTime解决了这些问题。LocalDateTime.now()、plusDays、minusMonths这些方法都特别直观。我当时从Date迁移到LocalDateTime后日期代码几乎没再出过Bug。2.4 进阶机制层异常、泛型、反射、注解异常体系的核心是Throwable下面分Error和Exception。Error是JVM层面的严重错误比如OutOfMemoryError程序无法处理Exception是程序可以处理的异常分受检异常编译期强制处理和运行时异常RuntimeException。一个实战经验异常不要用来控制业务流程。有些人在做参数校验时如果参数不合法就抛异常然后在外层捕获。这样做的代价是异常对象创建需要记录堆栈性能开销很大而且代码逻辑混乱。正确的做法是参数校验用if-else返回错误码或布尔值真正的异常才用异常处理。泛型的核心是类型擦除。Java的泛型是编译期泛型运行时泛型信息会被擦除。所以List 和List 在运行时其实是同一个Class。理解这一点就能明白为什么泛型T不能直接new也不能用T.class获取Class对象。反射是通过Class对象获取类的信息并操作类的成员。Spring IOC的核心就是反射工厂模式。反射的缺点也明显性能差因为需要动态解析字节码破坏了封装性可以绕过private访问限制。所以在实际项目中能用正常方式调用就不要用反射反射只用在框架底层。注解的本质是“标记信息”它本身不做事需要解析器来处理。比如Override只是给编译器看的而Spring的Component注解需要Spring容器去扫描解析。3. 集合框架深层解析高频考点与源码级理解集合是JavaSE里知识点最密集、面试最常考的一块。很多人在这个部分靠背题应付但面试官稍微深挖一下就露馅了。真正要理解的是底层数据结构到底是什么以及为什么这样设计。3.1 Collection体系List、Set、Queue的设计差异List是一个有序可重复的集合Set是无序不可重复的集合Queue是队列先进先出。但这里有几个细节值得注意。ArrayList底层是Object数组查询快、增删慢首尾除外。扩容机制是个高频考点默认容量是10扩容时新容量是旧容量的1.5倍具体代码是oldCapacity (oldCapacity 1)。为什么要1.5倍而不是2倍如果是2倍扩容后数组可能很大但实际用不上浪费内存如果是1.5倍兼顾了扩容次数和空间利用率。LinkedList底层是双向链表增删快、查询慢。但要注意LinkedList的“增删快”说的是在链表中间插入删除因为只需要改前后节点的指针而ArrayList在中间插入需要移动元素。但如果是在末尾追加ArrayList反而比LinkedList快因为ArrayList在末尾追加是直接赋值而LinkedList还需要new节点。HashSet的底层就是HashMap只是把所有value都指向了一个固定的Object对象PRESENT。所以理解了HashMapHashSet就理解了。3.2 HashMap源码级解析存储结构、put过程、扩容机制HashMap是JavaSE集合框架里最重要的类没有之一。我建议每个Java开发者都至少读一遍HashMap的源码读完之后很多问题就豁然开朗了。HashMap的存储结构是数组链表红黑树。数组的每个位置叫桶bucket当多个key的哈希值映射到同一个桶时用链表存储当链表长度超过8且数组长度大于64时链表转红黑树。put过程是这样的先对key计算hash值然后通过(n-1)hash计算桶的位置n是数组长度这个操作等价于hash对n取模但是位运算更快如果桶为空直接放进去如果不为空则遍历链表或树找到key相同的节点替换value找不到则在尾部追加新节点。为什么默认负载因子是0.75这是时间空间的一个折中。负载因子太大比如1空间利用率高但哈希碰撞概率增大链表会变长查询效率降低负载因子太小比如0.5哈希碰撞少但数组频繁扩容空间浪费。0.75是一个统计学上比较合理的值。HashMap的线程安全问题一定要重视。在多线程环境下JDK 7的HashMap在扩容时可能产生死循环虽然JDK 8修复了这个问题但多线程下put仍然可能丢失数据。所以多线程场景要用ConcurrentHashMap。3.3 Map体系对比HashMap、LinkedHashMap、TreeMap、Hashtable这四者的适用场景完全不同HashMap无序存取值效率最高日常开发首选。LinkedHashMap维护了插入顺序或访问顺序适合做LRU缓存。TreeMap按键的自然顺序或自定义Comparator排序适合需要有序遍历的场景。Hashtable线程安全但性能差整个map加锁基本被ConcurrentHashMap取代。有一个细节我面试时经常问候选人LinkedHashMap实现LRU的思路是什么答案是通过重写removeEldestEntry方法当size超过容量时返回true这样每次访问元素后该元素会被移到链表末尾最久未访问的元素就在链表头部会被移除。3.4 集合框架里的线程安全类与并发容器JavaSE里还提供了一些线程安全的集合比如Vector和Collections.synchronizedList。这些类的方法都加了synchronized但性能很差因为锁的粒度太大。而并发包下的CopyOnWriteArrayList采用了写时复制策略——写操作时复制一个新数组在新数组上修改修改完再替换原数组引用。这个策略适合读多写少的场景比如监听器列表。ConcurrentHashMap在JDK 8里的实现是数组链表红黑树使用CASsynchronized保证线程安全锁粒度细化到每个桶并发度更高。这才是并发场景下的首选。4. JVM内存模型与垃圾回收JavaSE的底层机制JVM可以说是JavaSE进阶的核心也是大厂面试的必考项。理解和掌握JVM对排查线上问题、优化应用性能特别有帮助。4.1 运行时数据区域划分JVM的内存分为五个区域程序计数器、虚拟机栈、本地方法栈、堆、方法区。其中虚拟机栈、本地方法栈、程序计数器是线程私有的随线程生灭堆和方法区是线程共享的。堆是Java对象存储的主要区域也是GC的重点照顾对象。堆按代划分年轻代Eden区两个Survivor区比例8:1:1老年代。对象优先在Eden区分配Eden区满了触发Minor GC存活的对象进入Survivor区经过多次GC后仍然存活的对象进入老年代。虚拟机栈是每个线程私有的每调用一个方法就会创建一个栈帧栈帧里存放局部变量表、操作数栈、动态链接、方法出口。栈深度不够就会抛出StackOverflowError这个我在写递归代码时遇到过——没有设置出口条件的递归栈空间很快被耗尽。方法区在JDK 8之后改名为元空间Metaspace存储类信息、常量、静态变量等。有一个变化值得注意JDK 8之前方法区在永久代里永久代是有大小上限的容易OOMJDK 8之后元空间使用直接内存默认不受启动参数限制但这样也可能因为使用无限制导致物理内存耗尽所以还是建议设置-XX:MaxMetaspaceSize来做兜底。4.2 判断对象已死的算法垃圾回收的前提是判断哪些对象需要回收。主要有两种算法引用计数法和可达性分析算法。引用计数法的思路是给对象维护一个引用计数器每增加一个引用就加1释放一个引用就减1计数器为0时说明对象可回收。这个方法实现简单但存在循环引用问题A持有B的引用B持有A的引用就算外部已经没有人再引用A和B它们的引用计数也不是0永远不会被回收。所以主流的JVM用的是可达性分析算法。可达性分析的核心是从GC Roots出发向下搜索搜索走过的路径叫引用链当一个对象不在任何引用链上时说明对象不可达可以被回收。GC Roots包括虚拟机栈中引用的对象、方法区中类的静态属性引用的对象、方法区中常量引用的对象、JNI即Java Native Interface引用的对象。这个方法能解决循环引用问题因为A和B即使互相引用只要到GC Roots没有引用链照样会被标记回收。4.3 JVM常见垃圾回收器与算法垃圾回收算法主要有四种标记-清除、标记-复制、标记-整理、分代收集。标记-清除算法分两步先标记需要回收的对象再统一回收。缺点有两个一是效率不高二是回收后会产生大量内存碎片。内存碎片多会导致以后分配大对象时找不到足够连续内存而触发Full GC。标记-复制算法把内存分成大小相等的两块每次只使用其中一块GC时把存活对象复制到另一块然后清理当前块。这种算法没有碎片问题但内存利用率只有50%。HotSpot的年轻代GC用的就是这种算法但比例是8:1:1这样内存利用率达到90%。标记-整理算法与标记-清除类似但标记后不是直接清除而是把所有存活对象向一端移动直接清理掉端边界以外的内存。这种算法适合老年代且没有内存碎片。在垃圾回收器的选择上JDK 8默认的是Parallel Scavenge年轻代 Parallel Old老年代JDK 9之后默认的是G1收集器。G1把堆划分成多个大小相等的Region可以设定停顿时间目标-XX:MaxGCPauseMillis让GC时间可控。我从JDK 8切到JDK 11之后明显感觉到GC停顿变少了对响应时间敏感的服务来说这个改善是实打实的。4.4 类加载机制加载、连接、初始化类加载机制也是JavaSE的核心考点。JVM加载一个类分为三个阶段加载、连接验证、准备、解析、初始化。加载阶段是把类的二进制字节流读入内存生成Class对象。连接阶段中验证是检查字节流格式是否正确准备是为类的静态变量分配内存并设置默认值解析是把常量池里的符号引用替换为直接引用。初始化阶段是执行类的初始化方法给静态变量赋真实的初始值。类加载器有启动类加载器Bootstrap ClassLoader、扩展类加载器Extension ClassLoader、应用类加载器Application ClassLoader它们形成了父委派模式。双亲委派模型的意思是一个类加载器收到加载请求时先请求父类加载器加载父类处理不了再由自己加载。这样做的主要原因是安全性防止核心API被篡改比如你自己写一个java.lang.String类也会由启动类加载器加载标准的String而不是加载你写的那个。常驻内容要记住Class.forName和ClassLoader.loadClass的区别在于前者执行类的初始化所以用Class.forName加载MySQL驱动没问题后者不执行初始化只完成加载。5. 多线程与并发从Thread到JUC的完整脉络多线程是JavaSE中让很多人望而却步的一部分。我的建议是不要一上来就背JUC包里的类而是先把Thread、synchronized、volatile这几个最基础的东西弄明白再去看Lock、Atomic、线程池就顺了。5.1 线程创建方式与生命周期状态创建线程有四种常见方式继承Thread类、实现Runnable接口、实现Callable接口配合FutureTask、使用线程池。前两种没有返回值Callable可以拿到返回值并抛出异常。这里有一个高频面试题实现Runnable和继承Thread哪个更好答案是实现Runnable更好因为Java是单继承的如果继承了Thread就不能继承其他类了而且Runnable把任务和线程分离开更符合面向对象的设计。线程的生命周期状态包括新建NEW、就绪RUNNABLE、阻塞BLOCKED、等待WAIT、超时等待TIMED_WAIT、终止TERMINATED。这里要注意区分阻塞和等待阻塞是等待获取锁等待是主动调用了wait或join等方法。5.2 synchronized的三种应用方式及锁升级synchronized可以修饰实例方法锁当前实例、静态方法锁当前类的Class对象、代码块锁指定的对象。本质上都是获取一个监视器锁。我见到不少人在代码里这样写private Object lock new Object()然后synchronized(lock)用来锁代码块。这种方式可以降低锁的粒度控制锁的持有时间。JDK 6之后synchronized有个锁升级机制无锁 → 偏向锁 → 轻量级锁 → 重量级锁。锁可以升级但不能降级。在只有一个线程访问同步代码时偏向锁的开销几乎为零。竞争增多时偏向锁升级为轻量级锁使用CAS来获取锁。如果自旋多次失败就升级为重量级锁进入阻塞状态。简单理解JVM做了很多优化让“无竞争加锁”和“轻度竞争加锁”的开销降到了很低。5.3 volatile与JMM可见性和有序性问题volatile是JavaSE面试中必问的关键字。它有两个核心作用保证可见性和禁止指令重排序。先看可见性。每个线程都有自己独立的工作内存变量从主内存读取后先存到工作内存里。当一个线程修改了变量的值其他线程不一定马上能看到因为它们读的是自己工作内存里的副本。volatile修饰的变量写操作会直接写入主内存读取操作会直接从主内存读取从而保证可见性。再看JMMJava内存模型的happens-before原则。这里有一个关键规则对一个volatile变量的写操作happens-before后续对这个变量的读操作。也就是说如果线程A写了一个volatile变量线程B读到这个变量的新值那么线程A在写之前的所有操作对线程B都是可见的。禁止指令重排的经典场景是双重检查锁单例模式DCL。代码如下public class Singleton { ... }其中instance变量必须用volatile修饰否则在new Singleton()这一步会发生指令重排。new一个对象其实是三步分配内存、初始化对象、把引用赋值给变量。如果不加volatileJVM可能把2和3的顺序调换导致另一个线程判断instance不为null拿到一个还没有初始化完成的对象。5.4 线程池参数、执行流程、拒绝策略线程池是JavaSE里最实用的并发工具但在实际项目中我看到不少误用。最典型的错误是Executors.newFixedThreadPool()创建了一个无界队列的线程池在任务量大的情况下任务全堆在队列里占用大量内存甚至OOM。这个情况严重到阿里Java开发规范里明确禁止Executors创建线程池。线程池的核心参数有七个corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime空闲线程存活时间、unit时间单位、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。执行流程是当提交一个任务时如果当前线程数小于corePoolSize创建核心线程执行任务如果大于等于corePoolSize优先把任务放入队列等待如果队列满了且当前线程数小于maximumPoolSize创建非核心线程执行任务如果线程数达到maximumPoolSize且队列也满了执行拒绝策略。拒绝策略有四种AbortPolicy抛异常、CallerRunsPolicy由提交任务的线程自己执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃队列最旧的任务。实际项目中如果是不可丢失的任务我会实现自定义拒绝策略将任务保存到日志或重新投递到消息队列保证最终不丢。5.5 悲观锁与乐观锁synchronized和CAS悲观锁假设并发冲突一定会发生所以先加锁再操作synchronized就是这种思路。乐观锁假设并发冲突很少发生操作时不加锁而是在更新时检查数据有没有被其他线程改过CAS比较并交换就是乐观锁的实现。CAS有三个参数内存地址、期望值、新值。只有当内存中的值和期望值相等时才把新值写入否则就说明其他线程已经修改过了需要重试。CAS的优点是性能好没有锁竞争的开销缺点是会出现ABA问题——线程A读到值为A这时线程B改为B又改回A线程A再CAS时还是会成功但它以为整个过程没人改过。ABA问题的解决方式是加版本号AtomicStampedReference就是用版本号来解决ABA问题的。另外CAS在竞争激烈时循环重试的开销会很大所以并发量很高时反而可能不如synchronized。6. IO、异常处理、泛型实战那些面试与实际中高频的小而精知识点6.1 IO体系从BIO到NIO再到AIO的演进逻辑Java的IO体系看着类很多实际上是有规律的。输入流、输出流、字节流、字符流这四类组合起来就是IO的基本骨架。InputStream/OutputStream处理字节Reader/Writer处理字符。处理文本文件用字符流处理图片、音频等二进制文件用字节流这是选型的基本原则。BIO是阻塞IO一个线程处理一个连接。如果连接数很多就要创建大量线程线程上下文切换开销巨大。所以后来有了NIO采用多路复用机制一个线程可以处理多个连接核心是Selector和ChannelNetty就是基于NIO封装的框架。理解NIO的模型再去看Netty会容易很多。IO流使用完一定要关闭这里有个使用细节我一直记着关闭流的顺序应该先关闭外层流再关闭内层流但其实只要关闭最外层流即可内层流会被自动关闭。使用try-with-resourcestry (FileInputStream in new FileInputStream(...))是更安全的写法因为它能自动关闭资源。6.2 运行时异常与受检异常的最佳实践异常处理这块我总结了几条实战经验都是踩过坑之后得出来的第一捕获异常时不要用catch (Exception e)要精确到具体异常类型。比如捕获IOException、SQLException等这样能精确定位到问题类型而且不会把不该处理的异常也吃掉。第二捕获异常后不要不做任何处理就丢着。e.printStackTrace()在本地调试阶段没问题但生产环境没人去看控制台应该打印到日志文件同时把上下文参数也打出来方便排查。第三在finally里return是绝对错误的做法。如果finally里有return语句会覆盖try或catch里的return而且会掩盖异常这属于严重的编码错误。第四自定义异常时继承RuntimeException比继承Exception更好。因为继承Exception的受检异常如果在方法里抛出方法签名上必须声明throws导致大量异常的向上传递代码看起来很冗余。继承RuntimeException就不会强制要求调用者处理但一定要在合适的地方捕获处理不能任它一路向上抛。6.3 泛型通配符、类型擦除、桥方法泛型面试最常见的问题是List和List 是同一个类型吗答案是不是因为泛型有不可变性。但List? extends Object可以是List 这就是通配符的作用。? extends T表示是T或T的子类支持读取操作也就是生产者? super T表示是T或T的父类支持写入操作也就是消费者。这个原则在PECS即Producer ExtendsConsumer Super里可以看到完整的表述。关于类型擦除还有一个桥方法的概念。比如父类定义了List foo()子类重写为List foo()编译后实际类型都是List为了让重写生效编译器会生成一个桥方法。这个细节通常在面试中不会单独问但阅读框架源码时经常看到这类现象。6.4 反射机制在框架中的应用落地反射在普通业务代码中很少直接用但它是Spring、MyBatis这类框架的核心。Spring通过反射创建bean实例、注入依赖MyBatis通过反射把查询结果映射到实体类。理解反射是在看框架源码时必备的基础。反射常用到的API包括Class.forName()获取Class对象、getDeclaredMethods()获取所有方法、getDeclaredFields()获取所有字段、method.invoke(obj, args)调用方法。如果需要访问private成员要调用setAccessible(true)放开权限但代价是破坏了封装。有一个性能优化的小经验使用反射调用方法比直接调用慢一个数量级这是正常的因为反射需要动态解析类信息。如果某个反射调用是在循环里执行大量次建议建一个缓存把Method对象缓存起来避免每次重新获取。7. 高频面试题与避坑指南从实战中提炼的JavaSE要点7.1 经典八大问快速自查我发现这些面试题能快速检测一个Javase基础是否扎实你可以对着自查第一问和equals的区别。比较的是栈中的值。基本类型比较的是值引用类型比较的是内存地址。equals默认也是比较内存地址但String重写过equals比较的是字符内容。这就是为什么new String(a) new String(a)是false但equals是true。第二问String、StringBuilder、StringBuffer的区别。字符串拼接场景用StringBuilder线程安全场景用StringBuffer少量字符串操作用String。另外要记住字符串常量池通过字面量创建的字符串会直接进常量池而new出来的字符串在堆上。第三问HashMap的put过程。上面已经详细讲过重点是把扩容条件size thresholdthreshold capacity * loadFactor、链表转红的条件说出来。第四问ArrayList和LinkedList的区别。从底层数据结构、时间复杂度、内存占用、适用场景几个方面答一般就过关了。第五问什么是反射有什么应用动态获取类信息并操作类成员的能力应用在Spring IOC、MyBatis映射、JDBC加载驱动等。第六问多线程有几种实现方式四种方式并说清楚有无返回值、是否推荐继承Thread。第七问synchronized和lock的区别。可以从锁的获取释放方式、是否可中断、公平非公平、读写锁等角度展开。第八问线程池的核心参数。七个参数说全然后补充执行流程。7.2 现场演示经典易错题深度拆解我挑一个最经典的题来演示怎么拆为什么重写equals必须重写hashCode。原因是为了保证hashCode契约如果两个对象equals相等它们的hashCode必须相等如果两个对象hashCode相等equals不一定相等。HashMap判断key是否相同时先比较hashCodehashCode不同的key直接不去equals所以如果两个equals相等的对象hashCode不同它们在HashMap中会被当成两个key存进去这就有问题了。我在一个Demo里试过写一个Person类只重写equals不重写hashCode然后把这个对象放入HashSet再做contains判断结果返回false。这就说明equals和hashCode必须成对重写。具体实现中hashCode的计算要能把关键字段包含进去用Objects.hash(name, age)就可以。7.3 面试中容易被追问到底的“连环坑”有些知识点面试官会一层层往下问直到你答不上来为止。这些都是重要的“连环坑”坑一HashMap的不安全点。想一下多线程同时put导致数据覆盖的细节主要是可能两个线程同时发现同一个位置为空同时写入后写的覆盖先写的。坑二线程池的核心线程能不能被回收。默认不能即使空闲也不会被回收。如果设置了allowCoreThreadTimeOut(true)核心线程超过keepAliveTime也会被回收。坑三JVM年轻代为什么要分Eden和两个Survivor。是为了避免内存碎片。复制算法把内存分成三块利用率达到90%对象从Eden进Survivor时是连续存放的没有碎片。坑四volatile能不能保证原子性。不能。比如多个线程同时对volatile变量做count操作最终结果仍然不对。因为count不是原子操作包含读、加、写三步volatile只保证读和写的可见性不保证复合操作的一致性。坑五ClassNotFoundException和NoClassDefFoundError的区别。前者通常是类路径配置有问题运行时找不到类后者是类在编译时存在、运行时加载失败比如类的静态初始化抛异常。7.4 亲历的线上事故一次系统排查回顾我记忆最深的一次事故是某线上服务频繁Full GCCPU飙高接口超时。开始怀疑是代码循环里new了大量对象排查后发现是某同事写了一个方法在循环里不断做字符串拼接导致创建了大量String对象而且这些对象很快就成了垃圾给GC造成巨大压力。修复很简单把改成StringBuilder.appendFull GC的次数立刻降下来了。还有一次是线程池使用不当导致线上OOM。当时的场景是使用了newFixedThreadPool它使用的是无界队列LinkedBlockingQueue任务生产速度远大于消费速度队列里堆了几百万个任务直接把堆内存打爆。这个事故之后我在团队里强制规定创建线程池必须使用ThreadPoolExecutor并显式指定有界队列和拒绝策略不允许用Executors快捷方法。7.5 自我查漏补缺了一个JavaSE自查清单根据我自己的复习经验整理了一个核心自查清单你可以按这个顺序逐项过[ ] 基本类型与引用类型的区别、自动装箱拆箱[ ] String常量池、StringBuilder/StringBuffer选择[ ] 面向对象三大特性的实际应用场景[ ] 重载与重写的区别、动态绑定原理[ ] 与equals、hashCode与equals的关系[ ] ArrayList扩容、LinkedList链表操作、HashMap扩容与put流程[ ] 异常体系、受检与非受检异常的选择[ ] 泛型类型擦除、通配符PECS[ ] 反射API的基本调用[ ] JVM内存区域、GC Roots、垃圾回收算法[ ] 类加载过程、双亲委派模型[ ] synchronized锁升级、volatile可见性、CAS与ABA[ ] 线程池参数与拒绝策略、线程状态转移[ ] IO流的选型、try-with-resources每一项如果能不看文档就讲清楚“是什么、为什么、怎么用”说明JavaSE这一关基本过了。8. 大总结学习JavaSE的正确姿势与资源推荐8.1 知识框架搭建的方法我在带新人时发现很多人学JavaSE最大的问题不是不努力而是没有框架。这里分享一个实用的方法画一张JavaSE知识地图。中间写JavaSE往外延伸出语法、面向对象、集合、IO、异常、泛型、反射、JVM、多线程、新特性Lambda和Stream这几个分支然后每个分支再展开到具体的类和关键知识点。画完地图之后做两件事第一每学一个知识点就在地图上标记“掌握”能讲给别人听还是“了解”知道概念不要欺骗自己掌握的标准是能独立写一段示例代码并说明原理。第二学完一个分支后找两道相关的面试题自测答不出来就回到知识点重新看。这样做比闷头刷题效率高很多。8.2 实战驱动的学习方法对于JavaSE学习我个人的体会是看书是必要的但真正记牢靠要靠实战。这里有个经验是“用面试题驱动学习”比如今天学习HashMap就打开IDE自己写几段代码验证扩容时机打个断点看put过程比读十遍源码记得牢。动手写代码还有一个好处能发现很多“我以为理解了但其实没理解”的地方。有一次我在看synchronized的锁升级机制时觉得看懂了但一写代码发现连“锁的是对象还是类”都没搞明白。动手验证一下永久记住。建议做一个小工具项目练手比如一个简单的文件批量重命名工具用上IO流、集合、异常处理、多线程多文件并行处理、Lambda和Stream。一个项目把所有JavaSE知识点串起来比你一个个孤立地学要高效得多。8.3 值得一提的经典学习资料最后一个部分是资源推荐。书的话入门看《Head First Java》或者《Java核心技术卷I》进阶看《Effective Java》和《深入理解Java虚拟机》。视频课的话各家培训机构的JavaSE课程差别不大关键是跟一套学完不要今天看这家明天看那家。源码阅读建议从这三个类开始ArrayList、HashMap、ThreadPoolExecutor。这三个类的源码量适中结构清晰看完之后对Java集合和并发的理解会上一个台阶。读源码的时候不用每个细节都抠抓住核心流程比如put、get、扩容就好。我最后再分享一个实用技巧整理一套自己的JavaSE笔记不用追求全面只记“自己容易忘的知识点”和“面试被问倒的题目”。考试和面试前翻自己的笔记效果远好于翻别人的总结。其实这就是我们常说的大总结礼包的真正价值所在——它可以是你学习的起点但最终你要把它变成属于你自己的东西。我在整理这套笔记时才真正感觉到JavaSE的知识体系大局在脑海里建立起来了希望这份总结也能给你同样的帮助。