前言
在Java面试中,基础概念的考察往往是最先开始的环节。很多开发者能在高并发、分布式等高级话题上侃侃而谈,却在这些基础问题上"翻车"。原因很简单:越是基础,越能体现功底的扎实程度。
本文将深入剖析四个经典的基础概念问题,从原理到实践,帮你构建完整的知识体系。
一、== 和 equals() 的区别
这是Java面试中出现频率最高的基础题,没有之一。
1.1 核心区别
| 比较方式 | 作用 | 适用场景 |
|---|---|---|
== |
比较内存地址(引用是否指向同一个对象) | 基本数据类型比较、判断两个引用是否指向同一对象 |
equals() |
比较内容是否相等(可被子类重写) | 对象内容的逻辑相等性判断 |
1.2 基本数据类型 vs 引用类型
基本数据类型(byte, short, int, long, float, double, char, boolean):
- 只能使用
==比较 - 比较的是值是否相等
java
int a = 10;
int b = 10;
System.out.println(a == b); // true
引用类型:
==比较的是内存地址(是否同一个对象)equals()默认行为也是比较地址(Object类实现),但子类可以重写
java
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false(不同对象)
System.out.println(s1.equals(s2)); // true(内容相同)
1.3 String 的特殊性
String 类重写了 equals() 方法,比较字符串内容。同时,String 有字符串常量池机制:
java
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2); // true(常量池复用)
System.out.println(s1 == s3); // false(堆中新对象)
System.out.println(s1.equals(s3)); // true
1.4 重写 equals() 的原则
如果自定义类需要内容比较,必须重写 equals() 方法,遵循自反性、对称性、传递性、一致性、非空性五大原则:
java
public class Person {
private String id;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true; // 同一对象
if (o == null || getClass() != o.getClass()) return false; // 类型检查
Person person = (Person) o;
return Objects.equals(id, person.id); // 核心字段比较
}
}
💡 面试话术 :
==比较的是栈中的值,基本类型是数值,引用类型是地址。equals()是方法,默认行为同==,但String、Integer等包装类重写了它,实现内容比较。
二、hashCode() 和 equals() 的约定
这是理解Java集合框架(尤其是HashMap)的关键。
2.1 核心约定
Java官方文档明确规定了 hashCode() 和 equals() 的契约:
- 一致性 :如果
equals()返回 true,那么两个对象的hashCode()必须相等 - 非强制但重要 :如果
equals()返回 false,hashCode()可以相等也可以不等 - 稳定性 :对象未改变时,多次调用
hashCode()应返回相同值
2.2 为什么需要这个约定?
因为基于哈希的集合(HashMap、HashSet、Hashtable)依赖这个约定来工作。
以 HashMap 为例:
- 存储时:先计算
hashCode()确定存储位置(桶) - 查找时:先定位桶,再用
equals()比较确认
如果违反约定:
- 只重写
equals()不重写hashCode()→ 相同对象可能散列到不同桶,导致重复存储或无法查找 - 只重写
hashCode()不重写equals()→ 不同对象可能落在同一桶,但equals()返回 false,无法正确匹配
2.3 正确示例
java
public class Student {
private String studentId;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(studentId, student.studentId);
}
@Override
public int hashCode() {
// 与 equals 中使用相同的字段计算 hashCode
return Objects.hash(studentId);
}
}
2.4 错误示例与后果
java
public class BadStudent {
private String studentId;
@Override
public boolean equals(Object o) {
// 重写了 equals,但没有重写 hashCode
return this.studentId.equals(((BadStudent) o).studentId);
}
}
// 使用时的问题
Set<BadStudent> set = new HashSet<>();
BadStudent s1 = new BadStudent("001");
BadStudent s2 = new BadStudent("001");
set.add(s1);
set.add(s2); // 本应重复,但 set 中会有两个对象!
System.out.println(set.size()); // 输出 2,而不是 1
2.5 hashCode() 的设计原则
- 使用相同字段 :与
equals()中使用相同的字段计算 - 散列均匀:尽量减少冲突,提高哈希表性能
- 不可变字段优先 :集合中存储对象后,如果参与
hashCode()的字段变化,会导致对象在集合中"丢失"
💡 面试话术 :
equals()用于逻辑相等判断,hashCode()用于散列存储。它们的约定是:equals 相等的两个对象,hashCode 必须相等。违反此约定会导致 HashMap、HashSet 等集合出现数据重复或无法查找的问题。
三、接口和抽象类的区别
这是面向对象设计的核心概念,考察对抽象层级的理解。
3.1 核心区别对比表
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 关键词 | abstract class |
interface |
| 继承/实现 | extends(单继承) |
implements(多实现) |
| 构造方法 | 可以有 | 不能有 |
| 成员变量 | 可以有任意类型 | 默认 public static final(Java 8前),Java 9后支持 private |
| 普通方法 | 可以有 | Java 8前只能有抽象方法,Java 8后支持 default/static 方法 |
| 访问修饰符 | 任意 | 默认 public(Java 9后支持 private) |
| 设计意图 | 表示 is-a 关系 | 表示 can-do 能力 |
3.2 代码示例
抽象类:
java
public abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
// 抽象方法
public abstract void sound();
// 具体方法
public void sleep() {
System.out.println(name + " is sleeping");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void sound() {
System.out.println(name + " says woof");
}
}
接口:
java
public interface Flyable {
// 常量(默认 public static final)
int MAX_HEIGHT = 10000;
// 抽象方法(默认 public abstract)
void fly();
// Java 8:默认方法
default void land() {
System.out.println("Landing...");
}
// Java 8:静态方法
static void checkWeather() {
System.out.println("Weather is good");
}
}
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
3.3 多实现 vs 单继承
接口支持多实现:
java
public class Duck implements Flyable, Swimmable {
// 可以实现多个接口
}
抽象类只支持单继承:
java
public class Penguin extends Animal {
// 只能继承一个抽象类
}
3.4 Java 8+ 接口的演进
| 版本 | 新增特性 |
|---|---|
| Java 7 | 只能有抽象方法和常量 |
| Java 8 | default 方法、static 方法 |
| Java 9 | private 方法(用于复用 default 方法逻辑) |
| Java 17+ | 接口逐渐拥有更多抽象类的特性 |
3.5 如何选择?
| 场景 | 推荐 |
|---|---|
| 需要定义模板方法,有公共状态 | 抽象类 |
| 定义能力/行为契约 | 接口 |
| 需要多重继承能力 | 接口 |
需要非 public 成员变量 |
抽象类 |
| 代码演进需要向后兼容 | 接口 + default 方法 |
💡 面试话术 :抽象类强调 是什么 (is-a),接口强调 能做什么(can-do)。Java 8 后接口功能增强,但单继承的限制依然存在。实际开发中,通常优先定义接口作为契约,再用抽象类提供默认实现。
四、深拷贝和浅拷贝
4.1 概念对比
| 拷贝类型 | 定义 | 复制程度 |
|---|---|---|
| 浅拷贝 | 复制对象的基本类型字段和引用字段的地址 | 原对象和副本共享引用类型的对象 |
| 深拷贝 | 复制对象的所有字段,包括引用类型指向的对象 | 原对象和副本完全独立 |
4.2 图解对比
浅拷贝:
原始对象 ----------→ 引用对象
↑ ↑
└---副本对象------┘
深拷贝:
原始对象 ----------→ 引用对象
副本对象 ----------→ 新引用对象(独立副本)
4.3 浅拷贝实现:clone() 方法
实现浅拷贝需要:
- 实现
Cloneable接口(标记接口) - 重写
clone()方法,调用super.clone()
java
public class Address {
String city;
public Address(String city) { this.city = city; }
}
public class Person implements Cloneable {
private String name;
private int age;
private Address address;
public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public Person clone() {
try {
return (Person) super.clone(); // 浅拷贝
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
// 测试
Address addr = new Address("Beijing");
Person p1 = new Person("Zhang", 25, addr);
Person p2 = p1.clone();
p2.address.city = "Shanghai";
System.out.println(p1.address.city); // 输出 "Shanghai" → 被修改了!
4.4 深拷贝实现方式
方式一:重写 clone() 递归拷贝
java
@Override
public Person deepClone() {
Person copy = (Person) super.clone();
// 手动拷贝引用类型
copy.address = new Address(this.address.city);
return copy;
}
方式二:序列化(推荐)
java
public Person deepCloneBySerialization() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (Person) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
方式三:拷贝工具类(如 Apache Commons)
java
import org.apache.commons.lang3.SerializationUtils;
Person p2 = SerializationUtils.clone(p1);
方式四:拷贝构造函数
java
public Person(Person original) {
this.name = original.name;
this.age = original.age;
this.address = new Address(original.address.city); // 深拷贝
}
Person p2 = new Person(p1);
4.5 各方式对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| 手动 clone | 性能好,精确控制 | 代码繁琐,需要维护 |
| 序列化 | 简单通用,无需逐字段处理 | 性能较差,需实现 Serializable |
| 拷贝工具类 | 代码简洁 | 依赖第三方库 |
| 拷贝构造函数 | 类型安全,易于理解 | 需要手动维护 |
4.6 注意事项
-
数组的 clone() 是浅拷贝:
javaint[][] arr1 = {{1, 2}, {3, 4}}; int[][] arr2 = arr1.clone(); // 浅拷贝,arr2[0] 和 arr1[0] 指向同一数组 -
集合框架的拷贝:
java// 浅拷贝 List<Person> newList = new ArrayList<>(oldList); // 深拷贝需要遍历处理 List<Person> deepCopy = oldList.stream() .map(Person::deepClone) .collect(Collectors.toList()); -
不可变对象:String、Integer 等不可变对象,深拷贝和浅拷贝没有区别(无法修改)
💡 面试话术 :浅拷贝只复制引用地址,深拷贝复制整个对象图。Java 的
clone()方法是浅拷贝,实现深拷贝需要递归处理引用类型,或使用序列化。实际开发中,我更推荐使用拷贝构造函数或工具类,避免直接使用clone()。
五、总结
| 知识点 | 核心要点 |
|---|---|
== vs equals() |
== 比较地址,equals() 比较内容(需重写) |
hashCode() 约定 |
equals 相等的对象,hashCode 必须相等 |
| 接口 vs 抽象类 | 抽象类表示 is-a,接口表示 can-do;Java 8+ 接口支持 default 方法 |
| 浅拷贝 vs 深拷贝 | 浅拷贝共享引用对象,深拷贝完全独立 |
这四个基础概念看似简单,却是构建 Java 知识体系的基石。希望这篇文章能帮助你在面试中从容应对基础题的考察。
六、高频面试题自测
- String s = new String("abc") 创建了几个对象?
- 为什么重写 equals 时必须重写 hashCode?
- Java 8 的接口有哪些新特性?
- 如何实现一个不可变类?
- 浅拷贝中,String 类型的字段会受影响吗?
欢迎在评论区留下你的答案,一起交流讨论!
📌 系列预告:后续将继续推出 Java 集合框架、JVM 内存模型、多线程进阶等系列文章,敬请期待!