Object 是 Java 中所有类的父类,即使没有显示声明,编译器也会默认加上,Object 这个父类存在的意义,大概就是给所有的类都添加几个默认的方法。为什么 Java 的开发者认为所有的类都需要这几个方法?这足以说明这几个方法的重要性,所以我认为,所有的 Java 开发者,都应该对这几个方法的功能有着一定的了解。
getClass()
这个方法能够返回某个对象在运行时的类型,即将这个对象实例化时使用的类,特别的,当一个类是匿名内部类的时候,因为它并没有一个对应的有名字的类,所以在求一个匿名内部类的 Class 类型的时候,得到的是一个代号形式的类,其类名只是系统自动生成的一个编号类。
一般情况下使用这个方法的时候有两种:在程序运行的过程中,我们可以通过获取两个对象的类型从而判断某两个对象是否是同一个类;或者,是为了反射。反射是 Java 提供的一种功能,通过反射,我们可以无视 Java 的一些限制访问机制,直接使用某个类的私有变量或私有方法。而进行反射的一般步骤就是,首先通过对象得到它所对应的类型,然后通过 Class 类提供的一些方法得到对应的变量或者方法,然后再通过这些 Field 类和 Method 类直接访问某个对象的某个变量或者方法,而不是通过一般的通过.操作符。反射相比较于一般情况下通过.操作符的调用机制来说,它一方面能够在运行时更加自由地操控对象的属性,另一方面,它能够无视 Java 的封装机制使用到一些在正常情况下无法使用的功能。
hashCode()
调用一个对象的hashCode()方法能够得到一个整形数值,这个整形数值就叫做对象的 hashCode ,一般来说 hashCode 会用于一些哈希表的实现,如 Java 中的 HashMap ,另外,对于hashCode()这个方法来说,有以下几个要求:
- 同一个对象多次调用
hashCode()方法时的结果必须相同 - 如果两个对象在
equals()方法看来是相等的,那么它们的 hashCode 也必须相同 - 如果两个对象在
equals()方法看来是不相等的,那么它们的 hashCode 并非必须不同
简而言之,就是hashCode()需要是对象恒定的,且与equals()在相同方面保持一致。对于第一个条件很好理解,如果同一个对象在不同的时刻拥有不同的 hash 值,那么那些基于 hash 值的数据结构如 HashMap 就无法正常工作了,这就违反了hashCode()存在的意义。其实第二个条件也是因为类似的原因,我们通过equals()方法来判断两个对象是否是相等的,以 HashMap 为例,存放一个键值对的时候,首先通过取 key 的 hash 值来决定它应该放在哈希表的哪个位置上,然后取值的时候也是通过取 key 的 hash 值到达对应的位置再继续寻找。那么如果出现两个 key 的在equals()看来相同,也就是它们被认为是相等的对象,但是 hash 值却不同的话,就可能出现存值的时候通过 hash 值存在了 i 位置上,但是取值的时候因为 hash 值不同而到 j 位置上取了,那么此时虽然本来能够取到值却没有取到的情况,所以,为了防止这种情况的发生,对于所有相等的对象,其 hash 值也就必须也是相同的才行。
equals()
equals()方法就是用来判断两个对象是否相等,如果我们有需要,我们可以重写某个类的equals()方法自定义两个对象在什么时候可以看作是相等的。关于equals()方法的实现也有几个条件:
- 自反性:
x.equals(x)必须返回 true - 对称性:如果有
x.equals(y),那么必须有y.equals(x) - 传递性:如果有
x.equals(y) = ture和y.equals(z) = true,那么必须有x.equals(z) = true - 恒定性:对于两个确定的对象 x y ,在任何时候
x.equals(y)的结果一定是相同的 - 对于任何一个不为空的对象 x ,
x.equals(null)必须返回 false
除了我们判断两个对象是否相等使用equals()方法之外,一些数据结构也需要使用这个方法,如 HashMap ,在哈希表中虽然要先根据 key 的 hashCode 决定索引位置,但是之后的查找还是要根据equals()方法判断 key 是否相等以判断这个值是否就是我们要取的。
所以在很多时候hashCode()和equals()方法都是配合使用的,这也就导致如果我们给某个类重写了其中的某一个方法后,还要重写另一个方法,以满足上述hashCode()的条件。
clone()
clone()方法就是用于一个对象的复制,首先,虽然 Object 类中有一个clone()方法,但是这是一个 protected 的方法,意味着如果在某个类中什么都不做的话,是不能直接使用这个方法的,如果要让某个类能够使用这个方法,在类里面就需要重写clone()方法,如下:
class A {
@Override
protected Object clone() {
return super.clone();
}
}
不仅如此,关于这个方法的使用还有一个要求,某个类如果要使用clone()方法进行自身复制,就必须实现Cloneable接口,否则就会抛出CloneNotSupportedException异常,但是这个接口本身并没有任何方法,真正的要实现复制还是要靠重写父类的clone()方法,接口只是起了标识作用。
如果要复制一个类,那么必然会伴随着类的成员变量的复制,Java 中的数据类型有两种:基本类型和自定义类。基本类型就是如 int、double 类型,所有的自定义类都是基于这些基本类型构成的,另外,基本类型与自定义类有不同之处:自定义类的使用一般包含两部分,引用和实例,其中引用存放于栈中,而实例存放于堆中,相比之下基本类型就是直接存于栈中的,它们没有引用和实例的区分。那么对于clone()方法来说,关于一个类内部成员变量的复制就有了需要考虑的问题,它是对引用的复制还是对实例的复制?
如果是对引用的复制的话,那么就会出现复制之后的对象与原始的对象共用同一个成员变量的情况:在复制对象中对成员变量所做的更改也会出现在原始对象的成员变量中,因为它们引用的是堆中的同一个实例,这就是浅拷贝。如下的例子:
public class A implements Cloneable{
private B b = new B();
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class B implements Cloneable{
int a = 1;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
在大部分的情况下,我们需要复制一个对象的时候,也希望能够复制这个对象的所有成员变量的实例,而不是复制引用,这是默认的clone()方法不能提供的,要实现对象的成员变量的实例复制,也就是深拷贝,需要对默认的clone()方法做一些更改,而这些更改就是手动增加对成员变量的复制:
public class A implements Cloneable{
private B b = new B();
@Override
protected Object clone() throws CloneNotSupportedException {
A a = (A) super.clone();
a.b = (B) b.clone();
return a;
}
}
public class B implements Cloneable{
int a = 1;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
如上,对A的clone()方法的修改,如果默认的不能完成对 b 的复制,那就手动增加一条语句完成对 b 的实例复制,同理,如果在B类中也有非基本类型的成员变量的话,那么也需要对B类的clone()方法作出修改,以此类推,便可以完成任何一个类的深拷贝。
toString()
这个方法会将对象以字符串的形式展示出来,一般情况下是为了让我们能够更直观地“看到”这个对象,这个方法有一个默认实现:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
一般情况下,使用这个方法是为了能够让我们在调试或者测试程序的时候更直观地看到对象中包含的数据,所以此时会重写这个方法,如 Java 中的 List Map 等数据结构,都通过重写这个方法,使得我们直接打印 List 的时候,不是像上面那个看到这个 List 对象的类名和 hashCode ,而是看到类似于[1,2,3,4]这种结构的结果,从而使得 List 中的数据显而易见。
notify() & notifyAll()
当调用某个对象的notify()方法的时候,系统就会将因为调用了这个对象的wait()方法而进入堵塞的线程唤醒,使得这些被堵塞的线程可以继续执行。有时可能有不止一个线程调用了wait()方法,但是notify()方法只能根据系统的调度随机唤醒一个线程,我们无法控制,而对应的notifyAll()方法则是唤醒所有调用了wait()方法的线程,各线程再争夺对象的锁。
wait()
这类方法有三种重载,不过最终都会调用wait(long)这个 native 方法,基本功能就是使当前线程等待一段时间之后再接着执行,期间如果有别的线程调用了notify()方法,就会唤醒正在等待的线程,使之接着往下执行。
调用某个对象的wait()方法的时候,当前线程需要有这个对象的锁,否则的话就会抛出IllegalMonitorStateException异常,一般情况下是使用关键字synchronized可以获取锁。调用了wait()方法之后当前线程就会进入堵塞状态并释放对象的锁,这时其他需要当前对象锁的线程就可以进入,并且这个线程可以通过调用notify()方法使这个被堵塞的线程进入等待锁状态,直到当前正在运行的线程释放了锁之后,这个线程就可以再获取对象锁然后接着执行wait()方法后面的语句了。
wait()和notify()方法是一对对应的方法,一个负责堵塞线程,一个负责唤醒线程,与之相关的还有 Thread 类的几个方法如sleep()、join()等,完成对线程的协调管理,如实现消费者-生产者模式等,都可以使用这一对方法实现。
finalize()
理论上来说,finalize()方法会在垃圾回收机制回收当前对象之前调用,开发者可以重写某个类的这个方法,完成一些类似于资源释放的问题。但是事实却并不如此,由于某些原因,这些已经被认为需要收集的对象,可能会重新成为活跃的对象,从而打断回收过程。并且,Java 并不保证finalize()方法一定会在对象被回收之前调用,但能够保证这个方法不会持有锁,当因为某些原因这个方法抛出异常的时候,Java 会忽略异常并终止这个方法的执行。所以由于以上种种原因,从 Java 9 开始,就讲finalize()方法标识成了Deprecated,不建议开发者继续使用这个方法完成某些功能。
同时 Java 还为释放资源等需要推荐了别的方式,如使用弱引用引用某个对象,能够使被引用的对象不会因为弱引用的存在而不能被回收,配合AutoCloseable接口,Cleaner类实现更加灵活、更加高效的资源释放的功能。