【Java.八股】高频面试题:==与equals(),Object 与 String方法 ,StringBuffer,StringBuilder
🔥个人主页:代码不加冰(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:LeetCode刷题日记 , 苍穹外卖日记,SSM框架深入,JavaWeb,
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
大家好,我是代码不加冰,今天上午用gpt5.5扫了好几个项目,然后写了个skill,用着挺爽的,稍微有点上瘾了(根本用不起),这还是群友分享的,在此特别鸣谢。自我感觉让AI写项目比自己写项目还累,于是休息一会写篇文章,至于写什么呢,那就分享一下前几天学的八股吧。
摘要:
本文面向 Java 初学者与面试备战者,硬核盘点
Object类与String类的核心八股文考点。前半部分深入剖析Object核心方法,用严谨的逻辑与大厂视角详解==与equals()的本质区别,并揭秘重写equals()必须重写hashCode()的底层契约及哈希冲突机制。后半部分对高频StringAPI 进行分类归纳,多维度横向对比String、StringBuffer与StringBuilder的优缺点,并直击循环体中使用+拼接字符串的编译底层大坑。全文拒绝死记硬背,助你轻松建立扎实的 Java 基础认知体系。
一。Object 类常用方法
在 Java 中,所有的类都隐式地继承了
java.lang.Object。它里面包含了一些核心方法,面试中常问的有以下几个:
equals(Object obj):用来指示其他某个对象是否与此对象“相等”。
hashCode():返回该对象的哈希码(整数),主要用于哈希表(如HashMap)。
toString():返回对象的字符串表示,默认是类名@十六进制内存地址(通常需要重写)。
getClass():返回对象的运行时类(反射的基础)。
clone():创建并返回此对象的一个副本(涉及浅拷贝与深拷贝)。
wait() / notify() / notifyAll():多线程协作的核心方法,用于线程的等待与唤醒。
二。== 与 equals() 对比
这是八股文中经典的送分题,但一定要答出深度。
1.
==运算符
==是一个比较运算符,它的核心逻辑非常纯粹:
基本数据类型(如
int,double):比较的是具体的值是否相等。引用数据类型(如
String, 自定义对象):比较的是内存中的首地址是否相同,即判断它们是不是同一个对象。2.
equals()方法
equals()是一个方法,只能由对象调用。
默认情况下:如果你不重写
equals(),它直接调用Object类中的默认实现。而Object的默认实现就是用==(见下方源码)。// Object 类中的原始源码 public boolean equals(Object obj) { return (this == obj); }重写之后:像
String、Integer、Date等内置类都重写了equals()方法,将其改为比较对象的内容(属性值)是否相等,而不是比较内存地址。3. 核心区别对比表
特征 == 运算符 equals() 方法 性质 运算符(Keyword) 类的方法(Method) 基本类型比较 比较值是否相等 无法用于基本类型(编译报错) 引用类型比较 比较内存地址(是否是同一个对象) 默认比较地址;重写后比较内容是否相等 可定制性 无法重写,逻辑固定 可以由开发者任意重写定制比较逻辑
三。hashCode() 与 equals()
这是八股文中的进阶题,面试官非常看重是否理解哈希表(如 HashMap、HashSet)的底层原理。
1. 什么是 hashCode()
hashCode()方法的作用是获取一个整型的哈希码(散列码)。这个哈希码的作用是确定该对象在哈希表中的索引位置。Java 中的哈希表利用它来大幅提高查询和插入的效率。2. 为什么重写 equals 必须要重写 hashCode
这是 Java 的官方规定。简单一句话总结:如果两个对象通过
equals()判定相等,那么它们的hashCode()必须相同。当你想往
HashSet里放一个新对象时,集合会按以下步骤判断是否重复:
先计算该对象的
hashCode,直接定位到它应该存放的位置。如果该位置没有其他对象:说明不重复,直接存入。
如果该位置已经有其他对象(发生哈希冲突):
此时集合会调用
equals()方法,拿新对象和该位置上的老对象挨个进行比较。如果
equals()返回true,说明内容重复了,拒绝存入。如果
equals()返回false,说明只是恰好哈希码一样,内容不同,则通过链表或红黑树将新对象连在后面。3. 如果不遵守契约会发生什么
假设你自定义了一个
User类,重写了equals(只要name相同就相等),但没有重写 hashCode。User u1 = new User("张三"); User u2 = new User("张三"); // 此时 u1.equals(u2) 为 true如果把它们放进
HashSet:
u1和u2的内存地址不同,因为没重写hashCode(),它们算出来的哈希码大概率不相同。
HashSet会把它们分到不同的桶里。结果:本该去重的
HashSet同时存入了u1和u2。集合的去重机制彻底失效4. 面试总结
如果两个对象相等(
equals()返回true),它们的hashCode一定相同。如果两个对象的
hashCode相同,它们并不一定相等(可能发生了哈希冲突)。因此,
equals()方法被重写过,hashCode()方法也必须被重写。先比
hashCode(效率高),再比equals(准确度高),这是兼顾性能与准确性的完美方案。
在 Java 中,String类(字符串)是开发中最常用的引用数据类型。为了方便日常开发和应对面试八股文,我们可以将String的常用方法按功能进行分类归纳。
四。 字符串方法
字符串获取
这类方法用于获取字符串的长度、特定位置的字符,或者与其他数据类型进行转换。
length():返回字符串的长度(字符个数)。String str = "Hello"; int len = str.length(); // 5
charAt(int index):返回指定索引处的字符(索引从 0 开始)。char ch = "Java".charAt(2); // 'v'
toCharArray():将字符串转换成一个全新的字符数组(char[])。
valueOf(...):这是一个静态方法,可以将基本数据类型(如int,double,boolean)或对象转换为字符串。String s = String.valueOf(123); // "123"字符串判断与比较
用于检查字符串的内容、状态或前后缀。
equals(Object anObject):比较两个字符串的内容是否相等(区分大小写)。
equalsIgnoreCase(String anotherString):比较两个字符串的内容是否相等,忽略大小写。boolean res = "abc".equalsIgnoreCase("ABC"); // true
isEmpty():判断字符串长度是否为 0(即"")。
isBlank()(Java 11+ 新增):判断字符串是否为空或者只包含空白字符(如空格、制表符\t)。" ".isEmpty(); // false " ".isBlank(); // true
startsWith(String prefix)/endsWith(String suffix):判断字符串是否以指定的子串开头或结尾。"Main.java".endsWith(".java"); // true字符串检索与查找
用于查找子字符串在原字符串中的位置。
contains(CharSequence s):判断字符串中是否包含指定的子文本,返回boolean。"Hello World".contains("or"); // true
indexOf(String str):返回子字符串第一次出现的索引,如果未找到则返回-1。
lastIndexOf(String str):返回子字符串最后一次出现的索引。字符串截取
注意:
String对象是不可变的(immutable)。以下方法看似“修改”了字符串,实际上都是返回了一个全新的 String 对象,原字符串并未改变。
substring(int beginIndex):从指定索引截取到字符串末尾。
substring(int beginIndex, int endIndex):截取[beginIndex, endIndex)范围内的字符串(左闭右开,包含开头,不包含结尾)。String sub = "Beautiful".substring(0, 4); // "Beau"
replace(CharSequence target, CharSequence replacement):替换字符串中所有的目标子串。String newStr = "aaa".replace("a", "b"); // "bbb"
replaceAll(String regex, String replacement):基于正则表达式替换所有匹配的内容。
split(String regex):根据匹配的正则表达式,将字符串分割成一个字符串数组(String[])。String[] parts = "A,B,C".split(","); // ["A", "B", "C"]
toLowerCase()/toUpperCase():将字符串转换为全小写或全大写。
trim():去除字符串首尾的普通空格(ASCII码小于等于32的字符)。
strip()(Java 11+ 新增):去除首尾的所有空白字符(支持 Unicode 里的各种空格,比trim()更强大)。字符串拼接与重复
join(CharSequence delimiter, CharSequence... elements)(Java 8+ 新增):用指定的连接符把多个字符串或集合拼接起来。String joined = String.join("-", "2026", "07", "08"); // "2026-07-08"
repeat(int count)(Java 11+ 新增):将字符串重复指定次数String multi = "A".repeat(3); // "AAA"面试加分项
length()是方法,不是属性:常考选择题。数组获取长度用的是.length属性,而String和集合(如List)获取长度/大小用的是.length()和.size()方法。
String的不可变性:在调用replace、substring、toLowerCase等方法时,一定要记得用新变量接收返回值,因为原字符串本身绝不会改变。
split()的坑:如果要用特殊符号(如.,|,*)作为分隔符,因为它们在正则表达式中有特殊含义,必须进行转义。例如用点分割:str.split("\\.")。
五。String、StringBuffer、StringBuilder
String:不可变字符串,每次操作都会生成新对象,效率低,线程安全。StringBuffer:可变字符串,方法加了同步锁(synchronized),效率较低,线程安全。StringBuilder:可变字符串,方法没有加锁,效率最高,线程不安全。
核心区别详解
1. 可变性(Immutability)
String内部是用char[]数组(Java 9 之后改为byte[]数组)来保存字符的。关键在于,这个数组被private final修饰,且String类没有暴露任何修改这个数组的方法。所以String对象一旦创建,其内容就绝对不可变。
StringBuffer与StringBuilder都继承自AbstractStringBuilder类,它们内部的字符数组没有final修饰。它们提供了append()、insert()、delete()等方法,可以直接在原有内存地址上修改字符串内容。2. 线程安全性(Thread Safety)
String:因为不可变,天生就是线程安全的,多个线程同时读它不会产生任何问题。
StringBuffer:绝大多数公开方法都加上了synchronized关键字(同步锁)。这意味着在多线程环境下,多个线程同时操作同一个StringBuffer会排队,虽然牺牲了性能,但保证了线程安全。
StringBuilder:去掉了synchronized锁。多线程同时操作同一个StringBuilder可能会发生数据错乱,它是线程不安全的。3. 性能(Performance)
StringBuilder>StringBuffer>String
StringBuilder因为没有锁的开销,速度最快。
StringBuffer其次。
String的速度最慢,因为任何修改、拼接操作(如str + "abc")在底层通常都需要创建新的对象,并涉及垃圾回收(GC)的开销。
内在联系
共同的父接口:三者都实现了
CharSequence接口,这意味着它们在很多方法(如length()、charAt()、subSequence())的设计上具有一致性。底层一脉相承:
StringBuffer和StringBuilder的绝大部分底层源代码和扩容机制是完全一样的,都依赖于AbstractStringBuilder。StringBuffer仅仅是在调用这些底层方法时套上了一层synchronized锁。可以相互转换:
String转后两者:通过构造函数new StringBuilder(str)。后两者转
String:调用普通的.toString()方法。
| 特性 | String | StringBuffer | StringBuilder |
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全性 | 安全(天生安全) | 安全(方法加同步锁) | 不安全(无锁) |
| 性能/速度 | 慢(频繁产生新对象) | 中等(受锁影响) | 快(无锁,直接修改) |
| 引入版本 | JDK 1.0 | JDK 1.0 | JDK 1.5 |
| 适用场景 | 少量字符串操作、常量定义 | 多线程环境下的字符串大量拼接 | 单线程环境下的字符串大量拼接 |
避坑与面试加分点
Q。既然 String 这么慢,为什么
str1 + str2没觉得卡顿A。
在现代 Java 中,编译器对
String的+拼接做了优化:
如果是常量拼接(如
String s = "a" + "b" + "c";),编译器在编译期就会将其直接优化为String s = "abc";,这叫常量折叠。如果是变量拼接(如
String s = str1 + str2;),在 JDK 8 中,编译器在底层会自动将其转化为StringBuilder.append()的方式来处理。但是,如果
+拼接发生在for循环里面,编译器每次循环都会重新new一个StringBuilder对象。所以,在循环体内进行字符串拼接,必须手动在循环外定义StringBuilder2. 总结
字符串改动少,优先用
String。单线程下大量拼接,优先用
StringBuilder(90% 的业务场景选它)。多线程(如多个线程共享同一个拼接缓冲器)下大量拼接,才考虑用
StringBuffer。
