Java 常用类(基础工具类,面试常问底层 / 使用)
什么是 Java 常用类?
简单来说,Java 常用类 是 JDK(Java 开发工具包)内置的、无需自己编写就能直接使用的基础工具类,它们封装了开发中最常用的功能(比如字符串处理、日期计算、数据类型转换、集合操作等),是 Java 程序员日常开发的 “工具箱”。
目录
1.String/StringBuffer/StringBuilder 模块
2.Object 类核心模块
3.包装类(Wrapper)模块
4.核心知识点总结
1. String/StringBuffer/StringBuilder 模块
1.1 核心面试题
String、StringBuffer、StringBuilder 的核心区别是什么?各自的使用场景?
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变(底层final数组) | 可变(底层非final数组) | 可变 |
| 线程安全 | 线程安全(不可变无修改操作) | 线程安全(方法加synchronized锁) | 线程不安全(无锁) |
| 性能 | 最低(拼接创建新对象) | 中等(加锁有开销) | 最高(无锁无额外开销) |
| 底层实现 | JDK8:char[];JDK9+:byte[] | 继承AbstractStringBuilder,char[] | 继承AbstractStringBuilder,char[] |
String: 少量、固定文本(如常量定义、方法返回值),无需频繁修改。
StringBuffer: 多线程环境下频繁拼接字符串(如线程池日志拼接)。
StringBuilder: 单线程环境下频繁拼接字符串(如循环拼接、接口返回值组装),开发中首选。
public class StringCompareDemo {
public static void main(String[] args) {
// String:不可变,拼接创建新对象
String str = "a";
String newStr = str + "b";
System.out.println("str == newStr ? " + (str == newStr)); // false(地址不同)
// StringBuffer:线程安全,原地修改
StringBuffer sb = new StringBuffer("a");
sb.append("b");
System.out.println("StringBuffer结果:" + sb); // "ab"
// StringBuilder:非线程安全,性能最优
StringBuilder sbd = new StringBuilder("a");
sbd.append("b");
System.out.println("StringBuilder结果:" + sbd); // "ab"
}
}
String 为什么是不可变的?不可变性带来了哪些好处?
不可变原因:
1.底层存储字符的数组被private final修饰(private final char[] value / private final byte[] value),数组引用不可修改;
2.类无对外暴露的修改数组内容的方法(无setter),所有修改操作(如substring、concat)都会返回新 String 对象。
不可变性好处
1.线程安全:多线程环境下无并发修改问题,无需加锁;
2.常量池复用:字符串常量池可缓存相同字符串,节省内存;
3.哈希值固定:String 作为HashMap键时,hashCode只需计算一次,提升性能;
4.安全可靠:避免字符串被意外修改(如密码、URL)。
public class StringImmutableDemo {
public static void main(String[] args) {
String s1 = "hello";
String s2 = s1.toUpperCase(); // 生成新对象
System.out.println("s1:" + s1); // "hello"(原对象未变)
System.out.println("s2:" + s2); // "HELLO"(新对象)
System.out.println("s1 == s2 ? " + (s1 == s2)); // false
}
}
JDK8 和 JDK9+ 中 String 的底层实现有什么变化?
JDK8 及之前:
-
底层用private final char[] value存储字符,每个字符占 2 字节(UTF-16 编码);
-
即使字符串全是拉丁字母(占 1 字节),也会占用 2 字节空间,内存利用率低。
**JDK9+: **
1.底层改为private final byte[] value存储字符,搭配coder字段标识编码(LATIN1/UTF16);
2.纯拉丁字母字符串用 1 字节存储,中文等字符用 2 字节存储,大幅节省内存,提升 GC 效率;
3.方法逻辑(如length()、charAt())适配byte[]实现,对外 API 不变。
// 该代码需在不同JDK版本运行,查看底层字段(反射验证)
import java.lang.reflect.Field;
public class StringJDKDiffDemo {
public static void main(String[] args) throws Exception {
String str = "abc";
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
Object value = valueField.get(str);
System.out.println("value类型:" + value.getClass().getName());
// JDK8:[C(char数组)
// JDK9+:[B(byte数组)
}
}
String s = new String("abc") 会创建几个对象?
经典答案: 1 个或 2 个,取决于字符串常量池中是否已存在"abc"
若常量池无"abc":
第一步:在字符串常量池创建"abc"对象;
第二步:在堆中创建String对象,引用指向常量池的"abc";
若常量池已有"abc":
仅在堆中创建String对象,复用常量池的"abc";
public class StringNewObjectDemo {
public static void main(String[] args) {
// 第一次:常量池无"abc",创建2个对象(池+堆)
String s1 = new String("abc");
// 第二次:常量池已有"abc",仅创建1个对象(堆)
String s2 = new String("abc");
System.out.println(s1 == s2); // false(堆中不同对象)
System.out.println(s1.intern() == s2.intern()); // true(复用常量池对象)
}
}
StringBuffer 为什么是线程安全的?StringBuilder 为什么不是?
StringBuffer 线程安全:
所有公共修改方法(append()、insert()、delete()等)都被synchronized关键字修饰,多线程调用时会加锁,保证原子性,避免并发修改导致数据错乱。
**StringBuilder 非线程安全: **
方法无synchronized修饰,无锁开销,单线程下性能更高;但多线程并发修改时,可能出现数据丢失、数组越界等问题。
public class StringBufferThreadSafeDemo {
public static void main(String[] args) throws InterruptedException {
// StringBuffer:线程安全,结果正确
StringBuffer sb = new StringBuffer();
// StringBuilder:线程不安全,结果可能小于预期
// StringBuilder sb = new StringBuilder();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) sb.append("a");
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) sb.append("a");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终长度:" + sb.length());
// StringBuffer:输出2000(正确)
// StringBuilder:输出可能<2000(并发丢失)
}
}
2. Object 类核心模块
equals() 和 == 的区别是什么?
| 对比项 | == | equals() |
|---|---|---|
| 基本类型 | 比较值是否相等 | 基本类型无法调用 equals() |
| 引用类型 | 比较对象内存地址是否相同 | 默认实现为 ==(比较地址),重写后可比较对象内容 |
| 典型场景 | 判断两个引用是否指向同一个对象 | 判断两个对象的业务内容是否相同(如 String 比较字符序列) |
public class EqualsVsDoubleEqualDemo {
public static void main(String[] args) {
// 基本类型:== 比较值
int a = 10;
int b = 10;
System.out.println(a == b); // true
// 引用类型:== 比较地址,equals() 重写后比较内容
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); // false(不同对象,地址不同)
System.out.println(s1.equals(s2)); // true(String 重写 equals,比较字符序列)
// 自定义类:默认 equals() 等价于 ==
User u1 = new User("张三", 18);
User u2 = new User("张三", 18);
System.out.println(u1 == u2); // false
System.out.println(u1.equals(u2)); // false(未重写 equals)
}
}
class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
为什么重写 equals() 必须重写 hashCode()?
这是 Java 集合框架(HashMap/HashSet)的契约要求:

import java.util.HashSet;
import java.util.Objects;
public class EqualsHashCodeDemo {
public static void main(String[] args) {
HashSet<User> set = new HashSet<>();
User u1 = new User("张三", 18);
User u2 = new User("张三", 18);
set.add(u1);
set.add(u2);
System.out.println("集合大小:" + set.size()); // 未重写 hashCode() → 2;重写后 → 1
}
}
class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 重写 equals:比较业务内容
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
// 必须重写 hashCode:与 equals 保持一致
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
toString() 的默认实现是什么?为什么要重写它?
getClass().getName() + "@" + Integer.toHexString(hashCode())

public class ToStringDemo {
public static void main(String[] args) {
User u = new User("张三", 18);
System.out.println(u); // 未重写 → User@1b6d3586;重写后 → User{name='张三', age=18}
}
}
class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 重写 toString():输出业务字段
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
wait()、notify()、notifyAll() 的作用是什么?使用时需要注意什么?

public class WaitNotifyDemo {
private static final Object LOCK = new Object();
private static boolean isReady = false;
public static void main(String[] args) {
// 等待线程
new Thread(() -> {
synchronized (LOCK) {
System.out.println("等待线程:开始等待");
while (!isReady) {
try {
LOCK.wait(); // 释放锁,进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("等待线程:收到通知,继续执行");
}
}, "Wait-Thread").start();
// 唤醒线程
new Thread(() -> {
synchronized (LOCK) {
isReady = true;
System.out.println("唤醒线程:修改标志位,通知等待线程");
LOCK.notify(); // 唤醒一个等待线程
}
}, "Notify-Thread").start();
}
}
hashCode() 的设计原则是什么?不同对象的 hashCode 可以相同吗?

import java.util.Objects;
public class HashCodePrincipleDemo {
public static void main(String[] args) {
User u1 = new User("张三", 18);
User u2 = new User("张三", 18);
User u3 = new User("李四", 20);
System.out.println(u1.hashCode() == u2.hashCode()); // true(equals 为 true)
System.out.println(u1.hashCode() == u3.hashCode()); // 可能为 false(尽量不同)
}
}
class User {
String name;
int age;
public User(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;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
3. 包装类(Wrapper)模块
本类型与包装类的区别是什么?
| 对比维度 | 基本类型(int/char 等) | 包装类(Integer/String 等) |
|---|---|---|
| 存储位置 | 栈内存(值类型) | 堆内存(引用类型) |
| 默认值 | 具体值(如 int 为 0) | null |
| 可空性 | 不可为null | 可为null |
| 泛型支持 | 不支持(List |
支持(List |
| 性能 | 更高(无对象开销) | 稍低(包含对象头、成员变量) |
| 使用场景 | 局部变量、数值计算 | 集合存储、数据库可空字段、泛型 |
public class WrapperVsPrimitiveDemo {
public static void main(String[] args) {
// 基本类型:默认值为0,不能为null
int primitiveNum = 0;
// int nullPrimitive = null; // 编译报错
// 包装类:默认值为null,可表示"不存在"状态
Integer wrapperNum = null;
if (wrapperNum == null) {
System.out.println("wrapperNum 未赋值");
}
// 泛型必须使用包装类
// List<int> list = new ArrayList<>(); // 编译报错
// List<Integer> list = new ArrayList<>(); // 合法
}
}
什么是自动装箱 / 自动拆箱?底层原理是什么?

public class AutoBoxingDemo {
public static void main(String[] args) {
// 自动装箱:等价于 Integer num1 = Integer.valueOf(100);
Integer num1 = 100;
// 自动拆箱:等价于 int num2 = num1.intValue();
int num2 = num1;
System.out.println("num1: " + num1); // 100
System.out.println("num2: " + num2); // 100
// 反编译后代码:
// Integer num1 = Integer.valueOf(100);
// int num2 = num1.intValue();
}
}
Integer 缓存池范围是多少?为什么要设计缓存池?

public class IntegerCacheDemo {
public static void main(String[] args) {
// 缓存范围内:复用对象
Integer a = 127;
Integer b = 127;
System.out.println("a == b ? " + (a == b)); // true
// 超出缓存范围:创建新对象
Integer c = 128;
Integer d = 128;
System.out.println("c == d ? " + (c == d)); // false
// 正确比较方式:使用equals()比较内容
System.out.println("c.equals(d) ? " + c.equals(d)); // true
}
}
Integer.valueOf() 与 new Integer() 的区别?

public class IntegerValueOfVsNewDemo {
public static void main(String[] args) {
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println("a == b ? " + (a == b)); // true(复用缓存)
Integer c = new Integer(127);
Integer d = new Integer(127);
System.out.println("c == d ? " + (c == d)); // false(堆中两个新对象)
}
}
自动装箱可能带来的性能问题?

public class AutoBoxingPerformanceDemo {
public static void main(String[] args) {
int loopTimes = 100000;
// 基本类型:无装箱开销,性能最优
long start1 = System.currentTimeMillis();
int sum1 = 0;
for (int i = 0; i < loopTimes; i++) {
sum1 += i;
}
long end1 = System.currentTimeMillis();
System.out.println("基本类型耗时:" + (end1 - start1) + "ms");
// 包装类型:循环中自动装箱,产生大量临时对象
long start2 = System.currentTimeMillis();
Integer sum2 = 0;
for (int i = 0; i < loopTimes; i++) {
sum2 += i; // 等价于 sum2 = Integer.valueOf(sum2.intValue() + i)
}
long end2 = System.currentTimeMillis();
System.out.println("包装类型耗时:" + (end2 - start2) + "ms");
}
}
Long/Short/Byte 缓存池与 Integer 一致吗?
| 包装类 | 缓存范围 | 是否可调整上限 |
|---|---|---|
| Integer | [-128, 127] | 可通过 -XX:AutoBoxCacheMax 调整 |
| Long/Short/Byte | [-128, 127] | 固定范围,不可调整 |
| Character | [0, 127] | 固定范围 |
| Boolean | TRUE/FALSE | 固定两个对象 |

public class OtherWrapperCacheDemo {
public static void main(String[] args) {
// Long 缓存测试
Long l1 = 127L;
Long l2 = 127L;
System.out.println("l1 == l2 ? " + (l1 == l2)); // true
Long l3 = 128L;
Long l4 = 128L;
System.out.println("l3 == l4 ? " + (l3 == l4)); // false
// Byte 缓存测试(范围-128~127,全覆盖)
Byte b1 = 127;
Byte b2 = 127;
System.out.println("b1 == b2 ? " + (b1 == b2)); // true
}
}
4. 核心知识点总结
4.1 String/StringBuffer/StringBuilder 核心
- 不可变性:String 底层
final数组保证不可变,线程安全但拼接性能差;StringBuffer/StringBuilder 可变,前者加锁线程安全,后者无锁性能最优。 - 性能选择:单线程频繁拼接用 StringBuilder,多线程用 StringBuffer,固定文本用 String。
- JDK9+ 优化:String 底层由
char[]改为byte[],大幅节省内存,对外 API 无感知。 - 常量池:
new String("abc")可能创建 1-2 个对象,intern()方法可手动入池复用字符串。
4.2 Object 类核心
- equals() vs ==:基本类型比数值,引用类型
==比地址、equals()重写后比内容。 - 契约要求:重写
equals()必须重写hashCode(),否则 HashMap/HashSet 会出现数据错乱。 - toString():默认输出「类名@哈希值」,重写后可直观展示对象业务字段,便于调试。
- 线程通信:
wait()/notify()必须在同步块中调用,wait()释放锁,sleep()不释放锁。 - hashCode 原则:同一对象哈希值不变,相等对象哈希值必相等,不等对象尽量不同。
4.3 包装类核心
- 基本类型 vs 包装类:基本类型高效无空值,包装类支持泛型/可空,按需选择场景。
- 自动装箱/拆箱:编译期语法糖,底层调用
valueOf()/xxxValue(),循环中避免滥用(性能损耗)。 - 缓存池:Integer/Long/Short/Byte 默认缓存
[-128,127],仅 Integer 可调整上限,小数字复用提升性能。 - 比较规则:包装类比较内容用
equals(),==可能因缓存产生「看似正确」的错误结果。 - 性能优化:优先使用
valueOf()而非new Integer(),循环计算用基本类型减少临时对象。
4.4 通用开发建议
- 性能优先:避免循环中字符串拼接、自动装箱,优先选择基本类型和 StringBuilder。
- 线程安全:多线程修改共享字符串用 StringBuffer,共享对象通信用
wait()/notify()。 - 代码规范:重写
equals()必重写hashCode(),重写toString()提升调试效率。 - 缓存复用:利用字符串/包装类缓存池减少对象创建,降低 GC 压力。

浙公网安备 33010602011771号