有趣但引人深入思考的Java知识点之Object几个方法
学Java有一个多月了,但是还没有好好的系统的去看API,都是需要什么方法就去学一下。在阿里的直播上看到孤尽老师的直播,感觉这个问题提出的很有趣。之前在学习之中,没有在乎Object类的几个方法。在后面的集合和String的学习中,突然发现hashcode()方法很是重要,而且在封装数据类型Integer中发现比较范围只在-128-127之间,愈发感觉源码的重要性,系统的学习要从底层来,那我就先回去一下下,从顶级父类Object的几个方法看看,感觉在后面很是重要,就我目前学到的序列来说,感觉equals()、hashcode()和toString()方法很重要。
从Java中最基本的类java.lang.Object开始谈起。
Object 是类层次结构的根类。每个类都使用 Object 作为超类。所有对象(包括数组)都实现这个类的方法。
构造方法:
Object()
方法
clone() 创建并返回此对象的一个副本
equals(Object obj) 指示其他某个对象是否与此对象相等
finalize() 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
getClass() 返回此 Object 的运行时类。
hashCode() 返回该对象的哈希码值。
notify() 唤醒在此对象监视器上等待的单个线程。
notifyAll() 唤醒在此对象监视器上等待的所有线程。
toString() 返回该对象的字符串表示。
wait() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
wait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
wait(long timeout, int nanos) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。
源码:
Java.long.Object
/*
* Copyright (c) 1994, 2010, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package java.lang;
/**
* Class {@code Object} is the root of the class hierarchy.
* Every class has {@code Object} as a superclass. All objects,
* including arrays, implement the methods of this class.
*
* @author unascribed
* @see java.lang.Class
* @since JDK1.0
*/
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
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 >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
protected void finalize() throws Throwable { }
}
首先,我们必须要了解的是代码块、成员变量和构造方法之间的先后执行顺序。
Code类:
public class Code {
String str = "我是成员变量。。。";
{
System.out.println("我是代码块。。。");
}
public Code(){
System.out.println("我是构造方法,并执行了" + str);
}
}
TestCode类:
public class Test {
public static void main(String[] args) {
new Code();
}
}
结果:
我是代码块。。。
我是构造方法,并执行了我是成员变量。。。
显而易见,代码块和成员变量在构造方法之前就执行了。
`然后开始说说Object的有趣的几个方法
1. static代码块和native方法
在Java中代码块是在构造器初始化之前进行的,恰在这里也进行了验证。在对象初始化时,自动调用了Object中的代码块,注册了本地方法,这样在后面的native方法执行中,系统就可以调用本地方法了。
static{
registerNatives();
}
其本地方法,具体是用C(C++)在DLL中实现的,然后通过JNI调用。
private static native void registerNatives();
public final native Class<?> getClass();
public native int hashCode();
protected native Object clone() throws CloneNotSupportedException;
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
2. 类构造器Object()
大部分情况中,Java中都是new一个对象,这种途径都是通过类中的构造器完成的。为了体现这一特性,Java中规定:在类定义过程中,对于未定义构造函数的类,默认会有一个无参数的构造函数,作为所有类的基类,Object类自然要反映出此特性,在源码中,未给出Object类构造函数定义,但实际上,此构造函数是存在的。
当然,并不是所有的类都是通过此种方式去构建,也自然的,并不是所有的类构造函数都是public。
3. registerNatives()
private static native void registerNatives();
static {
registerNatives();
}
其主要作用是将C/C++中的方法映射到Java中的native方法,实现方法命名的解耦。函数的执行是在静态代码块中执行的,在类首次进行加载的时候执行。
4. getClass()
public final native Class<?> getClass();
getClass()也是一个native方法,返回的是此Object对象的类对象/运行时类对象Class<>。效果与Object.class相同。该方法被final修饰,不能被重写。
5. clone()
protected native Object clone() throws CloneNotSupportedException;
其是一个被声明为native的方法,因此,我们知道了clone()方法并不是Java的原生方法。clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。
【顺便我们来理解一下protect权限修饰符】
Object a = new Object();
Object b = a.clone();
当写下这样的代码时,在编译时期eclipse就已经提示错误,运行系统给的提示是:
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method clone() from the type Object is not visible
clone()方法不存在。回到Object类中clone()方法的定义,可以看到其被声明为protected,估计问题就在这上面了,protected修饰的属性或方法表示:在同一个包内或者不同包的子类可以访问。显然,Object类与ObjectTest类在不同的包中,但是ObjectTest继承自Object,是Object类的子类,于是,现在却出现子类中通过Object引用不能访问protected方法,原因在于对”不同包中的子类可以访问”没有正确理解。
看这里:
* @return a clone of this instance.
* @exception CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
源码中给的注释写到,必须实现Cloneable接口
public class Test implements Cloneable{
public static void main(String[] args) throws CloneNotSupportedException {
Test a = new Test();
Test b = (Test)a.clone();
}
}
6. equals()
public boolean equals(Object obj) {
return (this == obj);
}
==和equals在Java中经常被使用,区别也是挺大的。前者(==)是变量内容的比较,值应该更为恰当,值相同,就为true。后者(equals)则不同,返回this==obj的值,在对象的初始化中,会在堆和栈中进行开辟空间和存储变量或对象,如下图中:
equals在比较的过程中,实际上比较的是对象所指向的地址,若不是一个对象,则其实不相等的,返回false。
正好用前面的clone(),举个栗子。
public class Test implements Cloneable{
public static void main(String[] args) throws CloneNotSupportedException {
Test a = new Test();
Test b = (Test)a.clone();
System.out.println(a);//默认调用了toString方法打印其hashCode码,不是真实的内存地址
System.out.println(b);//同上
System.out.println("a==b?------>"+a.equals(b));
}
}
结果:
demo1.Test@1774b9b
demo1.Test@104c575
a==b?------>false
其clone()之后,只是复制了对象,而不是重新让对象名换了指向,其是两个完全不同地址的内容相同对象。
在后面的学习中,也有很多地方都是需要重写equals方法,例如我们想创建了两个Student对象,分别为
Student A = new Student("KangKang",12);
Student B = new Student("Kangkang",12);
在调用默认的equlas()方法的时候,系统返回的A指向的堆地址和B指向的堆地址值,显然他们是不用的,但是实际生活中,我们认为二者是相同的。这也就需要我们重写equals()方法,我们需要比较姓名和年龄是不是一样,如果一样,我们认为是同一个对象,返回true。
public boolean equals(Object obj) {
return (this.name == obj.name && this.age == obj.age);
}
这样,就符合了实际生活中的需要了。接下来说说toString,这个方法也是在很多地方需要重写。
7. toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
在实际的开发中,这个toString()大多都被重写了。例如在一个类中,我们需要查看类的属性,我们希望toString()可以打印出其属性,已方便我们来进行调试错误,查看属性。
8. hashCode()
public native int hashCode();
hashCode()方法返回一个整形数值,表示该对象的哈希码值。
以集合类中,以Set为例,当新加一个对象时,需要判断现有集合中是否已经存在与此对象相等的对象,如果没有hashCode()方法,需要将Set进行一次遍历,并逐一用equals()方法判断两个对象是否相等,此种算法时间复杂度为o(n)。通过借助于hasCode方法,先计算出即将新加入对象的哈希码,然后根据哈希算法计算出此对象的位置,直接判断此位置上是否已有对象即可。
在此需要纠正一个理解上的误区:对象的hashCode()返回的不是对象所在的物理内存地址。甚至也不一定是对象的逻辑地址,hashCode()相同的两个对象,不一定相等,换言之,不相等的两个对象,hashCode()返回的哈希码可能相同。
所以,大多的时候,在重写equals()方法后,需要重写hashCode()方法,在孤尽老师的阿里巴巴Java规范书中也是写到了,真的是敲细心。
放出书籍中的截图(建议大家买一本看,真的很好,书籍不大不厚,一本规范,夜晚睡觉看一看,然后再去阿里云大学上考一个阿里巴巴Java规范证书,去年一出来考试,我就考了,还是挺难的,考了三次才过,而且那时候才大二,整本书就只学了基础Java到线程,数据库什么根本不知道,疯狂网上看博客,看了好几天,眼睛都快瞎了。孤尽老师说如果深刻理解了这本书,最低可以到阿里的二面,不过这些我也是可望不可及,悄悄的看着老师说话,哈哈哈哈哈哈,很满足,现在就是慢慢修炼吧,等待老师的下一本书)
敲黑板!在集合处理中,只要重写了equals,就必须重写hashCode,至于原因,老师说的很清楚。
9. finalize()
protected void finalize() throws Throwable { }
Object中定义finalize方法表明Java中每一个对象都将具有finalize这种行为,其具体调用时机在:JVM准备对此对形象所占用的内存空间进行垃圾回收前,将被调用。由此可以看出,此方法并不是由我们主动去调用的(虽然可以主动去调用,此时与其他自定义方法无异)。
finalize方法主要与Java垃圾回收机制有关。
emmmmmmmmm…..深入理解JVM第一遍还没有看完,虽然已经看了垃圾回收机制,但是吧。。。完全没看懂!!!等我看第二遍的时候再回来补上 >_<
10. wait(…) / notify() / notifyAll()
wait():调用此方法所在的当前线程等待,直到在其他线程上调用此方法的主调(某一对象)的notify()/notifyAll()方法。
wait(long timeout)/wait(long timeout, int nanos):调用此方法所在的当前线程等待,直到在其他线程上调用此方法的主调(某一对象)的notisfy()/notisfyAll()方法,或超过指定的超时时间量。
notify()/notifyAll():唤醒在此对象监视器上等待的单个线程/所有线程。
wait(…) / notify() | notifyAll()一般情况下都是配套使用。
emmmmmmmmm…..我还没学到线程,等我学到了线程再回来补上 >_<