Java基础常见面试题总结(中)
背诵重点
中篇只保留面向对象、Object 和 String 相关高频题。这部分最容易被追问“底层原因”和“容器里会不会出问题”,不要只背定义。
建议按这条线背:
- 面向对象:封装、继承、多态。
- 接口和抽象类:共同点、区别、怎么选。
Object:equals()、hashCode()、clone()。String:不可变、拼接、常量池、intern()。
面向对象三大特征是什么?
封装:把数据和行为放在对象内部,通过访问控制隐藏实现细节,对外暴露稳定接口。
继承:子类复用父类能力,表达 is-a 关系。Java 类只能单继承,避免多继承带来的菱形继承问题,但接口可以多实现。
多态:父类引用指向子类对象,运行时调用实际对象的方法。多态依赖继承、方法重写和向上转型。
一句话背诵:封装提高可维护性,继承提高复用性,多态提高扩展性。
构造方法有哪些特点?能被重写吗?
构造方法用于初始化对象,方法名和类名相同,没有返回值。
特点:
- 如果没有显式声明构造方法,编译器会生成无参构造方法。
- 如果已经声明了构造方法,编译器不会再自动生成无参构造方法。
- 构造方法不能被继承,因此不能被重写。
- 子类构造方法必须先调用父类构造方法,默认是
super()。
接口和抽象类有什么区别?
核心区别:抽象类描述“是什么”,接口描述“能做什么”。
具体区别:
- 类只能继承一个抽象类,但可以实现多个接口。
- 抽象类可以有成员变量、构造方法、普通方法;接口更适合定义能力规范。
- 接口字段默认是
public static final。 - Java 8 后接口可以有
default和static方法,Java 9 后接口可以有私有方法。 - 抽象类适合承载模板逻辑和共享状态,接口适合定义可插拔能力。
再补充几个容易被追问的点:
- 抽象类可以定义实例字段,接口不能定义普通实例字段。
- 抽象类可以有构造方法,但不能直接实例化;构造方法用于子类初始化父类状态。
- 接口方法默认是
public,抽象类方法可以有不同访问修饰符。 - 一个类实现接口必须实现接口约定的能力;继承抽象类通常意味着复用一套共同骨架。
选择:
- 多个类共享状态或公共流程,用抽象类。
- 只定义行为规范,或需要多实现,用接口。
为什么 Java 不支持类多继承?
主要是为了避免多继承带来的复杂性,尤其是菱形继承问题。
如果一个类同时继承两个父类,而两个父类中有同名方法或状态,子类调用时容易产生歧义。
Java 通过“类单继承 + 接口多实现”来平衡复用和扩展。接口默认方法发生冲突时,实现类必须显式重写解决。
多态怎么理解?
多态是同一个父类引用,在运行时根据实际子类对象调用不同方法。
Animal animal = new Dog();
animal.say();编译期看引用类型 Animal,运行期看实际对象 Dog。如果 Dog 重写了 say(),最终调用的是 Dog 的实现。
多态的价值:
- 调用方依赖抽象,不依赖具体实现。
- 新增子类时,调用方代码可以少改甚至不改。
- 常用于策略模式、模板方法、Spring Bean 注入等场景。
== 和 equals() 有什么区别?
==:
- 比基本类型:比较值。
- 比引用类型:比较地址。
equals():
- 默认来自
Object,本质也是比较地址。 - 很多类会重写它,比如
String、Integer,用于比较内容。
回答时一定要分“基本类型”和“引用类型”,不要只说 == 比地址。
hashCode() 有什么用?
hashCode() 常用于哈希容器,比如 HashMap、HashSet。
哈希容器会先用 hashCode() 定位桶,再用 equals() 判断桶内对象是否真正相等。
这样做的价值是提高查找效率:先快速缩小范围,再做精确比较。
为什么重写 equals() 必须重写 hashCode()?
因为 Java 约定:两个对象 equals() 为 true,它们的 hashCode() 必须相同。
如果只重写 equals() 不重写 hashCode(),对象放进 HashSet 或作为 HashMap 的 key 时,可能出现明明内容相等却查不到、去重失败的问题。
记忆:
equals()相等,hashCode()必须相等。hashCode()相等,equals()不一定相等,因为可能哈希冲突。
深拷贝、浅拷贝、引用拷贝有什么区别?
引用拷贝:只复制引用,两个引用指向同一个对象。
浅拷贝:复制对象本身,但对象里的引用字段仍指向原来的子对象。
深拷贝:复制对象本身,也递归复制引用字段指向的对象,新旧对象完全独立。
Object.clone() 默认是浅拷贝,因为它只复制当前对象字段值。
实现深拷贝的方式:
- 手写复制逻辑。
- 复制构造器。
- 序列化和反序列化。
- 使用对象映射工具。
String、StringBuilder、StringBuffer 有什么区别?
String 不可变,每次修改都会产生新对象。适合少量字符串和常量。
StringBuilder 可变,非线程安全,性能高,适合单线程大量拼接。
StringBuffer 可变,方法加了同步,线程安全但性能较低,现在使用场景较少。
实际建议:
- 少量拼接用
+,可读性好。 - 循环中大量拼接用
StringBuilder。 - 多线程共享同一个可变字符串对象时才考虑
StringBuffer,但更常见做法是避免共享可变对象。
StringBuffer 和 StringBuilder 性能有什么区别?
StringBuilder 通常比 StringBuffer 快,原因是 StringBuilder 不做同步控制;StringBuffer 的主要公开方法加了 synchronized,每次追加、删除、替换都要承担锁相关开销。
不过,性能差异要分场景看:
- 单线程大量拼接:优先使用
StringBuilder,没有锁开销。 - 多线程共享同一个字符串缓冲区:
StringBuffer可以保证方法级同步,但仍要注意复合操作是否满足业务原子性。 - 多线程但每个线程都有自己的局部变量:仍然用
StringBuilder,因为没有共享就不需要同步。 - 普通少量拼接:直接用
+即可,编译器或运行时通常会做优化,可读性更好。
面试一句话:StringBuilder 快在没有同步开销,StringBuffer 慢在方法级同步;但实际项目里更推荐避免多线程共享可变字符串对象。
String 为什么不可变?
原因:
String类是final,不能被继承破坏行为。- 内部字符数据不对外暴露可变引用。
- 字符串常量池可以安全复用。
- 不可变对象线程安全。
- 可以缓存
hashCode(),适合作为HashMap的 key。
注意:final 修饰引用只能保证引用不变,不能单独保证对象内容不变。String 不可变是类设计共同保证的结果。
更底层地说,String 的不可变性不是只靠一个关键字实现的,而是靠组合设计实现的:
- 类本身
final,避免子类重写方法破坏不可变语义。 - 保存字符内容的字段是私有的,外部拿不到内部数组或字节数组的可写引用。
- 任何看似修改字符串的方法,例如
substring()、replace()、concat(),都会返回新字符串,而不是修改原对象。
这样设计的理由主要有 4 个:
- 安全:类名、文件路径、URL、数据库连接信息等大量场景会用字符串,值不可变能降低被篡改风险。
- 线程安全:不可变对象天然可以在线程间共享,不需要额外同步。
- 常量池复用:字符串字面量可以放入字符串常量池,多处引用同一个对象也不会互相影响。
- 哈希缓存:
String常作为HashMap的 key,不可变可以保证hashCode()稳定,缓存哈希值才安全。
字符串拼接用 + 还是 StringBuilder?
分场景:
- 编译期常量拼接会被编译器直接优化。
- 普通少量拼接用
+没问题。 - 循环中大量拼接用
StringBuilder。
String s = "a" + "b"; // 编译期可优化成 "ab"循环里如果一直用 +,可能反复创建中间对象,影响性能。
字符串常量池有什么作用?
字符串常量池用于复用字符串对象,减少重复创建,节省内存。
String a = "abc";
String b = "abc";
System.out.println(a == b); // true两个字符串字面量都指向常量池里的同一个 "abc"。
new String("abc") 创建几个对象?
分情况:
- 如果常量池中没有
"abc",会创建两个对象:常量池中的"abc"和堆上的String对象。 - 如果常量池中已经有
"abc",只创建一个堆上的String对象。
String s = new String("abc");面试时不要死背“两个”,要说清是否已经存在于常量池。
String.intern() 有什么作用?
intern() 会返回字符串在常量池中的引用。
如果常量池中已经有相同内容的字符串,直接返回池中引用;如果没有,则尝试把当前字符串放入常量池,并返回池中引用。
面试重点:
intern()返回的是常量池引用。- JDK 7 后字符串常量池移到堆中,具体表现和早期版本不同。
- 实际业务中不要为了“优化”随便大量调用
intern(),要结合内存和重复率评估。
String 类型变量和常量做 + 运算有什么区别?
常量之间的 + 会在编译期优化。
String a = "a" + "b"; // 编译后等价于 "ab"变量参与拼接通常在运行期完成。
String x = "a";
String y = x + "b";运行期拼接可能通过 StringBuilder 或 invokedynamic 优化,具体和 JDK 版本有关。面试只需说清:常量拼接编译期优化,变量拼接运行期处理。
中篇口述模板
回答对象和字符串题时按这个顺序:
- 先说明概念。
- 再说明底层规则。
- 补充容器或常量池里的坑。
- 结合项目实践给建议。
比如问“为什么重写 equals() 必须重写 hashCode()”:
可以答:因为哈希容器会先用 hashCode() 定位桶,再用 equals() 判断对象是否相等。如果两个对象 equals() 为 true 但哈希值不同,它们可能落到不同桶里,导致 HashSet 去重失败或 HashMap 查不到 key。
