Java-Object类

11213 字
29 分钟

Object类

Java中Object类是所有类的父类,也就是说Java中所有的类都继承了Object,子类可以使用Object的所有方法。

当你在创建一个类时,如果没有显示的继承任何类,那么他会自动继承Object类:

 public class Runoob extends Object{

}
public class Runoob {

}

以上两种创建类的效果完全一致。

Object提供了一些通用的方法,Java中的所有类都有这些方法,所以可以实现一些通用的功能,也可以通过重写来实现自定义的操作。

Object.clone()

clone() 方法会返回调用对象的一份潜拷贝

protected native Object clone() throws CloneNotSupportedException;

重写 clone()方法时,可以修改为 public以拓宽访问条件

参数

返回值

  • 返回一个Object类的拷贝

浅拷贝与深拷贝

浅拷贝

  1. 基本数据类型 :直接复制其值。
  2. 引用类型 :复制引用地址,而不是引用对象本身。这意味着拷贝后的对象和原对象共享同一个引用对象。

深拷贝

完全独立的对象副本。

  1. 基本数据类型:复制其值
  2. 引用类型:创建一个拥有完全一样内容的独立副本。

由于 Object.clone()只提供浅拷贝,如果需要深拷贝,我们需要手动实现:

  1. 在重写的 clone()方法中,首先调用 super.clone()获取浅拷贝
  2. 对于所有需要深拷贝的引用类型成员变量,手动调用他们的 clone()方法(确保他们也实现了深拷贝的 clone()方法,且继承 Cloneable接口)

Cloneable接口

Cloneable接口是一个标记接口(Marker Interface)。它不包含任何方法,它的唯一作用就是指示一个类是可克隆的。

  • 重要性:如果一个类想要使用 Objectclone()方法进行克隆,它必须实现 Cloneable接口。
class MyClass implements Cloneable{
}

我们看如下例子:

class Work{
    String name;
    int salary;
}

class Person  implements Cloneable{
    String name;
    Work work;

    public static void main(String[] args) {
        // 创建一个 Person 对象并设置其属性
        Person person = new Person();
        person.name = "John Doe";
        person.work = new Work();
        person.work.name = "Software Engineer";
        person.work.salary = 100000;
  
        // 创建一个Person 的拷贝
        try{
            Person personCopy = (Person)person.clone();

            //修改拷贝对象的属性
            personCopy.name = "ulna";
            //修改引用对象内的属性
            personCopy.work.name = "Software Engineer 2";
            personCopy.work.salary = 200000;
            // 打印原对象和拷贝对象的属性
            System.out.println("Original Person: " + person.name + ", Work: " + person.work.name + ", Salary: " + person.work.salary);
            System.out.println("Copied Person: " + personCopy.name + ", Work: " + personCopy.work.name + ", Salary: " + personCopy.work.salary);
        }
        catch (Exception e) {
            System.out.println(e);
        }
  
    }
}

程序输出如下:

Original Person: John Doe, Work: Software Engineer 2, Salary: 200000
Copied Person: ulna, Work: Software Engineer 2, Salary: 200000

修改拷贝对象的属性后,Work类的引用由于是潜拷贝,只拷贝了引用对象的地址,所以对于拷贝对象的修改会影响原对象。

这时可能有人就会产生疑问了:

String 类也是一个引用对象,为什么对于拷贝对象的修改没有影响原对象?

这恰好是String类的不可变性造成的,在String类那一章节中我们讲到String 类是一个不可变类,因此在对String 类赋值或者修改时,会自动创建一个新的String 对象。正是由于这个原因,当我们对拷贝对象的String 类赋值时,其实是将其引用指向了另一个地址,原对象指向的String类自然不受影响。

但是倘若我们使用的是StringBuffer类呢?

class Work{
    StringBuffer name;
    int salary;
}

class Person  implements Cloneable{
    StringBuffer name;
    Work work;

    public static void main(String[] args) {
        // 创建一个 Person 对象并设置其属性
        Person person = new Person();
        person.name = new StringBuffer("John Doe");
        person.work = new Work();
        person.work.name = new StringBuffer("Software Engineer");
        person.work.salary = 100000;
  
        // 创建一个Person 的拷贝
        try{
            Person personCopy = (Person)person.clone();

            //修改拷贝对象的属性
            personCopy.name.replace(0, personCopy.name.length() , "ulna");
            //修改引用对象内的属性
            personCopy.work.name.replace(0,personCopy.work.name.length(),"Software Engineer 2");
            personCopy.work.salary = 200000;
            // 打印原对象和拷贝对象的属性
            System.out.println("Original Person: " + person.name + ", Work: " + person.work.name + ", Salary: " + person.work.salary);
            System.out.println("Copied Person: " + personCopy.name + ", Work: " + personCopy.work.name + ", Salary: " + personCopy.work.salary);
        }
        catch (Exception e) {
            System.out.println(e);
        }
  
    }
}

那么运行结果如下:

Original Person: ulna, Work: Software Engineer 2, Salary: 200000
Copied Person: ulna, Work: Software Engineer 2, Salary: 200000

Object.equals()

equals()方法判断两个对象的引用是否相等。

public boolean equals(Object obj) {
    return (this == obj);
}

参数

  • Object - 要比较的对象

返回值

  • boolean :相等返回true,否则返回false

默认的equals()只会比较两个对象的引用即地址是否相同,不会比较内容。

Object obj1 = new Object();
Object obj2 = new Object();
Object obj3 = obj1;
 

System.out.println(obj1.equals(obj2)); // false
System.out.println(obj1.equals(obj3)); // true

重写equals()

大多数情况下,我们想要的都不是比较对象引用是否相等,而是对象中一些内容是否相等。这时我们就需要重写equals()。

我们来看下面的例子:

class Person
{
    String name;
    int age;
    String address;
  
    public Person(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public boolean equals(Person person) {
        if(super.equals(person)) return true; // 检查是否是同一个对象
        return age == person.age && name.equals(person.name) && address.equals(person.address);
    }
}

或许作为java初学者的你会觉得这是一个可能略有瑕疵但总体没有大问题的equals()方法。但其实如上的写法是一个完全错误的equals重写。

  1. 首先这不是Java中的重写,而是重载。当传入不同类型的参数时会调用不同参数列表的equals()方法:

    public class Main{
        public static void main (String args[]){
            Person person1 = new Person("John", 25, "123 Main St");
            Person person2 = new Person("John", 25, "123 Main St");
            Object person3 = new Person("John", 25, "123 Main St");
            //这里调用euqals(Person person)方法
            System.out.println(person1.equals(person2)); // true
            //这里调用euqals(Object obj)方法
            System.out.println(person1.equals(person3)); // false
        }
    }
  2. 由于我们没有重写 equals()方法,所以在集合类数据结构中:HashSetHashMapHashTable这些集合中时,会导致错误重复数据或错误的查找行为,因为他们仍然在使用 euqals(Object obj)方法

    public class Main{
        public static void main (String args[]){
            Person person1 = new Person("John", 25, "123 Main St");
            Person person2 = new Person("John", 25, "123 Main St");
            Object person3 = new Person("John", 25, "123 Main St");
    
            HashSet<Person> set = new HashSet<>();
            set.add(person1);
            set.add(person2); // 这行代码会调用equals方法,判断person1和person2是否相等
    
            System.out.println(set.size()); // 结果为 2,而不是 1,因为 equals() 未被重写
        }
    } 
  3. 潜在的 NullPointerException风险,没有对传入的参数进行 null检查

    public class Main{
        public static void main (String args[]){
            Person person1 = new Person("John", 25, "123 Main St");
            Person person2 = null;
    
            //这里调用euqals(Person person)方法
            System.out.println(person1.equals(person2)); // true
        }
    }

    上面的程序编译时并不会报错,但是运行时会奔溃并打印错误:java.lang.NullPointerException

  4. 缺少类型检查,没有确认传入的类型是否为Person类

重写equals的正确步骤

Java规范明确规定了重写 equals()方法时必须遵循的五个约定。遵循这些约定是确保 equals()方法行为正确、可靠的关键。

  1. 自反性(Reflexive):
    • 对于任何非 null 的引用值 xx.equals(x) 必须返回 true
  2. 对称性(Symmetric):
    • 对于任何非 null 的引用值 xy,如果 x.equals(y) 返回 true,那么 y.equals(x) 也必须返回 true
  3. 传递性(Transitive):
    • 对于任何非 null 的引用值 xyz,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 也必须返回 true
  4. 一致性(Consistent):
    • 对于任何非 null 的引用值 xy,只要 equals 比较中使用的信息没有被修改,多次调用 x.equals(y) 必须始终返回相同的结果。
  5. null的处理:
    • 对于任何非 null 的引用值 xx.equals(null) 必须返回 false

由此,我们可以得到正确重写equals()的结果如下:

@Override
public boolean equals(Object obj) {

    // 1. 检查是否同一个对象
    if (this == obj) return true;   

    // 2. 检查传入对象是否为null
    // 3. 检查类是否相同
    if (obj == null || getClass() != obj.getClass()) return false;
  
    // 4. 将obj转化为当前类型
    Person person = (Person) obj;   //将Object类型转换为Person类型

    // 5. 比较所有参与equals判断的字段
    if (age != person.age) return false;  
    if (name != null ? !name.equals(person.name) : person.name != null) return false;
    return address != null ? address.equals(person.address) : person.address == null;
}

Object.hashCode()方法

hashCode()方法返回一个对象的哈希码(hash code),这是一个整数值。

public native int hashCode();

参数

返回值

  • 返回对象哈希值,是一个整数。

hashCode方法的主要用途在于:

  • 为了支持基于哈希的集合类:如 HashMapHashSetHashtable
    • 定位桶的位置
  • 这些集合需要快速查找、存储和检索对象
    • 先通过哈希码缩小查找范围,再使用equals()方法确认其相等性

hashCode()与equals()的关系

一致性约定:Java中约定如果两个对象 equals()方法比较是相等的,那么它们的 hashCode()方法必须返回相同的值

反之不成立:两个对象有相同的哈希码,它们不一定相等(“哈希冲突”)

重写hashCode()

当你重写了equals()方法时,你就必须重写hashCode()方法。这是java规范中的一个强制性约定。

方法:

  • 使用Objects.hash()方法,它会自动处理null值,并且是一个安全、高效的方式。
@Override
public int hashCode() {
    return Objects.hash(field1, field2, field3);
}
  1. 不包含所有影响equals()的字段hashCode()应包含所有在 equals()中用于比较的字段
  2. 使用可变字段 :尽量避免使用可变字段计算哈希码,否则对象插入集合后修改这些字段会导致查找失败
  3. 过度复杂 :哈希码计算应该简单高效,不要加入过多的复杂逻辑
  4. 忽略性能 :对于频繁使用的类,可以考虑缓存哈希码值

Object.getClass()方法

Object getClass() 方法用于获取对象的运行时对象的类。

public final native Class<?> getClass();

参数

返回值

  • 返回对象的类

特点

  1. 返回值
  • 返回一个 Class<?> 对象,表示调用该方法的对象的运行时类。
  1. 不可重写
  • getClass() 方法是 final 的,不能被子类重写
  1. 用途
  • 获取对象的运行时类型。
  • 用于反射操作(如获取类的字段、方法、构造函数等)。
  1. 运行时类型
  • 即使对象被赋值给父类引用,getClass() 方法仍然返回实际的运行时类,而不是引用变量的类型。
class Animal {
}

class Dog extends Animal {
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 父类引用指向子类对象
        System.out.println("运行时类: " + animal.getClass().getName());
        System.out.println("是否为 Dog 类: " + (animal.getClass() == Dog.class));
    }
}

输出示例:

运行时类: Dog
是否为 Dog 类: true

同时Java提供了另一种方式来获取类的 Class对象,直接通过类名后加 .class 的方式。这是一种静态的方式可以直接通过类名来获取 Class 对象。

与instanceof的区别

  • instanceof 检查的是对象是否是某个类或其子类的实例。
  • getClass() 检查的是对象的 精确类型
Animal animal = new Dog();
System.out.println(animal instanceof Animal); // true
System.out.println(animal.getClass() == Animal.class); // false

instanceof 只要前面的实例是后面的类或子类的实例就会返回true

getClass()只有在两个类对象完全相等时才会true

一个编译错误

对于如上的例子,可能有些同学会改为如下:

Dog animal = new Dog();
System.out.println(animal instanceof Animal); // true
System.out.println(animal.getClass() == Animal.class); // false

但是会发现一个报错导致无法编译通过:

Incompatible operand types Class<capture#1-of ? extends Dog> and Class<Animal>

两段代码的区别只有animal实例的引用为Animal类和Dog类

  • 当引用类型为 Animal类时,编译器将 animal.getClass() 的返回类型视为 Class<? extends Animal>
    • animal对象的类为 Animal类或者 Animal类的子类
  • 当引用类型为 Dog 类型时,编译器将 animal.getClass() 的返回类型视为 Class<? extends Dog>
    • animal对象的类为 Dog类或者 Dog类的子类

而Animal.class返回的类为 Class<Animal>

Java编译器认为:将 Class<? extends Dog>Class<Animal>进行比较是没有意义的,因为一个 Class<? extends Dog>类并不可能是 Animal的实例。

只有 Class<? extends Animal> 类才有可能是 Animal类的实例。这一点也与我们在Java多态中学习到的:

  • 父类的引用可以指向子类的实例
  • 子类引用不能指向父类实例

相契合。

Onject.toString()

toString() 方法是Onject类中一个非常基础且常用的方法。它的主要目的是返回一个表示该对象的字符串

默认实现:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

这意味着,如果不重写 toString() 方法,任何对象调用它时,都会返回一个由以下两部分组成的字符串:

  1. 对象的运行时类名(getClass().getName()):例如 java.lang.Objectcom.example.MyClass
  2. @符号:作为分隔符
  3. 对象的哈希码的十六进制表示(Integer.toHexString(hashCode())):这是调用hashCode()方法返回整数值的十六进制字符串的形式。

重写toString()

当你希望对象的字符串表示能够提供有意义、易于阅读和理解的信息时,你就需要重写 toString() 方法。

很多IDE都支持自动生成类的toString()方法。

vscode自动生成toString()

假设我们有一个类如下:

public class Person {
    private String id;
    private String name;
    private int age;

    public Person(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public static void main(String[] args) {
        Person p1 = new Person("001", "Alice", 30);
        Person p2 = new Person("002", "Bob", 25);

        // 当直接打印对象时,会自动调用其 toString() 方法
        System.out.println(p1); // 输出: Person{id='001', name='Alice', age=30}
        System.out.println(p2); // 输出: Person{id='002', name='Bob', age=25}

        // 也可以显式调用
        String p1Info = p1.toString();
        System.out.println(p1Info);
    }
}

鼠标空白处右键: 1753528067050

点击源代码操作:

1753528130553

点击Generate toString()…:

1753528195752

选择我们需要的成员变量。就会在光标处自动生成toString()方法。此外hashCode和equals也可以IDE自动生成。

wait()、notify()、notifyAll()

这三个方法是 Java 中实现线程间通信和协作的关键。它们允许线程在特定条件不满足时暂停执行(等待),并在条件满足时被其他线程唤醒。

核心概念:监视器锁(Moniter Lock)

在深入谅解这三个方法之前,理解监视器锁(Moniter Lock)是至关重要的。

  • 每个Java对象都有一把监视器锁。你可以把它想象成每个对象自带的一个”小房间”,只有获得了这把锁的线程才能进入房间执行代码。
  • synchronized关键字是获取监视器锁的方式。当一个线程执行synchronized块或方法时,它必须先获取到该synchronized块/方法所关联的对象的监视器。
  • notify()notifyAll()wait() 方法只能在 synchronized 块或方法内部调用 。如果你在非 synchronized 块中调用它们,会立即抛出 IllegalMonitorStateException,因为这些操作都与监视器锁的状态紧密相关。

wait()方法

作用:让当前线程释放它所持有的监视器锁,并进入等待状态,直到被其他线程唤醒(通过 notify()notifyAll())或等待超时。

位置:必须在 synchronized块/方法内部调用。

重载形式

  • wait():无参数,无限期等待,直到被唤醒
  • wait(long timeout):等待指定毫秒数,如果超时未被唤醒,线程会自动返回
  • wait(long timeout, int nanos):更精细的超时等待,精确到纳秒。

工作机制

  1. 当线程调用 obj.wait()时,它会检查自己是否持有 obj的监视器锁。如果没有,则抛出 IllegalMonitorStateException
  2. 如果持有锁,线程会立即释放obj监视器锁
  3. 线程进入obj对象的等待池,并转为等待(Waiting)状态。
  4. 线程被唤醒后(或超时),它会尝试重新获取 obj的监视器锁。只有成功获取锁后,线程才能从wait()方法返回,继续执行。

最佳实践wait()方法通常放在一个 while循环中,以防止虚假唤醒和条件再次变为不满足的情况。

synchronized(obj){
	while(conditionIsnotMet){
		obj.wait();
	}
}

notify()方法

作用:唤醒在该对象的监视器锁上等待池中的单个随机线程。

位置:必须在synchronized块/方法内部调用

工作机制

  1. 当线程调用 obj.notify()时,它会检查自己是否持有obj的监视器锁。如果没有,则抛出 IllegalMonitorStateException
  2. 如果持有锁,它会从obj的等待池中随机选择一个线程,将其从等待状态移到就绪状态。
  3. 被唤醒的线程不会立即执行,它仍需要等待当前的 notify()线程释放 obj的监视器锁。一旦锁被释放,被唤醒的线程才能去竞争这把锁,成功获取后才能从 wait()方法返回。

notifyAll()方法

作用:唤醒在该对象的监视器锁上等待池中的所有线程。

位置:必须在 synchronized块/方法内部调用

工作机制

  1. notify() 类似,调用 obj.notifyAll() 时,会检查是否持有 obj 的监视器锁。
  2. 如果持有锁,它会将obj等待池中的所有线程从等待状态移到就绪(Runnable)状态
  3. 所有被唤醒的线程都会进入竞争obj监视器锁的状态。一旦当前notifyAll()线程释放了锁,这些被唤醒的线程就会开始竞争。只有一个线程能成功获取锁并继续执行,其他线程则继续等待锁。

详细的应用可以参考java多线程这篇bolg的内容。

Object.finalize()方法

finalize()方法是Object类中一个特殊的方法,它在Java虚拟机(JVM)执行垃圾回收(Garbage Collection GC)时可能会被调用。

protected void finalize() throws Throwable { }

finalize()方法主要意图是让对象在被垃圾回收器回收之前,执行一些清理操作。这些清理操作通常是针对非JAVA资源(Non-Java Resources)的,例如:

  • 打开的文件句柄
  • 网络连接
  • 数据库连接
  • JNI(Java Native Interface)中分配的本地内存

当一个对象被判定为不再被任何引用所指向时,它就成为了垃圾回收的候选对象。在垃圾回收器最终回收这个对象之前,如果该对象重写了finalize()方法并且这个方法还没有被调用过,JVM就有机会调用它。

finalize()方法由于以下原因已经不被推荐使用:

  1. 不可预测性:你无法控制 finalize() 何时被调用,甚至不能保证它一定会被调用。这使得它不适合用于任何必须及时或可靠地释放资源的情况。
  2. 性能开销finalize() 的执行会引入额外的性能开销。JVM 需要在垃圾回收过程中进行额外的检查和操作来处理带有 finalize() 方法的对象。这可能会导致垃圾回收的效率降低。
  3. 潜在的死锁和错误:在 finalize() 方法中执行复杂的逻辑(尤其是涉及同步或线程的操作)可能会导致死锁或其他不可预知的行为。
  4. 异常处理问题finalize() 方法中抛出的异常会被 JVM 忽略,这意味着即使你的清理代码出错了,你也不会得到通知,从而导致资源泄露。
  5. 内存泄漏风险:如果 finalize() 方法本身存在缺陷(例如,在其中持有对其他对象的引用而不释放),可能会导致内存泄露。

鉴于 finalize() 方法的诸多缺点,Java 提供了更健壮、更可靠的机制来管理资源和执行清理操作,其中被推荐的方法是:try-with-resources语句。此处不展开讲解。