Java-Object类

Object类

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

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

1
2
3
4
5
6
 public class Runoob extends Object{

}
public class Runoob {

}

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

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

Object.clone()

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

1
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接口。
1
2
class MyClass implements Cloneable{
}

我们看如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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);
}

}
}


程序输出如下:

1
2
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类呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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);
}

}
}

那么运行结果如下:

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

Object.equals()

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

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

参数

  • Object - 要比较的对象

返回值

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

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

1
2
3
4
5
6
7
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()。

我们来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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)方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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检查

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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()的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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),这是一个整数值。

1
public native int hashCode();

参数

返回值

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

hashCode方法的主要用途在于:

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

hashCode()与equals()的关系

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

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

重写hashCode()

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

方法:

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

Object.getClass()方法

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

1
public final native Class<?> getClass();

参数

返回值

  • 返回对象的类

特点

  1. 返回值
  • 返回一个 Class<?> 对象,表示调用该方法的对象的运行时类。
  1. 不可重写
  • getClass() 方法是 final 的,不能被子类重写
  1. 用途
  • 获取对象的运行时类型。
  • 用于反射操作(如获取类的字段、方法、构造函数等)。
  1. 运行时类型
  • 即使对象被赋值给父类引用,getClass() 方法仍然返回实际的运行时类,而不是引用变量的类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
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));
}
}

输出示例:

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

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

与instanceof的区别

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

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

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

一个编译错误

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

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

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

1
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类中一个非常基础且常用的方法。它的主要目的是返回一个表示该对象的字符串

默认实现:

1
2
3
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()

假设我们有一个类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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循环中,以防止虚假唤醒和条件再次变为不满足的情况。

1
2
3
4
5
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)时可能会被调用。

1
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语句。此处不展开讲解。


Java-Object类
http://blog.ulna520.com/2025/04/06/Java-Object类_20250406_160811/
Veröffentlicht am
April 6, 2025
Urheberrechtshinweis