你知道Object中有哪些方法及其做用吗?

1、引言2、Object方法详解1.一、registerNatives()1.二、getClass()1.2.一、反射三种方式:1.三、hashCode()1.四、equals()1.四、clone()1.五、toString()1.六、wait()/ wait(long)/ waite(long,int)1.七、notify()/notifyAll()1.八、finalize()1.8.一、对象在内存中的状态1.8.二、垃圾回收机制1.8.三、强制垃圾回收3、总结html

1、引言

Object是java全部类的基类,是整个类继承结构的顶端,也是最抽象的一个类。你们每天都在使用toString()、equals()、hashCode()、waite()、notify()、getClass()等方法,或许都没有意识到是Object的方法,也没有去看Object还有哪些方法以及思考为何这些方法要放到Object中。本篇就每一个方法具体功能、重写规则以及本身的一些理解。java

2、Object方法详解

Object中含有:registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()、notifyAll()、wait(long)、wait(long,int)、wait()、finalize()共十二个方法。这个顺序是按照Object类中定义方法的顺序列举的,下面我也会按照这个顺序依次进行讲解。程序员

1.一、registerNatives()

public class Object {
    private static native void registerNatives();
    static {
        registerNatives();
    }
}
复制代码

什么鬼?哈哈哈,我刚看到这方法,一脸懵逼。从名字上理解,这个方法是注册native方法(本地方法,由JVM实现,底层是C/C++实现的)向谁注册呢?固然是向JVM,当有程序调用到native方法时,JVM才好去找到这些底层的方法进行调用。面试

Object中的native方法,并使用registerNatives()向JVM进行注册。(这属于JNI的范畴,9龙暂不了解,有兴趣的可自行查阅。)数据库

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
    {"notify",      "()V",                    (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",                    (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};
复制代码

为何要使用静态方法,还要放到静态块中呢?segmentfault

上一篇整理了类加载流程咱们知道了在类初始化的时候,会依次从父类到本类的类变量及类初始化块中的类变量及方法按照定义顺序放到< clinit>方法中,这样能够保证父类的类变量及方法的初始化必定先于子类。因此当子类调用相应native方法,好比计算hashCode时,必定能够保证可以调用到JVM的native方法。markdown

1.二、getClass()

public final native Class getClass():这是一个public的方法,咱们能够直接经过对象调用。网络

类加载的第一阶段类的加载就是将.class文件加载到内存,并生成一个java.lang.Class对象的过程。getClass()方法就是获取这个对象,这是当前类的对象在运行时类的全部信息的集合。这个方法是反射三种方式之一。app

1.2.一、反射三种方式:
  1. 对象的getClass();
  2. 类名.class;
  3. Class.forName();
class extends ObjectTest {
    private void privateTest(String str) {
        System.out.println(str);
    }

    public void say(String str) {
        System.out.println(str);
    }
}

public class ObjectTest {
    public static void main(String[] args) throws Exception {
        ObjectTest  = new ();
        //获取对象运行的Class对象
        Class<? extends ObjectTest> aClass = .getClass();
        System.out.println(aClass);

        //getDeclaredMethod这个方法能够获取全部的方法,包括私有方法
        Method privateTest = aClass.getDeclaredMethod("privateTest", String.class);
        //取消java访问修饰符限制。
        privateTest.setAccessible(true);
        privateTest.invoke(aClass.newInstance(), "private method test");

        //getMethod只能获取public方法
        Method say = aClass.getMethod("say", String.class);
        say.invoke(aClass.newInstance(), "Hello World");
    }
}
//输出结果:
//class test.
//private method test
//Hello World
复制代码

反射主要用来获取运行时的信息,能够将java这种静态语言动态化,能够在编写代码时将一个子对象赋值给父类的一个引用,在运行时经过反射能够或许运行时对象的全部信息,即多态的体现。对于反射知识仍是不少的,这里就不展开讲了。jvm

1.三、hashCode()

public native int hashCode(); 这是一个public的方法,因此子类能够重写它。这个方法返回当前对象的hashCode值,这个值是一个整数范围内的(-2^31 ~ 2^31 - 1)数字。

对于hashCode有如下几点约束

  • 在 Java 应用程序执行期间,在对同一对象屡次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改;
  • 若是两个对象 x.equals(y) 方法返回true,则x、y这两个对象的hashCode必须相等。
  • 若是两个对象x.equals(y) 方法返回false,则x、y这两个对象的hashCode能够相等也能够不等。可是,为不相等的对象生成不一样整数结果能够提升哈希表的性能。
  • 默认的hashCode是将内存地址转换为的hash值,重写事后就是自定义的计算方式;也能够经过System.identityHashCode(Object)来返回本来的hashCode。
public class HashCodeTest {
    private int age;
    private String name;

    @Override
    public int hashCode() {
        Object[] a = Stream.of(age, name).toArray();

        int result = 1;
        for (Object element : a) {
            result = 31 * result + (element == null ? 0 : element.hashCode());
        }
        return result;
    }
}
复制代码

推荐使用Objects.hash(Object… values)方法。相信看源码的时候,都看到计算hashCode都使用了31做为基础乘数,为何使用31呢?我比较赞同与理解result * 31 = (result<<5) - result。JVM底层能够自动作优化为位运算,效率很高;还有由于31计算的hashCode冲突较少,利于hash桶位的分布。

1.四、equals()

public boolean equals(Object obj);用于比较当前对象与目标对象是否相等,默认是比较引用是否指向同一对象。为public方法,子类可重写。

public class Object{
    public boolean equals(Object obj) {
        return (this == obj);
    }
}
复制代码

为何须要重写equals方法?

由于若是不重写equals方法,当将自定义对象放到map或者set中时;若是这时两个对象的hashCode相同,就会调用equals方法进行比较,这个时候会调用Object中默认的equals方法,而默认的equals方法只是比较了两个对象的引用是否指向了同一个对象,显然大多数时候都不会指向,这样就会将重复对象存入map或者set中。这就破坏了map与set不能存储重复对象的特性,会形成内存溢出

重写equals方法的几条约定:

  1. 自反性:即x.equals(x)返回true,x不为null;
  2. 对称性:即x.equals(y)与y.equals(x)的结果相同,x与y不为null;
  3. 传递性:即x.equals(y)结果为true, y.equals(z)结果为true,则x.equals(z)结果也必须为true;
  4. 一致性:即x.equals(y)返回true或false,在未更改equals方法使用的参数条件下,屡次调用返回的结果也必须一致。x与y不为null。
  5. 若是x不为null, x.equals(null)返回false。

咱们根据上述规则来重写equals方法。

public class EqualsTest{
    private int age;
    private String name;
    //省略get、set、构造函数等
     @Override
    public boolean equals(Object o) {
        //先判断是否为同一对象
        if (this == o) {
            return true;
        }
        //再判断目标对象是不是当前类及子类的实例对象
        //注意:instanceof包括了判断为null的状况,若是o为null,则返回false
        if (!(o instanceof )) {
            return false;
        }
         that = () o;
        return age == that.age &&
                Objects.equals(name, that.name);
    }

     public static void main(String[] args) throws Exception {
         EqualsTest1 equalsTest1 = new EqualsTest1(23"9龙");
        EqualsTest1 equalsTest12 = new EqualsTest1(23"9龙");
        EqualsTest1 equalsTest13 = new EqualsTest1(23"9龙");

        System.out.println("-----------自反性----------");
        System.out.println(equalsTest1.equals(equalsTest1));

        System.out.println("-----------对称性----------");
        System.out.println(equalsTest12.equals(equalsTest1));
        System.out.println(equalsTest1.equals(equalsTest12));

        System.out.println("-----------传递性----------");
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println(equalsTest12.equals(equalsTest13));
        System.out.println(equalsTest1.equals(equalsTest13));

        System.out.println("-----------一致性----------");
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println(equalsTest1.equals(equalsTest12));

        System.out.println("-----目标对象为null状况----");
        System.out.println(equalsTest1.equals(null));
    }
}
//输出结果
//-----------自反性----------
//true
//-----------对称性----------
//true
//true
//-----------传递性----------
//true
//true
//true
//-----------一致性----------
//true
//true
//-----目标对象为null状况----
//false
复制代码

从以上输出结果验证了咱们的重写规定是正确的。

注意:instanceof 关键字已经帮咱们作了目标对象为null返回false,咱们就不用再去显示判断了。

建议equals及hashCode两个方法,须要重写时,两个都要重写,通常都是将自定义对象放至Set中,或者Map中的key时,须要重写这两个方法。

1.四、clone()

protected native Object clone() throws CloneNotSupportedException;

此方法返回当前对象的一个副本。

这是一个protected方法,提供给子类重写。但须要实现Cloneable接口,这是一个标记接口,若是没有实现,当调用object.clone()方法,会抛出CloneNotSupportedException。

public class CloneTest implements Cloneable {
    private int age;
    private String name;
    //省略get、set、构造函数等

    @Override
    protected CloneTest clone() throws CloneNotSupportedException {
        return (CloneTest) super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest cloneTest = new CloneTest(23"9龙");
        CloneTest clone = cloneTest.clone();
        System.out.println(clone == cloneTest);
        System.out.println(cloneTest.getAge()==clone.getAge());
        System.out.println(cloneTest.getName()==clone.getName());
    }
}
//输出结果
//false
//true
//true
复制代码

从输出咱们看见,clone的对象是一个新的对象;但原对象与clone对象的String类型的name倒是同一个引用,这代表,super.clone方法对成员变量若是是引用类型,进行是浅拷贝。

那什么是浅拷贝?对应的深拷贝?

浅拷贝:拷贝的是引用。

深拷贝:新开辟内存空间,进行值拷贝。

那若是咱们要进行深拷贝怎么办呢?看下面的例子。

class Person implements Cloneable{
    private int age;
    private String name;
     //省略get、set、构造函数等
     @Override
    protected Person clone() throws CloneNotSupportedException {
        Person person = (Person) super.clone();
        //name经过new开辟内存空间
        person.name = new String(name);
        return person;
   }
}

public class CloneTest implements Cloneable {
    private int age;
    private String name;
    //增长了person成员变量
    private Person person;

    //省略get、set、构造函数等
    @Override
    protected CloneTest clone() throws CloneNotSupportedException {
        CloneTest clone = (CloneTest) super.clone();
        clone.person = person.clone();
        return clone;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
       CloneTest cloneTest = new CloneTest(23"9龙");
        Person person = new Person(22"路飞");
        cloneTest.setPerson(person);

        CloneTest clone = cloneTest.clone();
        System.out.println(clone == cloneTest);
        System.out.println(cloneTest.getAge() == clone.getAge());
        System.out.println(cloneTest.getName() == clone.getName());

        Person clonePerson = clone.getPerson();
        System.out.println(person == clonePerson);
        System.out.println(person.getName() == clonePerson.getName());
    }
}
//输出结果
//false
//true
//true
//false
//false
复制代码

能够看到,即便成员变量是引用类型,咱们也实现了深拷贝。若是成员变量是引用类型,想实现深拷贝,则成员变量也要实现Cloneable接口,重写clone方法。

1.五、toString()

public String toString();这是一个public方法,子类可重写,建议全部子类都重写toString方法,默认的toString方法,只是将当前类的全限定性类名+@+十六进制的hashCode值。

public class Object{
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
}
复制代码

咱们思考一下为何须要toString方法?

我这么理解的,返回当前对象的字符串表示,能够将其打印方便查看对象的信息,方便记录日志信息提供调试。

咱们能够选择须要表示的重要信息重写到toString方法中。为何Object的toString方法只记录类名跟内存地址呢?由于Object没有其余信息了,哈哈哈。

1.六、wait()/ wait(long)/ waite(long,int)

这三个方法是用来线程间通讯用的,做用是阻塞当前线程,等待其余线程调用notify()/notifyAll()方法将其唤醒。这些方法都是public final的,不可被重写。

注意:

  1. 此方法只能在当前线程获取到对象的锁监视器以后才能调用,不然会抛出IllegalMonitorStateException异常。
  2. 调用wait方法,线程会将锁监视器进行释放;而Thread.sleep,Thread.yield()并不会释放锁
  3. wait方法会一直阻塞,直到其余线程调用当前对象的notify()/notifyAll()方法将其唤醒;而wait(long)是等待给定超时时间内(单位毫秒),若是尚未调用notify()/nofiyAll()会自动唤醒;waite(long,int)若是第二个参数大于0而且小于999999,则第一个参数+1做为超时时间;
 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);
    }
复制代码

1.七、notify()/notifyAll()

前面说了,若是当前线程得到了当前对象锁,调用wait方法,将锁释放并阻塞;这时另外一个线程获取到了此对象锁,并调用此对象的notify()/notifyAll()方法将以前的线程唤醒。这些方法都是public final的,不可被重写。

  1. public final native void notify(); 随机唤醒以前在当前对象上调用wait方法的一个线程

  2. public final native void notifyAll(); 唤醒全部以前在当前对象上调用wait方法的线程

下面咱们使用wait()、notify()展现线程间通讯。假设9龙有一个帐户,只要9龙一发工资,就被女友给取走了。

//帐户
public class Account {
    private String accountNo;
    private double balance;
    private boolean flag = false;

    public Account() {
    }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    /**
     * 取钱方法
     *
     * @param drawAmount 取款金额
     */

    public synchronized void draw(double drawAmount) {
        try {
            if (!flag) {
                //若是flag为false,代表帐户尚未存入钱,取钱方法阻塞
                wait();
            } else {
                //执行取钱操做
                System.out.println(Thread.currentThread().getName() + " 取钱" + drawAmount);
                balance -= drawAmount;
                //标识帐户已没钱
                flag = false;
                //唤醒其余线程
                notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void deposit(double depositAmount) {
        try {
            if (flag) {
                //若是flag为true,代表帐户已经存入钱,取钱方法阻塞
                wait();
            } else {
                //存钱操做
                System.out.println(Thread.currentThread().getName() + " 存钱" + depositAmount);
                balance += depositAmount;
                //标识帐户已存入钱
                flag = true;
                //唤醒其余线程
                notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//取钱者
public class DrawThread extends Thread {
    private Account account;
    private double drawAmount;

    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    @Override
    public void run() {
        //循环6次取钱
        for (int i = 0; i < 6; i++) {
            account.draw(drawAmount);
        }
    }
}

//存钱者
public class DepositThread extends Thread {
    private Account account;
    private double depositAmount;

    public DepositThread(String name, Account account, double depositAmount) {
        super(name);
        this.account = account;
        this.depositAmount = depositAmount;
    }

    @Override
    public void run() {
        //循环6次存钱操做
        for (int i = 0; i < 6; i++) {
            account.deposit(depositAmount);
        }
    }
}
//测试
public class DrawTest {
    public static void main(String[] args) {
        Account brady = new Account("9龙"0);
        new DrawThread("女票", brady, 10).start();
        new DepositThread("公司", brady, 10).start();
    }
}
//输出结果
//公司 存钱10.0
//女票 取钱10.0
//公司 存钱10.0
//女票 取钱10.0
//公司 存钱10.0
//女票 取钱10.0
复制代码

例子中咱们经过一个boolean变量来判断帐户是否有钱,当取钱线程来判断若是帐户没钱,就会调用wait方法将此线程进行阻塞;这时候存钱线程判断到帐户没钱, 就会将钱存入帐户,而且调用notify()方法通知被阻塞的线程,并更改标志;取钱线程收到通知后,再次获取到cpu的调度就能够进行取钱。反复更改标志,经过调用wait与notify()进行线程间通讯。实际中咱们会时候生产者消费者队列会更简单。

注意:调用notify()后,阻塞线程被唤醒,能够参与锁的竞争,但可能调用notify()方法的线程还要继续作其余事,锁并未释放,因此咱们看到的结果是,不管notify()是在方法一开始调用,仍是最后调用,阻塞线程都要等待当前线程结束才能开始。

为何wait()/notify()方法要放到Object中呢?

由于每一个对象均可以成为锁监视器对象,因此放到Object中,能够直接使用。

1.八、finalize()

protected void finalize() throws Throwable ;

此方法是在垃圾回收以前,JVM会调用此方法来清理资源。此方法可能会将对象从新置为可达状态,致使JVM没法进行垃圾回收。

咱们知道java相对于C++很大的优点是程序员不用手动管理内存,内存由jvm管理;若是咱们的引用对象在堆中没有引用指向他们时,当内存不足时,JVM会自动将这些对象进行回收释放内存,这就是咱们常说的垃圾回收。但垃圾回收没有讲述的这么简单。

finalize()方法具备以下4个特色:

  1. 永远不要主动调用某个对象的finalize()方法,该方法由垃圾回收机制本身调用;
  2. finalize()什么时候被调用,是否被调用具备不肯定性;
  3. 当JVM执行可恢复对象的finalize()可能会将此对象从新变为可达状态;
  4. 当JVM执行finalize()方法时出现异常,垃圾回收机制不会报告异常,程序继续执行。
public class FinalizeTest {
    private static FinalizeTest ft = null;
    public void info(){
        System.out.println("测试资源清理得finalize方法");
    }

    public static void main(String[] args) {
        //建立FinalizeTest对象当即进入可恢复状态
        new FinalizeTest();
        //通知系统进行垃圾回收
        System.gc();
        //强制回收机制调用可恢复对象的finalize()方法
//        Runtime.getRuntime().runFinalization();
        System.runFinalization();
        ft.info();
    }

    @Override
    public void finalize(){
        //让ft引用到试图回收的可恢复对象,便可恢复对象从新变成可达
        ft = this;
        throw new RuntimeException("出异常了,你管无论啊");
    }
}
//输出结果
//测试资源清理得finalize方法
复制代码

咱们看到,finalize()方法将可恢复对象置为了可达对象,而且在finalize中抛出异常,都没有任何信息,被忽略了。

1.8.一、对象在内存中的状态

对象在内存中存在三种状态:

  1. 可达状态:有引用指向,这种对象为可达状态;
  2. 可恢复状态:失去引用,这种对象称为可恢复状态;垃圾回收机制开始回收时,回调用可恢复状态对象的finalize()方法(若是此方法让此对象从新得到引用,就会变为可达状态,不然,会变为不可大状态)。
  3. 不可达状态:完全失去引用,这种状态称为不可达状态,若是垃圾回收机制这时开始回收,就会将这种状态的对象回收掉。
1.8.二、垃圾回收机制
  1. 垃圾回收机制只负责回收堆内存种的对象,不会回收任何物理资源(例如数据库链接、网络IO等资源);
  2. 程序没法精确控制垃圾回收的运行,垃圾回收只会在合适的时候进行。当对象为不可达状态时,系统会在合适的时候回收它的内存。
  3. 在垃圾回收机制回收任何对象以前,总会先调用它的finalize()方法,该方法可能会将对象置为可达状态,致使垃圾回收机制取消回收。
1.8.三、强制垃圾回收

上面咱们已经说了,当对象失去引用时,会变为可恢复状态,但垃圾回收机制何时运行,何时调用finalize方法没法知道。虽然垃圾回收机制没法精准控制,但java仍是提供了方法能够建议JVM进行垃圾回收,至因而否回收,这取决于虚拟机。但彷佛能够看到一些效果。

public class GcTest {
    public static void main(String[] args){
        for(int i=0;i<4;i++){
            //没有引用指向这些对象,因此为可恢复状态
            new GcTest();
            //强制JVM进行垃圾回收(这只是建议JVM)
            System.gc();
            //Runtime.getRuntime().gc();
        }
    }

    @Override
    public void finalize(){
        System.out.println("系统正在清理GcTest资源。。。。");
    }
}
//输出结果
//系统正在清理GcTest资源。。。。
//系统正在清理GcTest资源。。。。
复制代码

System.gc(),Runtime.getRuntime().gc()两个方法做用同样的,都是建议JVM垃圾回收,但不必定回收,多运行几回,结果可能都不一致。

3、总结

本篇举例讲解了Objec中的全部方法的做用、意义及使用,从java最基础的类出发,感觉java设计之美吧。我是不会高诉你们,这好像面试也会问的【摊手】。

整理不易,以为能够,但愿你们点赞支持啊。转载请注明出处。

十分感谢如下连接的分享,参考连接:

registerNatives方法做用

计算hashCode为何选择31?