Object类是所有类的基类,所有类都默认隐式的继承Object类,该类的背后是面对对象思想的体现。
Object类位于java.lang包中,lang包里包含了java最核心和最重要的类,在编译时会自动导入,下面让我们了解Object有哪些方法和属性吧!
(1)public Objict();
Java中规定:对于任意类都必须含有构造函数,如果用户没有自定义,会由于继承Object类的原因默认存在一个空参的构造函数。通常情况下我们通过A a=new A(args...)
来创建对象,其中A是类名,A(args…)即该类中对应的构造函数。
我们发现Object中并没有构造函数,但其实构造函数是隐式存在,我们还要思考一个问题,构造函数的访问权限一定是public的吗?
答案是否定的。前面提到我们通常通过调用构造函数来创建对象,但是我们还有其他创建对象的方法,例如:反射或clone,所以我们可以说构造函数的权限并非一定需要是public。
(2)private static native void registerNative();
在刚开始阅读JDK源码的时候,我们需要关注一个关键字native
,这个关键字的要点如下:
- JNI:java native interface,即java本地接口
- 无方法体:被native修饰的方法不需要在java里实现方法体
- 底层操作:java进行底层硬件层面的操作能力很弱,于是我们通过JNI让例如C/C++实现我们的方法体,然后帮助java进行底层硬件层面的操作,具体实现可以自个去查阅
对于registerNative()
这个方法本身,类似于中介,它的作用是将C/C++实现的方法体,搬运到被native修饰的方法里面,使得该方法能够被使用,方法作用过程如下:
我们发现该方法的修饰符是private且并没有运行,那么这个方法是如何被使用的?实际上从Object类的源码我们发现该方法的下面紧邻的一个静态代码块:
private static native void registerNatives();
static {
registerNatives();
}
(3)protected native Object clone();
该方法的作用克隆一个调用clone()
方法的对象,克隆对象独立于原对象,是两个方法属性都相同的不同的对象,拥有不同的堆地址,该方法的返回值是指向克隆对象地址的一个变量。
举个栗子:
public class CloneTest {
public static void main(String[] args) {
Object o = new Object();
Object o1 = (CloneTest) o.clone();
}
}
很悲催的是,这段代码报错了:Error:java: clone() 在 java.lang.Object 中是 protected 访问控制
,怎么会呢?protected的访问权限是:在同一个包内或者不同包的子类可以访问,但是任何类都继承Object类,不就符合了不同包子类这个条件吗?
实际上很多人都误会了这句话的运用概念,它并不是说在子类中可以用父类的引用访问protected修饰符,正确的逻辑应该是在同一个包内或者不同包的子类的引用可以访问,将代码改成下面就行了:
public class CloneTest {
public static void main(String[] args) {
CloneTest o = new CloneTest();
CloneTest o1 = null;
try {
o1 = (CloneTest) o.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println(o.equals(o1));
}
}
输出如下:
alse
java.lang.CloneNotSupportedException: CloneTest
at java.lang.Object.clone(Native Method)
从输出结果我们可以确定克隆对象与原对象的地址并不相同,换句话来说就是开辟了一块堆空间来储存克隆对象,但是输出的异常信息:java.lang.CloneNotSupportedException
又是怎么回事呢?
原来在java里定义了这方法的语法规范:clone()的正确调用是需要实现Cloneable接口,如果没有实现Cloneable接口,则会抛出CloneNotSupportedException异常,于是将代码修改成如下即可:
public class CloneTest implements Cloneable{
public static void main(String[] args) {
CloneTest o = new CloneTest();
CloneTest o1 = null;
try {
o1 = (CloneTest) o.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println(o.equals(o1));
}
}
Cloneable和Serializable一样都是标记型接口,它们内部都没有方法和属性,前者用来标记该类可以调用clone()方法,后者用来标记该类可以被可序列化。
(4)public final native Class<?> getClass();
该方法的作用返回是调用者的类对象,类对象对类的方法和属性都进行解析封装,在反射中我们会经常使用。
(5)public boolean equals(Object obj);
该方法在日常开发中使用率极高,用于判断两个对象是否相等,常常与==进行比较辨析:
- ==:可用于基础数据类型和引用数据类型的比较,前者比较的是值,后者比较的是地址
- equals:只能用于引用类型的比较,比较的是地址
不妨来看看在Object中equals()方法的实现:
public boolean equals(Object obj) {
return (this == obj);
}
我们发现Object类中的equals()方法判断也是通过==来实现,那么我们猜想这个方法是不是冗余呢?
实际上存在即合理,在某种业务场景下,我们想要实现对引用类型的比较是通过比较值来判断的,这时使用==或原始的equals()方法无疑都是行不通的,那么我们就可以重写equals()方法,达到我们的需求,例如String类中就已经重写了equals()方法,因此两个String对象调用equals()方法进行比较时,实际比较的是它们的储存的字符串。
但是重写equals()方法时我们需要注意官方的一个说明:一旦重写此方法,通常需要同时重写hashCode()方法,以维护 hashCode() 方法的常规协定,该协定声明相等的对象必须具有相等的哈希码。如果重写equals()方法却不重写hashCode()方法,可能会存在对象相等而哈希值不相等的情况,最好避免这种情况的发生。
(6)public native int hashCode();
hashCode()方法的作用是返回调用者的哈希值,在官方中具有以下规范:
- 程序运行期间,同一对象多次调用hashCode()方法得到的哈希值一定相同,在程序多次运行中,同一对象的哈希值不必保持一致
- 两个对象相同,那么这两个对象的哈希值相同
- 两个对象的哈希值相同,两个对象不一定相同
那么这个哈希值的计算是如何得到的呢?
我们可以看到这个方法是native方法,因此无法直接看到用C/C++实现的方法体,有兴趣的可以看看:https://blog.youkuaiyun.com/xusiwei1236/article/details/45152201,这篇博客详细探究了hashCode()方法的原生实现方式,从里面可以知道:原生方法中实现5种计算hash值的方法,其中有一种是通过计算对象的地址得到hash值,因此我们不能笼统的说哈希值就是通过计算对象地址得到的。
最后面临一个很严峻的问题,hashCode()方法有什么用?比较对象?已经有equals()方法了呀!实际上该方法的主要作用是为了提高哈希表的性能,因为哈希表就是基于哈希值判断元素位置的呀!
以Set集合举例,我们都知道Set集合不允许存储重复元素,那么它底层是如何判断加入元素是否为重复元素的呢?
最简单的想法就是,将准备加入的元素和Set里元素进行一一遍历对比,这种做法的时间复杂度是O(n),作为程序猿的我们怎么能允许这种糟糕情况存在呢?
于是乎聪明的程序猿们就采用了空间换时间的方案,用hashCode()方法进行元素比较,根据得到哈希值来确定元素的位置,只要确定哈希值对应的位置没有元素就可以将新元素插入,这种方案的时间复杂度是O(1),大概逻辑如图:
实际上Set的内部是通过Map来实现的,通过Map的key不重复性进行去重。
(7)public String toString();
该方法用于返回调用对象的字符串表示,那么其内部如何实现将对象转化成字符串表示呢?首先我们来看看方法原型:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
由上面代码我们可以知道对象的所谓字符串表示是指:对象的包名类名@对象哈希值的16进制,举个栗子:
public class Test {
public static void main(String[] args) {
Object o=new Object();
System.out.println(o);
//输出java.lang.Object@677327b6
}
}
于是我们结合hashCode()方法的官方规范可以得到这样的结论:同类型的不同对象, 调用toString()方法的返回值可能相同。
延伸的问题来了,很多人都知道打印对象实际就是打印对象的toString()方法:
System.out.println(o);
System.out.println(o.toString());
但却不知道其内部是如何实现的,甚至不了解System.out.println()
这句代码进行了哪些执行过程,我觉得作为一名程序猿应该心存对这些技术细节的探究精神,一张图了解执行过程:
由上图不难发现System.out.println()
实际经历了3个过程:
- 通过System.out调用了System中PrinStream的静态变量out
- 通过变量out调用PrinStream类中的println()方法
- println()方法体中调用String类中的静态方法valueOf()
至此,对打印对象却隐式调用toString()方法的实现过程已经大致理清,这些都是我们通过源码可以轻易了解的知识。
(8)protected void finalize();
该方法与JVM的GC(垃圾回收)机制有关,当对一个对象进行垃圾回收时,它的finalize()方法会被自动调用,来看下它的方法原型:
protected void finalize() throws Throwable { }
我们发现该方法被定义成空方法体,任何Java类都可以重写Object类的finalize()方法,在该方法中进行清理对象占用资源的逻辑操作:
class Person {
@Override
public void finalize() {
System.out.println("该对象被垃圾回收...");
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p = null;
//通知垃圾回收器强制进行垃圾回收
System.gc();
//输出:该对象被垃圾回收...
}
}
需要注意的是在程序终止前若没有进行垃圾回收,finalize()方法不会被自动调用,例如我们不进行强制垃圾回收,在上面代码中并不会自动执行finalize()方法,这涉及到垃圾回收机制的执行时机,在日后进行JVM的学习中我会全面总结。
(9)public final native void wait(…);
该方法与多线程之间的协作有关,调用wait(…)方法的线程无法争夺CPU的使用权,即无法被执行,这一状态称为等待状态。
处于等待状态的线程会失去同步锁,只能被唤醒或超时或打断后且获得同步锁之后才能进行状态的转换。
举个栗子理解理解:
class MyThread extends Thread {
//重写run方法
public void run() {
while (true) {
System.out.println("线程被执行啦!");
synchronized (this) {
try {
//等待5秒后继续执行
wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
//调用start()方法启动线程
thread.start();
}
}
上面的栗子中,每次循环线程MyThread
都会进入5秒的等待状态,之后超时从等待状态转换成就绪状态继续执行,这里要注意的是诸如whit(…)notify()等方法都需要在同步代码块中使用,不然会报监视器异常:java.lang.IllegalMonitorStateException
。
(10) public final native void notify()/notifyAll();
该方法与多线程之间的协作有关,往往与wait(…)方法配套使用,notify()/notifyAll()方法可以唤醒处于等待状态的线程。
class MyThread extends Thread {
//重写run方法
public void run() {
synchronized (this) {
try {
System.out.println("语句1:线程被执行啦!");
//线程处于等待状态
wait();
System.out.println("语句2:线程被唤醒啦!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
//调用start()方法启动线程
thread.start();
try {
//使线程休眠5秒
thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (thread) {
//唤醒失去锁处于等待状态的thread线程
thread.notify();
}
}
}