Java四大基石从Object源码到包装类陷阱,有哪些全维度复盘细节?

摘要::::warning 其实还靠手敲来总结,汇总,编辑的人是比较笨的人,所以也许是最后一篇了吧,闲暇时光写的,耗时约3月.... 💡 根据 遗忘曲线:如果没有记录和回顾,6天后便会忘记75%的内容 自我PUA:
:::warning 其实还靠手敲来总结,汇总,编辑的人是比较笨的人,所以也许是最后一篇了吧,闲暇时光写的,耗时约3月.... 💡 根据 遗忘曲线:如果没有记录和回顾,6天后便会忘记75%的内容 自我PUA:有人说”成功“是完成一个目标,取得相应的成就,收获到目标的果实,这是成功的标志。有人说”成功“是钱,权,与地位,因为这是成功的体现和标志。说实话,我也想要这样的”成功“,因为它几乎可以无限满足我的一切欲望,可我本该糊涂的时候,却似乎又清醒着,世界上总有另一个我,在我的世界中疯狂捶打着我,告诉我,别想了,我并不是哪个幸运儿。 所以,我开始考虑到底什么是成功?什么是成功人士?成功,应该是一种无形的升华,而不是欲望的体现,不仅仅是只有实体的(成就、果实、钱、权、地位),才能被称为成功。付出,行动都不一定有收获的,那么,没有收获就是失败吗?我定下目标,我就有了第一个成功,我迈出第一步,我有了第二次成功。成功应当是唯物主义和唯心主义之间的平衡,站在某一个方向,讲成功,应当都是偏激的,不严谨的。所以请相信自己,你一直在成功的路上。即使在他人看来,你很失败,但请记住,你在成功的路上从未停止过。 成功 = 知行合一 ::: 书名 Mem Reduct 作者 一毛钱钢镚儿 状态 更新中..已完结 暂停更新 简介 本书精选柯维博士“七个习惯”的最核心思想和方法,为忙碌人士带来超价值的自我提升体验。用最少的时间,参透高效能人士的持续成功之路。 思维导图 这个图,比较鸡贼,简单看看就好。 背景 在 Java 的世界里,有一句经典的话:“万物皆对象”。 那么问题来了:时间是不是对象?文字是不是对象?我们日常处理的信息,能不能也变成对象? 让我们从两个常见的实际场景出发,看看开发者会遇到什么困惑。 场景一:如何在程序中获取“当前时间”? 你一定见过这样的界面: 直播画面右上角显示:2026 年 01 月 08 日 15:00:00(实时更新) 这个时间不是写死的,而是动态变化的,并且和你电脑、手机上的系统时间完全一致。 那么,如果你正在开发一个直播系统、日志记录器,或者一个简单的时钟应用,怎么让你的程序也拿到这个“当前时间”? - 难道要自己写一个 `MyTime` 类,手动维护年、月、日、时、分、秒? - 如果这样,你怎么知道“现在到底是几点”?你的程序又如何和操作系统的时间保持同步? 显然,这不该由每个开发者从零实现。时间是通用需求,必须有标准、可靠、高效的解决方案。 场景二:如何处理“文字信息”并实现关键词搜索? 再看另一个常见需求: 你想在程序中实现类似搜索引擎的功能。比如用户输入关键词 “Java” 和 “姑娘”,你的程序能从一堆文章标题中找出包含这些词的内容,例如: - 《为什么 Java 开发没有姑娘?》 - 《Java 工程师的浪漫:代码与她》 那么问题来了: - 这些“标题”是什么?是字符串,但字符串本身有没有“查找”“匹配”的能力? - 如果我要判断一段文字是否包含“Java”,是自己写循环逐个字符比对吗? - 如果以后还要支持模糊搜索、正则匹配、中文分词……难道每次都要重写一套逻辑? 这显然不现实。文字处理是基础能力,应该被封装成可复用的对象和方法。 Java 的答案:别重复造轮子,用 API! 面对这些问题,Java 的设计者早已替我们想好了——他们提供了一套强大、稳定、持续演进的标准类库,也就是我们常说的 JDK API(Application Programming Interface)。 当你安装 JDK 时,其实不只是装了一个编译器(javac)或虚拟机(JVM),你还获得了一整套“开箱即用”的工具箱,包括: JVM 虚拟机:运行 Java 程序的核心引擎 可执行程序:如 java、javac、javadoc 等命令行工具 配置与文档:帮助你理解和使用这些工具 最重要的是:JDK 提供的 API 类库 —— 成千上万个已经写好、测试过、优化过的类! 这些类覆盖了时间处理、字符串操作、集合管理、网络通信、文件读写等几乎所有通用场景。 总之:我们需要做的就是,按照面向对象的开发思想,实现:认识对象,获取对象,调用对象的方法,做出我们相要的功能! 常用类 JDK 提供的 API 类库 —— 成千上万个已经写好、测试过、优化过的类,不需要考虑它怎么实现的,不需要写底层逻辑,只需要认识它,获取它,执行它的功能。(如,已知手机:认识手机,读取说明书 | 听取发布会,获取手机,使用手机) 认识它:是什么?主要概括,说明书 获取它:面向对象的开发思想是,获取对象才能够使用对象,如何获取? 执行它:获取的对象,它有哪些功能是可以帮我们实现快速开发的;尝试使用这些功能,为每个常用的小功能写一个 Demo。 java.lang 包 Object Object:Java 中所有类的“祖先”。无论你或我定义什么类,又或者今后某一天你看到的类(包括但不限于 JDK-API 提供的标准类),它们都默认继承自 Object。只要你用的是 Java 开发,使用了 Java 开发功能,那么任何方式出现的类“逃不掉当儿子/孙子的命运”。当然这也意味着继承部分“家产”,也就是所有对象天生就具备一些基本能力(继承来自 Object 的能力) 方法 由于这种继承关系的默认存在, 因此所有的对象都自动获得了 Object 类中定义的方法,比如: toString():返回对象的一个字符串表示形式(默认行为:打印 类名@内存地址),常用于打印对象信息。 默认:打印 类名@内存地址 实战痛点:默认的输出在日志里毫无意义,你根本看不出对象里的具体数据是多少。 实战做法:用于日志打印、调试,必须重写以提供有意义的信息,避免默认的 类名@哈希值。 equals(Object obj):判断当前对象是否等于另一个对象。 默认比较引用地址(==);业务中常需重写(如用户ID相同即视为同一人); hashCode():返回对象的哈希码值-根据对象的内存地址计算出一个整数。通常与 equals() 方法一起重写(由 Java 对象契约规定的)以支持基于哈希的数据结构(如 HashMap)。 必须与 equals() 保持一致:Java 对象契约规定:若 a.equals(b) 为 true,则 a.hashCode() == b.hashCode(); getClass():返回运行时类的 Class<?> 对象。返回的是实际运行时类型,不是声明类型. :::warning 如果你只重写了 equals() 而没有重写 hashCode(),就会违反 Object 类中 equals() 与 hashCode() 的通用契约,导致对象在基于哈希的集合(如 HashSet、HashMap、Hashtable)中行为异常——即使两个对象逻辑上“相等”(equals() 返回 true),也可能被当作“不同元素”存储,从而破坏集合的唯一性语义。 代码示例超纲:请在学习完成数据结构模块后,在进行回溯! ::: public class Person extends Object{ // 显式的继承 private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } // ❌ 忘记重写 hashCode()! } 当然这种继承关系,默认都是隐式的,我们自定义类的同时,可以选择显式的继承,不影响基本逻辑。 // 这里隐藏式继承Object public class Demo {// extends Object{ public static void main(String[] args) { System.out.println("Hello World!"); // 创建对象, 调用继承得到toString()方法 new Demo().toString(); Person p1 = new Person("Alice", 30); Person p2 = new Person("Alice", 30); System.out.println(p1.equals(p2)); // true ✅ Set<Person> set = new HashSet<>(); set.add(p1); // set.add(p2); // 本应去重,但实际会添加成功! // System.out.println(set.size()); // 输出 2 ❌(错误!) System.out.println(set.contains(p2)); // ❌ 输出 false! } } 为什么 contains(p2) 返回 false? 超纲:请在学习完成数据结构模块后,在进行回溯! 看**HashSet**** **怎么保存值,怎么判断值是否存在的?查源码! public class HashSet<E> ...略 private transient HashMap<E,Object> map; public HashSet() { map = new HashMap<>(); } // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean contains(Object o) { return map.containsKey(o); } ...略 public class HashMap<K,V> extends ...省略 public boolean containsKey(Object key) { return getNode(hash(key), key) != null; } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ...省略 HashSet**** 数据结构特点: 唯一、不重复、无序 底层确实是 ****HashMap - `new HashSet()`其内部实际执行:赋值操作,为全局变量 `map = new HshMap<>();` - `add(E e)` 方法内部调用:`map.put(e, PRESENT)`(`PRESENT` 是一个静态常量) - 所以 `HashSet` 本质是 **只用 key、忽略 value 的 ****HashMap** HashMap.put(key, value)** 的关键逻辑** - 首先计算 `key.hashCode()` → 确定桶(bucket)位置 - 如果该桶已有元素,则: * **先比较 ****hashCode()**** 是否相等** * **只有 hashCode 相等时,才调用 ****equals()**** 进一步判断是否重复** - `p1` 和 `p2` 的 `equals()` 返回 `true`(逻辑相等) - 但它们继承自 `Object` 的 `hashCode()` 返回不同随机值(默认基于内存地址) - `HashMap` 发现 `hashCode` 不同 → 直接认为是不同 key → 存入不同桶 → 去重失败 String String:用来表示文本信息。它不仅是一个“容器”,更是一个功能丰富的对象。String 是 Java 中用于表示不可变字符序列的类。它是 Java 最核心、使用频率最高的类之一。你可以把它理解为一个封装好的、安全的文本容器。与其他对象不同,String 有着独特的内存管理机制(常量池),这使得它在性能和安全性上都有出色的表现。由于其不可变性(一旦创建内容不可更改,所有“修改”操作-如 replace都返回新对象),它在多线程环境下是绝对安全的,常被用作Map的键(Key)或配置信息; String 是 Java 中的“特权阶级” 对于其他对象(比如 User、Order),你必须手动 new,因为 JVM 根本不知道你要创建什么样的对象、参数是多少。但 String 太常用了,为了让你写代码更爽、运行效率更高,Java 给它开了“后门”,提供了语法糖和常量池机制(减少频繁 new 消耗的内存,提升性能)。 String 变量名 = "内容"; 过程:"内容" 被称为字符串字面量(String Literal)。JVM 在编译代码时,就已经把双引号里的内容识别出来了,并提前放入了字符串常量池。 例子:String s = "abc"; —— 编译时,"abc" 就已经作为一个常量存在于 class 文件中了。 为什么不需要 new,它也是个对象? 核心原因:字符串常量池(**String Pool**),这是 String 不需要 **new** 的根本原因。其他对象:没有“对象常量池”这种机制(Java “后门”)。User、Order每次 new,JVM 都会在堆里造一个新房子(对象)。 String:JVM 维护了一个字符串常量池(一个特殊的缓存 Map)。当你写 String s = "abc"; 时,JVM 会先去池子里看有没有 "abc"。 如果有,直接把池子里那个对象的引用给你(复用)。如果没有,JVM 会在池子里自动 new 一个 "abc" 放进去,然后再把引用给你。 所以,你没写 new,不代表没有 new,是 JVM 替你偷偷在常量池里 new 了,并且还帮你缓存了。 “特权阶级”的不可变和常量池 底层原理:String 底层使用 private final char[](JDK 9+ 为 byte[])存储数据。final 关键字保证了数组引用不可变,且 String 类没有提供任何修改数组内容的公共方法。 System.identityHashCode(Object x) 返回的是 JVM 默认给这个对象分配的哈希码,如果传入 null,它会返回 0。它无视任何重写****(Override) public class Demo { public static void main(String[] args) { System.out.println("========== 1. 不可变性验证 =========="); // 1. 声明一个字符串 String str = "Hello"; System.out.println("初始对象地址: " + System.identityHashCode(str)); // 2. 尝试“修改”字符串(实际上是拼接) str = str + " World"; System.out.println("拼接后对象地址: " + System.identityHashCode(str)); System.out.println("\n========== 2. 常量池验证 (复用) =========="); String s1 = "Hello"; String s2 = "Hello"; // 字符串常量池复用 System.out.println("s1 == s2 (地址比较): " + (s1 == s2)); // true,同一个对象 System.out.println("\n========== 3. new 和 不new 的区别 =========="); // 不new (字面量):走常量池 String s3 = "XYZ"; // new (构造器):强制在堆中新建对象,无视常量池(但内容可能共享) String s4 = new String("XYZ"); System.out.println("s3 == s4 (地址比较): " + (s3 == s4)); // false,不同对象 System.out.println("s3.equals(s4): " + s3.equals(s4)); // true,内容相同 } } 先“理”后“兵” 理论上不允许修改,那是正常情况下,多数请款下没人闲着出来推翻理论,更何况有些生存规则,不是推翻了,就更好用的,结合实际而言,还是原规则好一些(除非有一天,你能发明或创造出更加有利的替代品),但如果你的探索欲大于你的理智,那么可以想一想,暴力修改行不行? 这是最有趣的一个验证。既然说 String 不可变,那我能不能绕过 Java 的规则,用反射去强行修改它的内部数组? public class Demo { public static void main(String[] args) { String s1 = "Hello"; String s2 = "Hello"; // 字符串常量池复用 // 1. 获取 String 类中的 value 字段(字符数组) Field valueField = String.class.getDeclaredField("value"); valueField.setAccessible(true); // 暴力访问 private 字段 // 2. 获取 s1 内部的字符数组 char[] value = (char[]) valueField.get(s1); // 3. 修改数组内容 value[0] = 'h'; // 把第一个字符 'H' 改成 'h' // 4. 观察结果 System.out.println("s1: " + s1); // 输出: hello 修改成功 System.out.println("s2: " + s2); // 输出: hello (卧槽,我也变了!)天杀的,s2不干净了。居然也变了 } } 验证结论****常量池的连锁反应 物理层面是可变的:JVM 内部的字符数组其实是可以被修改的。 逻辑层面是不可变的:正常业务代码中,我们无法获取到 value 字段,也无法修改它。String 类通过将 value 设为 private 且不提供修改方法,对外呈现出了“不可变”的特性。 常量池的副作用:因为 s1 和 s2 指向常量池中的同一个对象,你通过反射改了内容,所有引用它的变量都会受影响(这在生产环境是灾难性的,所以正常代码绝对禁止反射修改 String)。 方法 concat(String str)** 与 + / ****StringBuilder**字符串拼接。 * 少量拼接用 ****+:代码最简洁,编译器会自动优化。 * 循环内大量拼接用 StringBuilder,拼接几千次字符串,千万别用 + 或 concat,否则会创建成千上万个中间 String 对象(因为 String 不可变),导致内存飙升,直接用 StringBuilder 的 append 方法,最后 toString() 一下,性能提升百倍。 length():获取字符串长度(即字符个数)。 * 实战避坑:如果字符串对象为 null`,调用此方法直接报错。 substring(int beginIndex, int endIndex):截取子串(从beginIndex开始,到endIndex结束,包含开始,不包含结束)。 * 实战避坑:要注意下标越界 contains(CharSequence s):判断字符串是否包含指定序列(底层其实是调用indexOf() != -1); * → 致命缺陷/实战避坑:参数绝对不能为 null! 如果传入 null,底层会执行 null.toString(),直接抛出 NullPointerException (NPE)。这在处理外部不可控参数时极易导致服务崩溃 * 在实战中,永远不要直接用原生 contains。请使用 StringUtils.contains(str, keyword)****(Apache Commons Lang),它对 null 输入会安全地返回 false,不会抛异常。 replace(CharSequence target, CharSequence replacement):替换字符串中的某部分(将所有匹配的target替换为replacement)。 split(String regex):根据正则表达式分割字符串(返回一个String数组,如果分隔符在正则中有特殊含义,记得要转义,如分割点号要用\\.)。 * 实战避坑:参数是正则表达式,不是普通字符串! equals(Object anObject) | equalsIgnoreCase(String str):判断字符串内容是否完全相等。 * 绝对不要用 == 判断业务数据! == 判断的是地址,equals 判断的是内容; * 防空指针:如果不确定字符串是否为空,建议把“确定不为空的字符串常量”放在前面调用,例如 "admin".equals(username) * 验证码校验、不区分大小写的搜索时,直接用 equalsIgnoreCase trim() / strip():去除字符串首尾的空白字符(trim()是老方法,只认ASCII空格;strip()是Java11引入的新方法,能正确处理Unicode空格,实战推荐优先使用strip())。 * <font style="color:rgb(6, 10, 38);">trim()</font>如果遇到全角空格(中文空格),它去不掉。实战推荐优先使用<font style="color:rgb(6, 10, 38);">strip()</font> indexOf(String str) | lastIndexOf(String str):查找子串在字符串中第一次或最后一次出现的位置(如果找不到返回-1,实战中使用前务必判断是否为-1)。 startsWith(String prefix) / endsWith(String suffix):判断字符串是否以指定前缀开头或以指定后缀结尾(常用于判断文件类型、URL路由等)。 isBlank():判断字符串是否为空白(Java 11+新增,如果字符串为null、长度为0或全是空格,则返回true,是判空的终极利器)。替代 str == null || str.trim().isEmpty() StringBuilder StringBuilder 单线程字符串操作的性能之王。它是 Java 1.5 引入的可变字符序列,在确定没有多线程共享的场景下(绝大多数应用场景),应优先于 String 和 StringBuffer 使用。它的核心价值在于通过“可变”和“无线程安全”两大特性,解决了 String 拼接时产生的大量临时对象问题,是保障应用高性能、高可用的底层基石。 :::warning 关于线程,属于超出纲内容,可在熟悉线程后,再回溯关于StringBuilder的概述。 ::: StringBuilder | StringBuffer 可变性:底层维护一个可变的字符数组(char[] 或 byte[]),所有的修改操作(如追加、插入)都是直接在原数组上进行,不会像 String 那样创建新对象。 非线程安全:这是它与 StringBuffer 的唯一区别。它的所有方法都没有 synchronized关键字修饰,因此不能在多线程环境下共享使用,但这也让它拥有了最高的执行效率。 动态扩容:内部数组容量不足时会自动扩容(通常策略为 原容量 * 2 + 2)。虽然扩容会涉及数组拷贝,成本较高,但对开发者是透明的。 链式调用:几乎所有修改方法都返回 this(自身),支持将多个操作用点号连接起来,使代码更简洁、易读。 :::warning StringBuffer - 自查 超纲:synchronized- 关键字,同步锁的一中,锁方法,锁代码块! 简单理解:多人同时操作,同一时间,只支持一个人操作某块业务内容。详情-参考线程 ::: 方法 append(x):最核心的方法。将任意类型的数据转换为字符串后,追加到序列末尾。 insert(int offset, x):将任意类型的数据转换为字符串后,插入到指定索引 offset 处。 delete(int start, int end):删除从 start 索引开始到 end 索引(左闭右开)之间的字符。 deleteCharAt(int index):删除指定索引处的单个字符。 replace(int start, int end, String str):将从 start 到 end(左闭右开)的字符替换为指定字符串 str。 reverse():将字符序列原地反转。 toString():关键收尾动作。将可变的 StringBuilder 转换为不可变的 String 对象。注意,这会创建一个新的 String 对象。 capacity():返回当前内部缓冲区的总容量。 setLength(int newLength):设置字符序列的长度。可用于截断字符串,或用空字符填充。 实战中的坑 性能优化-预设初始容量 如果有大量数据需要拼接,甚至在循环拼接,那么在此之前不指定容量,StringBuilder会从默认容量(通常是16)开始,不够用,从而频繁触发扩容和数组拷贝。这会消耗大量 CPU 并产生垃圾对象,可能导致 Full GC,严重影响高可用性。如果能预估最终字符串的大致长度,务必在构造时指定初始容量,以减少扩容次数。 扩容虽然是智能的,但这个智能体,也需要做很多工作: 当你调用 append() 等方法时,JVM 首先会计算:当前已用字符数(count) + 新增字符数(len)。 计算结果>当前 StringBuilder(实际维护的是数组)当前容量;触发扩容,按公式计算新容量 JVM 会在堆内存中开辟一块新的连续空间,大小为“新容量”。 调用 Arrays.copyOf()(底层是 System.arraycopy),将旧数组里的所有字符原封不动地复制到新数组中。 让 StringBuilder 内部的 value 指针指向这个全新的数组,旧的数组因为没有引用指向它了,等待垃圾回收器(GC)在合适的时候将其回收。 空间不够了,就建个更大的新房子(N2+2)*,把旧家当全搬过去,扔掉旧房子。 转为字符串时需要调用的 toString() 可能是隐藏的“性能杀手” 如果有需要转字符串时,绕不开调用 toString() ,此时会出现共生问题,JVM 会根据当前字符序列的内容,创建一个新的 String 对象。如果 StringBuilder 里拼接了海量数据(如几百MB),调用 toString() 会瞬间申请等量的内存来存放副本,在程序未结束前,至少要满足>200MB 的内存来保证程序的稳定性。对于超大字符串,要谨慎调用 toString(),防止引发 OutOfMemoryError。 大对象场景(几 MB 到几百 MB)如果必须在内存中处理,考虑使用 CharBuffer 或 ByteBuffer,并尽量复用缓冲区。 绝对禁止多线程共享 StringBuilder 是线程不安全的。在 Web 服务中,绝不能将其定义为 Controller 或 Service 的成员变量供多个请求线程共享。 多线程同时操作会导致字符错乱、丢失,甚至抛出运行时异常。 多线程场景必须使用线程安全的 StringBuffer,或者使用 ThreadLocal<StringBuilder> 来为每个线程提供独立副本。 包装类 基本数据类型:short、byte、int...基本类型不具备面向对象的特性; Java“万物皆对象”,包装类应运而生。它们让基本类型也能拥有“对象的身份”,同时提供了类型转换、进制转换等实用工具。 Number Java 为 8 种基本数据类型各提供了一个对应的包装类。 其中Number 子类(数值型合计 6 个):Integer、Long、Short、Byte、Double、Float; Object 子类(非数值型 2 个):Boolean、Character。 不可变性 不可变性,这一特点,也被应用于所有包装类,一旦创建,其包装的值就不能被改变。任何“改变”操作都会返回一个新的包装类对象。 “坑”特别强调,注意,改变它,就是新的,需要正确引用,否则数值,可能产生意外,而且还是看不见错误的意外! 拆装箱 JDK 1.5 引入的语法糖。编译器可以自动在基本类型和包装类之间转换,让代码看起来更简洁。 Integer i = 10; // 装箱 int j = i; // 拆箱 方法 parseXxx(String s):最常用。将字符串转换为对应的基本类型(如 Integer.parseInt("123"))。如果字符串格式不正确,会抛出 NumberFormatException。 valueOf(String s):将字符串转换为包装类对象。它内部通常会调用 parseXxx,并且会利用缓存机制。 xxxValue():拆箱方法。将包装类对象转换回基本类型(如 Integer 对象调用 intValue())。 toString():将包装的数值转换为字符串表示。 toXxxString(xxx i):进制转换。如 Integer.toBinaryString(int i) 转换为二进制字符串,Long.toHexString(long i) 转换为十六进制字符串。 缓存机制(-128~127): 为了提高性能,Integer、Short、Byte、Long 等数值包装类内部维护了一个静态缓存池,缓存了 -128 到 127 之间的对象。在这个范围内的值,使用 valueOf() 或自动装箱时,总是返回缓存中的同一个对象。 想不到吧 空指针异常(NPE) 基本类型(如 int)默认值是 0,而包装类(如 Integer)默认值是 null。包装类对象使用前,务必判空,否则一旦出现 null 值,那么这将直接抛出 NullPointerException。 比较 永远不要用 == 比较两个包装类的值是否相等。务必使用 .equals() 方法。 Integer a = 127; Integer b = 127; System.out.println(a == b); // true。 Integer c = 128; Integer d = 128; System.out.println(c == d); // false。 == 比较的是引用地址。由于缓存机制,-128~127 之间的对象是同一个,所以 == 为 true;超出这个范围是新创建的对象,引用不同,== 为 false。 相关资料 ProcessOn Mindmap Java图 净重21克 - 博客园 愛していますササ-CSDN博客