一、面向对象的特征有哪些方面
Java中面向对象的特征主要包括以下四个核心方面:
-
封装(Encapsulation)
封装是指将对象的属性(数据)和方法(操作)捆绑在一起,隐藏对象的内部实现细节,只通过公共接口与外部交互。通过访问修饰符(如private、public等)控制属性和方法的访问权限,提高了代码的安全性和可维护性。例如,将类的成员变量声明为private,通过public的getter和setter方法访问和修改。 -
继承(Inheritance)
继承允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码复用和扩展。子类可以拥有父类的非私有成员,并可以重写父类的方法以实现特定功能。Java中通过extends
关键字实现单继承,一个子类只能有一个直接父类,但支持多层继承。 -
多态(Polymorphism)
多态是指同一操作作用于不同对象时,产生不同的执行结果。它有两种实现方式:- 编译时多态:通过方法重载(同一类中方法名相同、参数列表不同)实现。
- 运行时多态:通过方法重写(子类重写父类方法)和向上转型(父类引用指向子类对象)实现。
多态提高了代码的灵活性和扩展性。
-
抽象(Abstraction)
抽象是指忽略次要细节,只关注本质特征。在Java中,通过抽象类(abstract class
)和接口(interface
)实现:- 抽象类可以包含抽象方法(无实现)和具体方法,子类必须实现抽象方法。
- 接口仅包含抽象方法(Java 8后可有默认方法),类通过
implements
关键字实现接口并必须实现其所有方法。
抽象简化了复杂系统,便于组件化开发。
这些特征相互配合,使Java代码具有更好的复用性、可扩展性和可维护性,是面向对象编程思想的核心体现。
二、在Java中,如何实现多态?
在Java中,多态的实现主要依赖于方法重写(Override) 和向上转型(Upcasting) 这两个机制,具体可以分为以下步骤:
1. 定义继承关系
多态通常建立在类的继承体系上,需要一个父类和至少一个子类。子类通过extends
关键字继承父类。
2. 子类重写父类方法
子类对父类的方法进行重写(方法名、参数列表、返回值类型必须与父类一致),实现子类特有的逻辑。
3. 向上转型(父类引用指向子类对象)
使用父类类型的引用变量指向子类的实例对象,此时通过该引用调用方法时,会动态绑定到子类的重写方法。
示例代码:
// 1. 定义父类
class Animal {// 父类方法public void makeSound() {System.out.println("动物发出声音");}
}// 2. 定义子类,重写父类方法
class Dog extends Animal {@Override // 注解表示重写public void makeSound() {System.out.println("狗叫:汪汪汪");}
}class Cat extends Animal {@Overridepublic void makeSound() {System.out.println("猫叫:喵喵喵");}
}// 3. 测试多态
public class PolymorphismDemo {public static void main(String[] args) {// 向上转型:父类引用指向子类对象Animal animal1 = new Dog(); Animal animal2 = new Cat();// 调用方法时,实际执行的是子类的重写方法animal1.makeSound(); // 输出:狗叫:汪汪汪animal2.makeSound(); // 输出:猫叫:喵喵喵}
}
多态的其他形式:
-
接口多态:类通过
implements
实现接口,多个类可以实现同一个接口并实现其方法,通过接口引用指向不同实现类的对象。interface Shape {void draw(); }class Circle implements Shape {@Overridepublic void draw() {System.out.println("画圆形");} }class Rectangle implements Shape {@Overridepublic void draw() {System.out.println("画矩形");} }
-
方法重载(Overload):严格来说是“编译时多态”,指同一类中方法名相同但参数列表不同,调用时根据参数自动匹配对应方法。
核心总结:
多态的本质是 “一个接口,多种实现”,通过父类/接口引用指向子类/实现类对象,在运行时动态确定调用的具体方法,从而提高代码的灵活性和可扩展性。
三、abstract class和interface有什么区别?
在Java中,abstract class
(抽象类)和interface
(接口)都是实现抽象的重要机制,但它们在设计目的和使用方式上有显著区别,主要体现在以下几个方面:
1. 定义与结构
-
抽象类:
使用abstract class
声明,可包含抽象方法(无实现,用abstract
修饰)和具体方法(有实现),也可以定义成员变量(包括非静态和非final的变量)。
示例:abstract class Animal {String name; // 成员变量// 具体方法public void breathe() {System.out.println("呼吸空气");}// 抽象方法(无实现)public abstract void makeSound(); }
-
接口:
使用interface
声明,Java 8之前只能包含抽象方法(默认public abstract
)和常量(默认public static final
);Java 8及以后可包含默认方法(default
修饰,有实现)和静态方法(static
修饰)。
示例:interface Flyable {int MAX_HEIGHT = 1000; // 常量(默认public static final)// 抽象方法(默认public abstract)void fly();// 默认方法(Java 8+)default void land() {System.out.println("降落");} }
2. 继承/实现方式
-
抽象类:
子类通过extends
关键字继承抽象类,只能单继承(一个类只能有一个直接父类)。
子类必须实现抽象类中所有的抽象方法,否则自身也需声明为抽象类。 -
接口:
类通过implements
关键字实现接口,支持多实现(一个类可同时实现多个接口)。
实现类必须实现接口中所有的抽象方法(默认方法和静态方法除外)。
3. 设计目的
-
抽象类:
用于表示 “is-a”关系(即继承体系中的“父子关系”),强调类之间的共性和继承性。
适合封装多个子类的共享实现细节(如通用方法、成员变量),是对类的抽象。 -
接口:
用于表示 “has-a”关系(即类具备某种能力或行为),强调功能的抽象和实现的多样性。
适合定义规范或契约(如“可飞行”“可序列化”),是对行为的抽象。
4. 访问修饰符
- 抽象类中的成员变量和方法可以使用
private
、protected
、public
等修饰符。 - 接口中的抽象方法和常量默认是
public
的,且不能使用private
或protected
修饰(Java 9后允许接口有private
方法)。
5. 实例化
- 抽象类和接口都不能直接实例化,但抽象类的子类(非抽象)和接口的实现类可以被实例化。
选择建议:
- 若需要定义类的基础结构和共享实现,且类之间是继承关系(is-a),使用抽象类。
- 若需要定义行为规范,且类可能具备多种独立的行为(has-a),使用接口(支持多实现)。
例如:
- 用抽象类
Animal
定义所有动物的共性(如呼吸、进食); - 用接口
Runnable
、Swimmable
定义“会跑”“会游泳”等独立行为,让不同动物类根据自身能力实现。
四、abstract的method是否可同时是static?是否可同时是native?是否可同时是synchronized?
在Java中,abstract
方法与static
、native
、synchronized
这三个修饰符存在冲突,不能同时使用,具体原因如下:
1. abstract
与 static
不能同时使用
-
原因:
static
方法属于类级别的方法,可通过类名直接调用,其实现是固定的(属于类本身);
而abstract
方法没有具体实现,需要子类重写后才能使用。
两者语义矛盾:static
要求方法“可直接调用且实现固定”abstract
要求方法“无实现且需子类重写”
-
错误示例:
abstract class Test {// 编译错误:abstract 与 static 不能同时使用public abstract static void method(); }
2. abstract
与 native
不能同时使用
-
原因:
native
方法表示方法的实现由非Java代码(如C/C++)提供,其底层有具体实现(只是不在Java代码中);
而abstract
方法要求“完全没有实现”,需要子类重写。
两者语义矛盾:native
隐含“有实现(非Java代码)”abstract
隐含“无实现”
-
错误示例:
abstract class Test {// 编译错误:abstract 与 native 不能同时使用public abstract native void method(); }
3. abstract
与 synchronized
不能同时使用
-
原因:
synchronized
用于修饰方法或代码块,作用是保证多线程环境下的同步(锁定对象或类),其前提是方法有具体实现;
而abstract
方法没有实现,同步机制无从谈起(没有可执行的代码需要同步)。
因此两者不能同时使用。 -
错误示例:
abstract class Test {// 编译错误:abstract 与 synchronized 不能同时使用public abstract synchronized void method(); }
总结:
abstract
方法的核心是 “无实现,需子类重写”,而 static
、native
、synchronized
均要求方法“有具体实现”或“与实现细节相关”,因此这三组组合在Java中都是不允许的,会导致编译错误。
五、JDK、JRE、JVM 三者有什么关系?
JDK、JRE、JVM 是 Java 生态系统中三个核心概念,它们的关系可以简单概括为:JDK 包含 JRE,JRE 包含 JVM,三者层层递进,共同支撑 Java 程序的开发与运行。具体关系如下:
1. JVM(Java Virtual Machine,Java 虚拟机)
- 定义:是运行 Java 字节码的虚拟计算机,负责将字节码翻译成机器码并执行。
- 作用:实现 Java 跨平台特性的核心(“一次编写,到处运行”),不同操作系统需要安装对应的 JVM 实现(如 Windows、Linux 版 JVM)。
- 特点:本身不包含任何 Java 类库,仅负责执行字节码。
2. JRE(Java Runtime Environment,Java 运行时环境)
- 定义:是运行 Java 程序的最小环境。
- 组成:
- JVM:字节码执行引擎
- 核心类库:Java 基础类库(如
java.lang
、java.util
等,位于rt.jar
中) - 其他支持文件:确保 JVM 正常运行的配置文件、资源文件等
- 作用:供用户运行已编译好的 Java 程序(
.class
或.jar
文件),普通用户只需安装 JRE 即可。
3. JDK(Java Development Kit,Java 开发工具包)
- 定义:是 Java 开发人员使用的工具包。
- 组成:
- JRE:包含完整的运行时环境(即 JVM + 类库)
- 开发工具:编译器(
javac
)、调试器(jdb
)、文档工具(javadoc
)、打包工具(jar
)等 - 额外类库:开发时专用的类库(如
tools.jar
)
- 作用:供开发人员编写、编译、调试 Java 程序,开发者必须安装 JDK。
三者关系总结:
-
包含关系:
JDK ⊇ JRE ⊇ JVM
(JDK 包含 JRE,JRE 包含 JVM 和运行类库) -
功能分工:
- 用 JDK 编写和编译代码(
javac Hello.java
→ 生成Hello.class
字节码); - 用 JRE 中的 JVM 运行字节码(
java Hello
→ JVM 加载并执行Hello.class
)。
- 用 JDK 编写和编译代码(
-
使用场景:
- 开发人员:需要安装 JDK(包含开发工具和运行环境);
- 普通用户:仅需安装 JRE(只需运行 Java 程序)。
简单来说,JVM 是运行核心,JRE 是运行环境,JDK 是开发工具集,三者协同工作完成 Java 程序的开发与执行。
六、JAVA 创建对象有哪些方式?
在Java中,创建对象的方式主要有以下几种,每种方式适用于不同的场景:
-
使用new关键字(最常用)
通过调用类的构造方法创建对象,这是最基本、最直接的方式。User user = new User(); // 调用无参构造 User user2 = new User("张三", 20); // 调用有参构造
-
使用Class类的newInstance()方法(反射机制)
通过类的字节码对象创建实例,要求类必须有无参构造方法(已过时,推荐用getDeclaredConstructor())。Class<User> clazz = User.class; User user = clazz.newInstance(); // 已过时
-
使用Constructor类的newInstance()方法(反射机制)
比Class的newInstance()更灵活,支持调用有参构造方法,且可以访问私有构造。Constructor<User> constructor = User.class.getDeclaredConstructor(String.class, int.class); User user = constructor.newInstance("张三", 20);
-
使用对象的clone()方法
通过复制已有对象创建新对象,要求类实现Cloneable
接口并重写clone()
方法。public class User implements Cloneable {@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();} } // 使用 User user = new User(); User user2 = (User) user.clone();
-
通过反序列化创建
将序列化的对象字节流恢复为对象,要求类实现Serializable
接口。// 序列化(保存对象到文件) ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.txt")); oos.writeObject(user);// 反序列化(从文件恢复对象) ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.txt")); User user2 = (User) ois.readObject();
-
通过工厂模式创建(设计模式)
封装对象创建逻辑,通过工厂类统一创建对象(不属于Java语法层面的方式,是设计模式)。public class UserFactory {public static User createUser() {return new User();} } // 使用 User user = UserFactory.createUser();
总结:
- 日常开发中最常用的是 new关键字 和 工厂模式。
- 反射机制(Class/Constructor)常用于框架底层(如Spring IoC)。
- clone()和反序列化适用于对象复制或跨网络/文件传输场景。
七、值传递和引用传递的区别?
在Java中,值传递和引用传递是关于方法参数传递机制的核心概念,它们的区别主要体现在参数传递的方式和对原数据的影响上:
- 值传递(Pass by Value)
-
定义:方法接收的是实际参数的副本(值的拷贝)。
-
特点:
- 方法内部对参数的修改,不会影响原数据(因为操作的是副本)。
- 适用于基本数据类型(如
int
、double
、boolean
等)和字符串(String)(特殊的引用类型,但具有不可变性)。
-
示例:
public static void main(String[] args) {int num = 10;System.out.println("修改前:" + num); // 输出:10changeValue(num);System.out.println("修改后:" + num); // 输出:10(原数据未变) }public static void changeValue(int a) {a = 20; // 仅修改副本,不影响原变量 }
- 引用传递(Pass by Reference)
-
定义:方法接收的是实际参数的引用地址(内存地址),而非值的副本。
-
特点:
- 方法内部通过引用修改对象的成员变量时,会影响原对象(因为操作的是同一块内存)。
- 适用于引用数据类型(如对象、数组、集合等)。
-
示例:
class Person {String name;Person(String name) { this.name = name; } }public static void main(String[] args) {Person p = new Person("张三");System.out.println("修改前:" + p.name); // 输出:张三changeName(p);System.out.println("修改后:" + p.name); // 输出:李四(原对象被修改) }public static void changeName(Person person) {person.name = "李四"; // 通过引用修改原对象的成员变量 }
关键区别总结:
对比维度 | 值传递 | 引用传递 |
---|---|---|
传递内容 | 实际参数的值副本 | 实际参数的内存地址 |
对原数据的影响 | 不影响原数据 | 会影响原对象的成员变量 |
适用类型 | 基本数据类型、String | 引用数据类型(对象、数组等) |
注意点:
- Java中只有值传递:对于引用类型,传递的是“引用地址的值”(即地址的副本),而非引用本身。这也是为什么在方法中修改引用变量的指向(如
person = new Person("王五")
)不会影响原对象的原因。 - String和包装类(如
Integer
)虽然是引用类型,但因不可变性(值一旦创建无法修改),表现类似值传递。
八、== 和 equals 有什么区别?
在Java中,==
和 equals()
都用于比较数据,但它们的比较逻辑和适用场景有本质区别:
==
运算符
-
比较内容:
- 对于基本数据类型(如
int
、double
、boolean
等):比较的是实际值是否相等。 - 对于引用数据类型(如对象、数组等):比较的是内存地址是否相同(即是否指向同一个对象)。
- 对于基本数据类型(如
-
示例:
// 基本数据类型比较 int a = 10; int b = 10; System.out.println(a == b); // true(值相等)// 引用数据类型比较 String s1 = new String("hello"); String s2 = new String("hello"); System.out.println(s1 == s2); // false(内存地址不同,是两个不同对象)
equals()
方法
-
本质:是
Object
类定义的实例方法,默认实现与==
相同(比较内存地址)。// Object类中equals()的默认实现 public boolean equals(Object obj) {return (this == obj); // 本质还是比较内存地址 }
-
特殊情况:
很多类(如String
、Integer
、List
等)会重写equals()
方法,使其比较对象的内容是否相等,而非内存地址。 -
示例:
String s1 = new String("hello"); String s2 = new String("hello");// String类重写了equals(),比较内容 System.out.println(s1.equals(s2)); // true(内容都是"hello")// 自定义类如果不重写equals(),则默认比较地址 class Person {String name;Person(String name) { this.name = name; } } Person p1 = new Person("张三"); Person p2 = new Person("张三"); System.out.println(p1.equals(p2)); // false(未重写equals(),比较地址)
核心区别总结:
对比维度 | == 运算符 | equals() 方法 |
---|---|---|
本质 | 运算符 | 实例方法 |
基本类型比较 | 比较值是否相等 | 不适用(基本类型没有方法) |
引用类型比较 | 比较内存地址是否相同 | 默认比较地址,重写后可比较内容 |
可修改性 | 无法重写,逻辑固定 | 可重写,自定义比较逻辑 |
常见误区:
- 认为
String
的==
比较内容:实际上String
是引用类型,==
仍比较地址,只是字符串常量池的存在可能造成误解(如String s1 = "hello"; String s2 = "hello";
中s1 == s2
为true
,因为指向常量池同一对象)。 - 自定义类未重写
equals()
却期望比较内容:必须手动重写equals()
(通常需同时重写hashCode()
,遵循“相等的对象必须有相等的哈希码”原则)。
九、hashCode() 的作用?
在Java中,hashCode()
是 Object
类定义的一个 native 方法,主要作用是为对象生成一个哈希值(整数),这个值在哈希表(如 HashMap
、HashSet
等集合)中用于快速定位对象,是实现高效数据查找的核心机制。
- 核心作用:支持哈希表的高效操作
哈希表(如HashMap
)的底层存储依赖数组 + 链表/红黑树结构,hashCode()
的值会被用来计算对象在数组中的存储索引。
- 当添加对象时,通过
hashCode()
快速定位可能的存储位置,避免遍历整个集合,大幅提升插入和查找效率。 - 当查找对象时,先通过
hashCode()
定位到候选位置,再用equals()
精确比较,减少比对次数。
- 与
equals()
的关系:必须满足的约定
Java 规范要求,hashCode()
和equals()
必须遵守以下规则:
- 如果两个对象通过
equals()
比较相等,则它们的hashCode()
必须返回相同的值。 - 如果两个对象的
hashCode()
返回不同的值,则它们通过equals()
比较一定不相等。 - 反之不成立:
hashCode()
相同的两个对象,equals()
可能不相等(哈希冲突)。
为什么需要这个约定?
如果两个对象 equals()
相等但 hashCode()
不同,在哈希表中会被存储在不同位置,导致查找时无法找到已存在的对象(如 HashSet
会认为是两个不同对象,出现重复元素)。
- 应用场景
- 哈希集合/映射:
HashMap
、HashSet
、HashTable
等依赖hashCode()
实现高效存储和查找。 - 对象去重:通过哈希值快速判断对象是否可能重复,再用
equals()
确认。 - 缓存机制:利用哈希值作为键存储缓存对象,加速查询。
- 自定义类的注意事项
- 重写
equals()
时必须同时重写hashCode()
,否则会违反上述约定,导致哈希表操作出错。 - 示例(正确实现):
class Person {String name;int age;@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return age == person.age && Objects.equals(name, person.name);}@Overridepublic int hashCode() {// 基于equals()中比较的字段生成哈希值return Objects.hash(name, age);} }
总结:
hashCode()
的核心价值是为哈希表提供快速定位对象的能力,其设计与 equals()
强关联,二者共同保证了哈希集合/映射的正确性和高效性。在自定义类中,重写 equals()
时必须同步重写 hashCode()
,这是Java开发的基本规范。
十、为什么要有 hashCode?
hashCode()
存在的核心意义是为哈希表(如 HashMap
、HashSet
等)提供高效的元素查找和存储机制,解决了“如何在大量数据中快速定位目标”的问题。
具体来说,hashCode()
的必要性体现在以下几点:
- 大幅提升哈希表的操作效率
哈希表的底层是数组结构,当需要插入或查找元素时:
- 直接遍历数组比对(不用
hashCode()
):时间复杂度是O(n)
,数据量越大,效率越低。 - 用
hashCode()
定位:- 第一步:通过
hashCode()
计算出一个整数(哈希值),再通过哈希算法转换为数组的索引,直接定位到元素可能存在的位置(时间复杂度接近O(1)
)。 - 第二步:如果该位置有多个元素(哈希冲突),再用
equals()
精确比对。
- 第一步:通过
例如,在 HashMap
中查找一个键时,hashCode()
能快速缩小查找范围,避免全表扫描,这是哈希表高效的核心原因。
- 解决“equals() 效率不足”的问题
equals()
可以精确判断两个对象是否相等,但存在局限性:
- 对于复杂对象,
equals()
可能需要逐个比对成员变量,耗时较长。 - 如果没有
hashCode()
,在集合中查找元素时,必须用equals()
与集合中所有元素比对,效率极低。
hashCode()
相当于给对象生成一个“摘要信息”,可以先通过哈希值快速排除不可能相等的对象,只对哈希值相同的少数对象进行 equals()
比对,大幅减少不必要的计算。
- 保证哈希表的正确性
哈希表的核心功能(如去重、键值映射)依赖hashCode()
与equals()
的配合:
- 例如
HashSet
要保证元素不重复,当添加新元素时,会先通过hashCode()
判断是否有“可能重复”的元素(哈希值相同),再用equals()
确认。 - 如果没有
hashCode()
,HashSet
无法高效判断元素是否已存在,可能导致重复元素插入,违背其设计初衷。
一句话总结:
hashCode()
是为了在哈希表中快速缩小查找范围,将原本需要全量比对的操作优化为“先通过哈希值定位,再精确比对”,从而在数据量较大时,显著提升集合的插入、查找、删除效率。没有 hashCode()
,哈希表就失去了高效性的基础。
正确重写 hashCode()
的关键原则是:与 equals()
保持一致,即 equals()
中用于比较的字段,都应参与 hashCode()
的计算。以下是几个不同场景的示例:
十一、hashCode()的示例
- 简单类(基于单个字段)
如果 equals()
仅通过一个字段(如 id
)判断相等,hashCode()
也应仅基于该字段生成。
import java.util.Objects;class User {private String id;private String name; // 不参与equals比较// 构造方法、getter/setter省略...@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;User user = (User) o;// 仅通过id判断相等return Objects.equals(id, user.id);}@Overridepublic int hashCode() {// 仅基于id生成哈希值(与equals保持一致)return Objects.hash(id);}
}
- 复杂类(基于多个字段)
如果 equals()
通过多个字段判断相等,hashCode()
必须包含所有这些字段。
import java.util.Objects;class Student {private String id;private String name;private int age;// 构造方法、getter/setter省略...@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Student student = (Student) o;// 通过id、name、age三个字段判断相等return age == student.age && Objects.equals(id, student.id) && Objects.equals(name, student.name);}@Overridepublic int hashCode() {// 基于所有参与equals比较的字段生成哈希值return Objects.hash(id, name, age);}
}
- 包含数组/集合的类
如果类中包含数组或集合,hashCode()
需通过 Arrays.hashCode()
或集合的 hashCode()
处理。
import java.util.Arrays;
import java.util.Objects;class Team {private String teamName;private String[] members; // 数组字段// 构造方法、getter/setter省略...@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Team team = (Team) o;// 比较teamName和members数组return Objects.equals(teamName, team.teamName) && Arrays.equals(members, team.members);}@Overridepublic int hashCode() {// 数组需用Arrays.hashCode()处理int result = Objects.hash(teamName);result = 31 * result + Arrays.hashCode(members);return result;}
}
关键技巧:
-
使用
Objects.hash()
简化实现:
该方法会自动处理null
值(null
的哈希值为 0),并对传入的所有字段计算哈希值。 -
为什么用 31 作为乘数?
在手动计算哈希值时(如示例3),常用31 * 结果 + 字段哈希值
的公式。31 是一个质数,且31 * i = (i << 5) - i
,可通过移位运算高效计算,减少哈希冲突。 -
IDE 自动生成:
现代 IDE(如 IDEA、Eclipse)可自动生成equals()
和hashCode()
,避免手动编写出错(通常在类中右键 → Generate → equals() and hashCode())。
核心原则再强调:
equals()
中比较的所有字段,必须全部参与hashCode()
的计算。equals()
中不比较的字段,不应参与hashCode()
的计算(否则会增加哈希冲突概率)。
遵循此原则,才能保证哈希表(如 HashMap
、HashSet
)的正确运行。
十二、hashCode()、equals() 的关系?
在Java中,hashCode()
和 equals()
是两个密切相关的方法,它们共同保证了哈希表(如 HashMap
、HashSet
等集合)的正确性和高效性,二者的关系由Java规范严格定义:
1、核心约定(必须遵守)
-
如果两个对象通过
equals()
比较返回true
,则它们的hashCode()
必须返回相同的整数。- 原因:哈希表通过
hashCode()
定位对象存储位置,如果equals()
相等的对象哈希值不同,会被存储在不同位置,导致哈希表无法识别它们是同一个对象(如HashSet
会允许重复元素)。
- 原因:哈希表通过
-
如果两个对象的
hashCode()
返回不同的值,则它们的equals()
比较必须返回false
。- 原因:哈希值不同意味着两个对象在哈希表中存储位置不同,逻辑上必然是不同的对象。
-
反之不强制:
- 两个对象的
hashCode()
相同(哈希冲突),equals()
可能返回false
(这是允许的,哈希表会通过链表/红黑树处理冲突)。
- 两个对象的
2、通俗理解
可以把 hashCode()
看作对象的“粗筛”,equals()
看作“精筛”:
- 先通过
hashCode()
快速排除绝对不同的对象(哈希值不同 → 一定不等)。 - 再对哈希值相同的对象用
equals()
精确判断是否真的相等。
这种“粗筛+精筛”的机制,让哈希表的查找效率从 O(n)
提升到接近 O(1)
。
3、违反约定的后果
如果自定义类重写 equals()
时未正确重写 hashCode()
,会破坏上述约定,导致哈希表出现逻辑错误:
- 示例:两个
equals()
相等的对象,hashCode()
不同。- 放入
HashSet
会被视为两个不同对象,导致重复存储。 - 在
HashMap
中查找时,可能找不到已存在的键值对。
- 放入
正确实践:
- 重写
equals()
时必须同时重写hashCode()
。 - 两者基于相同的字段计算(
equals()
比较哪些字段,hashCode()
就基于哪些字段生成哈希值)。
class Person {private String id;private String name;// equals() 基于 id 和 name 比较@Overridepublic 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) && Objects.equals(name, person.name);}// hashCode() 也必须基于 id 和 name 生成@Overridepublic int hashCode() {return Objects.hash(id, name); // 与 equals() 保持一致}
}
总结:
hashCode()
和 equals()
是“配套方法”:
hashCode()
用于快速定位对象(哈希表高效性的基础)。equals()
用于精确判断对象是否相等。- 二者的约定是哈希表正确工作的前提,违反约定会导致集合操作异常。
记住:相等的对象必须有相等的哈希码;哈希码相等的对象不一定相等。