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)的契约要求
image

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())

image

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() 的作用是什么?使用时需要注意什么?

image

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 可以相同吗?

image

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<>(); // 合法
    }
}

什么是自动装箱 / 自动拆箱?底层原理是什么?

image

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 缓存池范围是多少?为什么要设计缓存池?

image

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() 的区别?

image

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(堆中两个新对象)
    }
}

自动装箱可能带来的性能问题?

image

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 固定两个对象

image

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 核心

  1. 不可变性:String 底层 final 数组保证不可变,线程安全但拼接性能差;StringBuffer/StringBuilder 可变,前者加锁线程安全,后者无锁性能最优。
  2. 性能选择:单线程频繁拼接用 StringBuilder,多线程用 StringBuffer,固定文本用 String。
  3. JDK9+ 优化:String 底层由 char[] 改为 byte[],大幅节省内存,对外 API 无感知。
  4. 常量池new String("abc") 可能创建 1-2 个对象,intern() 方法可手动入池复用字符串。

4.2 Object 类核心

  1. equals() vs ==:基本类型比数值,引用类型 == 比地址、equals() 重写后比内容。
  2. 契约要求:重写 equals() 必须重写 hashCode(),否则 HashMap/HashSet 会出现数据错乱。
  3. toString():默认输出「类名@哈希值」,重写后可直观展示对象业务字段,便于调试。
  4. 线程通信wait()/notify() 必须在同步块中调用,wait() 释放锁,sleep() 不释放锁。
  5. hashCode 原则:同一对象哈希值不变,相等对象哈希值必相等,不等对象尽量不同。

4.3 包装类核心

  1. 基本类型 vs 包装类:基本类型高效无空值,包装类支持泛型/可空,按需选择场景。
  2. 自动装箱/拆箱:编译期语法糖,底层调用 valueOf()/xxxValue(),循环中避免滥用(性能损耗)。
  3. 缓存池:Integer/Long/Short/Byte 默认缓存 [-128,127],仅 Integer 可调整上限,小数字复用提升性能。
  4. 比较规则:包装类比较内容用 equals()== 可能因缓存产生「看似正确」的错误结果。
  5. 性能优化:优先使用 valueOf() 而非 new Integer(),循环计算用基本类型减少临时对象。

4.4 通用开发建议

  1. 性能优先:避免循环中字符串拼接、自动装箱,优先选择基本类型和 StringBuilder。
  2. 线程安全:多线程修改共享字符串用 StringBuffer,共享对象通信用 wait()/notify()
  3. 代码规范:重写 equals() 必重写 hashCode(),重写 toString() 提升调试效率。
  4. 缓存复用:利用字符串/包装类缓存池减少对象创建,降低 GC 压力。
posted @ 2026-03-13 12:13  白鹿为溪  阅读(30)  评论(0)    收藏  举报