理解Java的Object类

写在前面的话

我在 <了解Java的内存模型> 的这篇博文中写道: 要想玩转Java必须建立起Java的内存模型, 但是要想做到这一步, 必须要建立对Object的认识,
在我体系化自己所学知识的过程,也希望能够让你也建立对Java的Object类型比较宏观的认识.

Object类

Java语言不同于C++语言,是一种单根继承结构语言,也就是说,Java中所有的类都有一个共同的祖先。这个祖先就是Object类。

下图就是全文的焦点,Object类的内存模型
理解Java的Object类

理解markword

我们知道,Java对象存储在堆(Heap)内存。
那么一个Java对象到底包含什么呢?概括起来分为对象头、对象体和对齐字节。
当然作为祖先的Object 自然少不了这种特质.
用一张图来进行表述:
理解Java的Object类

对象的几个部分的作用:

1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode

2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例; (这部分在Java内存图中直接使用箭头进行标注)

3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

5.对齐字是为了减少堆内存的碎片空间(不一定准确)。

为了简化,没有在Object内存类型中完全体现出来



理解Class类型的反射类指针class

理解Class类型的超类指针super

很显然所有类都是继承于Object
超类指针自然指向 null

理解ITabl eoffsetintry(接口偏移量表指针)

理解IT abl elethodintry(接口方法表指针)

以上这四个后序更新



理解VirtualTable(虚方法表)

关于虚方法参见 博文 理解Java的继承与多态重要概念
这里主要介绍这五个虚方法,对应了0~4 一共五个槽,在出现继承的时候,子类肯定要先拷贝这五个虚方法到自己0到4的虚方法表的超类的虚方法前五个槽中,接着才能够添加其他的内容

public boolean equals(Object obj)

“==”运算符判断两个引用是否指向同一个对象。
对于Object类的equals()方法来说,它判断调用equals()方法的引用于传进来的引用是否一致,即这两个引用是否指向的是同一个对象。

Object类中的equals()方法如下:

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

即Object类中的equals()方法等价于==。

只有当继承Object的类 覆写(override) 了equals()方法之后,继承类实现了用equals()方法比较两个对象是否相等,才可以说equals()方法与==的不同。

equals()方法需要具有如下特点:

  • 自反性(reflexive):任何非空引用x,x.equals(x)返回为true。
  • 对称性(symmetric):任何非空引用x和y,x.equals(y)返回true当且仅当y.equals(x)返回true。
  • 传递性(transitive):任何非空引用x和y,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)返回true。
  • 一致性(consistent):两个非空引用x和y,x.equals(y)的多次调用应该保持一致的结果,(前提条件是在多次比较之间没有修改x和y用于比较的相关信息)。
  • 约定:对于任何非空引用x,x.equals(null)应该返回为false。

并且覆写equals()方法时,应该同时覆写hashCode()方法,反之亦然。

public String tostring()

JDK文档解释为:Returns a string representation of the object.

当打印引用,如调用System.out.println()时,会自动调用对象的toString()方法,打印出引用所指的对象的toString()方法的返回值,因为每个类都直接或间接地继承自Object,因此每个类都有toString()方法。

Object类中的toString()方法定义如下:

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

public native int hashCode()

JDK文档的解释为:Returns a hash code value for the object.

当你覆写(override)了equals()方法之后,必须也覆写hashCode()方法,反之亦然。
这个方法返回一个整型值(hash code value),如果两个对象被equals()方法判断为相等,那么它们就应该拥有同样的hash code。

Object类的hashCode()方法为不同的对象返回不同的值Object类的hashCode值表示的是对象的地址。

hashCode需要满足的条件如下:

  1. 在Java应用的一次执行过程中,如果对象用于equals比较的信息没有被修改,那么同一个对象多次调用hashCode()方法应该返回同一个整型值。应用的多次执行中,这个值不需要保持一致,即每次执行都是保持着各自不同的值。
  2. 如果equals()判断两个对象相等,那么它们的hashCode()方法应该返回同样的值。
  3. 并没有强制要求如果equals()判断两个对象不相等,那么它们的hashCode()方法就应该返回不同的值。

即,两个对象用equals()方法比较返回false,它们的hashCode可以相同也可以不同。但是,应该意识到,为两个不相等的对象产生两个不同的hashCode可以改善哈希表的性能。

哈希码产生的依据:哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同。也有相同的情况,看程序员如何写哈希码的算法。


在Java中,哈希码代表对象的特征。
例如对象 String str1 = “aa”, str1.hashCode= 3104
String str2 = “bb”, str2.hashCode= 3106
String str3 = “aa”, str3.hashCode= 3104
根据HashCode由此可得出str1!=str2,str1==str3
下面给出几个常用的哈希码的算法。
1、Object类的hashCode.返回对象的内存地址经过处理后的结构,由于每个对象的内存地址都不一样,所以哈希码也不一样。
2、String类的hashCode.根据String类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串所在的堆空间相同,返回的哈希码也相同。
3、Integer类,返回的哈希码就是Integer对象里所包含的那个整数的数值,例如Integer i1=new Integer(100),i1.hashCode的值就是100 。由此可见,2个一样大小的Integer对象,返回的哈希码也一样。


protected void finalize()

在Object中,这个方法定义如下:

    protected void finalize() throws Throwable { }

在对象中我们可以重定义这个方法。在这个方法中可以释放各种资源。
在<Java编程思想>中写了这些内容,我觉得很好,在这里摘录下来::

java提供finalize()方法,垃圾回收器准备释放内存的时候,会先调用finalize()。

   (1).对象不一定会被回收。

   (2).垃圾回收不是析构函数。

   (3).垃圾回收只与内存有关。

   (4).垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的。

有时当撤消一个对象时,需要完成一些操作。例如,如果一个对象正在处理的是非Java 资源,如文件句柄或window 字符字体,这时你要确认在一个对象被撤消以前要保证这些资源被释放。为处理这样的状况,Java 提供了被称为收尾(finalization )的机制。使用该机制你可以定义一些特殊的操作,这些操作在一个对象将要被垃圾回收程序释放时执行。

要给一个类增加收尾(finalizer ),你只要定义finalize ( ) 方法即可. Java 回收该类的一个对象时,就会调用这个方法。在finalize ( )方法中,你要指定在一个对象被撤消前必须执行的操作。垃圾回收周期性地运行,检查对象不再被运行状态引用或间接地通过其他对象引用。就在对象被释放之前,Java 运行系统调用该对象的finalize( ) 方法。

finalize()方法的通用格式如下:

protected void finalize( )
{
// finalization code here
}

其中,关键字protected是防止在该类之外定义的代码访问finalize()标识符。该标识符和其他标识符将在第7章中解释。

理解finalize( ) 正好在垃圾回收以前被调用非常重要。例如当一个对象超出了它的作用域时,finalize( ) 并不被调用。这意味着你不可能知道何时——甚至是否——finalize( ) 被调用。因此,你的程序应该提供其他的方法来释放由对象使用的系统资源,而不能依靠finalize( ) 来完成程序的正常操作。

注意:如果你熟悉C ,那你知道C 允许你为一个类定义一个撤消函数(destructor ),它在对象正好出作用域之前被调用。Java不支持这个想法也不提供撤消函数。finalize() 方法只和撤消函数的功能接近。当你对Java 有丰富经验时,你将看到因为Java使用垃圾回收子系统,几乎没有必要使用撤消函数。


这里我解释一下什么是析构函数:
析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如C++中在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

protected native Object clone()

JDK文档的解释为:Creates and returns a copy of this object.
简单的说,就是clone()函数的用途是用来另存一个当前存在的对象。
Object类中的说明是:

protected native Object clone() throws CloneNotSupportedException;

可以看出
第一Object类的clone()方法是一个native方法,native方法的效率一般来说都是远高于Java中的非native方法. 这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息复制到新对象中,虽然这也实现了clone功能。

(JNI是Java Native Interface的 缩写。查询资料可以得知, 从Java 1.1开始,Java Native Interface (JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少保证本地代码能工作在任何Java 虚拟机实现下。)

第二:Object类中的 clone()方法被protected修饰符修饰. 这也意味着如果要应用 clone()方 法,必须继承Object类,在 Java中所有的类是缺省继承 Object类的,也就不用关心这点了。然后重载 clone()方法。还有一点要考虑的是为了让其它类能调用这个 clone类的 clone()方法,重载之后要把 clone()方法的属性设置为 public。
第三:Object.clone()方法返回一个Object对象. 我们必须进行强制类型转换才能得到我们需要的类型。

这个方法比较特殊:
首先,使用这个方法的类必须实现java.lang.Cloneable接口,否则会抛出CloneNotSupportedException异常。
Cloneable接口中不包含任何方法,
没骗你,真的是空的

public interface Cloneable { 
}

实现它时只要在类声明中加上implements语句即可, 那么我们为什么要实现Cloneable接口呢?其实Cloneable接口仅仅是一个标志,而且这个标志也仅仅是针对 Object类中 clone()方法的,如果 clone 类没有实现 Cloneable 接口,并调用了 Object 的 clone() 方法(也就是调用了 super.Clone() 方法),那么Object 的 clone() 方法就会抛出 CloneNotSupportedException 异常。

第二个比较特殊的地方在于这个方法是protected修饰的,覆写clone()方法的时候需要写成public,才能让类外部的代码调用。

借鉴于java.lang.Object.clone()分析
介绍protected native Object clone() 一部分内容选自这里,这篇文章介绍的非常深刻


理解接口的虚方法

暂时先不写

理解本类的实方法

关于实方法,有的能够继承,有的

public final native Class<>getclass()

getClass方法是一个final方法,可以继承但是不允许子类重写,并且也是一个native方法。
后面我会写一篇关于native关键字的文章, 这个东西解决了我当初学java困惑一个月的东西

它返回当前运行时对象的Class对象,注意这里是运行时,比如以下代码中n是一个Number类型的实例,但是java中数值默认是Integer类型,所以getClass方法返回的是java.lang.Integer:

"str".getClass() // class java.lang.String
"str".getClass == String.class // true
Number n = 0;
Class<? extends Number> c = n.getClass(); // class java.lang.Integer

下面的内容参考Java根类Object的方法说明

public final native void wait (long timeout)

public final native void wait(long timeout) throws InterruptedException;

wait(long timeout)方法同样是一个native方法,并且也是final的,不允许子类重写。

wait方法会让当前线程等待直到另外一个线程调用对象的notify或notifyAll方法,或者超过参数设置的timeout超时时间。

跟notify和notifyAll方法一样,当前线程必须是此对象的监视器所有者,否则还是会发生IllegalMonitorStateException异常。

wait方法会让当前线程(我们先叫做线程T)将其自身放置在对象的等待集中,并且放弃该对象上的所有同步要求。出于线程调度目的,线程T是不可用并处于休眠状态,直到发生以下四件事中的任意一件:

  1. 其他某个线程调用此对象的notify方法,并且线程T碰巧被任选为被唤醒的线程
  2. 其他某个线程调用此对象的notifyAll方法
  3. 其他某个线程调用Thread.interrupt方法中断线程T
  4. 时间到了参数设置的超时时间。如果timeout参数为0,则不会超时,会一直进行等待

所以可以理解wait方法相当于放弃了当前线程对对象监视器的所有者(也就是说释放了对象的锁)

之后,线程T会被等待集中被移除,并且重新进行线程调度。然后,该线程以常规方式与其他线程竞争,以获得在该对象上同步的权利;一旦获得对该对象的控制权,该对象上的所有其同步声明都将被恢复到以前的状态,这就是调用wait方法时的情况。然后,线程T从wait方法的调用中返回。所以,从wait方法返回时,该对象和线程T的同步状态与调用wait方法时的情况完全相同。

在没有被通知、中断或超时的情况下,线程还可以唤醒一个所谓的虚假唤醒 (spurious wakeup)。虽然这种情况在实践中很少发生,但是应用程序必须通过以下方式防止其发生,即对应该导致该线程被提醒的条件进行测试,如果不满足该条件,则继续等待。换句话说,等待应总是发生在循环中,如下面的示例:

synchronized (obj) {
    while (<condition does not hold>)
        obj.wait(timeout);
        ... // Perform action appropriate to condition
}

如果当前线程在等待之前或在等待时被任何线程中断,则会抛出InterruptedException异常。在按上述形式恢复此对象的锁定状态时才会抛出此异常。


public final void wait(long timeout,int nanos)

查看源代码:

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(long timeout)方法类似,多了一个nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

需要注意的是 wait(0, 0)和wait(0)效果是一样的,即一直等待。

public final void wait()

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

跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。

以下这段代码直接调用wait方法会发生IllegalMonitorStateException异常,这是因为调用wait方法需要当前线程是对象监视器的所有者:

Factory factory = new Factory();
factory.wait();

#### public final native void notify() notify方法是一个native方法,并且也是final的,不允许子类重写。

唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果所有的线程都在此对象上等待,那么只会选择一个线程。选择是任意性的,并在对实现做出决定时发生。一个线程在对象监视器上等待可以调用wait方法。

直到当前线程放弃对象上的锁之后,被唤醒的线程才可以继续处理。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。

notify方法只能被作为此对象监视器的所有者的线程来调用。一个线程要想成为对象监视器的所有者,可以使用以下3种方法:

  • 执行对象的同步实例方法
  • 使用synchronized内置锁
  • 对于Class类型的对象,执行同步静态方法

一次只能有一个线程拥有对象的监视器。

如果当前线程不是此对象监视器的所有者的话会抛出IllegalMonitorStateException异常

注意点:因为notify只能在拥有对象监视器的所有者线程中调用,否则会抛出IllegalMonitorStateException异常

public final native void notifyAll()

跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

同样,如果当前线程不是对象监视器的所有者,那么调用notifyAll同样会发生IllegalMonitorStateException异常。

以下这段代码直接调用notify或者notifyAll方法会发生IllegalMonitorStateException异常,这是因为调用这两个方法需要当前线程是对象监视器的所有者:

Factory factory = new Factory();
factory.notify();
factory.notifyAll();

内容后面还会进行补充,有什么好的想法,可以说说哦,还请多多指教呐!!!