jdk1.0
日志

  • 2020-2-11 理清clone方法
  • 2020-2-12 大致理解了registerNativies的作用
  • notify、notifyAll、wait涉及多线程,后续再看

Object类位于java.lang包,Object类是所有类的根类,所有类都有一个超类Object。(equals,hashCode,toString,clone,finalize)都有明确的约定,因为它们被设计成被override的。如果不能遵循这些约定,其它依赖于这些约定的类就无法结合该类一起正常运作。

*一、equals

  1. public boolean equals(Object obj) {
  2. return (this == obj);
  3. }

在Object类中已经实现,比较的是两个对象的引用地址是否相等。

覆盖equals方法的约定:
equals方法实现了等价关系,其属下如下:

  • 自反性:对于任何非null的引用值x,x.equals(x)必须返回true。
  • 对称性:对于任何非null的引用值x和y,当前仅当y.equals(x)返回true时,x.equals(y)必须返回true.
  • 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true.
  • 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false.
  • 对于任何非null的引用值x,x.equals(null)必须返回false

有许多的类,包括所有的集合类在内,都依赖于传递给它们的对象是否遵循了这个约定。何为等价关系呢?不严格的说,是一种操作符,将一组元素划分到其元素与另一个元素等价的分组中,最后每个等价类中的所有元素都必须是可交换的。下面逐一查看以下5个要求。

  • 自反性,第一个要求仅仅说明对象必须等于其自身。很难想象会无意识地违反这一条。假如违背了这一条,然后把该类的实例添加到集合中,该集合的contains方法会告诉你,该集合不包括你刚刚添加的实例。
  • 对称性,第二个要求是说,任何两个对象对于“它们是否相等”的问题都必须保持一致。
  • 传递性,equals约定的第三个要求是,如果一个对象等于第二个对象,而第二个对象等于第三个对象。则第一个对象一定等于第三个对象。
  • 一致性,equals约定的第四个要求是,如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象(或者两个都)被修改了。
  • 非空性,所有的对象都不能等于null。

在进行比较类型时,使用instanceOf还是getClass进行比较?

  • instanceOf在比较子类时返回true,而getClass返回false。

总结实现equals方法的诀窍:

  1. 使用==操作符检查“参数是否为这个对象的引用”。
  2. 使用instanceof操作符检查“参数是否为正确的类型”。
  3. 把参数转换成正确的类型
  4. 对于该类中的每个“关键”属性,检查参数中的属性是否与该对象中对应的属性相匹配。对于float和double类型的比较推荐使用其包装类的compare(float,float)静态方法,因为存在NaN、-0.0f的常量。

该方法一般和hashCode方法一起使用。

*二、hashCode

  1. public native int hashCode();

本地方法,由C/C++语言编写的函数实现。
返回Object对象的哈希值。在每个覆盖了equals方法的类中,都必须覆盖该方法。

:::info hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。 :::

为什么要使用hash值呢?

  • 使用hash值后,我们可以更加快捷的查找,如HashTable、HashMap等,hashCode是用来在散列存储结构中确定对象的地址。
  • 如果两个对象相同,就是适用于equals(Object)方法,那么两个对象的hashCode一定要相同。
  • 如果对象的equals方法被重写,那么hashCode也尽量重写,并且产生hashCode使用的对象,一定和equals方法中使用的一致,否则就会违反上一条原则。
  • 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不适用于equals方法,只能说明这两个对象在散列存储结构中。如HashTable,他们”存放在同一个篮子”

:::info 1.hashcode是用来查找的,如果你学过数据结构就应该知道,在查找和排序这一章有
例如内存中有这样的位置
0 1 2 3 4 5 6 7
而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,如果不用hashcode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。但如果用hashcode那就会使效率提高很多。
我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除8求余数直接找到存放的位置了。

2.但是如果两个类有相同的hashcode怎么办那(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,回答是:可以这样。那么如何判断呢?在这个时候就需要定义 equals了。也就是说,我们先通过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过 equals 来在这个桶里找到我们要的类。那么。重写了equals(),为什么还要重写hashCode()呢?
想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶,光重写equals()有什么用啊 :::

三、toString

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

toString()方法是在Object类里面的方法,返回是字符串:类名+@+哈希值(16进制)

如果在自定类查看到类的详细信息,那么toString方法显得尤为重要,否则我们只能看到类名@哈希值的形式。不足的地方是,一旦指定格式,就必须始终如一地坚持这种格式。

四、getClass


  1. public final native Class<?> getClass();

getClass()方法是用来获取运行时的对象,不是编译类型,当声明对象和.class文件中真正的对象不一致时,该方法会返回.class中的对象。
final修饰,说明getClass()方法是不能被子类进行重写的,不能被重写是为了保证一个子类有多重继承时,其调用getClass()方法与其父类调用getClass()方法的表现是一致的,这也是实现反射的保证。
举个例子,A extends B,如果B重写了getClass方法,返回的Class是B,当A调用时,A没有重写getClass方法,返回的也会是B,而不是真正的实例A,这明显和getClass方法的预期不符。也会造成使用反射获取实例是,获取到的实例是B而不是实例A。
native,java中带有native关键字的方法都是原生方法,是由JVM底层的C来实现,这种方式称为JNI,需要注意一点因为JVM并不只有HotSpot,所以native方法在不同JVM上的表现结果有可能是不一致的。
返回的是Class类型,具体见Class章节。

五、clone

clone()方法创建并返回当前对象的一份拷贝。实现对象中各属性的复制,有一个前提,第一实现Cloneable接口,这是一个标记接口,自身没有方法。第二覆盖Object中的clone方法

Java赋值是复制对象的引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到的。

  1. @Test
  2. public void test(){
  3. Animal p1=new Animal();
  4. p1.setAge(31);
  5. p1.setName("Peter");
  6. Animal p2=p1;
  7. System.out.println(p1==p2);//true
  8. }

内存结构:
image.png

如果创建一个对象的新副本,也就是他们的状态完全一样,以后也可以改变各自的状态,两者互不影响,就需要用clone()方法。

  1. @Data
  2. public class CloneTest implements Cloneable{
  3. private String name;
  4. @Override
  5. protected Object clone() throws CloneNotSupportedException {
  6. return super.clone();
  7. }
  8. public static void main(String[] args) throws CloneNotSupportedException {
  9. CloneTest cloneTest = new CloneTest();
  10. cloneTest.setName("cloneTest");
  11. System.out.println(cloneTest);
  12. CloneTest clone = (CloneTest)cloneTest.clone();
  13. System.out.println(clone);
  14. System.out.println(clone==cloneTest); //false
  15. }
  16. }

该测试用例只有String类型的成员,也由于String的不可变性,使得克隆后的String对象和被克隆的String对象的引用指向不同的地址。如果换成StringBuilder,那克隆和被克隆后的对象都指向同一块StringBuilder的引用。测试用例已经达到了,clone对象和cloneTest对象中的内容是一致的,但是地址的引用是不同的。

debug查看堆栈信息
image.png

内存结构:
image.png
image.png

真的这么简单吗?该包下新添加一个类

  1. @Data
  2. public class InnerClone {
  3. private String type;
  4. }

在CloneTest类中添加一个新添加类的成员。

  1. @Data
  2. public class CloneTest implements Cloneable{
  3. private String name;
  4. private InnerClone innerClone;
  5. @Override
  6. protected Object clone() throws CloneNotSupportedException {
  7. return super.clone();
  8. }
  9. public static void main(String[] args) throws CloneNotSupportedException {
  10. CloneTest cloneTest = new CloneTest();
  11. InnerClone innerClone = new InnerClone();
  12. innerClone.setType("innerClone");
  13. cloneTest.setName("cloneTest");
  14. cloneTest.setInnerClone(innerClone);
  15. System.out.println(cloneTest);
  16. CloneTest clone = (CloneTest)cloneTest.clone();
  17. clone.getInnerClone().setType("被修改");
  18. System.out.println(clone);
  19. }
  20. }

克隆后,修改了clone的type类型,通过debug后看到克隆对象和被克隆的对象中的InnerClon对象中type属性都被修改了。
image.png

这是为什么呢?

这就需要想到深拷贝和浅拷贝了。
浅拷贝:被复制对象的所有值属性都含有与原来对象都相同,而所有的对象引用属性仍然指向原来的对象。以上测试就是浅拷贝。
深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行clone,并指向被复制过的新对象。

测试的内存结构(浅拷贝):
image.png

仅仅调用父类的clone方法是不够的,修改测试代码,新建的InnerClone类重写Object类的clone方法(记得实现Cloneable接口),在CloneTest类中添加新的逻辑。如下:

  1. protected Object clone() throws CloneNotSupportedException {
  2. //先克隆一个一摸一样的模型
  3. Object obj = super.clone();
  4. //拿到模型里的零件
  5. InnerClone inner = ((CloneTest)obj).getInnerClone();
  6. //复制一份一样的零件
  7. ((CloneTest)obj).setInnerClone((InnerClone)inner.clone());
  8. return obj;
  9. }

debug测试后,克隆对象和被克隆对象都是内容相同,地址引用不同。
image.png

Java语言中的clone方法满足:

  • 对任何对象x,都有x.clone() !=x,即克隆对象与原型对象不是同一个对象
  • 对任何对象x,都有x.clone().getClass()==x.getClass(),即克隆对象与原型对象的类型一样
  • 如果对象x的equals方法定义恰当,那么x.clone().equals(x)应该返回true


总结:

  1. 自定义类型想要使其成为可克隆的,必须实现Cloneable接口,并且实现Object中提供的clone方法。不要忘记提升为public可见。
  2. 对于类与类之间是包含关系的,如果想要拷贝一个对象的副本,就需要修改clone方法。如果类与类之间不是继承关系,那么每个类之间都要实现clone方法。

理解了clone方法,我们可以结合System类中的arraycopy方法分析。还是上面的案例:

public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest cloneTest = new CloneTest();
        InnerClone innerClone = new InnerClone();
        innerClone.setType("innerClone");
        cloneTest.setName("cloneTest");
        cloneTest.setInnerClone(innerClone);
        System.out.println(cloneTest);

        CloneTest clone = null;
        System.arraycopy(clone,0,cloneTest,0,1);
        clone.getInnerClone().setType("被修改");
        System.out.println(clone);
    }

对不是多个序列的用处并没有很大,其一clone为null,在执行arraycopy时就会抛出空指针异常。其二如果我们将clone进行实例化,arraycopy会抛出ArrayStoreException异常。所以arraycopy的应用场景主要是数组。

六、wait

    public final void wait() throws InterruptedException {
        wait(0);
    }
    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);
    }

该方法用来将当前线程置入休眠状态,直到在其他线程调用此对象的notify()或notifyAll()方法将其唤醒。
在调用wait()之前,线程必须要获得该对象的对象级别锁,因此只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException异常,它是一个运行时异常。
IllegalMonitorStateException异常是什么呢?
线程视图等待对象的监视器或者视图通知其他正在等待对象监视器的线程,但本身没有对应的监视器的所有权。wait()方法是一个本地方法,其底层是通过一个叫监视器锁的对象来完成的。

七、notify

/*
 * @throws  IllegalMonitorStateException  如果当前线程不是此对象的监视器的所有者。
 * @see        java.lang.Object#notifyAll()
 * @see        java.lang.Object#wait()
 */
public final native void notify();

notify()唤醒此对象监视器上等待的单个线程。如果多个线程都在此对象上等待,则会随机选择唤醒其中一个线程,对其发出通知notify(),并使它等待获取该对象的对象锁。
注意:“等待获取该对象的对象锁”,这意味着,即使收到了通知,wait()的线程也不会马上获取对象锁,必须等待notify()方法的线程释放锁才可以。

八、notifyAll


九、finalize

finalize()方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法。它在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或执行其他清理工作。

protected void finalize() throws Throwable { }

9.1 调用时机

与C++的析构函数不同,finalize方法不能保证会被及时的执行。从一个对象变得不可达到开始,到它的finalize被执行,所花费的这段时间是任意长的。这意味着注重时间的任务不应该由终结方法来完成。例如,用finalize方法来关闭已经打开的文件,这是一个严重的错误,因为打开文件的描述符仍然保留在打开的状态,当一个程序再也不能打开文件的时候,可能会运行失败。

9.2 为什么避免使用它

  • finalize方法不能保证会被及时的执行
  • 性能损失
  • 安全问题
  • 忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会被终止,并且相关的异常栈轨迹信息不会打印出来。

9.3 为什么应该使用它

当资源的所有者忘记调用它的close方法是,finalize方法可以充当”安全网”。虽然这样做并不能保证finalize方法会被及时运行,但是在客户端无法正常结束操作的情况下,迟一点释放资源总永远不释放要好。
与对象的本地对等体有关。本地等对体是一个本地(非java)对象,普通对象通过本地方法(native)委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的java对等体被回收的时候,它不会被回收。如果本地对等体没有关键资源,性能也可以接受的话,那么finalize方法可以交给合适的工具。否则该类就应该具有一个finalize方法

十、registerNatives

    private static native void registerNatives();
    static {
        registerNatives();
    }

Java有两种方法:一种是Java方法和本地方法。java方法是由java语言编写的,编译成字节码文件。本地方法是由(比如C,C++,或者汇编)编写的,编译成和处理器相关的机器代码。本地方法保存在动态链接库中,格式是各个平台专有的。Java方法是平台无关的。运行中的Java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。本地方法是联系Java程序和底层主机操作系统的连接方法。

registerNatives本质上就是一个本地方法,从方法名可以猜测该方法应该是用来注册本地方法的。

问题一:注册了哪些方法?
在Object类中有hashCode、clone等本地方法,而Class类中有forName0()这样的本地方法。也就是说,凡是包含registerNatives()本地方法的类,同时也包含了其他本地方法。当包含registerNatives()方法的类被加载的时候,注册的方法就是该类所包含的除了registerNatives()方法以外的所有本地方法。

问题二:为什么要注册?
一个Java程序要想调用一个本地方法,需要执行两个步骤:

  1. 通过System.loadLibrary()将包含本地方法实现的动态文件加载进内存
  2. 当Java程序需要调用本地方法时,虚拟机在加载的动态文件中定位并链接该本地方法,从而得以执行本地方法。registerNatives()方法的作用就是取代该步,让程序主动将本地方法链接到调用方,当Java程序需要调用本地方法时就可以直接调用,而不需要虚拟机再去定位并链接。

使用registerNatives()方法的好处:

  • 通过registerNatives()方法在类加载的时候就主动将本地方法链接到调用方,比当方法被使用时再由虚拟机来定位和链接更方便有效。
  • 如果本地方法在程序运行中更新了,可以通过调用registerNatives方法进行更新
  • 可以本地方法可以不遵循JNI命名规范。何为JNI命名规范?JNI规范要求本地方法名由”包名”+”方法名”构成

问题三:具体怎么注册?
涉及该方法的底层C++源码实现。目前水平不够,先跳过。

参考链接:https://blog.csdn.net/fenglibing/article/details/8905007