常用类与工具类
Java 提供了许多常用类,其中 String 和 Object 是最核心的两个类。理解它们的特性和底层原理,是 Java 面试的必考内容。
一、String 类
1.1 String 的特性
String 是 Java 中最常用的类,用于表示字符串。它有以下核心特性:
| 特性 | 说明 |
|---|---|
| 不可变性 | String 对象一旦创建,值就不能被修改 |
| final 类 | String 类被 final 修饰,不能被继承 |
| 实现接口 | Serializable、Comparable<String>、CharSequence |
| 存储方式 | JDK 8:char[];JDK 9+:byte[] + 编码标记 |
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// JDK 8:使用 char[] 存储
// private final char[] value;
// JDK 9+:使用 byte[] 存储,节省空间
private final byte[] value;
private final byte coder; // 编码标记(LATIN1 或 UTF16)
}1.2 String 的不可变性
不可变(Immutable) 是指字符串对象创建后,其内容不能被修改。
String s = "hello";
s = s + " world"; // 看起来是修改,实际是创建了新对象
// 原来的 "hello" 对象仍然存在,s 指向新的 "hello world" 对象为什么设计为不可变?
| 原因 | 说明 |
|---|---|
| 字符串常量池 | 多个引用可以指向同一个字符串对象,节省内存 |
| 线程安全 | 不可变对象天然线程安全,无需同步 |
| HashCode 缓存 | HashCode 可以缓存,提高 HashMap 等集合的效率 |
| 安全性 | 防止被篡改,如类名、文件路径、网络地址等 |
// 字符串常量池示例
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true,指向常量池同一对象
// 如果 String 可变,s1 修改会影响 s2,这是不安全的1.3 String 的创建方式
方式一:字面量
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true(常量池)方式二:new 关键字
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s3 == s4); // false(堆中不同对象)
System.out.println(s3.intern() == s1); // true(intern() 返回常量池引用)内存结构对比
字面量方式:
┌─────────────────┐
│ 栈(引用) │
│ s1 ────────────┼──→ ┌─────────────────┐
│ s2 ────────────┼──→ │ 字符串常量池 │
└─────────────────┘ │ "hello" 对象 │
└─────────────────┘
new 方式:
┌─────────────────┐ ┌─────────────────┐
│ 栈(引用) │ │ 堆内存 │
│ s3 ────────────┼──→ │ String 对象1 │
│ s4 ────────────┼──→ │ String 对象2 │
└─────────────────┘ └────────┬────────┘
│
┌────────↓────────┐
│ 字符串常量池 │
│ "hello" 对象 │
└─────────────────┘1.4 字符串常量池
字符串常量池(String Pool)是 JVM 为了节省内存而设计的特殊区域。
位置变化
| JDK 版本 | 位置 |
|---|---|
| JDK 6 及之前 | 方法区(永久代) |
| JDK 7 | 堆内存 |
| JDK 8+ | 堆内存(元空间取代永久代,但字符串池仍在堆) |
intern() 方法
intern() 是一个 native 方法,作用是:
- 如果常量池中已存在该字符串,返回常量池中的引用
- 如果不存在,将字符串添加到常量池,并返回引用
String s1 = new String("hello");
String s2 = s1.intern(); // 将 "hello" 加入常量池
String s3 = "hello";
System.out.println(s1 == s2); // false(s1 在堆,s2 在常量池)
System.out.println(s2 == s3); // true(都指向常量池)
// JDK 6 vs JDK 7 的区别
String s4 = new String("a") + new String("b");
s4.intern();
String s5 = "ab";
System.out.println(s4 == s5);
// JDK 6:false(intern() 复制到常量池)
// JDK 7+:true(intern() 直接引用堆中对象)1.5 String 常用方法
String s = "Hello, World!";
// 长度
s.length(); // 13
// 获取字符
s.charAt(0); // 'H'
// 截取
s.substring(7); // "World!"
s.substring(0, 5); // "Hello"
// 查找
s.indexOf("World"); // 7
s.lastIndexOf("o"); // 8
s.contains("World"); // true
s.startsWith("Hello"); // true
s.endsWith("!"); // true
// 大小写转换
s.toUpperCase(); // "HELLO, WORLD!"
s.toLowerCase(); // "hello, world!"
// 去除空白
" hello ".trim(); // "hello"
// 替换
s.replace("World", "Java"); // "Hello, Java!"
s.replaceAll("\\s+", "-"); // 正则替换
// 分割
"a,b,c".split(","); // ["a", "b", "c"]
// 拼接
String.join("-", "a", "b", "c"); // "a-b-c"
// 比较
"hello".equals("HELLO"); // false
"hello".equalsIgnoreCase("HELLO"); // true
"abc".compareTo("abd"); // -1(字典序比较)
// 空判断
String.isEmpty(s); // false(JDK 6+)
String.isBlank(" "); // true(JDK 11+,空白也算空)
// 类型转换
String.valueOf(123); // "123"
String.valueOf(true); // "true"
Integer.parseInt("123"); // 1231.6 String 拼接方式
| 方式 | 性能 | 适用场景 |
|---|---|---|
+ 拼接 | 低(大量循环时) | 少量拼接 |
concat() | 中 | 少量拼接 |
StringBuilder | 高 | 大量拼接、单线程 |
StringBuffer | 高 | 大量拼接、多线程 |
String.join() | 中 | 集合/数组拼接 |
// 1. + 拼接(编译器优化为 StringBuilder)
String s = "a" + "b" + "c"; // 编译后:String s = "abc";
String s = "a" + b + "c"; // 编译后:new StringBuilder().append("a").append(b).append("c")
// ⚠️ 循环中使用 + 效率低
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // 每次循环都创建新的 StringBuilder 和 String
}
// ✅ 使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
// 2. concat()
"hello".concat(" world"); // "hello world"
// 3. StringBuilder(推荐)
StringBuilder sb = new StringBuilder();
sb.append("hello").append(" ").append("world");
String s = sb.toString();
// 4. StringBuffer(线程安全)
StringBuffer sb = new StringBuffer();
sb.append("hello");1.7 常见面试题
Q1: String s = new String("abc") 创建了几个对象?
A:
- 1 个或 2 个:
- 如果常量池中没有 "abc":创建 2 个(堆中 1 个 + 常量池 1 个)
- 如果常量池中已有 "abc":创建 1 个(堆中 1 个)
Q2: String 为什么设计为不可变?
A:
- 字符串常量池:节省内存,多个引用可共享同一对象
- 线程安全:不可变对象天然线程安全
- HashCode 缓存:HashCode 可缓存,提高集合效率
- 安全性:防止敏感数据被篡改
Q3: String、StringBuilder、StringBuffer 的区别?
A:
| 对比项 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | 不安全 | 安全(synchronized) |
| 性能 | 最低 | 最高 | 中等 |
| 适用场景 | 少量字符串 | 单线程大量操作 | 多线程大量操作 |
Q4: 以下代码输出什么?
String s1 = "hello";
String s2 = "he" + new String("llo");
System.out.println(s1 == s2);A: 输出 false。new String("llo") 在堆中创建对象,"he" + 堆对象 结果也在堆中,不会进入常量池。
Q5: 如何判断两个字符串是否相等?
A: 使用 equals() 方法,不要用 ==。
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false(比较地址)
System.out.println(s1.equals(s2)); // true(比较内容)二、StringBuilder 与 StringBuffer
2.1 StringBuilder
StringBuilder 是可变的字符串序列,JDK 5 引入,用于解决 String 拼接性能问题。
// 继承关系
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence核心方法
StringBuilder sb = new StringBuilder();
// 追加
sb.append("hello");
sb.append(' ');
sb.append(123); // 支持多种类型
// 插入
sb.insert(5, ", world"); // 在位置 5 插入
// 删除
sb.delete(5, 12); // 删除 [5, 12) 范围
sb.deleteCharAt(0); // 删除指定位置
// 替换
sb.replace(0, 5, "hi");
// 反转
sb.reverse();
// 设置长度
sb.setLength(0); // 清空
// 转换为 String
String s = sb.toString();扩容机制
// 初始容量 16
StringBuilder sb = new StringBuilder();
// 指定初始容量
StringBuilder sb = new StringBuilder(100);
// 扩容公式:(旧容量 * 2) + 2
// 如:16 → 34 → 70 → 142 ...2.2 StringBuffer
StringBuffer 是 StringBuilder 的线程安全版本,所有方法都加了 synchronized。
public synchronized StringBuffer append(String str) { ... }
public synchronized StringBuffer insert(int offset, String str) { ... }
// 所有 public 方法都加了 synchronized2.3 性能对比
// 性能测试
int count = 100000;
// String:极慢
String s = "";
for (int i = 0; i < count; i++) {
s += i;
}
// StringBuilder:最快
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(i);
}
// StringBuffer:稍慢(同步开销)
StringBuffer sb = new StringBuffer();
for (int i = 0; i < count; i++) {
sb.append(i);
}2.4 使用建议
| 场景 | 推荐 |
|---|---|
| 少量字符串操作 | String |
| 大量字符串操作(单线程) | StringBuilder |
| 大量字符串操作(多线程) | StringBuffer |
| 方法链式调用 | StringBuilder |
// 链式调用
String result = new StringBuilder()
.append("Name: ")
.append(name)
.append(", Age: ")
.append(age)
.toString();三、Object 类
3.1 Object 类概述
Object 是 Java 所有类的根类,每个类都直接或间接继承自 Object。
public class Object {
// 共 11 个方法(不含构造器)
}3.2 Object 类的方法
| 方法 | 说明 |
|---|---|
getClass() | 返回对象的运行时类(Class 对象) |
hashCode() | 返回对象的哈希码 |
equals(Object obj) | 判断是否与另一个对象相等 |
clone() | 创建并返回对象的副本 |
toString() | 返回对象的字符串表示 |
notify() | 唤醒等待该对象锁的一个线程 |
notifyAll() | 唤醒等待该对象锁的所有线程 |
wait() | 使当前线程等待 |
wait(long timeout) | 等待指定毫秒数 |
wait(long timeout, int nanos) | 等待指定毫秒数+纳秒数 |
finalize() | 垃圾回收时调用(已废弃) |
四、equals() 与 hashCode()
4.1 equals() 方法
equals() 用于判断两个对象是否"相等"。
默认实现
// Object 类中的默认实现
public boolean equals(Object obj) {
return (this == obj); // 默认比较地址
}重写 equals()
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
// 1. 判断是否为同一对象
if (this == obj) return true;
// 2. 判断是否为 null 或类型不同
if (obj == null || getClass() != obj.getClass()) return false;
// 3. 类型转换并比较属性
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
}equals() 的 5 个特性(Java 规范)
| 特性 | 说明 |
|---|---|
| 自反性 | x.equals(x) 必须返回 true |
| 对称性 | x.equals(y) 为 true 时,y.equals(x) 也必须为 true |
| 传递性 | x.equals(y) 且 y.equals(z),则 x.equals(z) 必须为 true |
| 一致性 | 多次调用 x.equals(y) 结果必须一致 |
| 非空性 | x.equals(null) 必须返回 false |
4.2 hashCode() 方法
hashCode() 返回对象的哈希码(整数),用于散列存储结构(HashMap、HashSet 等)。
默认实现
// Object 类中的默认实现(native 方法)
public native int hashCode();hashCode() 的约定
- 一致性:对象未修改时,多次调用返回相同值
- 相等对象必须相等哈希码:
equals()返回true,则hashCode()必须相同 - 不等对象可以不等哈希码:
equals()返回false,hashCode()可以相同(哈希冲突)
4.3 为什么重写 equals() 必须重写 hashCode()?
核心原因:违反了 Java 规范——相等对象必须有相等的哈希码。
// 反例:只重写 equals(),不重写 hashCode()
public class Person {
private String name;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return Objects.equals(name, person.name);
}
// 没有重写 hashCode()
}
// 问题演示
Person p1 = new Person("张三");
Person p2 = new Person("张三");
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // false!
// HashSet/HashMap 会出问题
Set<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 2(期望是 1,因为 p1.equals(p2))正确做法
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}4.4 == 与 equals() 的区别
| 对比项 | == | equals() |
|---|---|---|
| 基本类型 | 比较值 | 不适用 |
| 引用类型 | 比较地址 | 默认比较地址,可重写比较内容 |
| 可重写 | 不可 | 可重写 |
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false(地址不同)
System.out.println(s1.equals(s2)); // true(内容相同)
int a = 10;
int b = 10;
System.out.println(a == b); // true(基本类型比较值)五、toString() 方法
5.1 默认实现
// Object 类中的默认实现
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
// 输出示例:com.example.Person@1a2b3c5.2 重写 toString()
public class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
Person p = new Person("张三", 25);
System.out.println(p); // Person{name='张三', age=25}
// IDEA 自动生成
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// Lombok 注解
@ToString
public class Person {
private String name;
private int age;
}5.3 使用场景
- 日志输出
- 调试信息
- 错误信息
六、clone() 方法
6.1 浅拷贝与深拷贝
| 类型 | 说明 |
|---|---|
| 浅拷贝 | 复制对象本身和基本类型属性,引用类型属性仍指向原对象 |
| 深拷贝 | 复制对象本身和所有属性(包括引用类型指向的对象) |
浅拷贝:
┌─────────────┐ ┌─────────────┐
│ 原对象 │ │ 新对象 │
│ name: "张三"│ │ name: "张三"│
│ address ───┼──┐ │ address ───┼──┐
└─────────────┘ │ └─────────────┘ │
│ │
└──→ ┌─────────────┐ ←┘
│ Address 对象 │(共享)
│ city: "北京" │
└─────────────┘
深拷贝:
┌─────────────┐ ┌─────────────┐
│ 原对象 │ │ 新对象 │
│ name: "张三"│ │ name: "张三"│
│ address ───┼──┐ │ address ───┼──┐
└─────────────┘ │ └─────────────┘ │
│ │
↓ ↓
┌─────────────┐ ┌─────────────┐
│ Address 对象 │ │ Address 对象 │(独立)
│ city: "北京" │ │ city: "北京" │
└─────────────┘ └─────────────┘6.2 实现克隆
方式一:实现 Cloneable 接口(浅拷贝)
public class Person implements Cloneable {
private String name;
private int age;
private Address address; // 引用类型
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅拷贝
}
}
public class Address {
private String city;
// ...
}
// 测试
Person p1 = new Person("张三", 25, new Address("北京"));
Person p2 = (Person) p1.clone();
System.out.println(p1 == p2); // false(对象不同)
System.out.println(p1.getAddress() == p2.getAddress()); // true(引用共享)方式二:深拷贝
// 方式1:递归克隆
public class Person implements Cloneable {
private String name;
private Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) address.clone(); // 递归克隆
return cloned;
}
}
public class Address implements Cloneable {
private String city;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
// 方式2:序列化/反序列化
public class Person implements Serializable {
private String name;
private Address address;
public Person deepCopy() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Person) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// 方式3:构造器复制
public class Person {
private String name;
private Address address;
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address); // 新建对象
}
}6.3 Cloneable 接口
Cloneable 是一个标记接口(Marker Interface),没有任何方法。
public interface Cloneable {
// 空接口,仅作为标记
}如果不实现 Cloneable 接口,调用 clone() 会抛出 CloneNotSupportedException。
七、getClass() 方法
7.1 获取 Class 对象
// 方式1:Object.getClass()
Person p = new Person();
Class<?> clazz1 = p.getClass();
// 方式2:类名.class
Class<?> clazz2 = Person.class;
// 方式3:Class.forName()
Class<?> clazz3 = Class.forName("com.example.Person");
// 三种方式获取的是同一个 Class 对象
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true7.2 Class 对象的作用
Class<?> clazz = Person.class;
// 获取类名
clazz.getName(); // "com.example.Person"
clazz.getSimpleName(); // "Person"
// 获取父类
clazz.getSuperclass();
// 获取接口
clazz.getInterfaces();
// 获取字段
clazz.getDeclaredFields();
// 获取方法
clazz.getDeclaredMethods();
// 创建实例
clazz.newInstance(); // 已废弃
clazz.getDeclaredConstructor().newInstance(); // 推荐八、finalize() 方法(已废弃)
8.1 说明
finalize() 在对象被垃圾回收前调用,用于释放资源。
@Override
protected void finalize() throws Throwable {
try {
// 释放资源
} finally {
super.finalize();
}
}8.2 为什么废弃?
| 问题 | 说明 |
|---|---|
| 不可靠 | 不保证一定会执行 |
| 性能差 | 影响垃圾回收效率 |
| 安全风险 | 可能导致对象"复活" |
| 替代方案 | try-with-resources、Cleaner |
// 推荐方式:try-with-resources
try (FileInputStream fis = new FileInputStream("test.txt")) {
// 使用资源
} // 自动关闭
// JDK 9+:Cleaner
Cleaner cleaner = Cleaner.create();
cleaner.register(obj, () -> {
// 清理逻辑
});九、综合面试题
Q1: String s = "a" + "b" + "c" 编译后是什么?
A: 编译后是 String s = "abc"。编译器会在编译期优化常量拼接。
Q2: String s = new String("a") + new String("b") 创建了几个对象?
A: 创建了 5 个对象:
- 2 个 String 对象("a" 和 "b")
- 2 个堆中 String 对象(new String("a") 和 new String("b"))
- 1 个 StringBuilder 对象(用于拼接)
- 注:JDK 9+ 后,"a" 和 "b" 可能不会在常量池创建
Q3: 如何避免创建重复的字符串对象?
A: 使用 intern() 方法:
String s1 = new String("hello").intern();
String s2 = "hello";
System.out.println(s1 == s2); // trueQ4: Object 类有哪些方法是线程相关的?
A: wait()、notify()、notifyAll(),用于线程间通信。
Q5: 为什么 String 的 hashCode() 可以缓存?
A: 因为 String 不可变,hashCode 计算一次后永远不会变。
public final class String {
private int hash; // 缓存 hashCode
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
}十、总结
| 类/方法 | 核心要点 | 面试关键词 |
|---|---|---|
| String | 不可变、常量池、final 类 | intern、不可变性、拼接优化 |
| StringBuilder | 可变、单线程高效 | append、扩容 |
| StringBuffer | 可变、线程安全 | synchronized |
| equals() | 比较内容、重写规则 | 对称性、传递性 |
| hashCode() | 哈希码、与 equals 关系 | 哈希一致性、HashSet/HashMap |
| clone() | 浅拷贝、深拷贝 | Cloneable、引用复制 |
| toString() | 字符串表示 | 调试、日志 |
最后更新:2026年3月2日