Java是单根继承的,每个类都继承自这个超类,那这个根是谁呢?
我们都知道是Object类,不过它的出镜率真的很低。
问题一:我们平常定义一个类的时候为什么不需要写extends Object?
如果没有显式继承自其它非Object类,编译器默认会让这个类继承成Object类。这一点可以通过反编译.class文件确认。
问题二:Object类有哪些属性或者成员方法?
属性是一个都没有,方法倒是有好几个。
1、getClass
源代码:
public final native Class<?> getClass();
复制代码
getClass
方法用于获取对象的运行时类,在反射的时候用的比较多。
注意到这个方法被final和native修饰,那说明
- 不能覆写
- 实现在C/C++层
返回的Class对象就是表示类静态同步方法锁定的对象。
2、hashCode
源代码:
public native int hashCode();
复制代码
hashCode
方法用户获取对象的hash值。
在强烈依赖hashCode的地方是必须的。比如HashMap中对key进行hash计算,其实是利用key的hashCode。
这个方法的实现依然在native层,具体实现暂时不得而知。
3、equals
源代码:
public boolean equals(Object obj) {
return (this == obj);
}
复制代码
equals方法用于比较两个对象是否相等。
默认的实现就是通过比较引用来判断是不是相等。 这种实现比较简单粗暴,首先引用相等就是同一个对象,肯定是相等的。但是在实际应用中引用不等,对象未必不相等。比如Integer类的版本:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
复制代码
注意:覆写equals方法之后,一定要覆写hashCode!!!
为什么?
首先这个是Object规范中要求的,两个相等的实例必须有相同的hashCode。那我们探究一下根本原因。
以HashMap为例,上面提到对key进行hash,其实是你用了key的hashCode。HashMap中判断两个key是否相同,除了equals还要判断hashCode是否一致。如果没有覆写,本来我们本意认为是同一个key的,现在变成了两个,这个影响可能很小但也可能是灾难性的。
4、clone
源代码:
protected native Object clone() throws CloneNotSupportedException;
复制代码
这个方法就是创建并返回一个对象的拷贝。
这个方法有三个值得注意的点:
- 方式是native实现信息的拷贝。
- 方法是protected,外部没法调用。
- 如果没有实现Cloneable接口,调用 的时候会抛出CloneNotSupportedException异常。
如果外部需要调用clone方法,要么通过反射,要么将它覆写成public方法。
这个Cloneable接口中没有定义任何方法,所以实现Cloneable接口没有别的作用,就代表具备了使用clone方法的权利。
官方文档上有这样一句话:
The precise meaning of "copy" may depend on the class of the object
意思拷贝的实际效果会因类而已。官方文档居然有这种模糊的表述,究竟是何原因? native的实现源码不得而知,但是从文档上看,是先创建一个对象,然后对属性做浅拷贝。那既然是浅拷贝,基本数据类型和复合数据类型拷贝的效果肯定是不一样的,一个是按值,一个是按引用。
我们自己要实现对象拷贝,该怎么做?
下面以ArrayList的clone方法为例:
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
复制代码
- 通过super.clone()由其父类的clone方法创建对象(前提是父类的clone方法实现的没问题)
- 对复杂类型数据进行深拷贝
- 对基本数据类型直接赋值即可
那如果有final成员变量,赋值就不管用了,这里就会出现了不可调和的矛盾,这里需要注意。
似乎final成员变量的问题可以通过构造函数解决,但是《Effective Java》中提到:
如果Cloneable接口是要对某个类起到作用,类和它的所有超类都必须遵守一个相当复杂的,不可实施的,并且基本没有文档说明的协议。由此得到一种语言之外的(extralingustic)机制:无需调用构造器就可以创建对象。
5、toString
源代码:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
复制代码
toString方法就是把对象用字符串的形式表示,这个在日志中使用较多,当然也可以是类型转换,比如Integer类。
默认的实现其实没有什么用,"类名@hashCode",直接打印对象输出就是这样的格式。
官方文档建议最好覆写这个方法,返回一些有价值的、可读性强的信息。
6、notify
源代码:
public final native void notify();
复制代码
notify方法就是唤醒等待一个的线程。
那如果有多个等待的线程怎么办?别慌,其实是会任意挑一个线程的,选择哪个线程取决于操作系统对多线程管理的实现。
notify之后,被唤醒的线程并不能立即运行,必须等到当前线程释放了锁以后。
调用notify方法的线程必须持有对象锁,那持有对象要通过如下三种途径:
- 调用该对象的同步实例方法
- 进入该对象的同步代码块
- 调用该对象的同步静态方法
7、notifyAll
源代码:
public final native void notifyAll();
复制代码
notifyAll和notify的区别就是notifyAll一次性唤醒所有等待的线程。
8、wait
源代码:
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
复制代码
notify的前提是有等待的线程,那线程为什么会处于等待状态,什么事情也做不了,那是因为wait方法。
wait方法重载了三个版本,主要分为有超时和没有超时。有超时的,时间一到就解除,除非中间被唤醒;没有超时的那就一直等待,直到被唤醒。
处于等待状态的线程,除了被唤醒、超时解除以外,还有两种方式,一种是常见的被其他线程中断(interrupt),另外一种是“虚假唤醒”。
至于这个“虚假唤醒”是啥,我现在还是没能很好的理解,需要继续研究。
9、finalize
源代码:
protected void finalize() throws Throwable { }
复制代码
finalize是GC准备回收对象的时候调用来执行清理工作的。
很遗憾的是,Java语言不能保证finalize及时执行,也不能保证它会执行。
那能保证什么?
保证调用finalize之后,线程持有的用户可见的同步锁都被释放掉。还有如果finalize方法执行过程中发生了异常,异常会被忽略,然后流程中止。
既然不可靠,那这个方法就比较鸡肋。如果我们真的需要清理一些资源,比如打开的文件, 或者其他流,需要关闭。我们可以使用try-finally来解决。