😎 作者介绍:我是程序员洲洲,一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。
🤓 同时欢迎大家关注其他专栏,我将分享Web前后端开发、人工智能、机器学习、深度学习从0到1系列文章。
🌼 同时洲洲已经建立了程序员技术交流群,如果您感兴趣,可以私信我加入社群,可以直接vx联系(文末有名片)
🖥 随时欢迎您跟我沟通,一起交流,一起成长、进步!
给大家推荐一个非常好用的求职招聘小程序,秋招、春招、国企、银行都能用:万码优才。
这里写目录标题
1、什么是序列化?
序列化和反序列化
序列化是指将对象的状态信息转换为可以存储或传输的形式的过程。在Java中,这意味着将对象的状态转换成字节流,以便可以将其持久化到磁盘或通过网络发送到其他JVM实例。要使Java对象可序列化,该对象的类需要实现java.io.Serializable
接口。
反序列化是序列化的逆过程,它是指将字节流重新转换回对象的过程。在Java中,这通常涉及到从文件、数据库或网络接收字节流,并将其恢复为原始对象。
示例代码(Java):
import java.io.*;
// 定义一个可序列化的类
class Person implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class SerializationExample {
public static void main(String[] args) {
// 创建Person对象
Person person = new Person("John", 30);
// 序列化对象到文件
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
out.writeObject(person);
System.out.println("Person object has been serialized");
} catch (IOException e) {
e.printStackTrace();
}
// 从文件反序列化对象
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"))) {
Person deserializedPerson = (Person) in.readObject();
System.out.println("Deserialized Person: " + deserializedPerson.name + ", " + deserializedPerson.age);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
2、什么是面向对象
面向对象
面向对象(Object-Oriented,OO)是一种编程范式,它将现实世界中的实体抽象为对象,通过对象之间的交互来设计和构建软件系统。面向对象的核心概念包括:
- 类(Class):类是对象的蓝图或模板,定义了一组属性(成员变量)和方法(成员函数),这些属性和方法描述了对象的状态和行为。
- 对象(Object):对象是类的实例,每个对象都拥有类中定义的属性和方法。对象是面向对象编程中的基本单位。
- 封装(Encapsulation):封装是将数据(属性)和操作数据的方法(行为)捆绑在一起,并隐藏内部实现细节的一种机制。这有助于减少系统各部分之间的耦合,并保护数据不被外部直接访问。
- 继承(Inheritance):继承是一种创建新类的方式,新类可以从现有类继承属性和方法。这有助于代码复用,并可以建立类之间的层次结构。
- 多态(Polymorphism):多态是指允许不同类的对象对同一消息做出响应的能力,即同一个接口可以被不同的实例以不同的方式实现。这使得代码更加灵活和可扩展。
示例代码(Java):
// 定义一个类
class Animal {
// 属性
String name;
// 构造方法
public Animal(String name) {
this.name = name;
}
// 方法
public void makeSound() {
System.out.println(name + " makes a sound");
}
}
// 继承Animal类
class Dog extends Animal {
// 构造方法
public Dog(String name) {
super(name); // 调用父类的构造方法
}
// 重写makeSound方法
@Override
public void makeSound() {
System.out.println("Woof woof!");
}
}
public class Main {
public static void main(String[] args) {
// 创建对象
Animal myAnimal = new Animal("Generic Animal");
Dog myDog = new Dog("Rex");
// 多态的体现
makeAnimalSound(myAnimal);
makeAnimalSound(myDog);
}
// 该方法接受Animal类型的对象,体现了多态
public static void makeAnimalSound(Animal animal) {
animal.makeSound();
}
}
3、介绍Java中的包
Java中的包
在Java中,包(Package)是一种将类和接口组织在一起的方式,以便于管理大型项目中的代码。使用包可以避免类名冲突,并且可以保护类的内部实现细节。以下是包的主要特点和用途:
- 组织代码:包允许开发者将功能相关的类和接口分组在一起,这有助于维护和理解大型项目的结构。
- 避免命名冲突:不同的包可以包含同名的类,因为包名作为前缀,从而避免了命名冲突。
- 封装性:包可以包含访问控制修饰符,如
public
和private
,以控制类和接口的可见性。 - 导入类:通过使用
import
语句,可以在不同的包之间导入和使用类,而不需要重新编写代码。
创建和使用包
- 创建包:在Java中创建包非常简单,只需在源文件的顶部声明包即可。例如,创建一个名为
com.example.myapp
的包:
package com.example.myapp;
public class MyClass {
// 类的实现
}
导入包:如果你想要使用其他包中的类,可以在你的源文件顶部使用import语句。例如,导入java.util包中的List接口:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> myList = new java.util.ArrayList<>();
// 使用myList
}
}
导入整个包:你也可以导入整个包中的所有类,但这不是推荐的做法,因为它可能会导致命名冲突:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> myList = new ArrayList<>();
// 使用myList
}
}
4、介绍Java中的文档注释
Java中的文档注释
Java中的文档注释(也称为Javadoc注释)是一种特殊的注释,用于生成HTML格式的API文档。文档注释以/**
开头,可以包含对类、方法、构造函数或字段的详细描述。这些注释通常位于要描述的代码之前。
以下是文档注释的一些关键点:
- 类注释:描述类的目的和功能。
- 方法注释:描述方法的作用、参数、返回值以及可能抛出的异常。
- 参数注释:在方法注释中,使用
@param
标签描述每个参数的作用。 - 返回值注释:使用
@return
标签描述方法返回值的信息。 - 异常注释:使用
@throws
或@exception
标签描述方法可能抛出的异常。 - 版本和作者信息:使用
@version
和@author
标签提供额外的信息。
示例代码(Java):
/**
* 这个类表示一个简单的计算器,提供加法和减法功能。
*/
public class Calculator {
/**
* 计算两个数的和。
* @param a 第一个加数
* @param b 第二个加数
* @return 两个数的和
*/
public int add(int a, int b) {
return a + b;
}
/**
* 计算两个数的差。
* @param a 被减数
* @param b 减数
* @return 两个数的差
* @throws IllegalArgumentException 如果b大于a
*/
public int subtract(int a, int b) {
if (b > a) {
throw new IllegalArgumentException("b不能大于a");
}
return a - b;
}
}
5、类设计的技巧
类设计的技巧
在面向对象编程中,类的设计是构建健壮、可维护和可扩展软件系统的关键。以下是一些有效的类设计技巧:
-
单一职责原则(SRP):
- 一个类应该只有一个引起它变化的原因。这意味着一个类应该只负责一个功能。
-
开闭原则(OCP):
- 类应该对扩展开放,对修改关闭。设计类时,应该允许在不修改现有代码的情况下扩展功能。
-
里氏替换原则(LSP):
- 子类对象应该能够替换其基类对象,而不影响程序的正确性。
-
接口隔离原则(ISP):
- 不应该强迫客户依赖于它们不使用的接口。设计接口时,应该保持接口的紧凑和特定性。
-
依赖倒置原则(DIP):
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
-
使用组合而非继承:
- 优先使用组合来实现代码复用,因为组合提供了更大的灵活性和更低的耦合度。
-
封装变化:
- 将变化的部分封装在单独的类中,这样可以减少变化对系统其他部分的影响。
-
最小化公开接口:
- 尽量减少类的公开方法和属性,以隐藏内部实现细节,提高封装性。
-
高内聚,低耦合:
- 一个类应该具有高内聚性,即类中的所有方法都应该与类的目的紧密相关。同时,类与类之间的耦合度应该尽可能低。
-
使用合适的访问修饰符:
- 合理使用
public
、protected
、private
等访问修饰符来控制成员的可见性。
- 合理使用
-
避免过度设计:
- 只设计当前需要的功能,避免过早优化和设计未来可能不会用到的功能。
-
编写清晰的文档:
- 为类和公共接口编写清晰的文档注释,这有助于其他开发者理解和使用你的代码。
-
重用已有的类库:
- 在可能的情况下,使用已经测试和验证过的类库,而不是重新发明轮子。
-
单一实例类(Singleton):
- 对于全局资源,如配置信息或线程池,可以使用单例模式来确保全局只有一个实例。
-
工厂模式:
- 使用工厂模式来创建对象,这样可以将对象创建的逻辑与使用逻辑分离,提高灵活性。
示例代码(Java):
/**
* 一个简单的例子,展示如何实现一个高内聚、低耦合的类。
*/
public class EmailService {
private final MailProvider mailProvider;
public EmailService(MailProvider mailProvider) {
this.mailProvider = mailProvider;
}
/**
* 发送电子邮件。
* @param to 收件人地址
* @param subject 邮件主题
* @param body 邮件正文
*/
public void sendEmail(String to, String subject, String body) {
// 使用组合的mailProvider发送邮件
mailProvider.deliver(to, subject, body);
}
}
/**
* 邮件提供者接口,用于发送邮件。
*/
interface MailProvider {
void deliver(String to, String subject, String body);
}
6、内存中的对象
在Java中,对象的内存分配和生命周期管理是一个复杂的主题,涉及到JVM(Java虚拟机)的多个部分,包括堆(Heap)、栈(Stack)、方法区(Method Area)等。以下是一些关键点:
-
对象创建:
- 当通过
new
关键字创建一个对象时,JVM会在堆内存中为该对象分配空间。
- 当通过
-
堆(Heap):
- Java堆是JVM中用于存储对象实例和数组的部分。它是所有线程共享的,并且是垃圾收集器管理的主要区域。
- 堆内存分为三个主要区域:新生代(Young Generation)、老年代(Old Generation)和元空间(Metaspace)。
-
栈(Stack):
- 每个线程都有自己的栈,用于存储局部变量和部分结果,以及执行方法调用和返回。
- 当一个方法被调用时,JVM会为该方法创建一个栈帧(Stack Frame),并将其压入栈中。
-
方法区(Method Area):
- 方法区用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
- 在Java 8及以后的版本中,方法区被替换为元空间(Metaspace),它位于本地内存中,而不是虚拟机内存中。
-
对象引用:
- 在Java中,对象的引用实际上存储在栈上,指向堆中对象的实际位置。
-
垃圾回收:
- Java的垃圾收集器定期检查堆中的对象,回收那些不再被引用的对象,以释放内存。
-
对象的生命周期:
- 对象的生命周期从创建开始,直到被垃圾收集器回收结束。
- 对象在新生代中经历多次垃圾回收后,如果仍然存活,会被移动到老年代。
-
对象访问:
- 通过对象的引用,可以访问对象的属性和方法。
示例代码(Java):
public class ObjectMemoryExample {
public static void main(String[] args) {
// 在栈上创建对象引用
ObjectMemoryExample obj = new ObjectMemoryExample();
// obj引用指向堆上的对象
System.out.println("对象已创建,引用存储在栈上,对象存储在堆上。");
}
}
7、String存储原理
String存储原理
在Java中,String
对象是不可变的,这意味着一旦创建了String
对象,它的值就不能被改变。这种不可变性对String
的存储和性能有着重要的影响。以下是String
对象在Java中的存储原理:
-
字符串常量池(String Pool):
- 在Java中,字符串常量池是一个特殊的存储区域,用于存储字符串字面量。它通常位于方法区内,用于存储所有字符串字面量和通过
String.intern()
方法显式要求的字符串。 - 字符串常量池的主要目的是为了节省内存空间,避免相同字符串的重复存储。
- 在Java中,字符串常量池是一个特殊的存储区域,用于存储字符串字面量。它通常位于方法区内,用于存储所有字符串字面量和通过
-
堆内存(Heap):
- 当通过
new
关键字创建String
对象时,对象实际上是在堆内存中创建的。 - 字符串常量池中存储的是字符串的引用,而实际的字符串值可能存储在堆内存中。
- 当通过
-
字符串拼接:
- 在Java中,使用
+
操作符进行字符串拼接时,实际上会创建新的String
对象。 - 例如,
String s = "hello" + "world";
会创建两个字符串常量"hello"
和"world"
,并在堆内存中创建一个新的String
对象来存储拼接后的结果"helloworld"
。
- 在Java中,使用
-
字符串不可变性:
- 由于
String
对象是不可变的,任何对String
对象的操作(如修改字符串内容)实际上都会创建一个新的String
对象。 - 这种特性使得
String
对象在多线程环境下安全使用,但同时也可能导致性能问题,因为频繁的创建新对象会增加垃圾回收的压力。
- 由于
-
String.intern()
方法:String.intern()
方法会返回字符串常量池中的字符串,如果常量池中已经存在该字符串,则返回常量池中的字符串,否则将当前字符串添加到常量池中。- 使用
intern()
方法可以减少堆内存的使用,但过度使用可能会增加方法区(或元空间)的压力。
示例代码(Java):
public class StringStorageExample {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
// 字符串常量池中的内容
String s3 = s1 + s2;
System.out.println(s3); // 输出 "helloworld"
// 使用String.intern()方法
String s4 = new String("hello").interned();
System.out.println(s4); // 输出 "hello"
// 检查s1和s4是否在字符串常量池中相同
System.out.println(s1 == s4); // 输出 true
}
}
8、String底层用的什么类型?
在Java中,String
对象的底层实现使用的是char
数组。具体来说,String
类内部使用了一个char
类型的数组来存储字符串的字符数据。这个数组是不可变的,一旦创建,其内容不能被改变。
示例代码(Java):
public class StringImplementation {
public static void main(String[] args) {
String str = "Hello, World!";
// 获取String对象的字符数组
char[] charArray = str.toCharArray();
// 输出字符数组的内容
for (char c : charArray) {
System.out.print(c);
}
// 输出: Hello, World!
}
}
在这个示例中,我们通过toCharArray()方法将String对象转换成了一个char数组,这表明String对象实际上是以char数组的形式存储其数据的。
从Java 9开始,字符串的存储机制有所变化,引入了一种新的压缩机制,称为String Deduplication。这种机制旨在减少相同字符串实例的数量,节省内存。在这种机制下,对于较短的字符串(通常是长度小于或等于一定阈值的字符串),JVM可能会使用一种更紧凑的存储方式,例如在Java堆中直接存储字符串值,而不是使用char数组。这种优化可以减少内存占用,但String对象的公共API和行为保持不变。
9、String为什么是不可变的?
保存字符串的数组被 fina1 修饰且为私有的,并且 string 类没有提供/暴露修改这个字符串的方法。string 类被 final 修饰导致其不能被继承,进而避免了子类破坏 string 不可变。
10、String常见方法有哪些?
Java中的String
类提供了大量的方法来操作字符串,以下是一些常见的方法:
-
字符串拼接:
concat(String str)
:将指定字符串连接到此字符串的末尾。+
或+=
:用于字符串拼接。
-
字符串比较:
equals(Object another)
:检查此字符串是否与指定对象相同。equalsIgnoreCase(String another)
:比较两个字符串,不考虑大小写。compareTo(String another)
:按字典顺序比较两个字符串。
-
字符串查找:
charAt(int index)
:返回指定索引处的字符。indexOf(int ch)
:返回指定字符在此字符串中第一次出现处的索引。lastIndexOf(int ch)
:返回指定字符在此字符串中最后一次出现处的索引。contains(CharSequence s)
:当且仅当此字符串包含指定的字符序列时,返回true
。
-
字符串长度:
length()
:返回此字符串的长度。
-
子字符串:
substring(int beginIndex, int endIndex)
:返回一个新字符串,它是此字符串的一个子字符串。substring(int index)
:返回一个新字符串,它是此字符串从指定索引开始到末尾的子字符串。
-
字符串分割:
split(String regex)
:根据匹配给定正则表达式的匹配项将此字符串分割成字符串数组。
-
字符串替换:
replace(char oldChar, char newChar)
:返回一个新的字符串,所有此字符串中出现旧字符的部分都替换成新字符。replaceAll(String regex, String replacement)
:使用给定的替换来替换此字符串中与正则表达式相匹配的子字符串。
-
字符串修剪:
trim()
:返回字符串的副本,忽略前导空白和尾部空白。
-
字符串转换:
toLowerCase()
:使用默认语言环境的规则将此字符串转换为小写。toUpperCase()
:使用默认语言环境的规则将此字符串转换为大写。valueOf()
:将各种数据类型转换为字符串形式。
-
字符串格式化:
format(String format, Object... args)
:根据指定的格式字符串和参数,生成格式化的字符串。
-
字符串编码和解码:
getBytes(Charset charset)
:使用指定的字符集将此字符串编码为字节序列。getBytes(String charsetName)
:使用指定字符集名称将此字符串编码为字节序列。
这些方法覆盖了大多数日常编程中对字符串处理的需求,使得String
类成为Java中使用最频繁的类之一。
示例代码(Java):
public class StringMethodsExample {
public static void main(String[] args) {
String str = "Hello, World!";
// 字符串拼接
String concatStr = str.concat(" Java");
System.out.println(concatStr); // 输出: Hello, World! Java
// 字符串比较
boolean isEqual = str.equals("Hello, World!");
System.out.println(isEqual); // 输出: true
// 字符串查找
int index = str.indexOf('W');
System.out.println(index); // 输出: 7
// 子字符串
String substring = str.substring(7, 12);
System.out.println(substring); // 输出: World
// 字符串替换
String replaceStr = str.replace('W', 'w');
System.out.println(replaceStr); // 输出: Hello, world!
// 字符串修剪
String trimStr = " " + str + " ".trim();
System.out.println(trimStr); // 输出: Hello, World!
// 字符串转换
String upperStr = str.toUpperCase();
System.out.println(upperStr); // 输出: HELLO, WORLD!
// 字符串格式化
String formatStr = String.format("Name: %s, Age: %d", "Kimi", 30);
System.out.println(formatStr); // 输出: Name: Kimi, Age: 30
}
}
11、什么是字符串常量池
字符串常量池(String Pool)是Java中一个特殊的内存区域,用于存储字符串常量和字符串字面量。它的主要目的是减少相同字符串的重复存储,节省内存空间,并提高字符串比较的效率。以下是字符串常量池的一些关键特性:
-
存储位置:
- 在Java 7及之前的版本中,字符串常量池位于方法区内。
- 从Java 7开始,字符串常量池被移到堆内存中,以减少方法区的压力。
- Java 8及以后的版本中,字符串常量池位于堆内存中,但方法区(或元空间)仍然用于存储类的其他信息,如类的结构、常量等。
-
字符串字面量:
- 直接在代码中声明的字符串字面量(例如
"hello"
)会被自动存储在字符串常量池中。 - 如果字符串常量池中已经存在相同的字符串,则不会重复存储。
- 直接在代码中声明的字符串字面量(例如
-
String.intern()
方法:String.intern()
方法可以将一个字符串添加到字符串常量池中,如果该字符串已经存在于常量池中,则返回常量池中的字符串引用。- 这个方法可以用来确保字符串的唯一性,但过度使用可能会导致堆内存的增加。
-
字符串比较:
- 由于字符串常量池中的字符串是唯一的,所以使用
==
操作符比较两个字符串常量时,可以直接比较它们在常量池中的引用,这比比较字符串的内容更快。
- 由于字符串常量池中的字符串是唯一的,所以使用
-
内存优化:
- 字符串常量池减少了相同字符串的重复存储,从而优化了内存使用。
示例代码(Java):
public class StringPoolExample {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello").interned();
// 由于字符串常量池中已经存在"hello",所以s1和s2引用的是同一个对象
System.out.println(s1 == s2); // 输出: true
// s3通过intern()方法添加到字符串常量池,也引用了常量池中的"hello"
System.out.println(s1 == s3); // 输出: true
}
}
12、介绍一下Object类
Object类
在Java中,Object
类是所有类的根类,位于类继承层次结构的顶端。这意味着Java中的每个类都是Object
类的子类或孙类。Object
类位于java.lang
包中,提供了一些基础的方法,这些方法在所有对象中都是通用的。以下是Object
类的一些核心方法和特性:
-
equals(Object obj)
:- 检查两个对象是否相等。默认行为是比较对象的引用,但通常需要被重写以提供实际的值比较逻辑。
-
hashCode()
:- 返回对象的哈希码值。根据
equals()
方法的实现,相等的对象必须有相同的哈希码。
- 返回对象的哈希码值。根据
-
toString()
:- 返回对象的字符串表示。默认情况下,它返回对象的类名和对象的哈希码的无符号十六进制表示。
-
getClass()
:- 返回
Class
对象,该对象代表对象的运行时类型。
- 返回
-
notify()
和notifyAll()
:- 唤醒在此对象监视器上等待的单个或所有线程。
-
wait()
、wait(long timeout)
和wait(long timeout, int nanos)
:- 导致当前线程等待,直到另一个线程调用此对象的
notify()
或notifyAll()
方法,或者超过指定的时间。
- 导致当前线程等待,直到另一个线程调用此对象的
-
clone()
:- 保护方法,用于创建并返回对象的副本。默认实现抛出
CloneNotSupportedException
,需要重写以实现对象的复制。
- 保护方法,用于创建并返回对象的副本。默认实现抛出
示例代码(Java):
public class ObjectExample {
public static void main(String[] args) {
Object obj = new Object();
// 检查对象是否等于它自己
System.out.println(obj.equals(obj)); // 输出: true
// 获取对象的哈希码
System.out.println(obj.hashCode());
// 获取对象的字符串表示
System.out.println(obj.toString());
// 获取对象的运行时类信息
System.out.println(obj.getClass());
}
}
13、==和equals()的区别?
-
==
运算符:==
用于比较两个引用是否指向同一对象(即它们是否具有相同的内存地址)。- 对于基本数据类型(如
int
、double
等),==
比较的是值。 - 对于对象,
==
比较的是引用,即它们是否指向堆内存中的同一个位置。
-
equals()
方法:equals()
是一个方法,需要被调用,用于比较对象的内容是否相等。- 默认行为是比较对象的引用,但通常在自定义类中被重写以比较对象的实际内容(例如,两个字符串对象包含的字符序列是否相同)。
equals()
方法可以接收一个Object
类型的参数,因此可以比较不同类型的对象。
示例代码(Java):
public class ComparisonExample {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
// 使用==比较引用
System.out.println(s1 == s2); // 输出: true,因为s1和s2指向字符串常量池中的同一个对象
System.out.println(s1 == s3); // 输出: false,因为s1指向字符串常量池中的对象,而s3指向堆中新创建的对象
// 使用equals()比较内容
System.out.println(s1.equals(s2)); // 输出: true,因为"hello"和"hello"的内容相同
System.out.println(s1.equals(s3)); // 输出: true,因为s1和s3的内容相同
}
}
14、hashCode()和equals()的关系
在Java中,hashCode()
方法和equals()
方法之间有着紧密的关系,它们共同用于正确处理对象的相等性和哈希表的性能。以下是hashCode()
和equals()
方法之间的关系和重要性:
-
相等性原则:
- 如果两个对象通过
equals()
方法比较是相等的(即equals()
返回true
),那么这两个对象的hashCode()
方法必须返回相同的值。 - 反之,如果两个对象的
hashCode()
方法返回不同的值,那么这两个对象肯定不相等。
- 如果两个对象通过
-
哈希表性能:
hashCode()
方法通常被用在哈希表(如HashMap
、HashSet
等)中,以快速确定对象存储的索引位置。- 如果两个对象相等(根据
equals()
),那么它们必须有相同的哈希码,这样它们才能被存储在哈希表的同一个位置,从而在查找时能够快速匹配。
-
一致性:
- 在Java对象的生命周期中,只要对象的状态没有改变,
hashCode()
方法返回的值必须是一致的。这意味着如果对象的属性没有改变,即使多次调用hashCode()
方法,也应该返回相同的值。 - 如果对象的
equals()
相等,那么它们的哈希码也必须相等,以保持一致性。
- 在Java对象的生命周期中,只要对象的状态没有改变,
-
重写约定:
- 当你重写
equals()
方法时,你通常也需要重写hashCode()
方法,以确保hashCode()
和equals()
方法之间的约定得到满足。 - 如果你只重写
equals()
而不重写hashCode()
,可能会导致哈希表的性能问题,因为相等的对象可能会被存储在不同的位置,从而影响查找效率。
- 当你重写
示例代码(Java):
import java.util.Objects;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public 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);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
public static void main(String[] args) {
Person p1 = new Person("John", 30);
Person p2 = new Person("John", 30);
System.out.println(p1.equals(p2)); // 输出: true
System.out.println(p1.hashCode() == p2.hashCode()); // 输出: true
}
}
15、介绍一下hashcode()
hashCode()
方法是Java中的一个非常重要的方法,它在java.lang.Object
类中定义,并被许多Java类继承。hashCode()
方法的主要目的是为对象提供一个可以用来在哈希表中快速定位对象的整数。以下是hashCode()
方法的一些关键点:
-
一致性:
- 在Java应用程序执行期间,只要对象的equals比较所用的信息没有被修改,那么该对象多次调用
hashCode()
方法必须始终返回相同的值。
- 在Java应用程序执行期间,只要对象的equals比较所用的信息没有被修改,那么该对象多次调用
-
相等性原则:
- 如果两个对象通过
equals(Object)
方法比较是相等的,那么这两个对象调用hashCode()
方法也必须产生相同的整数结果。
- 如果两个对象通过
-
不同对象可能返回相同的哈希码:
- 相反,两个不同的对象可以有相同的哈希码,但不同的对象必须有不同的哈希码并不是强制的。一个好的
hashCode()
实现会尽量减少这种冲突。
- 相反,两个不同的对象可以有相同的哈希码,但不同的对象必须有不同的哈希码并不是强制的。一个好的
-
哈希表的性能:
hashCode()
方法通常与哈希表(如HashMap
、HashSet
)一起使用,以快速确定对象存储的索引位置。一个好的hashCode()
实现可以显著提高哈希表的性能。
-
默认实现:
Object
类的hashCode()
方法返回对象的系统相关的哈希码,这通常是对象内存地址的转换值。大多数Java类会重写这个方法以提供更有意义的哈希码。
-
重写约定:
- 当重写
equals()
方法时,通常也需要重写hashCode()
方法,以保持一致性。
- 当重写
-
线程安全:
hashCode()
方法应该是线程安全的,即在多线程环境中,同一个对象多次调用hashCode()
方法应该返回相同的值。
-
不可预测性:
- 在不同的Java虚拟机实现中,
hashCode()
方法的返回值可能不同,因此不应该依赖于具体的哈希码值。
- 在不同的Java虚拟机实现中,
示例代码(Java):
import java.util.Objects;
public class CustomObject {
private int id;
private String data;
public CustomObject(int id, String data) {
this.id = id;
this.data = data;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CustomObject that = (CustomObject) o;
return id == that.id &&
Objects.equals(data, that.data);
}
@Override
public int hashCode() {
// 使用Objects.hash()生成哈希码,基于id和data
return Objects.hash(id, data);
}
public static void main(String[] args) {
CustomObject obj1 = new CustomObject(1, "data");
CustomObject obj2 = new CustomObject(1, "data");
System.out.println(obj1.hashCode()); // 输出哈希码
System.out.println(obj2.hashCode()); // 输出哈希码,应该与obj1相同
}
}