Appearance
Java 核心类库与字符串处理
1. 概述
Java 核心类库(java.lang、java.util、java.time)是所有 Java 程序的基础支撑。Object 根类定义了对象的通用契约,String 是使用最频繁的不可变类,包装类连接了基本类型与泛型体系,java.time 提供了现代化的日期时间 API。熟练掌握这些类的特性、底层实现与常见陷阱,直接影响代码的正确性、性能与可维护性,也是面试中的高频考点。
本笔记覆盖范围: Object 核心方法 → String 与字符串处理 → 包装类与数字处理 → java.time 日期时间 API
2. 核心概念总览
mermaid
mindmap
root((核心类库))
Object
equals / hashCode 契约
toString
clone(浅拷贝/深拷贝)
finalize(已废弃)
String
不可变性
字符串常量池
StringBuilder / StringBuffer
intern() 方法
正则 API
包装类
自动装箱拆箱
缓存池(IntegerCache)
BigDecimal 精确计算
java.time
LocalDate / LocalTime / LocalDateTime
ZonedDateTime / Instant
DateTimeFormatter(线程安全)
时区与夏令时3. Object 类核心方法
java.lang.Object 是 Java 类继承体系的根类,所有类都隐式继承它。其核心方法构成了 Java 对象的基础契约。
3.1 equals 与 hashCode 契约
3.1.1 契约规则
| 规则 | 说明 |
|---|---|
| 自反性 | $x.equals(x)$ 必须返回 true |
| 对称性 | $x.equals(y) = true$ ⟺ $y.equals(x) = true$ |
| 传递性 | $x.equals(y)$ 且 $y.equals(z)$,则 $x.equals(z)$ |
| 一致性 | 对象未修改时,多次调用结果一致 |
| 非空性 | $x.equals(null)$ 必须返回 false |
| hashCode 一致性 | equals 相等 → hashCode 必须相同 |
| hashCode 碰撞 | hashCode 相同 → equals 不一定相等 |
⚠️ 违反契约的后果: 对象放入
HashMap/HashSet后可能找不到,产生难以排查的 Bug。
3.1.2 equals 重写的完整规范
java
public class User {
private String name;
private int age;
private String[] tags;
@Override
public boolean equals(Object o) {
// ① 引用相等,直接返回 true(性能优化)
if (this == o) return true;
// ② instanceof 判断类型兼容性(同时处理了 null 的情况)
if (!(o instanceof User)) return false;
// ③ 强转后逐字段比较
User user = (User) o;
return age == user.age // 基本类型用 ==
&& Objects.equals(name, user.name) // 对象用 Objects.equals() 避免 NPE
&& Arrays.equals(tags, user.tags); // 数组用 Arrays.equals()
}
@Override
public int hashCode() {
int result = Objects.hash(name, age);
result = 31 * result + Arrays.hashCode(tags);
return result;
}
}Java 14+ Record 类自动基于所有组件字段生成正确的
equals()、hashCode()和toString():javapublic record User(String name, int age) {} // 无需手动重写任何方法
3.1.3 hashCode 高质量实现
核心目标: 分布均匀,减少哈希碰撞。
java
// 推荐方式一:Objects.hash()(简洁)
@Override
public int hashCode() {
return Objects.hash(name, age, email);
}
// 推荐方式二:手动计算(性能更优,避免自动装箱)
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age; // 基本类型直接参与计算
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}为什么选 31 作为乘数?
- 31 是奇素数,碰撞概率低
- JIT 编译器可将
31 * i优化为(i << 5) - i(移位 + 减法),性能极高
String 的 hashCode 缓存机制:
java
// JDK 源码(简化)
public final class String {
private int hash; // 默认 0,首次计算后缓存
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
for (char v : value) {
h = 31 * h + v;
}
hash = h; // 缓存,因为 String 不可变所以安全
}
return h;
}
}3.1.4 hashCode 在 HashMap 中的工作原理
mermaid
flowchart LR
A["key.hashCode()"] --> B["二次哈希<br/>h ^ (h >>> 16)"]
B --> C["计算桶索引<br/>index = hash & (n-1)"]
C --> D{"桶是否为空?"}
D -- 是 --> E["直接放入"]
D -- 否 --> F{"equals 相等?"}
F -- 是 --> G["覆盖 value"]
F -- 否 --> H{"链表长度 ≥ 8?"}
H -- 否 --> I["追加到链表尾"]
H -- 是 --> J["转为红黑树<br/>O(log n) 查找"]关键参数:
| 参数 | 值 | 说明 |
|---|---|---|
| 默认容量 | 16 | 桶数组初始大小 |
| 加载因子 | 0.75 | 达到 $容量 \times 0.75$ 时扩容 |
| 树化阈值 | 8 | 链表长度 ≥ 8 且容量 ≥ 64 时转红黑树 |
| 退化阈值 | 6 | 红黑树节点 ≤ 6 时退化回链表 |
二次哈希的目的: 将高 16 位的信息混入低位,使得即使用户的 hashCode() 实现质量不高,也能获得相对均匀的桶分布。
3.2 toString、clone 与 finalize
| 方法 | 默认行为 | 正确使用方式 |
|---|---|---|
toString() | 返回 类名@十六进制hashCode | 重写为有意义的字符串(Lombok @ToString) |
clone() | 浅拷贝(需实现 Cloneable) | 深拷贝需手动递归 or 序列化 |
finalize() | GC 前执行,Java 9 已废弃 | 用 try-with-resources 替代 |
浅拷贝 vs 深拷贝:
java
public class Order implements Cloneable {
private String orderId;
private List<Item> items; // 引用类型
// 浅拷贝:items 引用指向同一个 List 对象
@Override
protected Order clone() throws CloneNotSupportedException {
return (Order) super.clone();
}
// 深拷贝:手动复制引用类型字段
public Order deepClone() throws CloneNotSupportedException {
Order cloned = (Order) super.clone();
cloned.items = new ArrayList<>();
for (Item item : this.items) {
cloned.items.add(item.clone()); // Item 也需实现 clone
}
return cloned;
}
}实战建议: 避免使用
clone(),优先使用拷贝构造函数或序列化(如 JSON 序列化/反序列化)实现对象复制。
4. String 类与字符串处理
4.1 String 不可变性与底层存储
java
// JDK 8 及之前
public final class String {
private final char[] value; // UTF-16,每个字符 2 字节
}
// JDK 9+(Compact Strings)
public final class String {
private final byte[] value; // Latin-1 字符用 1 字节,其他用 2 字节
private final byte coder; // LATIN1 = 0, UTF16 = 1
}不可变性的好处:
- 线程安全:无需同步即可在多线程间共享
- hashCode 缓存:计算一次后永久有效
- 安全性:作为
HashMap的 key、类加载路径、网络连接参数等不会被篡改 - 字符串常量池复用:相同字面量只存一份
4.2 字符串常量池(String Pool)
java
String a = "hello"; // 字面量 → 放入常量池
String b = "hello"; // 复用常量池中的同一对象
System.out.println(a == b); // true(同一引用)
String c = new String("hello"); // 在堆上创建新对象
System.out.println(a == c); // false(不同引用)
System.out.println(a.equals(c)); // true(内容相等)mermaid
graph TB
subgraph "方法区 / 堆(JDK 7+)"
Pool["字符串常量池<br/>'hello' 对象"]
end
subgraph "堆"
HeapObj["new String('hello')<br/>堆上新对象"]
end
a["引用 a"] --> Pool
b["引用 b"] --> Pool
c["引用 c"] --> HeapObj
HeapObj -.->|"内部 value 指向相同 char[]"| Pool4.3 intern() 方法与常量池位置变迁
java
String s1 = new String("abc"); // 堆对象
String s2 = s1.intern(); // 将 "abc" 放入常量池(如已存在则返回池中引用)
String s3 = "abc"; // 字面量,指向常量池
System.out.println(s2 == s3); // true
System.out.println(s1 == s3); // false| JDK 版本 | 常量池位置 | 特点 |
|---|---|---|
| JDK 6 | PermGen(永久代) | 大小固定(-XX:MaxPermSize),大量 intern() 易 OOM |
| JDK 7 | 堆中 | 受 -Xmx 控制,OOM 压力降低 |
| JDK 8+ | 堆中(元空间替代 PermGen) | -XX:StringTableSize 调整哈希桶数 |
适用场景: 大量重复字符串(如 XML 标签名、CSV 字段名)使用
intern()可显著降低内存。但滥用会导致常量池哈希表过大,反而影响性能。
4.4 字符串拼接的底层实现
java
// 场景一:编译期常量折叠
String s = "a" + "b" + "c"; // 编译后等价于 String s = "abc";
// 场景二:变量拼接(JDK 8)
String name = "World";
String greeting = "Hello, " + name + "!";
// 编译后等价于:
// new StringBuilder().append("Hello, ").append(name).append("!").toString();
// 场景三:循环内拼接(❌ 性能陷阱)
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // 每次循环创建新的 StringBuilder + toString() → O(n²)
}
// 正确方式 ✅
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i); // 复用同一个 StringBuilder → O(n)
}
String result = sb.toString();JDK 9+ StringConcatFactory 优化:
- 使用
invokedynamic指令,在运行时由 JVM 选择最优拼接策略 - 简单场景性能接近手写
StringBuilder - 但循环内拼接仍需手动使用
StringBuilder
4.5 StringBuilder vs StringBuffer
| 特性 | StringBuilder | StringBuffer |
|---|---|---|
| 线程安全 | ❌ 否 | ✅ 是(synchronized) |
| 性能 | 快 | 慢(同步开销) |
| 引入版本 | JDK 5 | JDK 1.0 |
| 推荐场景 | 单线程(绝大多数场景) | 多线程共享(极罕见) |
| 初始容量 | 16 | 16 |
| 扩容策略 | (oldCap << 1) + 2 | 同 StringBuilder |
实战经验: 多线程场景下字符串构建通常通过局部变量天然保证线程安全,
StringBuffer几乎没有使用场景。
4.6 String 常用 API 速查
java
String s = " Hello, World! ";
// 基础操作
s.length(); // 19
s.charAt(7); // 'W'
s.isEmpty(); // false(JDK 6+)
s.isBlank(); // false(JDK 11+,检测是否全为空白字符)
// 查找
s.indexOf("World"); // 9
s.lastIndexOf('l'); // 14
s.contains("Hello"); // true
s.startsWith(" He"); // true
s.endsWith("! "); // true
// 截取与替换
s.substring(2, 7); // "Hello"
s.replace('l', 'L'); // " HeLLo, WorLd! "
s.replaceAll("\\s+", ""); // "Hello,World!"(正则)
// 分割(注意特殊字符需转义)
"a.b.c".split("\\."); // ["a", "b", "c"]("." 在正则中是通配符)
"a,,b".split(",", -1); // ["a", "", "b"](-1 保留尾部空串)
// 去空白
s.trim(); // "Hello, World!"(去除 ASCII ≤ 32 的字符)
s.strip(); // "Hello, World!"(JDK 11+,去除 Unicode 空白)
s.stripLeading(); // "Hello, World! "
s.stripTrailing(); // " Hello, World!"
// 格式化
String.format("Name: %s, Age: %d", "Tom", 25); // "Name: Tom, Age: 25"
"Name: %s".formatted("Tom"); // JDK 15+
// 转换
s.toCharArray(); // char[]
s.getBytes(StandardCharsets.UTF_8); // byte[]
String.join(", ", "a", "b", "c"); // "a, b, c"
// 多行文本块(JDK 15+)
String json = """
{
"name": "Tom",
"age": 25
}
""";4.7 正则表达式最佳实践
java
// ❌ 反复编译 Pattern(String.matches() 内部每次调用都会 Pattern.compile)
for (String line : lines) {
if (line.matches("\\d{4}-\\d{2}-\\d{2}")) { ... }
}
// ✅ 预编译 Pattern,复用(性能提升 5~10 倍)
private static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
for (String line : lines) {
if (DATE_PATTERN.matcher(line).matches()) { ... }
}5. 包装类与数字处理
5.1 自动装箱拆箱与缓存池
java
// 自动装箱(基本类型 → 包装类)
Integer a = 100; // 编译器转换为 Integer.valueOf(100)
// 自动拆箱(包装类 → 基本类型)
int b = a; // 编译器转换为 a.intValue()
// ⚠️ 拆箱 NPE 陷阱
Integer c = null;
int d = c; // NullPointerException!IntegerCache 缓存池(-128 ~ 127):
java
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true(从缓存池取同一对象)
Integer m = 128;
Integer n = 128;
System.out.println(m == n); // false(超出缓存范围,new 了不同对象)
System.out.println(m.equals(n)); // true ✅ 正确方式| 包装类 | 缓存范围 |
|---|---|
Byte | -128 ~ 127(全部) |
Short | -128 ~ 127 |
Integer | -128 ~ 127(可通过 -XX:AutoBoxCacheMax 调整上限) |
Long | -128 ~ 127 |
Character | 0 ~ 127 |
Boolean | TRUE / FALSE(仅两个实例) |
Float / Double | 无缓存 |
⚠️ 黄金法则:包装类比较永远使用
equals(),不要用==。
5.2 包装类常用方法
java
// 类型转换
int a = Integer.parseInt("123"); // String → int
long b = Long.parseLong("999999999999"); // String → long
String s = String.valueOf(123); // int → String
String s2 = Integer.toString(123); // int → String
// 进制转换
Integer.toBinaryString(10); // "1010"
Integer.toHexString(255); // "ff"
Integer.toOctalString(8); // "10"
// 安全比较(防止溢出)
Integer.compare(x, y); // 返回 -1, 0, 1(不要用 x - y,可能溢出)
// 常量
Integer.MAX_VALUE; // 2147483647(约 2.1×10⁹)
Integer.MIN_VALUE; // -2147483648
Long.MAX_VALUE; // 9223372036854775807(约 9.2×10¹⁸)
// Stream 方法引用(Java 8+)
stream.reduce(0, Integer::sum);
stream.reduce(Integer::max);5.3 BigDecimal 精确计算
java
// ❌ 经典错误:double 构造
new BigDecimal(0.1); // 实际值:0.1000000000000000055511151231257827021181583404541015625
// ✅ 正确方式:String 构造 或 valueOf
new BigDecimal("0.1"); // 精确的 0.1
BigDecimal.valueOf(0.1); // 内部先转 String 再构造
// 四则运算
BigDecimal a = new BigDecimal("10.25");
BigDecimal b = new BigDecimal("3");
a.add(b); // 13.25
a.subtract(b); // 7.25
a.multiply(b); // 30.75
a.divide(b, 2, RoundingMode.HALF_UP); // 3.42(必须指定精度和舍入模式)
// ❌ 除不尽不指定舍入模式 → ArithmeticException
a.divide(b); // 抛异常!
// 比较大小
a.compareTo(b); // > 0(a > b)
// ⚠️ equals 的陷阱
new BigDecimal("2.0").equals(new BigDecimal("2.00")); // false!(scale 不同)
new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) == 0; // true ✅5.3.1 RoundingMode 舍入模式
| 模式 | 说明 | 示例(2.5 → ?) |
|---|---|---|
HALF_UP | 四舍五入(最常用) | 3 |
HALF_DOWN | 五舍六入 | 2 |
HALF_EVEN | 银行家舍入(向最近偶数舍入) | 2 |
UP | 远离零方向进位 | 3 |
DOWN | 向零方向截断 | 2 |
CEILING | 向正无穷方向 | 3 |
FLOOR | 向负无穷方向 | 2 |
实战建议: 金融系统应与业务方确认舍入规则,统一配置为常量,避免在代码各处随意指定。
6. 日期与时间(java.time)
6.1 旧 API 的痛点 vs 新 API 的优势
| 痛点 | 旧 API(Date/Calendar) | 新 API(java.time) |
|---|---|---|
| 可变性 | 可变(线程不安全) | 不可变(线程安全) |
| 月份 | 0~11(0 = 一月,bug 根源) | 1~12(符合直觉) |
| 日期时间区分 | Date 同时表示日期和时间 | LocalDate、LocalTime、LocalDateTime 职责清晰 |
| 格式化 | SimpleDateFormat(线程不安全) | DateTimeFormatter(线程安全) |
| 时区 | TimeZone(API 繁琐) | ZoneId(清晰直观) |
6.2 核心类体系
mermaid
graph TB
subgraph "无时区"
LD["LocalDate<br/>2025-01-15"]
LT["LocalTime<br/>14:30:00"]
LDT["LocalDateTime<br/>2025-01-15T14:30:00"]
end
subgraph "有时区"
ZDT["ZonedDateTime<br/>2025-01-15T14:30:00+08:00[Asia/Shanghai]"]
ODT["OffsetDateTime<br/>2025-01-15T14:30:00+08:00"]
end
subgraph "时间戳"
INS["Instant<br/>UTC 时间点(纳秒精度)"]
end
subgraph "时间量"
DUR["Duration<br/>时间段(小时/分/秒)"]
PER["Period<br/>日期段(年/月/日)"]
end
LD --> LDT
LT --> LDT
LDT -->|"atZone(ZoneId)"| ZDT
ZDT -->|"toInstant()"| INS
INS -->|"atZone(ZoneId)"| ZDT6.3 常用操作示例
java
// ===== 创建 =====
LocalDate today = LocalDate.now(); // 2025-07-11
LocalDate birthday = LocalDate.of(1995, 3, 15); // 1995-03-15
LocalTime now = LocalTime.now(); // 14:30:45.123
LocalDateTime dateTime = LocalDateTime.of(today, now);
ZonedDateTime shanghai = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
Instant timestamp = Instant.now(); // UTC 时间戳
// ===== 操作(返回新对象) =====
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate adjusted = today.withDayOfMonth(1); // 本月第一天
LocalDate lastDay = today.withDayOfMonth(today.lengthOfMonth()); // 本月最后一天
// ===== 计算间隔 =====
Period period = Period.between(birthday, today); // 30年4月...
Duration duration = Duration.between(startTime, endTime); // PT2H30M
long days = ChronoUnit.DAYS.between(birthday, today); // 总天数
// ===== 判断 =====
today.isBefore(birthday); // false
today.isAfter(birthday); // true
Year.of(2024).isLeap(); // true(闰年)6.4 DateTimeFormatter 格式化
java
// ✅ 线程安全,可声明为 static final 常量
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化
String str = LocalDateTime.now().format(FORMATTER); // "2025-07-11 14:30:00"
// 解析
LocalDateTime parsed = LocalDateTime.parse("2025-07-11 14:30:00", FORMATTER);
// ⚠️ 类型要匹配
// LocalDate.parse("2025-07-11 14:30:00", FORMATTER); // 异常!LocalDate 不含时间
// 预定义格式
DateTimeFormatter.ISO_LOCAL_DATE; // "2025-07-11"
DateTimeFormatter.ISO_LOCAL_DATE_TIME; // "2025-07-11T14:30:00"对比 SimpleDateFormat 的线程不安全问题:
java
// ❌ 多线程共享 SimpleDateFormat → 数据错乱或异常
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用 sdf.parse() / sdf.format() 会出问题
// 如果必须使用旧 API,用 ThreadLocal 包装
private static final ThreadLocal<SimpleDateFormat> SDF_LOCAL =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));6.5 时区处理与夏令时
java
// 常用时区
ZoneId shanghai = ZoneId.of("Asia/Shanghai"); // UTC+8
ZoneId utc = ZoneId.of("UTC"); // UTC
ZoneId newYork = ZoneId.of("America/New_York"); // UTC-5 / UTC-4(夏令时)
// 时区转换
ZonedDateTime shanghaiTime = ZonedDateTime.now(shanghai);
ZonedDateTime newYorkTime = shanghaiTime.withZoneSameInstant(newYork);
// ⚠️ 夏令时陷阱
// 美国东部:2025-03-09 02:00 → 03:00(春季跳过一小时)
// LocalDateTime 不感知时区,无法处理夏令时
// 跨夏令时边界计算时长应使用 Duration + Instant/ZonedDateTime
// 数据库存储建议:UTC 时间戳
Instant dbTimestamp = ZonedDateTime.now(shanghai).toInstant();6.6 新旧 API 互转
java
// Date ↔ Instant
Date date = new Date();
Instant instant = date.toInstant(); // Date → Instant
Date backToDate = Date.from(instant); // Instant → Date
// Date → LocalDateTime
LocalDateTime ldt = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
// LocalDateTime → Date
Date date2 = Date.from(
ldt.atZone(ZoneId.systemDefault()).toInstant()
);
// Calendar → ZonedDateTime
Calendar cal = Calendar.getInstance();
ZonedDateTime zdt = cal.toInstant().atZone(cal.getTimeZone().toZoneId());最佳实践: 新项目全面使用
java.time,仅在与旧 API(如 JDBCjava.sql.Timestamp)交互的边界做转换。
7. 最佳实践与常见坑
7.1 String 相关
| # | 坑 | 正确做法 |
|---|---|---|
| 1 | 循环内用 + 拼接字符串 | 循环外创建 StringBuilder,循环内 append() |
| 2 | "str" == anotherStr 比较字符串 | 使用 "str".equals(anotherStr)(常量放前面防 NPE) |
| 3 | split(".") 得到空数组 | split("\\.")(. 是正则通配符) |
| 4 | String.matches() 在循环中使用 | 预编译 Pattern 复用 |
| 5 | 忽略 substring() 的内存问题(JDK 6) | JDK 7+ 已修复,substring() 会创建新数组 |
7.2 包装类相关
| # | 坑 | 正确做法 |
|---|---|---|
| 1 | Integer == Integer 比较 | 使用 equals() 或 Integer.compare() |
| 2 | 拆箱 null 导致 NPE | 先判空:if (integerObj != null) |
| 3 | new BigDecimal(0.1) | 使用 new BigDecimal("0.1") 或 BigDecimal.valueOf(0.1) |
| 4 | BigDecimal.equals() 比较值 | 使用 compareTo() == 0 |
| 5 | divide() 不指定舍入模式 | 始终指定 scale 和 RoundingMode |
7.3 日期时间相关
| # | 坑 | 正确做法 |
|---|---|---|
| 1 | 多线程共享 SimpleDateFormat | 使用 DateTimeFormatter(线程安全) |
| 2 | Calendar.MONTH 从 0 开始 | 使用 LocalDate.of(2025, 1, 15)(1 = 一月) |
| 3 | 用 LocalDateTime 处理跨时区场景 | 使用 ZonedDateTime 或 Instant |
| 4 | 数据库存本地时间 | 存 UTC 时间戳,展示时转换 |
8. 面试高频问题
Q1:为什么重写 equals 必须同时重写 hashCode?
答: 因为 HashMap/HashSet 先用 hashCode() 定位桶,再用 equals() 确认键是否相同。如果两个 equals 相等的对象 hashCode 不同,它们会被分到不同的桶中,导致 HashMap 中 put 的键 get 不到。这违反了 Object 类中 hashCode 的通用契约。
Q2:String、StringBuilder、StringBuffer 的区别?
答:
- String:不可变,线程安全,每次修改创建新对象
- StringBuilder:可变,非线程安全,性能高,单线程首选
- StringBuffer:可变,线程安全(
synchronized),性能低,几乎不用
循环拼接场景必须用 StringBuilder,避免 $O(n^2)$ 复杂度。
Q3:new String("abc") 创建了几个对象?
答: 最多 2 个。首先检查常量池中是否存在 "abc",不存在则在常量池创建一个;然后在堆上 new 一个 String 对象。如果常量池已有 "abc",则只创建堆上的 1 个对象。
Q4:BigDecimal 的 equals 和 compareTo 有什么区别?
答: equals() 比较值和精度(scale),new BigDecimal("2.0").equals(new BigDecimal("2.00")) 返回 false(scale 分别是 1 和 2)。compareTo() 只比较数学值,返回 0 表示相等。业务中比较大小应使用 compareTo()。
Q5:为什么 SimpleDateFormat 不是线程安全的?而 DateTimeFormatter 是?
答: SimpleDateFormat 内部使用 Calendar 实例存储中间解析状态(可变状态),多线程并发调用会导致状态混乱。DateTimeFormatter 采用不可变设计,所有解析方法不依赖实例状态,天然线程安全,可声明为 static final 常量全局复用。
9. 总结
mermaid
graph LR
A["Object"] -->|"equals+hashCode"| B["HashMap/HashSet 正确性"]
C["String"] -->|"不可变"| D["线程安全 + 常量池复用"]
C -->|"拼接优化"| E["StringBuilder > +"]
F["包装类"] -->|"equals 比较"| G["避免 == 陷阱"]
F -->|"BigDecimal"| H["精确计算 + compareTo"]
I["java.time"] -->|"不可变"| J["线程安全"]
I -->|"DateTimeFormatter"| K["替代 SimpleDateFormat"]| 核心要点 | 一句话记忆 |
|---|---|
| equals/hashCode | 重写 equals 必须重写 hashCode,用 Objects.hash() |
| String 不可变性 | 线程安全、hashCode 可缓存、常量池可复用 |
| 字符串拼接 | 循环内用 StringBuilder,JDK 9+ 简单拼接自动优化 |
| 包装类比较 | 永远用 equals(),不要用 == |
| BigDecimal | 用 String 构造,compareTo() 比较,divide() 指定舍入 |
| 日期时间 | 全面使用 java.time,DateTimeFormatter 线程安全可复用 |
| 存储时间 | 数据库存 UTC 时间戳,展示层按时区转换 |