概念
面向对象(Object-Oriented, 简称OO)是现代编程语言中一种重要的设计和实现思想,它在Java编程语言中的应用尤为广泛。面向对象的主要特点包括封装、继承、多态等,通过模拟现实世界实体之间的关系与行为,简化程序设计的复杂性,提高代码的可读性和可维护性。下面我们将结合一个实际案例——银行账户管理系统,来深入解析Java面向对象编程的思想。
不要觉得很复杂,所有的东西都是对象,是对象就可以被描述。
描述就两个东西:属性和方法(动作)
比如说人:身高,体重,年龄,这些都说属性。走路,跳高,吃饭这些就是动作,方法。
Java面向对象编程思想,简单来说,就像是我们用积木搭建现实世界的模型。它将现实生活中的实体(比如人、汽车、银行账户等)抽象成计算机世界中的类和对象。
面试常问的三大特征:封装继承多态。
-
封装:
- 类是封装的载体,它把属性和方法包装在一起,并对外提供有限的访问接口。如同一个包裹,你只能通过打开指定的方式查看或修改里面的东西。在Java中,通常使用private关键字保护内部数据,只通过public的getter和setter方法来操作。
-
继承:
- 继承好比是从父辈那里继承特性。比如,我们可以定义一个“交通工具”基类,然后让“汽车”和“自行车”从这个基类继承,这样它们自然而然地拥有“交通工具”的一些通用属性和方法,如都有“移动”的能力,但各自的实现方式不同(汽车靠发动机驱动,自行车靠人力蹬踏)。
-
多态:
- 多态意味着相同的消息可以被不同的对象以各自独特的方式响应。比如,所有交通工具都可以执行“行驶”这一动作,但是具体怎么行驶由其子类决定。在Java中,通过接口或抽象类以及重写(override)机制,我们可以写出通用的代码去处理不同类型的对象,体现了“狗叫的是‘汪汪’,猫叫的是‘喵喵’”这样的多样性。
Java面向对象编程就是模拟真实世界的方式,通过创建类来描述问题域的概念,然后根据这些类创建对象进行交互,利用封装隐藏复杂性、通过继承减少重复并支持扩展,同时利用多态提高代码的灵活性和可复用性。
一、定义类——封装
在银行账户系统中,我们可以首先创建一个“BankAccount”类,用于封装账户的基本属性和操作:
public class BankAccount {
private String accountNumber; // 账户编号
private double balance; // 账户余额
public BankAccount(String accountNumber) {
this.accountNumber = accountNumber;
this.balance = 0.0;
}
// 封装方法:存钱
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
// 封装方法:取钱
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}
// 封装方法:查询余额
public double getBalance() {
return balance;
}
}
上述代码中,我们通过private关键字对账户号和余额进行了封装,外部无法直接访问这些私有变量,只能通过deposit、withdraw和getBalance等公共方法进行操作,这就是面向对象中的封装特性。
二、继承与多态
假设我们的银行系统还需要处理不同类型的账户,如储蓄账户(SavingsAccount)和信用卡账户(CreditCardAccount),这两种账户都具有BankAccount的基本功能,但又有各自特有的业务逻辑,这时就可以利用面向对象的继承和多态特性:
public class SavingsAccount extends BankAccount {
private double interestRate; // 利率
public SavingsAccount(String accountNumber, double interestRate) {
super(accountNumber);
this.interestRate = interestRate;
}
// 储蓄账户特有功能:计算利息
public void calculateInterest() {
double interest = balance * interestRate;
deposit(interest);
}
}
public class CreditCardAccount extends BankAccount {
private double creditLimit; // 信用额度
public CreditCardAccount(String accountNumber, double creditLimit) {
super(accountNumber);
this.creditLimit = creditLimit;
}
// 信用卡账户特有功能:检查是否超过信用额度
public boolean isOverCreditLimit(double amount) {
return amount > creditLimit + balance;
}
}
在这里,SavingsAccount和CreditCardAccount分别继承了BankAccount,并根据自身业务需求添加了特有的属性和方法,体现了面向对象的继承性。同时,尽管它们都是BankAccount的子类,但在具体使用时可以调用父类或子类的方法,展现出面向对象的多态性。
通过以上银行账户管理系统的案例,我们展示了Java面向对象编程的核心思想——封装、继承和多态的实际应用。在设计和实现复杂的软件系统时,遵循面向对象的原则,可以使我们的代码更易于理解和维护,也更容易适应变化的需求。
类与对象
类用于描述对象,但对象是单独的实例。类是抽象的,对象是具体的。
-
类(Class): 类是一种抽象的概念,它定义了一组属性(变量)和行为(方法)。类是对现实世界实体或概念的一种逻辑上的描述或模板。例如,可以定义一个
Car
类,该类包含汽车的颜色、品牌、速度等属性以及启动、刹车等方法。类相当于一个蓝图,它并不实际占有内存空间。public class Car { String color; String brand; int speed; public void start() { // 启动汽车的逻辑 } public void stop() { // 刹车的逻辑 } }
-
实例(Instance 或 Object): 实例则是类的具体化,也称为对象。当你根据类创建一个新的对象时,就得到了这个类的一个实例。每个实例都有自己独立的一份数据存储空间,用于存储类中定义的属性值,并且可以执行类中定义的方法。例如,基于上面的
Car
类,你可以创建多个不同颜色和品牌的汽车实例:Car myCar = new Car(); // 创建Car类的一个实例 myCar.color = "Red"; myCar.brand = "Toyota"; myCar.start(); // 调用start()方法 Car yourCar = new Car(); // 创建另一个实例 yourCar.color = "Blue"; yourCar.brand = "Ford"; yourCar.start();
在这里,
myCar
和yourCar
都是Car
类的实例,它们各自拥有独立的属性值,并能够独立执行类中定义的方法。
类是“模板”,而实例是依据模板创建出的具有具体属性和行为的个体。在Java程序运行过程中,类被加载到内存后,通过new
关键字创建的每一个实例都占有独立的内存区域,并根据类提供的构造函数初始化自己的状态。
额外的知识,创建实例的方式有哪些?
在Java中,创建对象实例(即类的实例)主要有以下几种方式:
-
使用
new
关键字: 这是最常见也是最直接的创建对象实例的方式。通过调用类的构造方法来初始化对象。// 假设有一个名为Car的类 Car myCar = new Car(); // 调用无参构造函数创建实例 Car yourCar = new Car("Toyota", "Corolla"); // 调用有参构造函数创建实例
-
克隆(Clone): 如果一个类实现了
Cloneable
接口并重写了clone()
方法,那么可以使用clone方法创建该类的副本。class MyCloneableClass implements Cloneable { // ... @Override public MyCloneableClass clone() throws CloneNotSupportedException { return (MyCloneableClass) super.clone(); } } MyCloneableClass obj1 = new MyCloneableClass(); MyCloneableClass obj2 = obj1.clone();
-
反射机制创建实例: 通过Java反射API中的
Class.newInstance()
方法或者Constructor.newInstance()
方法来动态地创建对象实例。Class<?> clazz = Class.forName("com.example.Car"); Car carInstance = (Car) clazz.newInstance(); // 使用默认构造函数 // 或者使用特定构造函数 Constructor<Car> constructor = Car.class.getDeclaredConstructor(String.class, String.class); Car anotherCar = constructor.newInstance("Honda", "Civic");
-
反序列化(Deserialization): 当对象已经被序列化到磁盘或网络流中时,可以通过反序列化过程重新创建对象实例。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("car.ser")); Car deserializedCar = (Car) ois.readObject();
在Java中,创建对象实例通常主要依赖于new
关键字,但在特定情况下,如实现深度复制、运行时动态加载和创建对象、从序列化数据恢复对象等场景下,会采用克隆、反射以及反序列化等方式。
单例
Java单例模式是一种设计模式,它保证在Java应用程序中一个类只有一个实例,并且提供一个全局访问点来获取这个唯一的实例。单例模式的核心要素包括:
-
私有构造方法: 为了防止外部直接通过
new
关键字创建多个实例,单例类的构造方法必须声明为私有的(private
)。 -
静态成员变量: 存储单例对象的引用,通常是一个静态(
static
)的实例变量。 -
静态工厂方法: 提供一个公共的、静态的方法,用于返回该类的唯一实例。此方法通常是
getInstance()
,并且内部负责检查并确保实例只被创建一次。
以下是几种常见的Java单例实现方式:
- 饿汉式(Eager Initialization): 单例对象在类加载时就初始化完成,一旦类加载完毕,单例就已经创建好了。
public class SingletonEager {
private static final SingletonEager INSTANCE = new SingletonEager();
private SingletonEager() {}
public static SingletonEager getInstance() {
return INSTANCE;
}
}
- 懒汉式(Lazy Initialization): 单例对象在第一次需要使用时才进行初始化,从而延迟了实例化的时间,提高了效率。但需要注意线程安全问题。
// 线程不安全版本
public class SingletonLazyUnsafe {
private static SingletonLazyUnsafe instance;
private SingletonLazyUnsafe() {}
public static SingletonLazyUnsafe getInstance() {
if (instance == null) {
instance = new SingletonLazyUnsafe();
}
return instance;
}
}
// 线程安全版本,使用synchronized关键字
public class SingletonLazySafe {
private static volatile SingletonLazySafe instance;
private SingletonLazySafe() {}
public static synchronized SingletonLazySafe getInstance() {
if (instance == null) {
instance = new SingletonLazySafe();
}
return instance;
}
}
// 双重检查锁定(Double-Checked Locking)
// 这种方式既实现了懒加载,又避免了每次调用getInstance()时都进行同步操作
public class SingletonLazyDCL {
private static volatile SingletonLazyDCL instance;
private SingletonLazyDCL() {}
public static SingletonLazyDCL getInstance() {
if (instance == null) {
synchronized (SingletonLazyDCL.class) {
if (instance == null) {
instance = new SingletonLazyDCL();
}
}
}
return instance;
}
}
- 静态内部类(Initialization-on-demand holder idiom): 结合了饿汉式的内存效率和懒汉式的延迟初始化特性,同时无须担心多线程问题。
public class SingletonHolderPattern {
private SingletonHolderPattern() {}
private static class SingletonHolder {
private static final SingletonHolderPattern INSTANCE = new SingletonHolderPattern();
}
public static SingletonHolderPattern getInstance() {
return SingletonHolder.INSTANCE;
}
}
每种实现都有其优缺点,选择哪种实现取决于实际应用场景中的性能需求、并发需求等因素。
关于内存模型(了解)
不需要懂,混个脸熟,随着代码写多了,工作时间长了,就自然理解了。
前面我们new对象,创建对象,创建实例。
那这些东西在内存中是怎么存储的呢?
Java内存模型(Java Memory Model,JMM)是Java虚拟机(Java Virtual Machine, JVM)规范中定义的一种抽象概念模型,用于描述程序执行时内存的结构和交互方式。它并不直接对应计算机硬件中的内存结构,而是对物理内存的一种逻辑抽象。
在Java编程中,主要关注以下几个内存区域:
-
程序计数器(Program Counter Register): 每个线程都有一个独立的程序计数器,用于记录当前线程正在执行的字节码指令地址,如果是Native方法,则为空。
-
虚拟机栈(Java Virtual Machine Stacks): 每个线程同样拥有一个私有的虚拟机栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法调用都会创建一个新的栈帧压入栈顶,方法执行完毕则弹出栈帧。
-
本地方法栈(Native Method Stack): 类似于虚拟机栈,只不过它是为虚拟机使用到的Native方法服务的。
-
堆(Heap): 所有线程共享的一块内存区域,主要用于存放对象实例以及数组。几乎所有的对象都在这里分配内存,垃圾回收机制的主要工作区域也是堆内存。
-
方法区(Method Area)/ 元空间(Metaspace): 在Java 8及以后版本中,原本的方法区被元空间替代。这部分内存用于存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-
运行时常量池(Runtime Constant Pool): 方法区的一部分,存储类的字段和方法字面量以及符号引用等内容。
当程序运行时,JVM通过上述不同内存区域的划分和管理,确保了程序高效、安全地运行。同时,Java内存模型还涉及到了并发环境下的可见性、有序性和原子性问题,这是为了保证多线程环境下对共享数据访问的一致性。
static关键字
static
关键字在Java中主要用于定义静态成员,它具有以下几种应用场景和特性:
- 静态变量(Static Field):
- 当一个变量被声明为
static
时,这个变量称为静态变量或类变量。静态变量存储在方法区的静态存储区,而不是每个对象实例中。 - 静态变量属于类,而不属于类的任何对象,所有该类的对象共享这同一个静态变量。也就是说,无论创建多少个类的实例,它们访问到的都是同一份静态变量。
- 当一个变量被声明为
public class MyClass {
public static int count = 0;
}
- 静态方法(Static Method):
- 静态方法也是属于类的,不依赖于类的任何对象就可以直接调用,因此在静态方法内部不能直接访问非静态成员变量和非静态方法,因为这些需要通过对象来调用。
- 常见的使用场景包括工具类中的工具方法,如Math类中的各种数学计算方法。
public class UtilClass {
public static void printMessage() {
System.out.println("This is a static method.");
}
}
// 使用方式:无需创建对象即可调用
UtilClass.printMessage();
- 静态块(Static Block):
- 静态初始化块在类加载时执行,并且只会被执行一次。通常用于初始化静态变量。
public class MyClass {
public static String message;
static {
message = "Hello, World!";
}
}
- 静态内部类(Static Nested Class):
- 静态内部类不需要依赖外部类的对象即可创建实例,而非静态内部类则必须依赖于外部类的对象。
static
关键字主要用来表示类级别的成员或者行为,它们与类的生命周期相关联,而不是与类的实例关联。同时,由于其特殊的生命周期和访问权限,静态成员在内存管理和多线程环境中有特殊的意义和应用。
final关键字
final
关键字在Java中具有多种用途,主要用于表示不可变性。以下是final
关键字的几种应用场景:
- final变量:
- 当一个变量被声明为
final
时,它就是一个常量(在编译期间就能确定其值),一旦赋值后就无法更改。 - 对于基本数据类型,如int、double等,
final
变量的值不能改变。 - 对于引用类型,如对象引用,虽然不能改变引用本身指向另一个对象,但可以修改引用的对象内部状态。
- 当一个变量被声明为
public class Example {
public static final int CONSTANT = 10; // 基本类型常量
public final String name; // 引用类型常量,在构造器中必须初始化
public Example(String name) {
this.name = name;
}
}
- final方法:
final
方法不能被子类重写(override)。这保证了无论在哪一个子类中调用这个方法,行为都是固定的,不会因为继承而有所变化。
public class BaseClass {
public final void display() {
System.out.println("This is a final method.");
}
}
public class DerivedClass extends BaseClass {
// 不能重写display方法
}
- final类:
- 当一个类被声明为
final
时,意味着这个类不能有任何子类。这样做的目的是为了防止其他类对它的扩展,以确保类的行为和结构保持不变。
- 当一个类被声明为
public final class FinalClass {
// ...
}
final
关键字的主要作用是提供一种编程方式来确保代码中的部分元素(变量、方法或类)在整个程序执行过程中保持不变,从而提升代码的稳定性和可预测性。
this关键字
this
关键字在Java中是一个特殊的引用变量,它始终指向当前对象的引用。以下是this
关键字在不同场景下的应用:
- 访问成员变量: 当类中有多个同名的变量时(如局部变量和实例变量),使用
this
关键字可以明确地指代当前对象的实例变量。
public class MyClass {
private int value;
public void setValue(int value) {
this.value = value; // 这里的this.value指的是当前对象的value实例变量
}
}
- 调用成员方法:
this
可以用于调用当前对象的其他方法。
public class MyClass {
public void method1() {
this.method2(); // 调用当前对象的method2方法
}
public void method2() {
// ...
}
}
- 构造器之间的调用: 在一个类的构造器内部,可以使用
this(...)
调用本类的另一个构造器。
public class MyClass {
private String name;
private int age;
public MyClass(String name) {
this(name, 0); // 调用带两个参数的构造器
}
public MyClass(String name, int age) {
this.name = name;
this.age = age;
}
}
- 作为方法或构造器的返回值: 在Java 8及以上版本中,可以将
this
用作方法的返回值,表示返回当前对象自身,常见于链式调用的场景。
public class BuilderPatternExample {
private String name;
private String address;
public BuilderPatternExample setName(String name) {
this.name = name;
return this; // 返回当前对象以便进行链式调用
}
public BuilderPatternExample setAddress(String address) {
this.address = address;
return this;
}
// 其他方法...
}
this
关键字在Java编程中主要用于区分成员变量与局部变量、调用成员方法以及构造器间的相互调用等,是面向对象编程中非常重要的概念之一。
Java代码的执行顺序
Java代码的执行顺序遵循一定的规则,可以分为以下几个阶段:
-
类加载(Class Loading):
- 当Java虚拟机(JVM)需要使用到某个类时,它会通过类加载器将对应的
.class
文件加载进内存,并进行验证、准备和初始化等过程。 - 初始化过程中,如果类包含有静态初始化块(static initializer blocks)或静态成员变量,则按照它们在源码中的出现顺序依次执行初始化。
- 当Java虚拟机(JVM)需要使用到某个类时,它会通过类加载器将对应的
-
静态成员初始化:
- 静态变量和静态初始化块的执行顺序基于它们在源代码中的位置,静态成员变量先于静态初始化块执行,且只执行一次,无论创建多少个该类的对象。
-
主方法执行:
- JVM开始执行应用程序的入口点,即
public static void main(String[] args)
方法。只有当所有静态内容都已初始化后,main方法才会被执行。
- JVM开始执行应用程序的入口点,即
-
实例化对象:
- 在非静态环境中,当创建类的一个实例(对象)时,首先执行任何实例初始化块(instance initializer blocks),然后按照声明顺序对非静态成员变量进行初始化。
- 接着调用构造方法完成对象的构建。构造方法也是按照其在类中定义的顺序执行,如果有父类的话,先执行父类的构造方法。
-
多线程环境:
- 在多线程环境下,类加载与初始化是线程安全的,确保同一类只会被初始化一次。而对象的实例化以及非静态成员的初始化则遵循线程可见性和同步规则。
综上所述,Java代码的执行顺序可以总结为以下流程:
- 类加载及类初始化(包括静态成员变量和静态初始化块)
- 主函数执行
- 对象实例化及实例成员变量和实例初始化块执行
- 构造方法执行
注意:对于不同线程同时操作同一个类,特别是涉及到类级别的初始化时,JMM会保证线程安全。而对于对象实例的创建,程序员可能需要手动处理并发问题以确保正确性。
内存泄露与内存溢出
内存泄露(Memory Leak)和内存溢出(Out of Memory,OOM)是两种常见的内存管理问题,它们都与程序运行时的内存使用有关。
内存泄露(Memory Leak): 内存泄露是指程序在申请分配了一定数量的内存空间后,在后续的执行过程中未能释放那些不再使用的内存区域。尽管这部分内存对当前的程序逻辑来说已经没有用处,但由于某种原因(例如程序员忘记释放、错误的引用导致无法释放等),操作系统或垃圾回收机制(对于Java这类具有自动垃圾回收机制的语言)无法识别并回收这些内存。随着时间推移,未释放的内存逐渐累积,虽然单次泄露可能很小,但长期运行可能导致系统可用内存越来越少,最终影响到其他正常请求内存的程序功能。
public class MemoryLeakExample {
private List<String> list = new ArrayList<>();
public void keepAdding() {
while (true) {
list.add(new String("Unused memory"));
}
}
// 在这个例子中,不断地添加字符串对象到list中,但从未清空或删除旧的元素,
// 这就造成了内存泄露,因为这些无用的对象永远不会被垃圾回收器回收。
}
内存溢出(Out of Memory,OOM): 内存溢出则是指程序在运行过程中尝试申请更多的内存,但是系统的可用内存不足以满足其需求,导致无法成功分配所需的内存资源,进而引发程序崩溃或者异常。内存溢出通常是因为程序本身的内存消耗过大(如创建了大量大对象,或者数据结构设计不合理导致内存不断增长),或者是由于内存泄露积累到一定程度引起的。
public class OutOfMemoryExample {
public static void main(String[] args) {
try {
List<byte[]> hugeList = new ArrayList<>();
while (true) {
hugeList.add(new byte[1024 * 1024]); // 每次添加1MB的字节数组
}
} catch (Throwable t) {
System.out.println("Out of memory!");
}
}
// 在这个例子中,程序会无限循环地添加大容量数组到列表中,直到耗尽系统所有内存,
// 导致内存溢出(OutOfMemoryError)发生。
}
内存泄露是内存占用持续增加且不能被有效回收的过程,而内存溢出是内存需求超过了系统所能提供的最大限制,造成程序无法继续执行。内存泄露往往是内存溢出的一种重要原因,但并非唯一原因,合理有效地管理和使用内存资源是避免这两种问题的关键。
接口
Java接口是Java编程语言中一种重要的概念,它是一种引用数据类型,主要用于定义一组抽象方法。接口主要用来表示类的行为规范或合同(contract),即声明了一组方法的签名和相关常量,而没有给出这些方法的具体实现。
在Java中定义接口的关键字是 interface
,其基本语法结构如下:
public interface InterfaceName {
// 常量,默认为 public static final
int CONSTANT_VALUE = 100;
// 抽象方法,默认为 public abstract
void method1();
String method2(int arg);
// Java 8 及以后版本可以有默认方法(default method)
default void defaultMethod() {
// 默认方法的实现
}
// Java 8 及以后版本还可以有静态方法(static method)
static void staticMethod() {
// 静态方法的实现
}
}
特性摘要: - 接口中的所有方法都是公开抽象的,即使没有使用 abstract
关键字声明。 - 接口中不能包含实例变量,但可以包含公开的、静态的和最终的(final)常量。 - 一个类可以通过 implements
关键字实现一个或多个接口,并提供接口中所有抽象方法的具体实现。 - 自Java 8开始,接口中可以包含带有具体实现的默认方法和静态方法。 - 类继承单个类的同时可以实现多个接口,从而实现多重继承的功能(因为Java不支持多继承,但通过接口可以间接实现类似效果)。
Java接口的设计鼓励了面向接口编程的原则,使得设计更加灵活且解耦度更高,能够更好地支持多态性。
抽象类与抽象方法
在Java编程语言中,抽象类(Abstract Class)和抽象方法(Abstract Method)是面向对象设计中的两个重要概念,用于表示类的抽象层次和规范。
抽象类(Abstract Class) - 用 abstract
关键字来修饰的类被称为抽象类。 - 抽象类不能被实例化,即不能直接创建抽象类的对象。 - 抽象类可以包含抽象方法(没有方法体的方法),也可以包含非抽象(具体实现)的方法。 - 抽象类的主要目的是为了被其他类继承,并且强制要求子类去实现某些功能。也就是说,任何继承了抽象类的非抽象子类都必须重写其父类中所有的抽象方法。 - 抽象类可以有构造方法,这些构造方法主要用于子类的实例化过程中调用父类构造器初始化一些共享的状态或属性。
抽象方法(Abstract Method) - 抽象方法同样使用 abstract
关键字进行声明,但不提供方法体,只有方法签名(返回类型、方法名以及参数列表)。 - 抽象方法必须存在于抽象类或者接口中。 - 如果一个类包含至少一个抽象方法,那么这个类就必须声明为抽象类。 - 当一个非抽象子类继承了抽象类时,它必须提供抽象方法的具体实现,否则该子类也必须声明为抽象类。
抽象类和抽象方法是用来描述一种类的设计蓝图,它定义了一组行为的框架,而具体的细节则由它的子类根据需求来填充和完善。通过这种方式,抽象类和抽象方法能够促进代码复用,提高软件系统的灵活性和可扩展性。
对象的比较
在Java中,对象的比较主要涉及两种情况:
-
引用比较(Identity Comparison):
- 使用
==
运算符来判断两个对象引用是否指向同一个对象实例。例如:Object obj1 = new Object(); Object obj2 = obj1; System.out.println(obj1 == obj2); // 输出:true
- 当比较的是基本数据类型包装类的对象时,如果它们都表示的是同一个基本类型的值且为常量池中的对象(如Integer的-128至127范围内的整数),则也可能返回true。
- 使用
-
内容比较(Equality Comparison):
- 对于非基本类型的对象,使用
equals()
方法进行内容比较,判断两个对象的内容或状态是否相等。String str1 = "Hello"; String str2 = new String("Hello"); System.out.println(str1.equals(str2)); // 输出:true
- 默认情况下,Object类的equals()方法与"=="运算符的行为相同,都是比较对象引用。但通常我们会重写equals()方法以满足业务需求,比如String、Integer等类都重写了equals()方法,用于比较对象的实际内容。
- 注意,当重写equals()方法时,应同时重写hashCode()方法,以保持equals和hashCode的一致性,这对于集合类的操作至关重要。
- 对于非基本类型的对象,使用
另外,对于实现了Comparable接口的类对象,可以使用compareTo()方法来进行排序相关的比较。而对于自定义类,可以根据具体业务逻辑实现Comparator接口来自定义比较规则。
"=="与equals的区别
,"=="和equals()方法的主要区别在于它们的用途和行为:
-
"=="运算符:
- 对于基本数据类型(如int, char, boolean等),"=="用于比较它们各自的值是否相等。
int a = 5; int b = 5; System.out.println(a == b); // 输出 true,因为a和b的值都是5
- 对于引用类型(对象),"=="比较的是两个引用变量是否指向内存中的同一块地址,即判断它们是否引用同一个对象实例。
String str1 = new String("Hello"); String str2 = str1; System.out.println(str1 == str2); // 输出 true,因为str1和str2指向了相同的字符串对象
-
equals()方法:
- 在Object类中,equals()方法默认的行为也是比较对象引用是否指向同一个对象实例,与"=="作用相同。
Object obj1 = new Object(); Object obj2 = new Object(); System.out.println(obj1.equals(obj2)); // 输出 false,因为obj1和obj2指向不同的对象实例
- 但是,很多Java内置类(如String、Integer等)以及自定义类都重写了equals()方法,使其能够基于对象的内容或属性进行比较。例如:
String str1 = new String("Hello"); String str2 = new String("Hello"); System.out.println(str1.equals(str2)); // 输出 true,虽然str1和str2是不同的对象,但内容相同
当你在编写自定义类时,通常需要根据类的具体逻辑重写equals()方法来正确地比较两个对象是否“相等”(基于它们的状态或属性)。同时,当重写equals()方法时,为了保持一致性,通常也需要重写hashCode()方法。
==主要用于比较基本类型的值或引用类型的引用地址;而equals()方法在被正确重写后,则可以用来比较对象的内容或状态是否相等。对于大多数业务场景下的对象比较,我们通常使用equals()而非==。