Appearance
第12课:常用类
🎯 学习目标
- 理解 String 的不可变性原理与字符串常量池机制
- 掌握 StringBuilder vs StringBuffer 的选择
- 理解包装类、自动装箱拆箱、Integer 缓存陷阱
- 掌握 java.time(JDK 8+)现代日期时间 API
- 识别常见陷阱(
==比较字符串、SimpleDateFormat 线程不安全、Integer 缓存)
📖 一、String 类
1. String 的不可变性
java
String s = "Hello";
s = s + " World"; // 看似修改 s,实际创建了新对象,原 "Hello" 不变原理:String 内部用 private final char[](JDK 9+ 改为 byte[] + coder 标记,节省内存)存储字符。final 修饰数组引用、类本身也是 final(不能被继承、字段不可改),所以 String 对象一旦创建内容就不可变。所有"修改"方法(concat/replace/substring)都返回新对象。
为什么设计成不可变:
- 线程安全:不可变对象天然可被多线程共享,无需同步。
- hashCode 可缓存:String 频繁用于 HashMap 的 key,不可变使 hashCode 可在首次计算后缓存,提升性能。
- 安全性:String 作类加载器参数、数据库连接 URL,不可变防止被篡改。
- 字符串常量池:不可变才能让多个引用安全指向同一常量池对象(见下)。
2. 字符串常量池(String Pool)
java
String a = "hello"; // 常量池中的字面量
String b = "hello"; // 复用常量池同一对象
String c = new String("hello"); // 堆中新对象 + 常量池(若不存在)
String d = c.intern(); // 把 c 指向常量池对象
System.out.println(a == b); // true(同一常量池对象)
System.out.println(a == c); // false(c 是堆新对象)
System.out.println(a == d); // true(intern 后指向常量池)内存图:
栈 堆 字符串常量池(堆中,JDK7+)
a ─────────────┐ ┌──────────┐ ┌─────────┐
b ─────────────┼──→ │ "hello"? │ No │ "hello" │ ← a, b, d 指向
d ─────────────┘ │ (新)? │ └─────────┘
└──────────┘
c ────────────────→ new String("hello") 在堆关键:new String("hello") 会创建堆对象,但"hello"字面量仍在常量池。intern() 主动把字符串放入/返回常量池对象。
3. String 常用方法
java
String s = "Hello World";
s.length(); // 11
s.charAt(0); // 'H'
s.substring(0,5); // "Hello"
s.indexOf("World"); // 6
s.contains("World"); // true
s.replace("World","Java");// "Hello Java"
"a,b,c".split(","); // ["a","b","c"]
s.toUpperCase(); // "HELLO WORLD"
" hi ".trim(); // "hi"
s.equals("Hello World"); // true(比较内容,勿用 ==)
s.equalsIgnoreCase("hello world"); // true📖 二、StringBuilder / StringBuffer
为什么需要
java
String result = "";
for (int i = 0; i < 10000; i++) result += i; // ❌ 每次创建新 String + 临时 StringBuilder+= 在循环里每次都创建新的 String 对象和临时 StringBuilder,O(n²) 开销。用 StringBuilder 是 O(n):
java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) sb.append(i);
String result = sb.toString();StringBuilder vs StringBuffer
| 特性 | StringBuilder | StringBuffer |
|---|---|---|
| 线程安全 | ❌ | ✅(方法 synchronized) |
| 性能 | 快 | 慢 |
| 出现版本 | JDK 5 | JDK 1.0 |
| 推荐 | 单线程(绝大多数) | 多线程共享同一实例时 |
绝大多数场景用 StringBuilder——单线程下同步是纯开销。即使多线程也优先考虑无共享设计,而非 StringBuffer。
📖 三、包装类
基本类型与包装类对应
| 基本类型 | 包装类 | 默认值(包装) |
|---|---|---|
| byte/short/int/long | Byte/Short/Integer/Long | null |
| float/double | Float/Double | null |
| char | Character | null |
| boolean | Boolean | null |
自动装箱拆箱
java
Integer n = 10; // 自动装箱 = Integer.valueOf(10)
int v = n; // 自动拆箱 = n.intValue()
int parsed = Integer.parseInt("123"); // 字符串转 int
String s = String.valueOf(123); // int 转字符串装箱原理:Integer.valueOf(int) 而非 new Integer()——这引出下面的缓存陷阱。
📖 四、Math 类
java
Math.PI; Math.E;
Math.abs(-10); // 10
Math.max(10,20); Math.min(10,20);
Math.pow(2,3); // 8.0
Math.sqrt(16); // 4.0
Math.round(3.5); // 4(四舍五入,返回 long)
Math.ceil(3.1); // 4.0(向上取整)
Math.floor(3.9); // 3.0(向下取整)
Math.random(); // [0.0, 1.0) 双精度Math.random() 内部用 new java.util.Random(),频繁调用且需可控随机时直接用 Random 或 ThreadLocalRandom。
📖 五、Random
java
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
Random r = new Random();
r.nextInt(100); // [0,100)
r.nextDouble(); // [0.0,1.0)
// 多线程推荐 ThreadLocalRandom(无竞争,更快)
ThreadLocalRandom.current().nextInt(0, 100);Random vs ThreadLocalRandom:Random 用 CAS 保证线程安全,多线程竞争同一实例有性能损耗;ThreadLocalRandom 每个线程独立实例,无竞争,多线程下首选。
📖 六、日期时间
旧 API(已过时,了解即可)
java.util.Date:设计糟糕,年份从 1900 偏移、月份 0 起、可变不线程安全。SimpleDateFormat:线程不安全(内部用 Calendar 共享状态),多线程下解析会出错。Calendar:繁冗、可变、月份 0 起。
现代 API(JDK 8+ java.time,推荐)
java
import java.time.*;
import java.time.format.DateTimeFormatter;
LocalDate today = LocalDate.now(); // 2026-06-21
LocalTime now = LocalTime.now(); // 10:30:45.123
LocalDateTime dt = LocalDateTime.now();
// 格式化/解析(DateTimeFormatter 线程安全,可共享)
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String s = dt.format(f); // "2026-06-21 10:30:45"
LocalDateTime parsed = LocalDateTime.parse("2026-06-21 10:30:45", f);
// 不可变操作(返回新对象)
LocalDate tomorrow = today.plusDays(1);
LocalDate nextMonth = today.plusMonths(1);
Period age = Period.between(LocalDate.of(2000,1,1), today); // P26Y5M20D
// 时间戳/时长
Instant inst = Instant.now(); // UTC 时间戳
Duration d5 = Duration.between(time1, time2);java.time 优点:不可变(线程安全)、API 清晰(LocalDate 日期 / LocalTime 时间 / LocalDateTime 日期时间 / ZonedDateTime 带时区 / Instant 时间戳)、DateTimeFormatter 线程安全可共享。
⚠️ 七、常见陷阱
陷阱1:用 == 比较字符串
java
String a = "hello"; String b = "he" + "llo"; // b 编译期折叠为 "hello"
System.out.println(a == b); // true(常量折叠)
String c = "he"; String d = c + "llo";
System.out.println(a == d); // false(c 是变量,运行期拼接,结果在堆)
System.out.println(a.equals(d)); // true结论:比较字符串内容永远用 equals,绝不依赖 ==。== 比较的是引用,因常量池/折叠偶尔"成立"是巧合,不可靠。
陷阱2:Integer 缓存
java
Integer a = 100, b = 100; System.out.println(a == b); // true(缓存 -128~127)
Integer c = 200, d = 200; System.out.println(c == d); // false(超出缓存,是不同对象)原理:Integer.valueOf(int) 对 -128~127 返回缓存的同一对象(IntegerCache,范围可由 -XX:AutoBoxCacheMax 调上限)。超出范围则 new 新对象。所以 Integer 之间用 == 比较结果不确定,必须用 equals。
陷阱3:SimpleDateFormat 线程不安全
java
// ❌ 多线程共享一个 SimpleDateFormat,parse 结果错乱
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");规避:用 DateTimeFormatter(线程安全,可 static 共享),或每次 new SimpleDateFormat。
陷阱4:自动拆箱 NPE
java
Integer n = null;
int v = n; // ❌ NullPointerException(n 为 null 时拆箱)规避:包装类可能为 null,拆箱前判空。
陷阱5:循环里用 += 拼接字符串
见第二节,用 StringBuilder。
🆚 八、Java vs C 对比
| 特性 | C 语言 | Java |
|---|---|---|
| 字符串 | char* / char[],手动管理内存、易溢出 | String 不可变对象,自动 GC、安全 |
| 字符串拼接 | strcat(原地改,需保证容量) | +(编译器优化为 StringBuilder) |
| 数值包装 | 无(int 就是 int) | Integer 等包装类,支持 null、缓存、集合泛型 |
| 随机数 | rand()(需 srand 播种) | Random / ThreadLocalRandom |
| 日期时间 | time.h 的 struct tm、time() | java.time 不可变、API 丰富、带时区 |
| 数学 | math.h 函数 | Math 静态方法 |
对 C 程序员:Java 把字符串/日期等都对象化、不可变化、自动管理内存,代价是性能略低但安全性与开发效率大幅提升。注意 String 不可变与 C 字符串可原地修改的根本差异。
💡 九、最佳实践
- 字符串内容比较用 equals,勿用 ==。
- 循环拼接用 StringBuilder,单线程优先于 StringBuffer。
- Integer/Long 比较用 equals,警惕 -128~127 缓存假象。
- 日期时间用 java.time,弃用 Date/SimpleDateFormat/Calendar。
- DateTimeFormatter 可 static 共享(线程安全),SimpleDateFormat 不可。
- 包装类拆箱前判空,防 NPE。
- 多线程随机用 ThreadLocalRandom。
📖 十、BigDecimal
金额和精确小数应使用 BigDecimal。
java
BigDecimal price = new BigDecimal("19.99");
BigDecimal count = new BigDecimal("3");
BigDecimal total = price.multiply(count);不要这样写:
java
BigDecimal bad = new BigDecimal(0.1);原因是 0.1 作为 double 已经有二进制误差。
除法要指定舍入:
java
BigDecimal result = new BigDecimal("10")
.divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP);如果除不尽且不指定舍入,会抛 ArithmeticException。
📖 十一、Objects 工具类
java.util.Objects 能简化 null 安全操作。
java
Objects.equals(a, b);
Objects.hash(name, age);
Objects.requireNonNull(user, "user must not be null");常用场景:
text
equals 方法中比较字段。
hashCode 方法中生成哈希。
方法入口参数校验。
避免 a.equals(b) 中 a 为 null 导致 NPE。示例:
java
if (Objects.equals(input, "yes")) {
System.out.println("confirmed");
}📖 十二、UUID
UUID 常用于生成全局唯一标识。
java
String id = UUID.randomUUID().toString();特点:
text
生成方便。
冲突概率极低。
字符串较长。
随机 UUID 对数据库索引不够友好。适合:
text
请求追踪 ID。
临时文件名。
分布式场景的外部标识。如果是数据库主键,高并发写入场景可能要考虑雪花 ID、数据库自增、ULID 等方案。
📖 十三、常用类选型清单
| 需求 | 推荐 |
|---|---|
| 文本内容 | String |
| 循环拼接 | StringBuilder |
| 多线程共享拼接 | 尽量避免共享,必要时 StringBuffer |
| 金额计算 | BigDecimal |
| null 安全比较 | Objects.equals |
| 日期时间 | java.time |
| 多线程随机数 | ThreadLocalRandom |
| 唯一标识 | UUID 或业务 ID 生成器 |
这张表能解决大部分初学阶段“该用哪个类”的问题。
🛠 十四、常用类排查清单
常见问题:
text
String 用 == 比较内容。
循环中使用 += 拼接字符串。
Integer 用 == 比较。
包装类自动拆箱 NPE。
SimpleDateFormat 被多个线程共享。
BigDecimal 用 double 构造。
BigDecimal 除法未指定舍入。
Random 在高并发下竞争。优先修复:
text
字符串用 equals。
循环拼接用 StringBuilder。
包装类比较用 equals。
可能为 null 的包装类拆箱前检查。
日期时间使用 java.time。
金额使用 BigDecimal(String)。✅ 十五、掌握标准
学完本课后,应能做到:
text
能解释 String 不可变和字符串常量池。
能区分 StringBuilder 和 StringBuffer。
能说明 Integer 缓存和自动拆箱 NPE。
能使用 Math、Random、ThreadLocalRandom。
能使用 java.time 处理日期时间。
能用 BigDecimal 处理金额。
能使用 Objects 做 null 安全比较。
能知道 UUID 的适用场景和局限。常用类看似零散,但它们是日常开发最高频的基础工具。熟悉这些类能减少大量低级 bug。
📝 练习预告
完成 练习/Ex12_CommonClasses.java 中的 6 道题:
- String 反转 / 判回文
- StringBuilder 拼接性能对比
- Integer 缓存陷阱
- LocalDateTime 计算年龄
- Random vs ThreadLocalRandom
- 综合:身份证号解析(地区/生日/性别)
完成后对比 答案/Sol12.java,查看逐行讲解与多解法。
🎓 下一步
- 第13课:异常处理 — try-catch-finally、throws/throw、自定义异常、异常链