简介:本资源是一份面向Java初学者与备考者的Core Java核心知识点精要笔记,聚焦语言基础与关键机制,助力快速构建扎实的编程底层认知。资料以单个PDF文件形式呈现(1.64MB),内容结构清晰、模块完整,涵盖原码/反码/补码原理、char类型特性、移位与位运算符细节、switch与break/continue流程控制逻辑、构造方法与重载机制、this与super关键字用法、继承与多态实现、static/final/abstract等修饰符语义,以及instanceof类型判断等高频考点。目录层级分明,从二进制表示到面向对象核心概念逐层递进,每节均含要点提炼与典型说明,适合作为课堂笔记补充、面试前速查或自学知识图谱梳理。目前已有473人学习下载,内容紧扣Java SE基础体系,对夯实编码基本功、理解JVM底层逻辑及应对技术笔试具有明确支撑价值。1. 这份 CoreJava 笔记不是“速成宝典”,而是你调试 NullPointerException 时翻得最勤的那页纸你有没有过这种经历:线上服务突然抛出ConcurrentModificationException,堆栈里只有一行ArrayList$Itr.checkForComodification(ArrayList.java:901),而你手边连个能快速定位modCount和expectedModCount关系的参考资料都没有?或者在写泛型工具类时,对着? super T和? extends T发呆半小时,最后靠试错加注释硬凑出能编译的代码?这份《CoreJava全套详细笔记资料.pdf》不是那种把 API 文档复制粘贴再加点颜色的“PPT式笔记”,它是一份从 JVM 字节码层面反推语法设计、用真实调试断点截图佐证原理、甚至把String对象池里intern()调用前后内存地址变化都标出来的实战手札。它覆盖了从原码/补码底层表示(第 5 页)到ObjectInputStream反序列化安全校验(第 46 页)、从static初始化顺序陷阱(第 11 页)到ThreadLocal内存泄漏根因(第 55 页)的完整链条。适合两类人:一是刚脱离“Hello World”阶段、正被final修饰符作用域和static块执行时机搞晕的新手;二是需要快速回溯某个冷门机制(比如ClassLoader.loadClass()的双亲委派中断条件)的老手。它不教你“怎么写 Spring Boot”,但当你写的自定义 ClassLoader 在热部署时莫名其妙加载失败,翻到第 12 页“类加载的顺序”那一栏,你会立刻明白问题出在哪——这才是它不可替代的价值。2. 从二进制补码到集合线程安全:这份笔记如何用“可验证”的方式讲透 Java 核心机制2.1 原码/反码/补码:为什么Integer.MIN_VALUE == -Integer.MIN_VALUE是合法的?很多教程把补码讲成“符号位不变,其余位取反再加一”,这没错,但没解释清楚为什么这样设计。这份笔记在第 5 页直接给出 8 位二进制示例:-1 的原码:1000 0001 -1 的反码:1111 1110 (符号位 1 不变,其余位取反) -1 的补码:1111 1111 (反码 + 1) -128 的原码:1000 0000 -128 的反码:1111 1111 -128 的补码:0000 0000 (溢出!但这是设计使然)关键点在于:补码让加减法电路可以统一用加法器实现。笔记用(-1) + 1验证:1111 1111 + 0000 0001 = 0000 0000(溢出位丢弃),结果为 0,完全正确。而Integer.MIN_VALUE(即-128在 byte 范围)的补码1000 0000取负时,按规则计算:1000 0000 → 0111 1111 → 1000 0000,结果还是自己——这不是 bug,是补码系统对称性的必然结果。笔记特别标注:“JVM 规范明确要求整数溢出不抛异常,此行为是Math.abs()在Integer.MIN_VALUE上返回负值的根源”。你在写数值校验逻辑时,如果漏掉这个边界,就会埋下静默错误。提示:实际开发中,不要依赖Math.abs(x)处理可能为MIN_VALUE的输入。安全写法是x == Integer.MIN_VALUE ? Integer.MAX_VALUE : Math.abs(x),或直接用BigInteger.abs()。2.2 char 类型与 Unicode:为什么char c = '\u0000';是合法的,但c = -1;编译报错?第 6 页用一张表格厘清char的本质:属性值说明存储大小2 字节(16 bit)固定长度,非 UTF-8 变长数值范围0到65535(即0x0000到0xFFFF)无符号整数,不是 -32768 ~ 32767字面量表示'a','\u0041','\u20AC'(欧元符号)单引号包裹,支持 Unicode 转义隐式转换可自动转为int,long,float,double因其本质是无符号 16 位整数重点来了:char c = -1;报错不是因为“字符不能为负”,而是因为-1是int类型字面量(32 位有符号),其值-1超出char的0~65535范围。而'\uFFFF'是合法的,因为\uFFFF表示的是数值65535,在范围内。笔记在旁边手写批注:“char是无符号整数类型,不是‘字符容器’——这是理解String底层char[]和codePointAt()差异的起点”。2.3 移位与位运算:>>和>>>在处理网络字节序时为何必须严格区分?第 6 页的移位运算符部分,没有停留在“>>算术右移,>>>逻辑右移”的定义,而是给出一个 TCP/IP 协议解析的真实场景:// 假设收到 4 字节数据:0xFF, 0x80, 0x00, 0x00(大端序) // 解析为有符号 int(需保持符号位) int signedValue = (bytes[0] << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); // 此时 signedValue = -8388608(正确) // 但如果误用 >>>: int wrong = (bytes[0] << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); // bytes[0] 是 0xFF(255),左移 24 位后变成 0xFF000000, // 若用 >> 会符号扩展,但此处是位或,关键在 bytes[0] 本身是 int! // 所以必须先 & 0xFF 再移位,否则高位被符号位污染。笔记强调:byte到int的提升是符号扩展。byte b = (byte)0xFF; int i = b;结果i是-1(0xFFFFFFFF),不是255(0x000000FF)。因此网络字节解析必须写b & 0xFF强制转为无符号值。这个细节在 Netty 或 MINA 框架源码里高频出现,笔记在第 6 页用红框标出:“所有byte参与位运算前,必先& 0xFF”。2.4 集合框架的线程安全真相:Vector的synchronized方法为何在高并发下反而更慢?第 21 页对比ArrayList/Vector/Collections.synchronizedList()时,笔记没停留在“Vector是线程安全的”这种模糊说法,而是画出方法调用链:// Vector.add() 源码逻辑(简化) public synchronized boolean add(E e) { ensureCapacityInternal(size + 1); // 同步块内 elementData[size++] = e; // 同步块内 return true; }问题在于:整个方法体被一把锁保护。当多个线程同时调用add()和get(int index)时,它们会互相阻塞——即使操作的是不同索引位置。笔记用一个实验数据佐证:在 100 线程并发向Vector添加 1000 个元素的测试中,平均耗时 128ms;而用CopyOnWriteArrayList(写时复制)仅需 42ms(读操作无锁)。但笔记也冷静指出:CopyOnWriteArrayList适用于“读多写少”场景,若写操作频繁,ArrayList+ReentrantLock分段锁才是更优解。它在第 21 页底部手绘了一个决策树:读操作占比 > 95%?→CopyOnWriteArrayList读写比例均衡?→Collections.synchronizedList(new ArrayList<>())(锁粒度比Vector更可控)写操作密集且需强一致性?→ConcurrentHashMap替代Map,或自定义分段锁List3. 从this到super:构造方法链与多态调用的“时间陷阱”3.1this()和super()必须是构造方法的第一条语句:为什么 JVM 要强制这个顺序?第 8 页和第 9 页用一个经典反例揭示本质:class Parent { String name; Parent(String name) { this.name = name; // ① System.out.println("Parent init: " + this.name); // ② } } class Child extends Parent { int age; Child(String name, int age) { // 错误!不能在这里调用 super() // super(name); // 编译错误:必须第一行 this.age = age; // ③ super(name); // 编译错误:super() 必须第一行 } }笔记解释:JVM 在对象创建时,内存分配和父类字段初始化必须在子类字段初始化之前完成。super()的作用是触发父类构造器,确保父类状态(如name)在子类使用前已就绪。如果允许this.age = age在super()之前,那么③执行时this对象的父类部分(name)还是未初始化的垃圾值,②中的this.name就会输出null或随机值。笔记在旁白写道:“这不是语法糖,是 JVM 对象模型的铁律——this指针只有在父类构造完成之后才真正‘活’过来”。3.2 多态中的instanceof与getClass():为什么obj instanceof String比obj.getClass() == String.class更安全?第 10 页对比这两个操作时,笔记给出一个String子类(假设存在)的思考实验:// 假设 Java 允许继承 String(实际 final,但用于说明原理) class MyString extends String { /* ... */ } MyString myStr = new MyString("hello"); System.out.println(myStr instanceof String); // true System.out.println(myStr.getClass() == String.class); // false!instanceof检查的是运行时类型是否为指定类型或其子类,而getClass()返回的是最具体的类对象。笔记强调:在框架开发中(如 JSON 序列化器),若用getClass() == String.class判断字符串类型,遇到MyString实例就会跳过字符串优化逻辑,导致性能下降。而instanceof天然支持继承体系。但笔记也提醒一个坑:instanceof对null安全(返回false),而getClass()对null会抛NullPointerException。所以安全写法是obj != null && obj instanceof String。3.3static块的执行时机:为什么在类加载时初始化单例比static final更灵活?第 11 页“类加载的顺序”中,笔记用一个延迟初始化单例的案例:class LazySingleton { private static LazySingleton instance; static { // ① static 块在类首次主动使用时执行(如调用 getInstance()) // ② 可以包含复杂逻辑:读配置、连数据库、检查权限 System.out.println("Static block executed"); instance = new LazySingleton(); } private LazySingleton() {} // 私有构造 public static LazySingleton getInstance() { return instance; // ③ 简单返回,无同步开销 } }对比public static final LazySingleton INSTANCE = new LazySingleton();:static final字段在类加载时立即初始化,无法加入条件判断;static块可以写if (Config.isProd()) { instance = new ProdImpl(); } else { instance = new MockImpl(); };但笔记也警告:static块中若抛出异常,该类将永久处于“初始化失败”状态,后续任何使用都会抛NoClassDefFoundError。所以务必在static块中捕获所有异常并记录日志。4. 从String对象池到StringBuilder:字符串操作的内存与性能黑匣子4.1String a = "hello"vsString c = new String("hello"):对象池的“可见性”边界在哪?第 16-17 页用 JVM 内存图解构字符串创建:String a = "hello"; // ① 从字符串常量池(方法区)获取,若无则创建 String b = "hello"; // ② 直接引用池中同一对象 → a == b 为 true String c = new String("hello"); // ③ 在堆中新建对象,内容同"hello",但地址不同 → a == c 为 false String d = c.intern(); // ④ 将 c 的内容放入常量池(若无),并返回池中引用 → a == d 为 true笔记的关键洞见是:intern()不是“把对象放进去”,而是“把字符串内容注册到池,并返回池中对应引用”。它在 JDK 7+ 后将常量池移到堆中,所以intern()成本降低,但仍有开销。笔记给出一个生产环境踩坑案例:某日志模块对每条日志msg.intern(),导致堆内存中常量池占用暴增 2GB,GC 频繁。解决方案是:仅对有限且重复率高的字符串(如状态码"SUCCESS","ERROR")调用intern(),而非所有日志消息。4.2StringBuffer与StringBuilder:synchronized关键字在方法签名上的“重量级”代价第 17 页的对比表格直击本质:特性StringBufferStringBuilder线程安全✅synchronized修饰所有公共方法❌ 无同步,纯堆操作性能(单线程)比StringBuilder慢 15%~20%(基准测试)最快适用场景多线程共享的字符串构建(如 Servlet 全局缓存)绝大多数场景(局部变量、方法内)笔记用一段反编译字节码说明synchronized的开销:// StringBuffer.append(String) 反编译后伪代码 public synchronized StringBuffer append(String str) { // ① 进入方法前:monitorenter 指令(获取对象锁) // ② 执行逻辑:扩容、复制数组、追加字符 // ③ 方法结束前:monitorexit 指令(释放锁) return this; }而StringBuilder.append()没有这两条指令。笔记结论:“除非你明确需要跨线程共享同一个StringBuilder实例,否则永远选StringBuilder。StringBuffer是 Java 1.0 遗留的‘过度设计’”。4.3String不可变性的安全价值:为什么HashMap的 key 必须是不可变对象?第 16 页在String介绍末尾,突然转向一个关键推论:String的hashCode()在对象创建时就缓存(private int hash;),且因内容不可变,哈希值永不改变。笔记画出HashMap插入流程:put("key", value) → 1. 计算 "key".hashCode() → 3214(缓存)→ 2. 定位桶位置 index = 3214 & (table.length-1) → 3. 将 Node 放入 table[index]如果String可变,"key"被修改为"newKey"后,hashCode()变为 5678,但该Node仍留在index=3214的桶里,get("key")就再也找不到它——哈希表结构被破坏。笔记在此处加粗:“String的不可变性,是HashMap/HashSet正确性的基石,不是为了‘优雅’,是为了‘不死’”。5. 从try-catch-finally到try-with-resources:异常处理的进化与陷阱5.1finally块中的return如何“吃掉”catch中抛出的异常?第 34 页的异常处理章节,用一个令人窒息的代码示例:public static String readFile() { try { throw new IOException("read failed"); } catch (IOException e) { System.out.println("Caught: " + e.getMessage()); throw new RuntimeException("wrap it", e); // ① 准备抛出 } finally { return "from finally"; // ② finally 中的 return 会压制上面的 throw! } } // 调用结果:返回 "from finally",且无任何异常抛出!笔记解释:JVM 规范规定,finally块中若有return、throw或System.exit(),它会终结当前方法的执行流,覆盖try/catch中的任何异常或返回值。这是finally的“最高优先级”特性,也是最易被忽视的玄学。解决方案只有两个:方案一:finally中绝不写return或throw(只做资源清理);方案二:若必须返回,先保存try/catch的结果,再在finally后return。5.2try-with-resources的自动关闭原理:AutoCloseable接口的close()方法为何必须幂等?第 34 页对比传统try-catch-finally时,笔记展示try-with-resources的字节码等价逻辑:// 你的代码 try (FileInputStream fis = new FileInputStream("a.txt")) { // do something } // 编译器自动插入:fis.close() 在此处,无论是否异常等价于:FileInputStream fis = null; try { fis = new FileInputStream("a.txt"); // do something } finally { if (fis != null) { try { fis.close(); // ① 可能抛 IOException } catch (Throwable t) { // ② 如果此处抛异常,会压制 try 块中的原始异常! // 所以 close() 必须尽量不抛异常 } } }笔记强调:close()方法必须是幂等的(多次调用无副作用)且尽量避免抛异常。例如BufferedWriter.close()会先flush(),若flush()失败则close()抛IOException——这就是为什么生产代码中常看到:try (BufferedWriter writer = new BufferedWriter(...)) { writer.write("data"); } catch (IOException e) { // 处理 write 异常 } // close() 的异常被吞掉,但业务逻辑已执行完毕5.3 自定义异常类的cause链:为什么initCause()不能多次调用?第 36 页的“自定义异常类”部分,笔记给出标准模板:public class BusinessException extends Exception { public BusinessException(String message) { super(message); } public BusinessException(String message, Throwable cause) { super(message, cause); // ① 正确:通过父类构造器设置 cause // super(message); this.initCause(cause); // ② 错误!initCause() 只能调用一次 } }initCause()内部有状态检查:if (this.cause != this) throw new IllegalStateException("Can't overwrite cause");。笔记提醒:在框架中封装异常时(如 DAO 层将SQLException包装为DataAccessException),必须用带cause的构造器,否则调用栈丢失,getCause()返回null,上游无法做精准异常分类。这是日志追踪和熔断降级的命脉。6. 从反射到ClassLoader:动态加载的边界与安全红线6.1Class.forName()与ClassLoader.loadClass():静态初始化的“开关”由谁控制?第 38 页的反射章节,笔记用一个static块日志对比:class TestClass { static { System.out.println("Static block executed!"); } } // 场景1:Class.forName("TestClass") Class<?> c1 = Class.forName("TestClass"); // 输出:Static block executed! // 场景2:ClassLoader.getSystemClassLoader().loadClass("TestClass") Class<?> c2 = ClassLoader.getSystemClassLoader().loadClass("TestClass"); // 无输出!原因:Class.forName(String)默认调用Class.forName(String, true, ClassLoader),第二个参数initialize=true表示触发类初始化(执行static块);而ClassLoader.loadClass()默认resolve=false,只加载不初始化。笔记警示:“在热部署或插件系统中,若用loadClass()加载类后直接newInstance(),可能因static块未执行导致 NPE。此时应显式调用Class.forName(className, true, loader)”。6.2ClassLoader双亲委派的“破环”场景:为什么 OSGi 和 Tomcat 要自定义类加载器?第 12 页“触发类加载的几种情况”中,笔记列出双亲委派的例外:场景是否遵循双亲委派说明Class.forName()✅ 是默认走AppClassLoader→ExtClassLoader→BootstrapClassLoaderThread.currentThread().getContextClassLoader().loadClass()❌ 否上下文类加载器可被应用设置,常用于 SPI 服务发现URLClassLoader加载 JAR✅ 是但可指定 parent 为null,彻底切断委派笔记以 Tomcat 为例:每个 WebApp 有自己的WebAppClassLoader,其 parent 是SharedClassLoader(非AppClassLoader),从而实现应用间类隔离。而JDBC DriverManager通过Thread.currentThread().getContextClassLoader()加载驱动,所以驱动 JAR 放在WEB-INF/lib下就能被找到——这是双亲委派被“绕过”的经典设计。笔记总结:“双亲委派不是教条,是隔离与共享的平衡术。破环的前提,是清楚知道你要隔离什么、共享什么”。6.3 反射调用私有方法的安全检查:setAccessible(true)在安全管理器下的失效风险第 37 页“反射需要用到的类”中,笔记提到AccessibleObject.setAccessible(true):Field field = SomeClass.class.getDeclaredField("privateField"); field.setAccessible(true); // ① 绕过访问控制检查 Object value = field.get(obj);但笔记加了一行小字警告:“若 JVM 启动时设置了安全管理器(-Djava.security.manager),setAccessible(true)会抛SecurityException”。生产环境(尤其金融、政务系统)常启用安全管理器,此时反射私有成员会失败。笔记建议的替代方案:方案一:用@SuppressWarnings("removal")(JDK 17+)标记,但需评估风险;方案二:改用MethodHandle(Lookup.findSpecial()),其权限检查更细粒度;方案三:根本上重构,通过public接口暴露必要能力,而非依赖反射。注意:setAccessible(true)的“成功”不等于“安全”。它只是关闭了 JVM 的访问检查,但若字段是final且已初始化,修改其值仍可能导致 JVM 行为未定义(如String.value被修改后hashCode()缓存失效)。7. 从File到NIO.2:文件操作的可靠性与跨平台陷阱7.1File类的delete()方法为何在 Windows 下经常失败?第 40 页“File 构造方法”后,笔记直指痛点:File.delete()在 Windows 上返回false的常见原因:文件被其他进程占用(如 Excel 打开 CSV,IDE 打开日志);文件在回收站中(File对象指向已删除路径);路径含非法字符(Windows 对CON,PRN,AUX等保留名敏感);权限不足(非管理员无法删除系统目录下文件)。笔记给出健壮删除方案(基于Files.deleteIfExists()):import java.nio.file.*; Path path = Paths.get("target/file.txt"); try { Files.deleteIfExists(path); // ① 原子性删除,失败抛具体异常 } catch (NoSuchFileException e) { // 文件不存在,正常 } catch (DirectoryNotEmptyException e) { // 目录非空,需先递归删除 Files.walk(path) .sorted(Comparator.reverseOrder()) .forEach(p -> { try { Files.delete(p); } catch (IOException ex) { /* log */ } }); } catch (AccessDeniedException e) { // 权限不足,尝试设置可写 try { Files.setAttribute(path, "dos:readonly", false); Files.delete(path); } catch (IOException ex) { /* log */ } }FilesAPI 的优势在于:异常类型明确(NoSuchFileException,AccessDeniedException),且walk()支持逆序遍历,天然适合递归删除。7.2ObjectInputStream的反序列化安全:为什么必须重写resolveClass()?第 46 页“ObjectInputStream和ObjectOutputStream”中,笔记用一个血泪经验开场:“某次上线后,服务 CPU 100%,线程堆栈全是ObjectInputStream.readObject(),排查发现是恶意客户端发送了超深嵌套的LinkedList,触发readObject()无限递归”。根本原因是:ObjectInputStream默认接受任意类,包括Runtime、ProcessBuilder等危险类。笔记给出防御方案——继承ObjectInputStream并重写resolveClass():public class SafeObjectInputStream extends ObjectInputStream { private static final Set<String> ALLOWED_CLASSES = Set.of( "java.lang.String", "java.util.ArrayList", "com.example.User" ); public SafeObjectInputStream(InputStream in) throws IOException { super(in); } @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className = desc.getName(); if (!ALLOWED_CLASSES.contains(className)) { throw new InvalidClassException("Unauthorized class: " + className); } return super.resolveClass(desc); } }笔记强调:白名单制比黑名单制可靠。ALLOWED_CLASSES应严格限定业务所需类,且需定期审计。JDK 9+ 提供ObjectInputFilter,但笔记认为白名单resolveClass()更直观可控。7.3Thread的七种状态与park()/unpark():wait()/notify()为何在LockSupport面前过时?第 52 页“线程状态的转换”中,笔记用状态流转图对比:BLOCKED → (等待 monitor 锁) → RUNNABLE WAITING → (调用 wait()) → RUNNABLE (需 notify) TIMED_WAITING → (sleep(), wait(ms)) → RUNNABLE而LockSupport.park()的状态是WAITING,但它不需要 synchronized 块,也不依赖对象监视器:// 传统 wait/notify(必须 synchronized) synchronized (lock) { lock.wait(); // 释放 lock,进入 WAITING } // notify 必须在 synchronized(lock) 内调用 // LockSupport(无锁) LockSupport.park(); // 当前线程 WAITING LockSupport.unpark(thread); // 唤醒指定线程,无需锁笔记结论:“LockSupport是AQS(AbstractQueuedSynchronizer)的基石,ReentrantLock、Semaphore的底层都用它。wait/notify是面向对象监视器的遗留 API,park/unpark是面向线程的现代原语。新代码请拥抱LockSupport”。从那以后我每次写涉及static块或finally的代码,都会强制走一遍javap -c看字节码,确认monitorenter/monitorexit的位置和return指令的顺序;每次用反射,第一反应不是setAccessible(true),而是问自己:“有没有public接口能替代?”;每次设计序列化,必画一张类图,标出哪些字段要transient,哪些类要进白名单。这份笔记的价值,不在于它写了多少页,而在于它让我养成了对每一个语法糖背后 JVM 行为的条件反射式质疑。希望帮到你。本文还有配套的精品资源,点击获取
简介:本资源是一份面向Java初学者与备考者的Core Java核心知识点精要笔记,聚焦语言基础与关键机制,助力快速构建扎实的编程底层认知。资料以单个PDF文件形式呈现(1.64MB),内容结构清晰、模块完整,涵盖原码/反码/补码原理、char类型特性、移位与位运算符细节、switch与break/continue流程控制逻辑、构造方法与重载机制、this与super关键字用法、继承与多态实现、static/final/abstract等修饰符语义,以及instanceof类型判断等高频考点。目录层级分明,从二进制表示到面向对象核心概念逐层递进,每节均含要点提炼与典型说明,适合作为课堂笔记补充、面试前速查或自学知识图谱梳理。目前已有473人学习下载,内容紧扣Java SE基础体系,对夯实编码基本功、理解JVM底层逻辑及应对技术笔试具有明确支撑价值。1. 这份 CoreJava 笔记不是“速成宝典”,而是你调试 NullPointerException 时翻得最勤的那页纸你有没有过这种经历:线上服务突然抛出ConcurrentModificationException,堆栈里只有一行ArrayList$Itr.checkForComodification(ArrayList.java:901),而你手边连个能快速定位modCount和expectedModCount关系的参考资料都没有?或者在写泛型工具类时,对着? super T和? extends T发呆半小时,最后靠试错加注释硬凑出能编译的代码?这份《CoreJava全套详细笔记资料.pdf》不是那种把 API 文档复制粘贴再加点颜色的“PPT式笔记”,它是一份从 JVM 字节码层面反推语法设计、用真实调试断点截图佐证原理、甚至把String对象池里intern()调用前后内存地址变化都标出来的实战手札。它覆盖了从原码/补码底层表示(第 5 页)到ObjectInputStream反序列化安全校验(第 46 页)、从static初始化顺序陷阱(第 11 页)到ThreadLocal内存泄漏根因(第 55 页)的完整链条。适合两类人:一是刚脱离“Hello World”阶段、正被final修饰符作用域和static块执行时机搞晕的新手;二是需要快速回溯某个冷门机制(比如ClassLoader.loadClass()的双亲委派中断条件)的老手。它不教你“怎么写 Spring Boot”,但当你写的自定义 ClassLoader 在热部署时莫名其妙加载失败,翻到第 12 页“类加载的顺序”那一栏,你会立刻明白问题出在哪——这才是它不可替代的价值。2. 从二进制补码到集合线程安全:这份笔记如何用“可验证”的方式讲透 Java 核心机制2.1 原码/反码/补码:为什么Integer.MIN_VALUE == -Integer.MIN_VALUE是合法的?很多教程把补码讲成“符号位不变,其余位取反再加一”,这没错,但没解释清楚为什么这样设计。这份笔记在第 5 页直接给出 8 位二进制示例:-1 的原码:1000 0001 -1 的反码:1111 1110 (符号位 1 不变,其余位取反) -1 的补码:1111 1111 (反码 + 1) -128 的原码:1000 0000 -128 的反码:1111 1111 -128 的补码:0000 0000 (溢出!但这是设计使然)关键点在于:补码让加减法电路可以统一用加法器实现。笔记用(-1) + 1验证:1111 1111 + 0000 0001 = 0000 0000(溢出位丢弃),结果为 0,完全正确。而Integer.MIN_VALUE(即-128在 byte 范围)的补码1000 0000取负时,按规则计算:1000 0000 → 0111 1111 → 1000 0000,结果还是自己——这不是 bug,是补码系统对称性的必然结果。笔记特别标注:“JVM 规范明确要求整数溢出不抛异常,此行为是Math.abs()在Integer.MIN_VALUE上返回负值的根源”。你在写数值校验逻辑时,如果漏掉这个边界,就会埋下静默错误。提示:实际开发中,不要依赖Math.abs(x)处理可能为MIN_VALUE的输入。安全写法是x == Integer.MIN_VALUE ? Integer.MAX_VALUE : Math.abs(x),或直接用BigInteger.abs()。2.2 char 类型与 Unicode:为什么char c = '\u0000';是合法的,但c = -1;编译报错?第 6 页用一张表格厘清char的本质:属性值说明存储大小2 字节(16 bit)固定长度,非 UTF-8 变长数值范围0到65535(即0x0000到0xFFFF)无符号整数,不是 -32768 ~ 32767字面量表示'a','\u0041','\u20AC'(欧元符号)单引号包裹,支持 Unicode 转义隐式转换可自动转为int,long,float,double因其本质是无符号 16 位整数重点来了:char c = -1;报错不是因为“字符不能为负”,而是因为-1是int类型字面量(32 位有符号),其值-1超出char的0~65535范围。而'\uFFFF'是合法的,因为\uFFFF表示的是数值65535,在范围内。笔记在旁边手写批注:“char是无符号整数类型,不是‘字符容器’——这是理解String底层char[]和codePointAt()差异的起点”。2.3 移位与位运算:>>和>>>在处理网络字节序时为何必须严格区分?第 6 页的移位运算符部分,没有停留在“>>算术右移,>>>逻辑右移”的定义,而是给出一个 TCP/IP 协议解析的真实场景:// 假设收到 4 字节数据:0xFF, 0x80, 0x00, 0x00(大端序) // 解析为有符号 int(需保持符号位) int signedValue = (bytes[0] << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); // 此时 signedValue = -8388608(正确) // 但如果误用 >>>: int wrong = (bytes[0] << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); // bytes[0] 是 0xFF(255),左移 24 位后变成 0xFF000000, // 若用 >> 会符号扩展,但此处是位或,关键在 bytes[0] 本身是 int! // 所以必须先 & 0xFF 再移位,否则高位被符号位污染。笔记强调:byte到int的提升是符号扩展。byte b = (byte)0xFF; int i = b;结果i是-1(0xFFFFFFFF),不是255(0x000000FF)。因此网络字节解析必须写b & 0xFF强制转为无符号值。这个细节在 Netty 或 MINA 框架源码里高频出现,笔记在第 6 页用红框标出:“所有byte参与位运算前,必先& 0xFF”。2.4 集合框架的线程安全真相:Vector的synchronized方法为何在高并发下反而更慢?第 21 页对比ArrayList/Vector/Collections.synchronizedList()时,笔记没停留在“Vector是线程安全的”这种模糊说法,而是画出方法调用链:// Vector.add() 源码逻辑(简化) public synchronized boolean add(E e) { ensureCapacityInternal(size + 1); // 同步块内 elementData[size++] = e; // 同步块内 return true; }问题在于:整个方法体被一把锁保护。当多个线程同时调用add()和get(int index)时,它们会互相阻塞——即使操作的是不同索引位置。笔记用一个实验数据佐证:在 100 线程并发向Vector添加 1000 个元素的测试中,平均耗时 128ms;而用CopyOnWriteArrayList(写时复制)仅需 42ms(读操作无锁)。但笔记也冷静指出:CopyOnWriteArrayList适用于“读多写少”场景,若写操作频繁,ArrayList+ReentrantLock分段锁才是更优解。它在第 21 页底部手绘了一个决策树:读操作占比 > 95%?→CopyOnWriteArrayList读写比例均衡?→Collections.synchronizedList(new ArrayList<>())(锁粒度比Vector更可控)写操作密集且需强一致性?→ConcurrentHashMap替代Map,或自定义分段锁List3. 从this到super:构造方法链与多态调用的“时间陷阱”3.1this()和super()必须是构造方法的第一条语句:为什么 JVM 要强制这个顺序?第 8 页和第 9 页用一个经典反例揭示本质:class Parent { String name; Parent(String name) { this.name = name; // ① System.out.println("Parent init: " + this.name); // ② } } class Child extends Parent { int age; Child(String name, int age) { // 错误!不能在这里调用 super() // super(name); // 编译错误:必须第一行 this.age = age; // ③ super(name); // 编译错误:super() 必须第一行 } }笔记解释:JVM 在对象创建时,内存分配和父类字段初始化必须在子类字段初始化之前完成。super()的作用是触发父类构造器,确保父类状态(如name)在子类使用前已就绪。如果允许this.age = age在super()之前,那么③执行时this对象的父类部分(name)还是未初始化的垃圾值,②中的this.name就会输出null或随机值。笔记在旁白写道:“这不是语法糖,是 JVM 对象模型的铁律——this指针只有在父类构造完成之后才真正‘活’过来”。3.2 多态中的instanceof与getClass():为什么obj instanceof String比obj.getClass() == String.class更安全?第 10 页对比这两个操作时,笔记给出一个String子类(假设存在)的思考实验:// 假设 Java 允许继承 String(实际 final,但用于说明原理) class MyString extends String { /* ... */ } MyString myStr = new MyString("hello"); System.out.println(myStr instanceof String); // true System.out.println(myStr.getClass() == String.class); // false!instanceof检查的是运行时类型是否为指定类型或其子类,而getClass()返回的是最具体的类对象。笔记强调:在框架开发中(如 JSON 序列化器),若用getClass() == String.class判断字符串类型,遇到MyString实例就会跳过字符串优化逻辑,导致性能下降。而instanceof天然支持继承体系。但笔记也提醒一个坑:instanceof对null安全(返回false),而getClass()对null会抛NullPointerException。所以安全写法是obj != null && obj instanceof String。3.3static块的执行时机:为什么在类加载时初始化单例比static final更灵活?第 11 页“类加载的顺序”中,笔记用一个延迟初始化单例的案例:class LazySingleton { private static LazySingleton instance; static { // ① static 块在类首次主动使用时执行(如调用 getInstance()) // ② 可以包含复杂逻辑:读配置、连数据库、检查权限 System.out.println("Static block executed"); instance = new LazySingleton(); } private LazySingleton() {} // 私有构造 public static LazySingleton getInstance() { return instance; // ③ 简单返回,无同步开销 } }对比public static final LazySingleton INSTANCE = new LazySingleton();:static final字段在类加载时立即初始化,无法加入条件判断;static块可以写if (Config.isProd()) { instance = new ProdImpl(); } else { instance = new MockImpl(); };但笔记也警告:static块中若抛出异常,该类将永久处于“初始化失败”状态,后续任何使用都会抛NoClassDefFoundError。所以务必在static块中捕获所有异常并记录日志。4. 从String对象池到StringBuilder:字符串操作的内存与性能黑匣子4.1String a = "hello"vsString c = new String("hello"):对象池的“可见性”边界在哪?第 16-17 页用 JVM 内存图解构字符串创建:String a = "hello"; // ① 从字符串常量池(方法区)获取,若无则创建 String b = "hello"; // ② 直接引用池中同一对象 → a == b 为 true String c = new String("hello"); // ③ 在堆中新建对象,内容同"hello",但地址不同 → a == c 为 false String d = c.intern(); // ④ 将 c 的内容放入常量池(若无),并返回池中引用 → a == d 为 true笔记的关键洞见是:intern()不是“把对象放进去”,而是“把字符串内容注册到池,并返回池中对应引用”。它在 JDK 7+ 后将常量池移到堆中,所以intern()成本降低,但仍有开销。笔记给出一个生产环境踩坑案例:某日志模块对每条日志msg.intern(),导致堆内存中常量池占用暴增 2GB,GC 频繁。解决方案是:仅对有限且重复率高的字符串(如状态码"SUCCESS","ERROR")调用intern(),而非所有日志消息。4.2StringBuffer与StringBuilder:synchronized关键字在方法签名上的“重量级”代价第 17 页的对比表格直击本质:特性StringBufferStringBuilder线程安全✅synchronized修饰所有公共方法❌ 无同步,纯堆操作性能(单线程)比StringBuilder慢 15%~20%(基准测试)最快适用场景多线程共享的字符串构建(如 Servlet 全局缓存)绝大多数场景(局部变量、方法内)笔记用一段反编译字节码说明synchronized的开销:// StringBuffer.append(String) 反编译后伪代码 public synchronized StringBuffer append(String str) { // ① 进入方法前:monitorenter 指令(获取对象锁) // ② 执行逻辑:扩容、复制数组、追加字符 // ③ 方法结束前:monitorexit 指令(释放锁) return this; }而StringBuilder.append()没有这两条指令。笔记结论:“除非你明确需要跨线程共享同一个StringBuilder实例,否则永远选StringBuilder。StringBuffer是 Java 1.0 遗留的‘过度设计’”。4.3String不可变性的安全价值:为什么HashMap的 key 必须是不可变对象?第 16 页在String介绍末尾,突然转向一个关键推论:String的hashCode()在对象创建时就缓存(private int hash;),且因内容不可变,哈希值永不改变。笔记画出HashMap插入流程:put("key", value) → 1. 计算 "key".hashCode() → 3214(缓存)→ 2. 定位桶位置 index = 3214 & (table.length-1) → 3. 将 Node 放入 table[index]如果String可变,"key"被修改为"newKey"后,hashCode()变为 5678,但该Node仍留在index=3214的桶里,get("key")就再也找不到它——哈希表结构被破坏。笔记在此处加粗:“String的不可变性,是HashMap/HashSet正确性的基石,不是为了‘优雅’,是为了‘不死’”。5. 从try-catch-finally到try-with-resources:异常处理的进化与陷阱5.1finally块中的return如何“吃掉”catch中抛出的异常?第 34 页的异常处理章节,用一个令人窒息的代码示例:public static String readFile() { try { throw new IOException("read failed"); } catch (IOException e) { System.out.println("Caught: " + e.getMessage()); throw new RuntimeException("wrap it", e); // ① 准备抛出 } finally { return "from finally"; // ② finally 中的 return 会压制上面的 throw! } } // 调用结果:返回 "from finally",且无任何异常抛出!笔记解释:JVM 规范规定,finally块中若有return、throw或System.exit(),它会终结当前方法的执行流,覆盖try/catch中的任何异常或返回值。这是finally的“最高优先级”特性,也是最易被忽视的玄学。解决方案只有两个:方案一:finally中绝不写return或throw(只做资源清理);方案二:若必须返回,先保存try/catch的结果,再在finally后return。5.2try-with-resources的自动关闭原理:AutoCloseable接口的close()方法为何必须幂等?第 34 页对比传统try-catch-finally时,笔记展示try-with-resources的字节码等价逻辑:// 你的代码 try (FileInputStream fis = new FileInputStream("a.txt")) { // do something } // 编译器自动插入:fis.close() 在此处,无论是否异常等价于:FileInputStream fis = null; try { fis = new FileInputStream("a.txt"); // do something } finally { if (fis != null) { try { fis.close(); // ① 可能抛 IOException } catch (Throwable t) { // ② 如果此处抛异常,会压制 try 块中的原始异常! // 所以 close() 必须尽量不抛异常 } } }笔记强调:close()方法必须是幂等的(多次调用无副作用)且尽量避免抛异常。例如BufferedWriter.close()会先flush(),若flush()失败则close()抛IOException——这就是为什么生产代码中常看到:try (BufferedWriter writer = new BufferedWriter(...)) { writer.write("data"); } catch (IOException e) { // 处理 write 异常 } // close() 的异常被吞掉,但业务逻辑已执行完毕5.3 自定义异常类的cause链:为什么initCause()不能多次调用?第 36 页的“自定义异常类”部分,笔记给出标准模板:public class BusinessException extends Exception { public BusinessException(String message) { super(message); } public BusinessException(String message, Throwable cause) { super(message, cause); // ① 正确:通过父类构造器设置 cause // super(message); this.initCause(cause); // ② 错误!initCause() 只能调用一次 } }initCause()内部有状态检查:if (this.cause != this) throw new IllegalStateException("Can't overwrite cause");。笔记提醒:在框架中封装异常时(如 DAO 层将SQLException包装为DataAccessException),必须用带cause的构造器,否则调用栈丢失,getCause()返回null,上游无法做精准异常分类。这是日志追踪和熔断降级的命脉。6. 从反射到ClassLoader:动态加载的边界与安全红线6.1Class.forName()与ClassLoader.loadClass():静态初始化的“开关”由谁控制?第 38 页的反射章节,笔记用一个static块日志对比:class TestClass { static { System.out.println("Static block executed!"); } } // 场景1:Class.forName("TestClass") Class<?> c1 = Class.forName("TestClass"); // 输出:Static block executed! // 场景2:ClassLoader.getSystemClassLoader().loadClass("TestClass") Class<?> c2 = ClassLoader.getSystemClassLoader().loadClass("TestClass"); // 无输出!原因:Class.forName(String)默认调用Class.forName(String, true, ClassLoader),第二个参数initialize=true表示触发类初始化(执行static块);而ClassLoader.loadClass()默认resolve=false,只加载不初始化。笔记警示:“在热部署或插件系统中,若用loadClass()加载类后直接newInstance(),可能因static块未执行导致 NPE。此时应显式调用Class.forName(className, true, loader)”。6.2ClassLoader双亲委派的“破环”场景:为什么 OSGi 和 Tomcat 要自定义类加载器?第 12 页“触发类加载的几种情况”中,笔记列出双亲委派的例外:场景是否遵循双亲委派说明Class.forName()✅ 是默认走AppClassLoader→ExtClassLoader→BootstrapClassLoaderThread.currentThread().getContextClassLoader().loadClass()❌ 否上下文类加载器可被应用设置,常用于 SPI 服务发现URLClassLoader加载 JAR✅ 是但可指定 parent 为null,彻底切断委派笔记以 Tomcat 为例:每个 WebApp 有自己的WebAppClassLoader,其 parent 是SharedClassLoader(非AppClassLoader),从而实现应用间类隔离。而JDBC DriverManager通过Thread.currentThread().getContextClassLoader()加载驱动,所以驱动 JAR 放在WEB-INF/lib下就能被找到——这是双亲委派被“绕过”的经典设计。笔记总结:“双亲委派不是教条,是隔离与共享的平衡术。破环的前提,是清楚知道你要隔离什么、共享什么”。6.3 反射调用私有方法的安全检查:setAccessible(true)在安全管理器下的失效风险第 37 页“反射需要用到的类”中,笔记提到AccessibleObject.setAccessible(true):Field field = SomeClass.class.getDeclaredField("privateField"); field.setAccessible(true); // ① 绕过访问控制检查 Object value = field.get(obj);但笔记加了一行小字警告:“若 JVM 启动时设置了安全管理器(-Djava.security.manager),setAccessible(true)会抛SecurityException”。生产环境(尤其金融、政务系统)常启用安全管理器,此时反射私有成员会失败。笔记建议的替代方案:方案一:用@SuppressWarnings("removal")(JDK 17+)标记,但需评估风险;方案二:改用MethodHandle(Lookup.findSpecial()),其权限检查更细粒度;方案三:根本上重构,通过public接口暴露必要能力,而非依赖反射。注意:setAccessible(true)的“成功”不等于“安全”。它只是关闭了 JVM 的访问检查,但若字段是final且已初始化,修改其值仍可能导致 JVM 行为未定义(如String.value被修改后hashCode()缓存失效)。7. 从File到NIO.2:文件操作的可靠性与跨平台陷阱7.1File类的delete()方法为何在 Windows 下经常失败?第 40 页“File 构造方法”后,笔记直指痛点:File.delete()在 Windows 上返回false的常见原因:文件被其他进程占用(如 Excel 打开 CSV,IDE 打开日志);文件在回收站中(File对象指向已删除路径);路径含非法字符(Windows 对CON,PRN,AUX等保留名敏感);权限不足(非管理员无法删除系统目录下文件)。笔记给出健壮删除方案(基于Files.deleteIfExists()):import java.nio.file.*; Path path = Paths.get("target/file.txt"); try { Files.deleteIfExists(path); // ① 原子性删除,失败抛具体异常 } catch (NoSuchFileException e) { // 文件不存在,正常 } catch (DirectoryNotEmptyException e) { // 目录非空,需先递归删除 Files.walk(path) .sorted(Comparator.reverseOrder()) .forEach(p -> { try { Files.delete(p); } catch (IOException ex) { /* log */ } }); } catch (AccessDeniedException e) { // 权限不足,尝试设置可写 try { Files.setAttribute(path, "dos:readonly", false); Files.delete(path); } catch (IOException ex) { /* log */ } }FilesAPI 的优势在于:异常类型明确(NoSuchFileException,AccessDeniedException),且walk()支持逆序遍历,天然适合递归删除。7.2ObjectInputStream的反序列化安全:为什么必须重写resolveClass()?第 46 页“ObjectInputStream和ObjectOutputStream”中,笔记用一个血泪经验开场:“某次上线后,服务 CPU 100%,线程堆栈全是ObjectInputStream.readObject(),排查发现是恶意客户端发送了超深嵌套的LinkedList,触发readObject()无限递归”。根本原因是:ObjectInputStream默认接受任意类,包括Runtime、ProcessBuilder等危险类。笔记给出防御方案——继承ObjectInputStream并重写resolveClass():public class SafeObjectInputStream extends ObjectInputStream { private static final Set<String> ALLOWED_CLASSES = Set.of( "java.lang.String", "java.util.ArrayList", "com.example.User" ); public SafeObjectInputStream(InputStream in) throws IOException { super(in); } @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className = desc.getName(); if (!ALLOWED_CLASSES.contains(className)) { throw new InvalidClassException("Unauthorized class: " + className); } return super.resolveClass(desc); } }笔记强调:白名单制比黑名单制可靠。ALLOWED_CLASSES应严格限定业务所需类,且需定期审计。JDK 9+ 提供ObjectInputFilter,但笔记认为白名单resolveClass()更直观可控。7.3Thread的七种状态与park()/unpark():wait()/notify()为何在LockSupport面前过时?第 52 页“线程状态的转换”中,笔记用状态流转图对比:BLOCKED → (等待 monitor 锁) → RUNNABLE WAITING → (调用 wait()) → RUNNABLE (需 notify) TIMED_WAITING → (sleep(), wait(ms)) → RUNNABLE而LockSupport.park()的状态是WAITING,但它不需要 synchronized 块,也不依赖对象监视器:// 传统 wait/notify(必须 synchronized) synchronized (lock) { lock.wait(); // 释放 lock,进入 WAITING } // notify 必须在 synchronized(lock) 内调用 // LockSupport(无锁) LockSupport.park(); // 当前线程 WAITING LockSupport.unpark(thread); // 唤醒指定线程,无需锁笔记结论:“LockSupport是AQS(AbstractQueuedSynchronizer)的基石,ReentrantLock、Semaphore的底层都用它。wait/notify是面向对象监视器的遗留 API,park/unpark是面向线程的现代原语。新代码请拥抱LockSupport”。从那以后我每次写涉及static块或finally的代码,都会强制走一遍javap -c看字节码,确认monitorenter/monitorexit的位置和return指令的顺序;每次用反射,第一反应不是setAccessible(true),而是问自己:“有没有public接口能替代?”;每次设计序列化,必画一张类图,标出哪些字段要transient,哪些类要进白名单。这份笔记的价值,不在于它写了多少页,而在于它让我养成了对每一个语法糖背后 JVM 行为的条件反射式质疑。希望帮到你。本文还有配套的精品资源,点击获取