Java 核心四大基石:从 Object 源码到包装类陷阱的全维度复盘
让我们从两个常见的实际场景出发看看开发者会遇到什么困惑。场景一如何在程序中获取“当前时间”你一定见过这样的界面直播画面右上角显示2026 年 01 月 08 日 15:00:00实时更新这个时间不是写死的而是动态变化的并且和你电脑、手机上的系统时间完全一致。那么如果你正在开发一个直播系统、日志记录器或者一个简单的时钟应用怎么让你的程序也拿到这个“当前时间”难道要自己写一个MyTime类手动维护年、月、日、时、分、秒如果这样你怎么知道“现在到底是几点”你的程序又如何和操作系统的时间保持同步显然这不该由每个开发者从零实现。时间是通用需求必须有标准、可靠、高效的解决方案。场景二如何处理“文字信息”并实现关键词搜索再看另一个常见需求你想在程序中实现类似搜索引擎的功能。比如用户输入关键词 “Java” 和 “姑娘”你的程序能从一堆文章标题中找出包含这些词的内容例如《为什么 Java 开发没有姑娘》《Java 工程师的浪漫代码与她》那么问题来了这些“标题”是什么是字符串但字符串本身有没有“查找”“匹配”的能力如果我要判断一段文字是否包含“Java”是自己写循环逐个字符比对吗如果以后还要支持模糊搜索、正则匹配、中文分词……难道每次都要重写一套逻辑这显然不现实。文字处理是基础能力应该被封装成可复用的对象和方法。Java 的答案别重复造轮子用 API面对这些问题Java 的设计者早已替我们想好了——他们提供了一套强大、稳定、持续演进的标准类库也就是我们常说的 JDK APIApplication Programming Interface。当你安装 JDK 时其实不只是装了一个编译器javac或虚拟机JVM你还获得了一整套“开箱即用”的工具箱包括JVM 虚拟机运行 Java 程序的核心引擎可执行程序如 java、javac、javadoc 等命令行工具配置与文档帮助你理解和使用这些工具最重要的是JDK 提供的 API 类库 —— 成千上万个已经写好、测试过、优化过的类这些类覆盖了时间处理、字符串操作、集合管理、网络通信、文件读写等几乎所有通用场景。总之我们需要做的就是按照面向对象的开发思想实现认识对象获取对象调用对象的方法做出我们相要的功能常用类JDK 提供的 API 类库 —— 成千上万个已经写好、测试过、优化过的类不需要考虑它怎么实现的不需要写底层逻辑只需要认识它获取它执行它的功能。如已知手机认识手机读取说明书 | 听取发布会获取手机使用手机认识它是什么主要概括说明书获取它面向对象的开发思想是获取对象才能够使用对象如何获取执行它获取的对象它有哪些功能是可以帮我们实现快速开发的尝试使用这些功能为每个常用的小功能写一个 Demo。java.lang 包ObjectObjectJava 中所有类的“祖先”。无论你或我定义什么类又或者今后某一天你看到的类包括但不限于 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()}当然这种继承关系默认都是隐式的我们自定义类的同时可以选择显式的继承不影响基本逻辑。// 这里隐藏式继承Objectpublic 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 ✅ SetPerson 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…略private transient HashMapE,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(https://github.com/micbarefielda4/9tiv9rkr/issues/75) { return map.put(https://github.com/micbarefielda4/9tiv9rkr/issues/74)null; } public boolean contains(https://github.com/micbarefielda4/9tiv9rkr/issues/72) { return map.containsKey(https://github.com/micbarefielda4/9tiv9rkr/issues/73); } ...略public class HashMapK,V extends…省略public boolean containsKey(https://github.com/micbarefielda4/9tiv9rkr/issues/71) {return getNode(hash(key), key) ! null;}static final int hash(Object key) {int h;return (key null) ? 0 : (h key.hashCode()) ^ (h 16);}…省略HashSet**** 数据结构特点唯一、不重复、无序底层确实是 ****HashMapnew HashSet()其内部实际执行赋值操作为全局变量map new HshMap();add(E e)方法内部调用map.put(e, PRESENT)PRESENT是一个静态常量所以HashSet本质是只用 key、忽略 value 的HashMapHashMap.put(key, value)的关键逻辑首先计算key.hashCode()→ 确定桶bucket位置如果该桶已有元素则先比较hashCode()是否相等只有 hashCode 相等时才调用equals()进一步判断是否重复p1和p2的equals()返回true逻辑相等但它们继承自Object的hashCode()返回不同随机值默认基于内存地址HashMap发现hashCode不同 → 直接认为是不同 key → 存入不同桶 → 去重失败StringString用来表示文本信息。它不仅是一个“容器”更是一个功能丰富的对象。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每次 newJVM 都会在堆里造一个新房子对象。StringJVM 维护了一个字符串常量池一个特殊的缓存 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。它无视任何重写****Overridepublic 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)验证码校验、不区分大小写的搜索时直接用 equalsIgnoreCasetrim() / strip()去除字符串首尾的空白字符trim()是老方法只认ASCII空格strip()是Java11引入的新方法能正确处理Unicode空格实战推荐优先使用strip()。trim()如果遇到全角空格中文空格它去不掉。实战推荐优先使用strip()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()StringBuilderStringBuilder 单线程字符串操作的性能之王。它是 Java 1.5 引入的可变字符序列在确定没有多线程共享的场景下绝大多数应用场景应优先于 String 和 StringBuffer 使用。它的核心价值在于通过“可变”和“无线程安全”两大特性解决了 String 拼接时产生的大量临时对象问题是保障应用高性能、高可用的底层基石。:::warning关于线程属于超出纲内容可在熟悉线程后再回溯关于StringBuilder的概述。:::StringBuilder | StringBuffer可变性底层维护一个可变的字符数组char[] 或 byte[]所有的修改操作如追加、插入都是直接在原数组上进行不会像 String 那样创建新对象。非线程安全这是它与 StringBuffer 的唯一区别。它的所有方法都没有 synchronized关键字修饰因此不能在多线程环境下共享使用但这也让它拥有了最高的执行效率。动态扩容内部数组容量不足时会自动扩容通常策略为 原容量 * 2 2。虽然扩容会涉及数组拷贝成本较高但对开发者是透明的。链式调用几乎所有修改方法都返回 this自身支持将多个操作用点号连接起来使代码更简洁、易读。:::warningStringBuffer - 自查超纲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在合适的时候将其回收。空间不够了就建个更大的新房子N22*把旧家当全搬过去扔掉旧房子。转为字符串时需要调用的 toString() 可能是隐藏的“性能杀手”如果有需要转字符串时绕不开调用 toString() 此时会出现共生问题JVM 会根据当前字符序列的内容创建一个新的 String 对象。如果 StringBuilder 里拼接了海量数据如几百MB调用 toString() 会瞬间申请等量的内存来存放副本在程序未结束前至少要满足200MB 的内存来保证程序的稳定性。对于超大字符串要谨慎调用 toString()防止引发 OutOfMemoryError。大对象场景几 MB 到几百 MB如果必须在内存中处理考虑使用 CharBuffer 或 ByteBuffer并尽量复用缓冲区。绝对禁止多线程共享StringBuilder 是线程不安全的。在 Web 服务中绝不能将其定义为 Controller 或 Service 的成员变量供多个请求线程共享。多线程同时操作会导致字符错乱、丢失甚至抛出运行时异常。多线程场景必须使用线程安全的 StringBuffer或者使用 ThreadLocal 来为每个线程提供独立副本。包装类基本数据类型short、byte、int…基本类型不具备面向对象的特性Java“万物皆对象”包装类应运而生。它们让基本类型也能拥有“对象的身份”同时提供了类型转换、进制转换等实用工具。NumberJava 为 8 种基本数据类型各提供了一个对应的包装类。其中Number 子类数值型合计 6 个Integer、Long、Short、Byte、Double、FloatObject 子类非数值型 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) 转换为十六进制字符串。缓存机制-128127为了提高性能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。 比较的是引用地址。由于缓存机制-128127 之间的对象是同一个所以 为 true超出这个范围是新创建的对象引用不同 为 false。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2451606.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!