Java基础(二):字符串、核心类与对象约定
Java基础(二):字符串、核心类与对象约定
导语:本篇聚焦字符串与核心类库。String 家族与包装类、字符串拼接的底层原理、常量池与 intern,以及
==/equals/hashCode 契约与 Object 类,共 14 题。
一、字符串与包装类
1. String、StringBuffer、StringBuilder 的区别?
答:
| 维度 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变天然安全) | 安全(方法加 synchronized) | 不安全 |
| 性能 | 频繁拼接产生大量对象 | 比 StringBuilder 略慢(同步开销) | 单线程下最快 |
| 适用场景 | 少量/常量字符串 | 多线程频繁修改 | 单线程频繁修改 |
使用建议:操作少量数据用 String;单线程大量拼接用 StringBuilder;多线程大量拼接用 StringBuffer。
2. 为什么 String 是不可变的?这样设计有什么好处?
答: 在 Java 8 及之前,String 内部是 private final char value[];Java 9 起改为 private final byte[] value(紧凑字符串 Compact Strings,节省空间),且都用 final 修饰,字符串内容不可修改,任何"修改"都返回新对象。
好处:
- 字符串常量池得以实现:相同字面量复用同一对象,节省内存。
- 线程安全:不可变对象天然线程安全,可被共享。
- 哈希缓存:
hashCode计算一次后可缓存,适合作为HashMap的 key。 - 安全:类加载、网络地址、SQL 等场景不会被意外篡改。
补充(Java 9 紧凑字符串 Compact Strings):底层
byte[]同时支持 Latin-1 与 UTF-16 两种编码。若字符串仅含 Latin-1 范围内的字符(绝大多数英文/数字),每个字符只占 1 字节;只有含中文等超出 Latin-1 的字符时才用 UTF-16(每字符 2 字节)。相比char[](固定 2 字节),纯拉丁字符串可省约一半内存。
3. String str = "abc" 与 String str = new String("abc") 一样吗?
答: 不一样,涉及对象数量与存储位置不同:
"abc"来自字符串常量池,赋值给str引用指向池中的对象;new String("abc")会在堆上创建一个新的String对象,其内容拷贝自常量池中的"abc"。
因此 new String("abc") 在常量池已存在 "abc" 的情况下会创建 1 个堆对象(常量池对象在类加载/首次使用时已建);若该字面量此前未出现过,则常量池 + 堆共 2 个对象。比较:
String a = "abc";
String b = new String("abc");
System.out.println(a == b); // false(地址不同)
System.out.println(a.equals(b)); // true(内容相同)4. 什么是自动装箱与拆箱?Integer 缓存机制是什么?
答: 装箱是基本类型转包装类(如 int→Integer),拆箱反之。Java 5 引入自动机制:
Integer i = 100; // 自动装箱,等价于 Integer.valueOf(100)
int n = i; // 自动拆箱,等价于 i.intValue()Integer 缓存:Integer.valueOf(int) 对 -128~127 范围的值做了缓存,返回同一对象;超出范围才 new。
Integer a = 127, b = 127;
System.out.println(a == b); // true(命中缓存)
Integer c = 128, d = 128;
System.out.println(c == d); // false(新建对象)
Byte/Short/Integer/Long均有 -128~127 缓存;Character缓存 0~127;Boolean缓存 TRUE/FALSE。
5. 如何将字符串反转?
答: 使用 StringBuilder/StringBuffer 的 reverse() 方法(最高效):
String s = new StringBuilder("abcdefg").reverse().toString(); // "gfedcba"也可手动 char[] 双指针反转或 Collections.reverse 配合 Arrays.asList,但不如前法简洁。
6. 数组有 length() 方法吗?String 有 length() 方法吗?
答: 数组没有 length() 方法,只有 length 属性(arr.length);String 有 length() 方法(返回字符数)。Collection 用 size() 方法。这三者的取法容易混淆,注意区分。
7. int 和 Integer 有什么区别?
答: int 是 8 种基本类型之一,直接存值、不为对象、默认 0;Integer 是 int 的包装类,是对象、可为 null、默认 null。泛型、集合只能使用 Integer(不能放基本类型),通过自动装箱/拆箱在二者间转换。
二、字符串拼接
8. 字符串拼接用"+"还是 StringBuilder?底层原理是什么?
答: Java 的 +/+= 是仅为 String 重载过的运算符(Java 不支持一般运算符重载)。编译期会把这种拼接改写为 StringBuilder 的 append() 调用,最后 toString() 得到新 String:
String str4 = str1 + str2 + str3;
// 编译后约等于:
String str4 = new StringBuilder().append(str1).append(str2).append(str3).toString();但在循环中使用 + 拼接有明显缺陷:每次循环迭代都会 new 一个 StringBuilder,产生大量临时对象。因此:
- 少量/编译期可确定的拼接(如常量折叠
"a"+"b")用+即可,简洁且会被编译器优化进常量池; - 循环内或大量拼接必须显式使用
StringBuilder(单线程)或StringBuffer(多线程),复用同一对象避免重复创建。
补充:JDK 9 起字符串拼接改为
invokedynamic+makeConcatWithConstants实现(JEP 280),简单拼接可减少部分临时对象,但循环内大量拼接仍不如手写StringBuilder高效。final修饰的字符串常量参与拼接时,编译器在编译期即可确定其值(常量折叠)。
三、字符串常量池与 intern
9. String 的 intern() 方法有什么作用?字符串常量池的作用是什么?
答: 字符串常量池是 JVM 为提升性能、减少内存占用,专门为 String 开辟的一块区域,目的是避免字符串重复创建。
String.intern() 是 native 方法:
- 若常量池中已存在
equals相等的字符串,返回池中该对象的引用; - 否则把当前字符串加入常量池,并返回池中引用。
String a = new String("ab");
String b = new String("ab");
System.out.println(a.intern() == b.intern()); // true(都指向池中同一对象)
System.out.println(a.intern() == a); // false(a 在堆,intern 返回池中)注意:字符串字面量(如
"ab")默认就会进入常量池;new String("ab")创建的是堆对象,调用intern()才可能复用池对象。JDK 7 起字符串常量池从永久代移到了堆中。
四、== 与 equals 与 hashCode
10. == 和 equals() 的区别?
答:
==:比较基本类型时比较值;比较引用类型时比较内存地址(是否同一对象)。equals():Object默认实现即==(比地址);子类(如String、Integer)重写后用于比较内容。equals不能用于基本类型变量本身(但包装类可调用equals)。
Integer a = 128, b = 128;
System.out.println(a == b); // false(不同对象)
System.out.println(a.equals(b)); // true(值相等)11. 为什么重写 equals() 时必须重写 hashCode()?
答: Object 规范要求:如果两个对象 equals 返回 true,它们的 hashCode 必须相等。若只重写 equals 而不重写 hashCode,两个内容相等的对象可能 hashCode 不同,放入 HashMap/HashSet 时会被放到不同桶中,导致"重复元素"无法去重、查找不到,破坏集合正确性。
反之,hashCode 相同的两个对象 equals 不一定为 true(哈希冲突允许存在)。
12. hashCode() 的作用是什么?以 HashSet 检查重复为例说明。
答: hashCode() 返回 int 哈希码,用于在哈希表中快速定位桶位置。以 HashSet 添加元素为例:
- 先算对象的
hashCode(),定位应存入的桶; - 若桶为空,直接存入;
- 若桶中已有元素,再逐一用
equals()比较;equals为 false 则作为冲突节点挂入,true 则视为重复、不添加。
这样把"逐个 equals 比对"降为"先哈希定位、再少量比对",显著提升性能。
13. 高质量 equals() 实现要满足哪些约定?
答: 必须自反、对称、传递、一致,且 x.equals(null) 返回 false。实现技巧:先用 == 判断是否为同一引用;再用 instanceof/getClass 判断类型;比较关键字段;写完检查对称/传递/一致性;重写 equals 必重写 hashCode;参数保持 Object 并用 @Override。
五、Object 类
14. Object 类的常见方法有哪些?
答: Object 是所有类的根父类,主要提供以下方法(部分):
getClass():final native,返回运行时类的Class对象;hashCode():native,返回哈希码,主要用于哈希表定位;equals(Object obj):默认即==比地址,String等重写了它以比值;clone():protected native,创建并返回对象拷贝(浅拷贝);toString():默认返回类名@hash,建议子类重写;finalize():GC 回收前调用,Java 9 起已废弃,不要用;wait()/wait(long)/notify()/notifyAll():native,用于线程间等待/唤醒(需在synchronized内调用)。
其中
getClass、notify、notifyAll、wait为final/native不可重写;equals/hashCode/toString/clone通常建议重写,且需遵守一致约定。
