Skip to content

第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)都返回新对象

为什么设计成不可变

  1. 线程安全:不可变对象天然可被多线程共享,无需同步。
  2. hashCode 可缓存:String 频繁用于 HashMap 的 key,不可变使 hashCode 可在首次计算后缓存,提升性能。
  3. 安全性:String 作类加载器参数、数据库连接 URL,不可变防止被篡改。
  4. 字符串常量池:不可变才能让多个引用安全指向同一常量池对象(见下)。

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

特性StringBuilderStringBuffer
线程安全✅(方法 synchronized)
性能
出现版本JDK 5JDK 1.0
推荐单线程(绝大多数)多线程共享同一实例时

绝大多数场景用 StringBuilder——单线程下同步是纯开销。即使多线程也优先考虑无共享设计,而非 StringBuffer。


📖 三、包装类

基本类型与包装类对应

基本类型包装类默认值(包装)
byte/short/int/longByte/Short/Integer/Longnull
float/doubleFloat/Doublenull
charCharacternull
booleanBooleannull

自动装箱拆箱

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.hstruct tmtime()java.time 不可变、API 丰富、带时区
数学math.h 函数Math 静态方法

对 C 程序员:Java 把字符串/日期等都对象化、不可变化、自动管理内存,代价是性能略低但安全性与开发效率大幅提升。注意 String 不可变与 C 字符串可原地修改的根本差异。


💡 九、最佳实践

  1. 字符串内容比较用 equals,勿用 ==。
  2. 循环拼接用 StringBuilder,单线程优先于 StringBuffer。
  3. Integer/Long 比较用 equals,警惕 -128~127 缓存假象。
  4. 日期时间用 java.time,弃用 Date/SimpleDateFormat/Calendar。
  5. DateTimeFormatter 可 static 共享(线程安全),SimpleDateFormat 不可。
  6. 包装类拆箱前判空,防 NPE。
  7. 多线程随机用 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 道题:

  1. String 反转 / 判回文
  2. StringBuilder 拼接性能对比
  3. Integer 缓存陷阱
  4. LocalDateTime 计算年龄
  5. Random vs ThreadLocalRandom
  6. 综合:身份证号解析(地区/生日/性别)

完成后对比 答案/Sol12.java,查看逐行讲解与多解法。


🎓 下一步

  • 第13课:异常处理 — try-catch-finally、throws/throw、自定义异常、异常链