Java常见类
Java常见类
51. 为什么要有 hashCode?
回答:hashCode() 提供对象的哈希码,是哈希容器(如 HashMap、HashSet)查找元素位置的依据。它提升了查找性能,是 equals 的辅助工具。
分析:hashCode() 方法返回对象的哈希值,用于快速定位哈希表中的存储位置。在如 HashMap、HashSet 等集合中,Java 会先使用 hashCode() 快速筛选对象所属的桶,再用 equals() 精确判断是否相等。
这就是两者并存的原因:
hashCode()提供快速定位,提高查找效率equals()提供精准判断,避免哈希冲突误判
为什么不能只用 hashCode()?因为不同对象可能产生相同的哈希值,这种情况称为"哈希碰撞"。而只使用 equals() 又会导致查找效率下降(需遍历所有元素)。
总结来说:
- 如果两个对象的
hashCode()不相等,它们一定不相等; - 如果
hashCode()相等,需借助equals()再判断一次。
这种"先粗略、后精确"的匹配机制,是哈希容器性能与准确性的平衡体现。
52. 为什么重写 equals() 时必须重写 hashCode() 方法?
回答:
因为两个相等的对象必须拥有相等的 hashCode(),否则在哈希集合中可能导致查找失败、插入重复等问题。
分析:
Java 容器如 HashMap、HashSet 的基本原则是:两个对象如果通过 equals() 判断为相等,则它们的 hashCode() 值也必须相等。
这是因为容器首先通过 hashCode() 决定存储桶,之后才通过 equals() 判断相等性。如果 hashCode() 不一致,对象可能存储在不同桶中,即便 equals() 结果为 true,也无法查找到该对象。
示例:
Set<User> users = new HashSet<>();
User u1 = new User("Tom", 20);
User u2 = new User("Tom", 20); // equals 返回 true
users.add(u1);
users.contains(u2); // 若 u2 的 hashCode 不等,查找失败所以只重写 equals() 而忽略 hashCode() 会破坏集合结构,导致程序行为异常。IDE(如 IntelliJ)通常会提示开发者同步重写两者,以维护一致性。
53. String、StringBuffer、StringBuilder 有何区别?
回答:
三者主要区别在于可变性、线程安全与性能:String 不可变,线程安全;StringBuffer 可变,线程安全;StringBuilder 可变,但非线程安全,性能最佳。
分析:
可变性:
String对象不可变,每次修改都会创建新对象。StringBuffer和StringBuilder是可变字符串,底层使用字符数组维护内容。
线程安全性:
String因不可变本质天然线程安全。StringBuffer方法使用了synchronized修饰,适合多线程。StringBuilder不加锁,适用于单线程高性能场景。
性能对比:
在字符串拼接频繁的场景中,使用String会导致大量中间对象生成,性能低;StringBuffer安全但稍慢;StringBuilder性能最好,推荐在局部单线程中使用。
总结使用建议:
- 少量拼接:用
String - 单线程、大量操作:用
StringBuilder - 多线程、大量操作:用
StringBuffer
54. String 为什么是不可变的?
回答:String 之所以不可变,是因为其内部使用 final 修饰的字符数组存储数据,且类本身被 final 修饰,且不暴露任何可修改数据的接口。
分析:String 类设计为不可变主要出于以下考虑:
- 安全性:如网络地址、文件路径、Class 名称等经常以字符串形式传递,不可变保证其在传输过程不被篡改。
- 性能优化:不可变对象可以被缓存、共享,JVM 利用字符串常量池(String Pool)重用字符串,避免重复创建。
- 线程安全:不可变对象天然线程安全,不需要额外同步。
底层设计体现如下:
public final class String {
private final char[] value;
}其中字符数组被 final 和 private 修饰,无法修改内容,且 String 类自身也被 final 修饰,防止被继承破坏不变性。
Java 9 之后,String 改用 byte[] 存储,并支持 Latin-1 与 UTF-16 编码,提升内存利用率,但不变性设计依然保持不变。
55. 字符串拼接使用 "+" 还是 StringBuilder 更好?
回答:
小范围、编译期常量拼接可用 "+",但频繁拼接特别是在循环中,应使用 StringBuilder 或 StringBuffer 以提升性能。
分析:
在 Java 中,"+" 是对 String 特别支持的运算符。Java 编译器会将常量之间的拼接在编译阶段优化为单个字符串,例如:
String a = "Hello" + "World"; // 编译器优化为 "HelloWorld"但当涉及变量时,Java 会在字节码中自动使用 StringBuilder 进行拼接:
String a = str1 + str2; // 编译器等效于 new StringBuilder().append(str1).append(str2).toString()在循环中使用 "+" 会重复创建 StringBuilder 对象,影响性能:
String s = "";
for (String part : list) {
s += part; // 每次循环都新建一个 StringBuilder
}最佳做法是:
StringBuilder sb = new StringBuilder();
for (String part : list) {
sb.append(part);
}因此,推荐:
- 小规模拼接、常量拼接可用 "+"
- 大量拼接、循环拼接推荐使用
StringBuilder(单线程)或StringBuffer(多线程)
56. String#equals() 和 Object#equals() 有何区别?
回答:Object#equals() 默认比较引用地址是否相同;String#equals() 被重写为按内容逐字符比较字符串是否相等。
分析:
Java 中所有类都继承自 Object,其默认的 equals() 方法是基于地址的比较,等价于 == 运算符:只有引用相同的对象才被认为相等。
而 String 类重写了该方法,改为逐字符比较两个字符串的实际内容。这种行为更符合字符串语义,避免了地址相等误判。
例如:
String a = new String("abc");
String b = new String("abc");
System.out.println(a == b); // false,不同对象
System.out.println(a.equals(b)); // true,内容相同因此,在使用字符串比较时,应优先使用 equals() 方法,避免使用 == 判断内容是否相等。IDE(如 IDEA)也会自动提示替换为 equals()。
57. 字符串常量池的作用了解吗?
回答:
字符串常量池(String Pool)是 JVM 中专门用于存储字符串常量的内存区域,可避免重复创建相同内容的字符串,提高内存利用率与性能。
分析:
Java 为了优化字符串的创建与管理,引入了字符串常量池机制。凡是通过字面量方式声明的字符串(如 "abc")都会被存储到常量池中,当程序再次使用相同字面量时,会直接复用池中已有的对象。
例如:
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true,共享常量池对象使用 new String("hello") 创建的字符串对象则存在于堆中,即便内容相同,其引用地址不同。
常量池的作用在于:
- 避免重复对象占用内存
- 加快字符串比较(可用
==) - 提升执行效率
此外,通过 intern() 方法可以手动将字符串加入常量池,进一步实现复用,适用于动态生成字符串的场景。
58. String s1 = new String("abc"); 创建了几个对象?
回答:
可能创建一个或两个对象,取决于常量池中是否已有 "abc" 字符串。
分析:
这条语句执行过程如下:
String s1 = new String("abc");"abc"是字符串字面量,会首先在常量池中创建(若不存在)。new String("abc")在堆中创建一个新的String对象,内容复制自常量池中的 "abc"。
因此:
- 若常量池中原本没有 "abc",则创建一个常量池对象 + 一个堆对象,共 2 个。
- 若已有 "abc",则只在堆中创建一个对象。
可以理解为:new String("abc") 永远创建堆对象,而字面量是否创建常量池对象,取决于是否存在。
这种行为常用于构造新字符串而不影响常量池缓存,但若过度使用会影响性能。
59. intern 方法有什么作用?
回答:intern() 方法用于将字符串加入常量池,或返回常量池中已有字符串的引用,实现内容共享。
分析:intern() 是 String 类提供的 native 方法。其行为如下:
- 若常量池中已有与当前字符串内容相同的对象,则返回该对象的引用;
- 若没有,则将当前对象的引用加入常量池,并返回。
示例:
String s1 = new String("Java");
String s2 = s1.intern();
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true该机制的主要用途包括:
- 优化内存:避免重复存储相同字符串
- 提升性能:比较字符串时可用
== - 支持动态字符串共享:如 JSON、日志、数据库字段等场景
注意:频繁使用 intern() 可能增加常量池负担,应根据应用场景谨慎使用。
60. String 类型的变量和常量做 "+" 运算时发生了什么?
回答:
如果拼接的两端在编译期可确定为常量,编译器会进行常量折叠并存入常量池;否则使用 StringBuilder 进行运行时拼接,结果为堆中对象。
分析:
Java 对字符串拼接进行了两种处理方式:
- 常量折叠(编译优化):拼接表达式全为常量,编译器会预先计算出结果并将其直接存入常量池。
String s1 = "ab" + "cd"; // 编译期优化为 "abcd"- 运行时拼接:含变量拼接时,Javac 编译器会将其转换为
StringBuilder.append()调用:
String s2 = a + b; // 等价于 new StringBuilder().append(a).append(b).toString()因此:
String s3 = "abc";
String s4 = "a" + "bc"; // 编译器优化为 "abc"
System.out.println(s3 == s4); // true
String s5 = "a";
String s6 = s5 + "bc"; // 运行时拼接,堆对象
System.out.println(s3 == s6); // false此外,使用 final 修饰的变量如果在编译期可确定值,也能被折叠参与常量池优化。否则,即使被 final 修饰但运行期才能确定值,仍视为变量。
建议:若涉及大量拼接操作,推荐使用 StringBuilder 以避免性能浪费与频繁创建对象。