Java基础概念四连问:==与equals、hashCode约定、接口vs抽象类、深拷贝vs浅拷贝

前言

在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() 是方法,默认行为同 ==,但 StringInteger 等包装类重写了它,实现内容比较。


二、hashCode() 和 equals() 的约定

这是理解Java集合框架(尤其是HashMap)的关键。

2.1 核心约定

Java官方文档明确规定了 hashCode()equals() 的契约:

  1. 一致性 :如果 equals() 返回 true,那么两个对象的 hashCode() 必须相等
  2. 非强制但重要 :如果 equals() 返回 false,hashCode() 可以相等也可以不等
  3. 稳定性 :对象未改变时,多次调用 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() 方法

实现浅拷贝需要:

  1. 实现 Cloneable 接口(标记接口)
  2. 重写 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 注意事项

  1. 数组的 clone() 是浅拷贝

    java 复制代码
    int[][] arr1 = {{1, 2}, {3, 4}};
    int[][] arr2 = arr1.clone();  // 浅拷贝,arr2[0] 和 arr1[0] 指向同一数组
  2. 集合框架的拷贝

    java 复制代码
    // 浅拷贝
    List<Person> newList = new ArrayList<>(oldList);
    
    // 深拷贝需要遍历处理
    List<Person> deepCopy = oldList.stream()
        .map(Person::deepClone)
        .collect(Collectors.toList());
  3. 不可变对象:String、Integer 等不可变对象,深拷贝和浅拷贝没有区别(无法修改)

💡 面试话术 :浅拷贝只复制引用地址,深拷贝复制整个对象图。Java 的 clone() 方法是浅拷贝,实现深拷贝需要递归处理引用类型,或使用序列化。实际开发中,我更推荐使用拷贝构造函数或工具类,避免直接使用 clone()


五、总结

知识点 核心要点
== vs equals() == 比较地址,equals() 比较内容(需重写)
hashCode() 约定 equals 相等的对象,hashCode 必须相等
接口 vs 抽象类 抽象类表示 is-a,接口表示 can-do;Java 8+ 接口支持 default 方法
浅拷贝 vs 深拷贝 浅拷贝共享引用对象,深拷贝完全独立

这四个基础概念看似简单,却是构建 Java 知识体系的基石。希望这篇文章能帮助你在面试中从容应对基础题的考察。


六、高频面试题自测

  1. String s = new String("abc") 创建了几个对象?
  2. 为什么重写 equals 时必须重写 hashCode?
  3. Java 8 的接口有哪些新特性?
  4. 如何实现一个不可变类?
  5. 浅拷贝中,String 类型的字段会受影响吗?

欢迎在评论区留下你的答案,一起交流讨论!


📌 系列预告:后续将继续推出 Java 集合框架、JVM 内存模型、多线程进阶等系列文章,敬请期待!

相关推荐
yyt3630458415 分钟前
spring单例bean线程安全问题讨论
java·spring
weixin_649555679 分钟前
C语言程序设计第四版(何钦铭、颜晖)第十一章指针进阶之奇数值结点链表
c语言·开发语言·链表
书到用时方恨少!25 分钟前
Python os 模块使用指南:系统交互的瑞士军刀
开发语言·python
我是大猴子26 分钟前
事务失效的几种情况以及是为什么(详解)
java·开发语言
武藤一雄1 小时前
C#:nameof 运算符全指南
开发语言·microsoft·c#·.net·.netcore
wertyuytrewm1 小时前
Java面试——Java基础
java·jvm·面试
czlczl200209251 小时前
RAG实现思路流程
java·jvm
带娃的IT创业者1 小时前
WeClaw_40_系统监控与日志体系:多层次日志架构与Trace追踪
java·开发语言·python·架构·系统监控·日志系统·链路追踪
Y001112361 小时前
JDBC原理
java·开发语言·数据库·jdbc
程序员侠客行2 小时前
Tomcat 从陌生到熟悉
java·tomcat·web