:::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 ),也可能被当作"不同元素"存储,从而破坏集合的唯一性语义。
代码示例超纲:请在学习完成数据结构模块后,在进行回溯!
:::
java
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()!
}
当然这种继承关系,默认都是隐式的,我们自定义类的同时,可以选择显式的继承,不影响基本逻辑。
java
// 这里隐藏式继承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**** **怎么保存值,怎么判断值是否存在的?查源码!
java
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);
}
...略
java
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)
java
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 的规则,用反射去强行修改它的内部数组?
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 引入的语法糖。编译器可以自动在基本类型和包装类之间转换,让代码看起来更简洁。
java
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() 方法。
java
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。