Java-Object类
Object类
Java中Object类是所有类的父类,也就是说Java中所有的类都继承了Object,子类可以使用Object的所有方法。
当你在创建一个类时,如果没有显示的继承任何类,那么他会自动继承Object类:
1 |
|
以上两种创建类的效果完全一致。
Object提供了一些通用的方法,Java中的所有类都有这些方法,所以可以实现一些通用的功能,也可以通过重写来实现自定义的操作。
Object.clone()
clone() 方法会返回调用对象的一份潜拷贝。
1 |
|
重写
clone()
方法时,可以修改为public
以拓宽访问条件
参数
- 无
返回值
- 返回一个Object类的拷贝
浅拷贝与深拷贝
浅拷贝
- 基本数据类型 :直接复制其值。
- 引用类型 :复制引用地址,而不是引用对象本身。这意味着拷贝后的对象和原对象共享同一个引用对象。
深拷贝
完全独立的对象副本。
- 基本数据类型:复制其值
- 引用类型:创建一个拥有完全一样内容的独立副本。
由于 Object.clone()
只提供浅拷贝,如果需要深拷贝,我们需要手动实现:
- 在重写的
clone()
方法中,首先调用super.clone()
获取浅拷贝 - 对于所有需要深拷贝的引用类型成员变量,手动调用他们的
clone()
方法(确保他们也实现了深拷贝的clone()
方法,且继承Cloneable
接口)
Cloneable
接口
Cloneable
接口是一个标记接口(Marker Interface)。它不包含任何方法,它的唯一作用就是指示一个类是可克隆的。
- 重要性:如果一个类想要使用
Object
的clone()
方法进行克隆,它必须实现Cloneable
接口。
1 |
|
我们看如下例子:
1 |
|
程序输出如下:
1 |
|
修改拷贝对象的属性后,Work类的引用由于是潜拷贝,只拷贝了引用对象的地址,所以对于拷贝对象的修改会影响原对象。
这时可能有人就会产生疑问了:
String 类也是一个引用对象,为什么对于拷贝对象的修改没有影响原对象?
这恰好是String类的不可变性造成的,在String类那一章节中我们讲到String 类是一个不可变类,因此在对String 类赋值或者修改时,会自动创建一个新的String 对象。正是由于这个原因,当我们对拷贝对象的String 类赋值时,其实是将其引用指向了另一个地址,原对象指向的String类自然不受影响。
但是倘若我们使用的是StringBuffer类呢?
1 |
|
那么运行结果如下:
1 |
|
Object.equals()
equals()方法判断两个对象的引用是否相等。
1 |
|
参数
- Object - 要比较的对象
返回值
- boolean :相等返回true,否则返回false
默认的equals()只会比较两个对象的引用即地址是否相同,不会比较内容。
1 |
|
重写equals()
大多数情况下,我们想要的都不是比较对象引用是否相等,而是对象中一些内容是否相等。这时我们就需要重写equals()。
我们来看下面的例子:
1 |
|
或许作为java初学者的你会觉得这是一个可能略有瑕疵但总体没有大问题的equals()方法。但其实如上的写法是一个完全错误的equals重写。
首先这不是Java中的重写,而是重载。当传入不同类型的参数时会调用不同参数列表的equals()方法:
1
2
3
4
5
6
7
8
9
10
11public 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
}
}由于我们没有重写
equals()
方法,所以在集合类数据结构中:HashSet
、HashMap
、HashTable
这些集合中时,会导致错误重复数据或错误的查找行为,因为他们仍然在使用euqals(Object obj)
方法1
2
3
4
5
6
7
8
9
10
11
12
13public 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() 未被重写
}
}潜在的
NullPointerException
风险,没有对传入的参数进行null
检查1
2
3
4
5
6
7
8
9public 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
缺少类型检查,没有确认传入的类型是否为Person类
重写equals的正确步骤
Java规范明确规定了重写 equals()
方法时必须遵循的五个约定。遵循这些约定是确保 equals()
方法行为正确、可靠的关键。
- 自反性(Reflexive):
- 对于任何非
null
的引用值x
,x.equals(x)
必须返回true
。
- 对于任何非
- 对称性(Symmetric):
- 对于任何非
null
的引用值x
和y
,如果x.equals(y)
返回true
,那么y.equals(x)
也必须返回true
。
- 对于任何非
- 传递性(Transitive):
- 对于任何非
null
的引用值x
、y
和z
,如果x.equals(y)
返回true
,并且y.equals(z)
返回true
,那么x.equals(z)
也必须返回true
。
- 对于任何非
- 一致性(Consistent):
- 对于任何非
null
的引用值x
和y
,只要equals
比较中使用的信息没有被修改,多次调用x.equals(y)
必须始终返回相同的结果。
- 对于任何非
- 对
null
的处理:- 对于任何非
null
的引用值x
,x.equals(null)
必须返回false
。
- 对于任何非
由此,我们可以得到正确重写equals()的结果如下:
1 |
|
Object.hashCode()方法
hashCode()方法返回一个对象的哈希码(hash code),这是一个整数值。
1 |
|
参数
- 无
返回值
- 返回对象哈希值,是一个整数。
hashCode方法的主要用途在于:
- 为了支持基于哈希的集合类:如
HashMap
、HashSet
、Hashtable
等- 定位桶的位置
- 这些集合需要快速查找、存储和检索对象
- 先通过哈希码缩小查找范围,再使用equals()方法确认其相等性
hashCode()与equals()的关系
一致性约定:Java中约定如果两个对象 equals()
方法比较是相等的,那么它们的 hashCode()
方法必须返回相同的值
反之不成立:两个对象有相同的哈希码,它们不一定相等(”哈希冲突”)
重写hashCode()
当你重写了equals()方法时,你就必须重写hashCode()方法。这是java规范中的一个强制性约定。
方法:
- 使用Objects.hash()方法,它会自动处理null值,并且是一个安全、高效的方式。
1 |
|
- 不包含所有影响equals()的字段 :
hashCode()
应包含所有在equals()
中用于比较的字段 - 使用可变字段 :尽量避免使用可变字段计算哈希码,否则对象插入集合后修改这些字段会导致查找失败
- 过度复杂 :哈希码计算应该简单高效,不要加入过多的复杂逻辑
- 忽略性能 :对于频繁使用的类,可以考虑缓存哈希码值
Object.getClass()方法
Object getClass() 方法用于获取对象的运行时对象的类。
1 |
|
参数
- 无
返回值
- 返回对象的类
特点
- 返回值 :
- 返回一个
Class<?>
对象,表示调用该方法的对象的运行时类。
- 不可重写 :
getClass()
方法是final
的,不能被子类重写。
- 用途 :
- 获取对象的运行时类型。
- 用于反射操作(如获取类的字段、方法、构造函数等)。
- 运行时类型 :
- 即使对象被赋值给父类引用,
getClass()
方法仍然返回实际的运行时类,而不是引用变量的类型。
1 |
|
输出示例:
1 |
|
同时Java提供了另一种方式来获取类的 Class
对象,直接通过类名后加 .class
的方式。这是一种静态的方式可以直接通过类名来获取 Class
对象。
与instanceof的区别
instanceof
检查的是对象是否是某个类或其子类的实例。getClass()
检查的是对象的 精确类型 。
1 |
|
instanceof
只要前面的实例是后面的类或子类的实例就会返回true
而 getClass()
只有在两个类对象完全相等时才会true
一个编译错误
对于如上的例子,可能有些同学会改为如下:
1 |
|
但是会发现一个报错导致无法编译通过:
1 |
|
两段代码的区别只有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 |
|
这意味着,如果不重写 toString()
方法,任何对象调用它时,都会返回一个由以下两部分组成的字符串:
- 对象的运行时类名(
getClass().getName()
):例如java.lang.Object
、com.example.MyClass
。 @
符号:作为分隔符- 对象的哈希码的十六进制表示(
Integer.toHexString(hashCode())
):这是调用hashCode()方法返回整数值的十六进制字符串的形式。
重写toString()
当你希望对象的字符串表示能够提供有意义、易于阅读和理解的信息时,你就需要重写 toString()
方法。
很多IDE都支持自动生成类的toString()方法。
vscode自动生成toString()
假设我们有一个类如下:
1 |
|
鼠标空白处右键:
点击源代码操作:
点击Generate toString()…:
选择我们需要的成员变量。就会在光标处自动生成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)
:更精细的超时等待,精确到纳秒。
工作机制:
- 当线程调用
obj.wait()
时,它会检查自己是否持有obj
的监视器锁。如果没有,则抛出IllegalMonitorStateException
- 如果持有锁,线程会立即释放obj监视器锁
- 线程进入obj对象的等待池,并转为等待(Waiting)状态。
- 线程被唤醒后(或超时),它会尝试重新获取
obj
的监视器锁。只有成功获取锁后,线程才能从wait()方法返回,继续执行。
最佳实践:wait()
方法通常放在一个 while
循环中,以防止虚假唤醒和条件再次变为不满足的情况。
1 |
|
notify()方法
作用:唤醒在该对象的监视器锁上等待池中的单个随机线程。
位置:必须在synchronized
块/方法内部调用
工作机制:
- 当线程调用
obj.notify()
时,它会检查自己是否持有obj的监视器锁。如果没有,则抛出IllegalMonitorStateException
- 如果持有锁,它会从obj的等待池中随机选择一个线程,将其从等待状态移到就绪状态。
- 被唤醒的线程不会立即执行,它仍需要等待当前的
notify()
线程释放obj
的监视器锁。一旦锁被释放,被唤醒的线程才能去竞争这把锁,成功获取后才能从wait()
方法返回。
notifyAll()方法
作用:唤醒在该对象的监视器锁上等待池中的所有线程。
位置:必须在 synchronized
块/方法内部调用
工作机制:
- 与
notify()
类似,调用obj.notifyAll()
时,会检查是否持有obj
的监视器锁。 - 如果持有锁,它会将obj等待池中的所有线程从等待状态移到就绪(Runnable)状态
- 所有被唤醒的线程都会进入竞争obj监视器锁的状态。一旦当前notifyAll()线程释放了锁,这些被唤醒的线程就会开始竞争。只有一个线程能成功获取锁并继续执行,其他线程则继续等待锁。
详细的应用可以参考java多线程这篇bolg的内容。
Object.finalize()方法
finalize()方法是Object类中一个特殊的方法,它在Java虚拟机(JVM)执行垃圾回收(Garbage Collection GC)时可能会被调用。
1 |
|
finalize()
方法主要意图是让对象在被垃圾回收器回收之前,执行一些清理操作。这些清理操作通常是针对非JAVA资源(Non-Java Resources)的,例如:
- 打开的文件句柄
- 网络连接
- 数据库连接
- JNI(Java Native Interface)中分配的本地内存
当一个对象被判定为不再被任何引用所指向时,它就成为了垃圾回收的候选对象。在垃圾回收器最终回收这个对象之前,如果该对象重写了finalize()方法并且这个方法还没有被调用过,JVM就有机会调用它。
finalize()
方法由于以下原因已经不被推荐使用:
- 不可预测性:你无法控制
finalize()
何时被调用,甚至不能保证它一定会被调用。这使得它不适合用于任何必须及时或可靠地释放资源的情况。 - 性能开销:
finalize()
的执行会引入额外的性能开销。JVM 需要在垃圾回收过程中进行额外的检查和操作来处理带有finalize()
方法的对象。这可能会导致垃圾回收的效率降低。 - 潜在的死锁和错误:在
finalize()
方法中执行复杂的逻辑(尤其是涉及同步或线程的操作)可能会导致死锁或其他不可预知的行为。 - 异常处理问题:
finalize()
方法中抛出的异常会被 JVM 忽略,这意味着即使你的清理代码出错了,你也不会得到通知,从而导致资源泄露。 - 内存泄漏风险:如果
finalize()
方法本身存在缺陷(例如,在其中持有对其他对象的引用而不释放),可能会导致内存泄露。
鉴于 finalize()
方法的诸多缺点,Java 提供了更健壮、更可靠的机制来管理资源和执行清理操作,其中被推荐的方法是:try-with-resources
语句。此处不展开讲解。