java.lang.Object类详解

Object类详解

一、引言

​ Object类作为所有类的父类,因此被默认继承所以省略了 extends Object,该类中定义了其它所有类都需要的方法,本文将针对这些方法进行一个详细解释。

二、源代码

省略大部分原注释,并增加了部分个人理解

/*
 * Copyright (c) 1994, 2012, 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外的其它本地方法,方便JVM调用
        //https://blog.csdn.net/Saintyyu/article/details/90452826
        registerNatives();
    }
	//本地方法-获取该对象的Class类
    public final native Class<?> getClass();
    
	//本地方法-获取该对象的Hash值
    public native int hashCode();

    //判断该对象与传入的对象是否为同一对象(即判断引用在内存中是否指向同一地址)
    public boolean equals(Object obj) {
        return (this == obj);
    }

    //本地方法-获得该对象的一个副本(浅拷贝) 必须实现java.lang.Cloneable接口,否则抛出异常
    protected native Object clone() throws CloneNotSupportedException;

    //输出为字符串 输出格式【 类名 + @ + Hash值转换为16进制 】
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    
    //本地方法-随机唤醒一个因调用该共享变量的wait()而阻塞的线程
    public final native void notify();
    
    //本地方法-唤醒所有因调用该共享变量的wait()而阻塞的线程
    public final native void notifyAll();

    //本地方法-获得该对象监视器的线程才有资格调用该方法,调用该方法会使调用线程被阻塞并挂起。
    //timeout 为超时时间(单位毫秒),如果超过该时间该线程还没被唤醒那么会自动返回。
    //输入0代表无超时时间,则无限等待。
    //注:wait方法会释放监视器锁而Thread.sleep不会.
    public final native void wait(long timeout) throws InterruptedException;
	
    //本质是调用wait(timeOut)方法
    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
		//1毫秒 = 1000000纳秒
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
		//因为wait的最小单位为毫秒,在上面已经做了纳秒的判断,所以只要纳秒值>0,就给毫秒值+1
        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

 	//本质是调用 wait(0) 则不做超时,无限等待
    public final void wait() throws InterruptedException {
        wait(0);
    }
	//用于资源回收,所有对象被JVM回收之前,都会先调用一次finalize方法。
    //如果一个对象想要自我拯救,那么可以重写该方法,并在该方法内让其它对象在此对该对象进行引用。
    //但是只能拯救一次,因为一个对象的finalize方法只会被调用一次。
    protected void finalize() throws Throwable { }
}

三、方法详解

1.registerNatives()

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

​ 不光是Object类,甚至System类、Class类、ClassLoader类、Unsafe类等等都能在类中找到该方法,而且他们都有一个共同点,那就是只要有该方法的类,那么在类中必定其它有本地方法。

​ 一个Java程序要想调用一个本地方法,需要执行两个步骤:第一,通过System.loadLibrary()将包含本地方法实现的动态文件加载进内存;第二,当Java程序需要调用本地方法时,虚拟机在加载的动态文件中定位并链接该本地方法,从而得以执行本地方法。

​ registerNatives()方法的作用就是取代第二步,让程序主动将本地方法链接到调用方,当Java程序需要调用本地方法时就可以直接调用,而不需要虚拟机再去定位并链接。

​ 以上摘自 https://blog.csdn.net/Saintyyu/article/details/90452826 内容较多,感兴趣的可以去了解。

2.getClass()

public final native Class<?> getClass();	

​ 该方法被final关键词修饰表示不可被重写,其返回的是运行时该对象的类型,得到此对象就等于得到了该对象的各种信息,其中包括类名,定义的属性,定义的方法、类加载器,父类等等。

3.hashCode()

 public native int hashCode();

​ 该方法是一个本地方法,但是没有用final修饰目的就是为了让子类可以重写该方法,调用该方法会返回此对象的哈希值。这个哈希值的作用是确定该对象在哈希表中的索引位置。

4.equals(Object obj)

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

​ 该方法没有用final修饰目的就是为了让子类可以重写该方法,该方法用于判断档期啊对象与传入的对象是否为同一对象(即判断引用在内存中是否指向同一地址)。

​ String对象重写了这两种方法,目的就是让值相同的两个对象返回相同的哈希值以及满足equals条件。

​ 我们经常会把equals 和 hashCode 方法联系在一起;其实如果单纯的在Object类中,他们俩是没有任何关系的。但是我们常使用的集合框架都建议我们重写投入元素的hashCode和equals方法,例如HashSet、HashMap(key 需要重写)等等,这是因为它们底层的数据结构都采用了哈希表,因此会通过hashCode来确定元素在hash表中的索引位置,当两个对象拥有同样的哈希值时即表示产生了哈希冲突,此时会通过equals来判断待投入元素和已存在元素是否为同一对象来决定是新增还是覆盖。

​ 所以如果在哈希表中,两个元素满足equals条件,那么其哈希值一定相同;但是如果两个元素哈希值相同,却不一定满足equals条件;如果两个元素的哈希值不相同,那么一定不满足equals条件。

​ 举个栗子,现在有一个Set,集合中存放了Person对象,Person对象只包含姓名name和年龄age。那么现在new出来两个Person对象,它们的name和age完全相同,但是由于它们在内存中的指向并不相同,所以不为同一对象,在加入到Set中就不会被认为是同一元素,那么调用set.size()会发现返回结果为2;那么如果我们希望集合将它俩视为同样的元素怎么办,只需要重写它俩的equals方法和 hashCode方法即可,让其通过计算属性值来返回哈希值,和通过判断属性值来确认是否满足equals条件。

5.clone()

  protected native Object clone() throws CloneNotSupportedException;

​ 该方法是一个本地方法可以被子类重写并且访问修饰符为protected,只允许子类和本类中调用。

​ 该类若想使用该方法必须要实现java.lang.Cloneable接口,否则会直接抛出异常会。

​ 该方法会返回该对象的一个副本,此副本为浅拷贝。

​ 例:Student类中有三个属性 姓名 name ,年龄 age,学校 school(school也是一个对象,包含 校名name 和校址 address)。

@Data
public class Student implements Cloneable {
    private String name;
    private Integer age;
    private School school;

    public Student(String name, Integer age, School school) {
        this.name = name;
        this.age = age;
        this.school = school;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        School school = new School("清华大学", "北京...");
        Student student1 = new Student("小陈", 18, school);
        //克隆一个对象 student2
        Student student2 = (Student) student1.clone();
        //修改student1的年龄为17
        student1.setAge(17);
        //打印一下 是否对student2产生影响
        System.out.println("修改student1的年龄...");
        System.out.println(student1);
        System.out.println(student2);

        //修改student2的学校信息
        System.out.println("修改student2的学校名称...");
        student2.getSchool().setName("北京大学");
        //打印一下 是否对student1产生影响
        System.out.println(student1);
        System.out.println(student2);

    }

    @Data
    static class School {
        private String name;
        private String address;

        public School(String name, String address) {
            this.name = name;
            this.address = address;
        }
    }
}
//控制台输出
克隆后的初始状态...
Disconnected from the target VM, address: '127.0.0.1:58308', transport: 'socket'
Student(name=小陈, age=18, school=Student.School(name=清华大学, address=北京...))
Student(name=小陈, age=18, school=Student.School(name=清华大学, address=北京...))
修改student1的年龄...
Student(name=小陈, age=17, school=Student.School(name=清华大学, address=北京...))
Student(name=小陈, age=18, school=Student.School(name=清华大学, address=北京...))
修改student2的学校名称...
Student(name=小陈, age=17, school=Student.School(name=北京大学, address=北京...))
Student(name=小陈, age=18, school=Student.School(name=北京大学, address=北京...))

​ 可以看到当我们修改学生1的年龄时并没有对学生2产生影响,这是因为在JAVA中基本数据类型在传递时是值传递;我们都知道,在通过方法传递基本数据类型时,即使在方法中修改了该参数,方法外的原参数也不会产生任何影响。

​ 但是我们修改学生2的学校名称时,却发现学生1的学校姓名也改变了。这就是浅拷贝,只复制了该学校对象的引用,所以我们在使用clone方法时要特别注意;那么我们不想在修改学生2时也对学生1产生影响怎么办,那就要实现对象的深拷贝才行。

深拷贝的两种实现

1.自行处理非基本类型的成员变量,重写clone方法并让成员变量也实现Cloneable接口

(如果成员变量的成员变量也是非基本类型,那么需要一层一层处理)。

//代码示例
@Data
public class Student implements Cloneable {
   	
    //省略相同代码...
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student clone = (Student) super.clone();
        try {
            School cloneSchool = (School) this.getSchool().clone();
            clone.setSchool(cloneSchool);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return clone;
    }

    @Data
    static class School implements Cloneable {
        //省略相同代码...
        @Override
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
}

2.序列化和反序列化,对象和成员变量对象也都必须实现 Serializable接口

//代码示例
@Data
public class Student implements Cloneable,Serializable {
    //省略相同代码...
    @Override
    protected Object clone() throws CloneNotSupportedException {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            oos.flush();
            oos.close();
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            Object cloneObject = ois.readObject();
            ois.close();
            return cloneObject;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @Data
    static class School implements Serializable {
         //省略相同代码...
    }


//控制台输出
克隆后的初始状态...
Student(name=小陈, age=18, school=Student.School(name=清华大学, address=北京...))
Student(name=小陈, age=18, school=Student.School(name=清华大学, address=北京...))
修改student1的年龄...
Student(name=小陈, age=17, school=Student.School(name=清华大学, address=北京...))
Student(name=小陈, age=18, school=Student.School(name=清华大学, address=北京...))
修改student2的学校名称...
Student(name=小陈, age=17, school=Student.School(name=清华大学, address=北京...))
Student(name=小陈, age=18, school=Student.School(name=北京大学, address=北京...))

​ 可以看到以上两种办法,在修改克隆的成员变量时均不会对原对象造成任何影响。

6.toString()

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

​ 该方法通常会被子类重写,用于打印该对象的信息。如果没被重写的话,那么会输出格式为:

​ 【全限定性类名 + @ + 哈希码(转为16进制)】

7.wait()、wait(long timeout)、wait(long timeout, int nanos)

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

​ 另外两种重载函数本质都是调用wait(timeout)方法,这是一个本地方法且不允许重写;调用该方法前必须要获取该对象的监视器锁否则会抛出 IllegalMonitorStateException异常;当一个线程调用了该变量的wait()方法时,该调用线程就会被阻塞挂起,直到以下三种情况才会返回:

​ 1.在调用wait方法时传递了超时时间,如果到达该时间还没因为其它原因返回就会自动返回。

​ 2.其它线程调用了该共享变量的notify方法或notifyAll()方法。

​ 3.其它线程调用了该调用线程的interrupt()方法,该线程会抛出 InterruptedException异常并返回。

如何获取一个共享变量的监视器锁?

​ 方法一:使用 synchronized同步代码块

synchronized (共享变量){
    //do something
}

​ 方法二:调用该共享变量被synchronized修饰的方法

public synchronized void test(){
	//do something
}

虚假唤醒

​ 在使用wait方法时我们结合while使用,以此防止虚假唤醒,例:

	LinkedList<String> list = new LinkedList<>();
        Object obj = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (obj) {
                //错误示例
                //if(list.isEmpty()){...}
                //正确示例
                while (list.isEmpty()) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 阻塞了");
                        obj.wait();
                        System.out.println(Thread.currentThread().getName() + " 被唤醒了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println(list.pop());
        }, "线程A");
        threadA.start();
        Thread threadB = new Thread(() -> {
            synchronized (obj) {
                 //错误示例
                //if(list.isEmpty()){...}
                //正确示例
                while (list.isEmpty()) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 阻塞了");
                        obj.wait();
                        System.out.println(Thread.currentThread().getName() + " 被唤醒了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println(list.pop());
        }, "线程B");
        threadB.start();
        Thread.sleep(3000);
        list.add("test");
        synchronized (obj) {
            obj.notifyAll();
        }

​ 集合元素个数初始为0,如果使用if做判断条件的话,一开始线程A、B同时消费一个产品,因为集合元素为空,所以A、B线程都被阻塞挂起,当生产者线程添加了一个元素后元素个数为1,并调用了obj.notifyAll()方法,A、B线程都被唤醒了,因为两个线程都是用if 做条件判断,所以唤醒之后拿到锁之后都执行了list.pop,list在第二次pop时,因为集合元素个数为0导致抛出了异常,所以第二个执行list.pop的线程就是被虚假唤醒的线程;

​ 如果使用while的话,A、B线程在唤醒后并拿到锁之后都会再进行一次条件判断,确保list元素不为空时才会执行list.pop操作,这就避免了虚假唤醒。

8.notify()、notifyAll()

public final native void notify();
public final native void notifyAll();

​ 这两个方法都是本地方法且不允许被重写,调用共享变量的notify()方法,会随机唤醒一个因为调用了该共享变量wait()方法而阻塞的线程,而notifyAll()则会唤醒所有因为调用了该共享变量wait()方法而阻塞的线程。

​ 注:notifyAll()只会唤醒在调用该方法前 阻塞的那些线程,如果调用了notifyAll之后,有一个线程调用了共享变量的wait()方法而阻塞是不会被唤醒的;

​ 另外和wait相同的是想调用该共享变量的notify/notifyAll方法必须获取该共享变量的监视器锁。

9.finalize()

​ JVM在垃圾回收之前,会先判定对象是否已经“死去”(无用),那么如何判定对象是否“死去”呢?主要有两种算法:1.引用计数法 2.可达性分析算法 。本文针对这两种算法进行详解,我们只需知道目前主流的商用程序语言都是使用的可达性分析算法。

​ 那么如果在可达性分析算法中已经被认为是不可达的对象,就一定会被消灭吗?答案是不一定。如果一个对象在经过可达性分析算法后被发现为不可达,那么该对象会被进行一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或finalize()方法已经被调用过一次了,虚拟机便将这两种情况都视为“没有必要执行”。

​ 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

​ finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那么基本上它就真的被回收了。

​ 总结:finalize()并不是必须要执行的,它只能执行一次或者0次。如果在finalize中建立对象关联,则当前对象可以复活一次。

//一次对象自我拯救的演示
@Data
public class FinalizeDemo {
    
    private String name;
    private static FinalizeDemo fz = null;

    public FinalizeDemo(String name) {
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("excute finalize method ~");
        super.finalize();
        fz = this;
    }

    public static void main(String[] args) throws InterruptedException {

        fz = new FinalizeDemo("demo");
        
		//第一次进行自我拯救  
        isAlive(fz);
        fz = null;
        System.gc();
        //因为Finalizer线程的优先级很低,所以暂停1s来等待它
        Thread.sleep(1000);
        isAlive(fz);
        
        System.out.println("================================================");
        
        //第二次进行自我拯救  
        isAlive(fz);
        fz = null;
        System.gc();
       	//因为Finalizer线程的优先级很低,所以暂停1s来等待它
        Thread.sleep(1000);
        isAlive(fz);

    }

    private static void isAlive(FinalizeDemo fz) {
        if (fz == null) {
            System.out.println("no, i am dead ! " + fz);
        } else {
            System.out.println("yes, i am alive !" + fz);
        }
    }
}

//控制台输出
yes, i am alive !FinalizeDemo(name=demo)
excute finalize method ~
yes, i am alive !FinalizeDemo(name=demo)
================================================
yes, i am alive !FinalizeDemo(name=demo)
no, i am dead ! null


​ 从运行结果可以看到,finalize方法只被执行了一次,并且在被执行过后,fz对象成功的完成了自我拯救并没有被回收;而第二次进行自我拯救失败了,这是因为finalize只会被执行一次,如果已经被执行过了的话,那么虚拟机就认为它已经没必要再执行了。

​ 特别说明的是,finalize方法其实是JAVA诞生时为了让C/C++程序员更容易接受它所做的一个妥协;在我们的开发中可以完全忘记这个方法的存在,尽量不要调用,因为它的不确定性太大了。

以上就是Object类中的所有内容了,如果有错误或遗漏的地方,欢迎指出~

你可能感兴趣的:(小陈的学习日记,java,object)