JDK源码解读(第一弹:Object)

java.lang.Object类是java中所有类的父类,所有类默认继承Object。这也就意味着,Object类中的所有公有方法也将被任何类所继承。既然是最基本的类,那我们的JDK源码解读就从这个类开始。
(我这里研究的源码都是基于JDK1.8。)

Object类中的方法如下:
Object():构造方法。
registerNatives():装载本地库。
getClass():返回此 Object的运行时类。
hashCode():返回对象的哈希码值。
equals(Object):指示一些其他对象是否等于此。
clone():创建并返回此对象的副本。
toString():返回对象的字符串表示形式。
notify():唤醒正在等待对象监视器的单个线程。
notifyAll():唤醒正在等待对象监视器的所有线程。
wait(long):使当前线程等待,直到另一个线程调用 notify()方法或该对象的 notifyAll()方法,或者指定的时间已过。
wait(long, int):使当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法,或者指定的时间已过。
wait():使当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法。
finalize():当垃圾收集确定不再有对该对象的引用时,在对象上调用该方法。

Object()

类构造器是创建Java对象的途径之一。一个类必须要有一个构造器的存在,如果没有显示声明,那么系统会默认创造一个无参构造器。
在JDK的Object类源码中,我们可以发现并没有看到构造器的,系统会自动添加一个无参构造器。
我们可以通过以下代码构造一个Object类的对象:
Object obj = new Object();

registerNatives()

源码如下:
private static native void registerNatives();

这是一个本地方法。
我们接着往下看,可以看到这样一段代码:

    static {
        registerNatives();
    }

我们知道一个类定义了本地方法后,想要调用操作系统的实现,必须还要装载本地库。上面的这段代码是一个静态代码块,所以在类加载的时候会执行该方法,通过该方法来注册本地方法。

getClass()

接下来来看该方法的源码:
public final native Class<?> getClass();

该方法被声明为public final native方法,这说明该方法无法被重写,且是一个本地方法。
通过API文档说明,我们可以知道该方法的作用是返回一个对象的运行时类,通过这个类对象我们可以获取该运行时类的相关属性和方法。

我们知道Java中还可以通过 类名.class 获取这个类的类对象,我们来比较一下这两种用法有什么区别,来加深对这个方法的理解。
我们写一个父类:
public class Parent {}
再写一个子类:
public class Son extends Parent {}
再来一段测试代码:

public class Test {

    public static void main(String[] args) {
        Parent p = new Son();
        System.out.println(p.getClass());
        System.out.println(Parent.class);
    }
}

我们运行一下,输出如下:

class test.Son
class test.Parent

看到结果,我们就可以知道了,class 是一个类的属性,使用 类名.class 能获取该类编译时的类对象,而 getClass() 是一个类的方法,它能获取该类运行时的类对象。

hashCode()

来看源码:
public native int hashCode();

这也是一个用 native 声明的本地方法,作用是返回对象的散列码,是 int 类型的数值。
通过刚才查看API文档,我们可以知道hashcode的3个约定:
1. 在程序运行时期间,只要对象的变化不会影响equals方法的判断结果,那么,在这个期间无论调用多少次hashCode,都必须返回同一个散列码。
2. 通过equals调用返回true的2个对象的hashCode一定一样。
3. 通过equasl返回false的2个对象的散列码可以一样也可以不一样。

也就是说:
两个对象相等,其hashCode一定相同;
两个对象不相等,其hashCode有可能相同;
hashCode相同的两个对象,不一定相等;
hashCode不相同的两个对象,一定不相等;

那么这个方法有什么用呢?
我们知道在Java 中有几种集合类,其中Set存放的元素是不可重复的。
在Set中,每增加一个元素,我们就通过equals方法判断集合中的每一个元素是否重复。如果集合中有10000个元素,我们新加入一个元素时,那就需要进行10000次equals方法的调用,这显然效率很低。
于是,Java就采用了哈希表来实现。哈希算法也称为散列算法,是将数据依特定算法产生的结果直接指定到一个地址上。这个结果就是由hashCode方法产生。当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了;不相同的话,也就是发生了Hash key相同导致冲突的情况,那么就在这个Hash key的地方产生一个链表,将所有产生相同HashCode的对象放到这个单链表上去,串在一起。这样一来实际调用equals方法的次数就大大降低了。

那也许我们会想,对于不能重复的集合比如Set,为什么不让hashCode对于每个元素都产生唯一的值,相等的元素就产生一样的hashcode,不一样的元素就产生不一样的hashcode,这样不就完全不需要调用equals方法来判断是否相同了吗?
实际上对于元素不是很多的情况下,直接通过hashCode产生唯一的索引值,通过这个索引值能直接找到元素,而且还能判断是否相同,这很好。就像数据库存储的数据的主键或者是rowid一样,我们能通过主键或者rowid直接找到某个元素。但实际上,很难实现hashCode的唯一算法,就算能实现,产生的hashCode码也是非常大的,这会大的超过int所能表示的范围,很占内存空间。

所以,我们在编写hashcode方法时要注意以下几点:
- 不同对象的hash码应该尽量不同,避免hash冲突,也就是算法获得的元素要尽量均匀分布。
- hash 值是int 类型,在Java中占用4个字节,也就是2的32次方,不要溢出。
- 对于Map集合,我们可以选取Java中的基本类型,以及String作为 key,因为它们都按照规范重写了equals方法和hashCode方法。但是如果你用自定义对象作为key,那么一定要重写 equals方法和hashCode方法。

像Integer类,String类等都重写了hashCode方法,我们自定义对象的hashCode方法可以参考这些类来写。

equals(Object)

方法源码如下:

    public boolean equals(Object obj) {
        return (this == obj);
    }

看了源码我们知道,在Object类中,==运算符和equals方法是等价的,都是比较两个对象的引用是否相等。对于我们自定义的一个对象,如果不重写equals方法,那么在比较对象的时候就是调用Object类的equals方法,也就是用==运算符比较两个对象。

这里我们看一下String类中重写equals方法的源码:

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

可以看到,String类定义两个对象相等的标准是字符串内容都相同,而不是引用是否相等。

假如我们有一个自定义的Parent类,里面有两个属性,我们自定义两个对象相等的标尺为Parent对象的两个属性都相等,则对象相等,否则不相等。
那么我们来仿照上面String对equals的重写,我们来重写一下Parent的equals方法:

public class Parent {

    private String name;

    private String age;

    public Parent() {
    }

    public Parent(String name, String age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        // 引用相等那么两个对象当然相等
        if (this == obj) {
            return true;
        }
        // 对象为空则不相等
        if (obj == null) {
            return false;
        }
        // 对象不是Parent类的实例则不相等
        if (!(obj instanceof Parent)) {
            return false;
        }
        Parent otherParent = (Parent) obj;
        if (otherParent.getName().equals(this.getName()) 
            && otherParent.getAge() == this.getAge()) {
            return true;
        }
        return false;
    }

}

然后写一个测试方法:

public class Test {

    public static void main(String[] args) {
        Parent p1 = new Parent("Alice", "20");
        Parent p2 = new Parent("Tom", "21");
        Parent p3 = new Parent("Alice", "20");
        System.out.println(p1.equals(p2));
        System.out.println(p1.equals(p3));
    }
}

看到上面的测试main方法输出,第一个是false,第二个是true,我们觉得这个equals实现的好像没什么问题。
如果我们有个Son类继承了Parent类,Son类也同样重写了equals方法,Son类如下:

public class Son extends Parent {

    private String idNo;

    private String score;

    public Son() {
    }

    public Son(String name, String age, String idNo, String score) {
        super(name, age);
        this.idNo = idNo;
        this.score = score;
    }

    public String getIdNo() {
        return idNo;
    }

    public void setIdNo(String idNo) {
        this.idNo = idNo;
    }

    public String getScore() {
        return score;
    }

    public void setScore(String score) {
        this.score = score;
    }

    @Override
    public boolean equals(Object obj) {
        // 引用相等那么两个对象当然相等
        if (this == obj) {
            return true;
        }
        // 对象为空则不相等
        if (obj == null) {
            return false;
        }
        // 对象不是Son类的实例则不相等
        if (!(obj instanceof Son)) {
            return false;
        }
        Son otherSon = (Son) obj;
        if (otherSon.getIdNo().equals(this.getIdNo()) 
            && otherSon.getScore() == this.getScore()) {
            return true;
        }
        return false;
    }

}

这时候我们再来写一个测试方法:

public class Test {

    public static void main(String[] args) {
        Parent p = new Parent("Alice", "20");
        Son s = new Son("Alice", "20", "001", "95");
        System.out.println(p.equals(s));
        System.out.println(s.equals(p));
    }
}

我们看到第一个输出为true,第二个输出竟然是false!这明显违反了我们上面说的equals的几个约定中的对称性。
细看一下我们发现,问题出现在 instanceof 关键字上,Son是Parent的子类,所以Son instanceof Parent当然是true,但是Parent instanceof Son就自然是false了。
所以,instanceof是做不到对称性的要求的,我们可以使用getClass()方法来代替instanceof运算符,把上面Parent类中使用instanceof的地方改为以下代码:

if (getClass() != obj.getClass()) {
    return false;
}

再次运行测试方法,可以看到输出都是false,满足对称性。

但是我们也要注意,也不是什么时候都要使用getClass,毕竟自定义对象是否相等的标准是由自己定义的。而且使用getClass不符合多态的设计思想。比如一个父类有两个子类,子类中没有自定义相等的概念,也就是说没有重写equals,如果使用getClass实现父类equals方法的重写,那么就不能在两个不同子类的对象进行相等的比较。
所以,如果子类能够拥有自己的相等概念,则根据对称性需求,我们需要采用getClass();如果由父类决定相等的概念,那么就可以使用instanceof,这样可以在不同的子类的对象之间进行相等的比较。

下面给出重写equals方法的最佳实践:
1. 参数命名为otherObject,稍后会将它转换成另一个叫做other的变量。
2. 用==判断比较的两个对象引用是否相等,如果引用相等那么表示是同一个对象,那么当然相等。
3. 如果otherObject为null,直接返回false。
4. 比较this和otherObject是否是同一个类:如果equals的语义在每个子类中有所改变,就使用getClass()判断;如果所有的子类都有统一的定义,那么使用instanceof判断。
5. 将otherObject转换成对应类的变量。
6. 最后对对象的属性进行比较。使用==比较基本类型,使用equals比较对象。如果都相等则返回true,否则返回false。注意如果是在子类中定义equals,则要包含 super.equals(other)。

那么根据刚才的最佳实践,我们来重新写一下上面的Parent类的equals方法:

    @Override
    public boolean equals(Object otherObject) {
        // 用==判断比较的两个对象引用是否相等,如果引用相等那么表示是同一个对象,那么当然相等。
        if (this == otherObject) {
            return true;
        }
        // 如果otherObject为null,直接返回false。
        if (otherObject == null) {
            return false;
        }
        // 比较this和otherObject是否是同一个类
        // 以下两种写法二选一
        // 如果equals的语义在每个子类中有所改变,就使用getClass()判断
        if (getClass() != otherObject.getClass()) {
            return false;
        }

        // 如果所有的子类都有统一的定义,那么使用instanceof判断
        if (!(otherObject instanceof Parent)) {
            return false;
        }

        // 将otherObject转换成对应类的变量。
        Parent otherParent = (Parent) otherObject;

        // 最后对对象的属性进行比较。使用==比较基本类型,使用equals比较对象。如果都相等则返回true,否则返回false。
        if (otherParent.getName().equals(this.getName()) 
            && otherParent.getAge() == this.getAge()) {
            return true;
        }
        return false;
    }

最后再提醒一下不要忘记,API中的最后有一句话,一般来说重写equals方法,就要重写hashCode方法,以便保持hashCode方法的通用约定,即相等的对象必须具有相等的hashcode。

clone()

该方法源码如下:
protected native Object clone() throws CloneNotSupportedException;

其实文档讲的还是蛮清楚的,我们再次强调一下,clone()的正确调用是需要实现Cloneable接口,如果没有实现Cloneable接口,子类直接调用Object类的clone()方法,则会抛出CloneNotSupportedException异常。
来看一下Cloneable的源码:

public interface Cloneable {
}

可以发现,Cloneable接口仅是一个表示接口,它本身不包含任何方法,用来指示Object.clone()可以合法的被子类所调用。

使用clone方法的优点是速度快,而且灵活。速度快是因为clone方法最终会调用Object.clone()方法,这是一个native方法,本质是内存块复制,所以在速度上比使用new创建对象要快。灵活是因为可以在运行时动态的获取对象的类型以及状态,从而创建一个对象。

但是clone创建对象的缺点也很明显:实现深拷贝较为困难,需要整个类继承的所有类以及所有的属性都很好的实现clone方法。另外就是需要处理CloneNotSupportedException异常。

因此,我们如果想使用clone方法的话,需要非常谨慎。事实上,更建议的不是实现Cloneable接口,而是编写拷贝构造器。

toString()

该方法源码如下:

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

打印某个对象时,默认是调用toString方法。不过从源码我们可以看到,默认的toString返回的内容可读性并不好,所以通常都要重写toString,输出需要的实例信息,比如某些属性的值。

notify()

该方法源码如下:
public final native void notify();
这个方法是用于多线程操作的,我们先来看一下同样也是用于多线程的另外几个方法。

notifyAll()

通过查看API,我们知道这个notifyAll()和刚才的notify()差不多,只是notify()是随机唤醒一个等待的线程,而notifyAll()是唤醒正在等待对象监视器的所有线程。

该方法源码如下:
public final native void notifyAll();

wait(long)

方法源码如下:
public final native void wait(long timeout) throws InterruptedException;

wait(long, int)

再来看一下源码:

    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);
    }

wait()

源码如下:

    public final void wait() throws InterruptedException {
        wait(0);
    }

以上这几个方法,notify,notifyAll,wait,都是public final native的,不可以被子类重写,且都是本地方法。这几个方法提供了java线程间等待、挂起等协同机制,是java多线程的基础。这里我们就先大致看一下源码,等后面研究到多线程的时候再来仔细解读它们。

finalize()

源码如下:
protected void finalize() throws Throwable { }

该方法用于垃圾回收,一般由 JVM 自动调用,一般不需要程序员去手动调用该方法。这里我们就只是先看一下API怎么讲,后面细究 JVM 的时候再详细描述。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值