美文网首页Java 杂谈JavaConcurrent
深入Java基础(一)--Object类分析

深入Java基础(一)--Object类分析

作者: JackFrost_fuzhu | 来源:发表于2019-01-17 09:39 被阅读5次

    Object类是java中所有类的父类,所有类默认(而非显式)继承Object。这也就意味着,Object类中的所有公有方法也将被任何类所继承。如果,整个java类体系是一颗树,那么Object类毫无疑问就是整棵树的根。

    文章结构:

    1)源码分析
    2)浅拷贝与深拷贝
    3)探讨hashcode与equals的设计使用。

    一、源码分析

    public   class  Object {   
    
        /* 一个本地方法,具体是用C(C++)在DLL中实现的,然后通过JNI调用。*/     
         private   static   native   void  registerNatives(); 
    
       /* 对象初始化时自动调用此方法*/  
         static  {   
            registerNatives();   
        }   
    
        /* 返回这个Object的运行时Class对象。这个Class对象被所代表的类的static synchronized 
          方法锁定。*/   
         public   final   native  Class<?> getClass();   
    
    /*   
    hashCode 的常规协定是:(本质 上是 返回该对象的哈希码值。 )
    
    1.同一个对象在没修改的情况下的hashCode必须相同。在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。    
    2.如果根据 equals(Object) 方法比较,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。    
    3.如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。支持这个方法是为了提高哈希表的性能,例如HashMap。 
    实际上,被Object类定义的hashCode方法对不同的对象确实返回不同的整数。(这是通过 
    把对象的内部地址转化为一个整数来实现的,但是这个实现技巧不被Java编程语言需要。)
    */   
    
         public   native   int  hashCode();   
    
      //没有重写过的equals,就是使用==,比较内存地址。
         public   boolean  equals(Object obj) {   
            return  ( this  == obj);   
        }   
    
         /*本地CLONE方法,用于对象的复制。*/   
         protected   native  Object clone()  throws  CloneNotSupportedException;   
    
         /*返回该对象的字符串表示。非常重要的方法*/   
         public  String toString() {   
         return  getClass().getName() +  "@"  + Integer.toHexString(hashCode());   
        }   
    
        /*唤醒在此对象监视器上等待的单个线程。*/   
         public   final   native   void  notify();   
    
        /*唤醒在此对象监视器上等待的所有线程。*/   
         public   final   native   void  notifyAll();   
    
    
    /*在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。    
    当前线程必须拥有此对象监视器。该线程发布对此监视器的所有权并等待,直到其他线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。*/   
         public   final   void  wait()  throws  InterruptedException {   
        wait( 0 );   
        }   
    
    
    
        /*在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。*/   
         public   final   native   void  wait( long  timeout)  throws  InterruptedException;   
    
         /* 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。*/   
         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 >=  500000  || (nanos !=  0  && timeout ==  0 )) {   
            timeout++;   
        }   
    
        wait(timeout);   
        }   
    
         /*当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。*/   
         protected   void  finalize()  throws  Throwable { }   
    } 
    

    equals方法详解:

    可以看出默认情况下equals进行对象比较时只判断了对象是否是其自身(也就是对比的是内存地址),当我们有特殊的“相等”逻辑时,则需要覆盖equals方法。

    equals方法的通用约定:

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

    clone方法详解:

    1)简介:

    clone方法将创建和返回该对象的一个拷贝。这个“拷贝”的精确含义取决于该对象的类。一般的含义是,对于任何对象x,表达式x.clone() != x 将会是true,并且,表达式x.clone().getClass() == x.getClass()将会是true。并且通常情况下,表达式x.clone().equals(x)将会是true。
    clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。但是hashCode会不一样了。

    2)一个标准的clone实现需要做到的两点:

    1)调用super.clone()方法
    2)对于对象中的所有引用类型,均需要实现Cloneable接口,并重写clone方法,然后对每个引用执行clone方法。

    @Override
    protected Fruit clone() throws CloneNotSupportException{
        Fruit fruit = (Fruit) super.clone();
        fruit.color = new String(color);
        return fruit;
    }
    

    3)使用clone方法的优点:

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

    4)使用clone方法创建对象的缺点同样非常明显:

    1)实现深拷贝较为困难,需要整个类继承系列的所有类都很好的实现clone方法。
    2)需要处理CloneNotSupportedException异常。Object类中的clone方法被声明为可能会抛出CloneNotSupportedException,因此在子类中,需要对这一异常进行处理。

    建议:对于浅拷贝,我们不应该实现Cloneable接口,而应该使用拷贝构造器或者拷贝工厂。

    为什么?
    它们不依赖于对象创建机制;它们不会与final域的正常使用发生冲突;它们不会抛出不必要的受检异常(check exception);他们不需要进行类型转换。
    更关键的,这样更加的面向对象、


    二、浅拷贝与深拷贝:

    任何变成语言中,其实都有浅拷贝和深拷贝的概念,Java 中也不例外。我们需要对其概念非常清晰,才能帮助自己快速排查问题。

    (1)什么是浅拷贝和深拷贝:

    首先我们知道拷贝是针对一个已有对象的操作。
    在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。而一般使用 = 号做赋值操作的时候。对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。

    而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。

    (2)举例说明:

    浅拷贝:

    public class CloneDemo implements Cloneable {
        public String name;
        public CloneChildDemo cloneChildDemo;
        @Override
        public Object clone(){
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    public class CloneChildDemo{
        private String name;
    }
    

    场景验证:

        public static void main(String[]args){
            CloneDemo cloneDemo = new CloneDemo();
            cloneDemo.name = "辅助";
            cloneDemo.cloneChildDemo = new CloneChildDemo();
            CloneDemo cloneDemo1 = (CloneDemo) cloneDemo.clone();
    
            System.out.println(cloneDemo.name);
            System.out.println(cloneDemo1.name);
            System.out.println(cloneDemo.hashCode());
            System.out.println(cloneDemo1.hashCode());
            System.out.println(cloneDemo == cloneDemo1);
            System.out.println("--------------");
            System.out.println(cloneDemo.cloneChildDemo == cloneDemo1.cloneChildDemo);
            System.out.println(cloneDemo.cloneChildDemo.hashCode());
            System.out.println(cloneDemo1.cloneChildDemo.hashCode());
        }
    /*
    辅助
    辅助
    460141958
    1163157884
    false
    ------------
    true
    159413332
    159413332
    */
    

    可以看到,使用 clone() 方法,从 == 和 hashCode 的不同可以看出,clone() 方法实则是真的创建了一个新的对象。
    但是,这只是一个浅拷贝操作。为什么?从最后对 cloneChildDemo的输出可以看到,cloneDemo 和cloneDemo1 的 child 对象,实际上还是指向了统一个对象,只对它的引用进行了传递。

    深拷贝:

    既然已经了解了对 clone() 方法,只能对当前对象进行浅拷贝,引用类型依然是在传递引用。
    那么我们如何进行一次深拷贝?
    1)序列化(serialization)这个对象,再反序列化回来,就可以得到这个新的对象,无非就是序列化的规则需要我们自己来写。
    2)继续利用 clone() 方法,既然 clone() 方法,是我们来重写的,实际上我们可以对其内的引用类型的变量,再进行一次 clone()。

    public class CloneDemo implements Cloneable {
        public String name;
        public CloneChildDemo cloneChildDemo;
    
        @Override
        public Object clone(){
            try {
                CloneDemo cloneDemo = (CloneDemo) super.clone();
                cloneDemo.cloneChildDemo = (CloneChildDemo) this.cloneChildDemo.clone();
                return cloneDemo;
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return null;
        }
     }
    

    验证深拷贝:

    public static void main(String[]args){
            CloneDemo cloneDemo = new CloneDemo();
            cloneDemo.name = "辅助";
            cloneDemo.cloneChildDemo = new CloneChildDemo();
            CloneDemo cloneDemo1 = (CloneDemo) cloneDemo.clone();
    
            System.out.println(cloneDemo.name);
            System.out.println(cloneDemo1.name);
            System.out.println(cloneDemo.hashCode());
            System.out.println(cloneDemo1.hashCode());
            System.out.println(cloneDemo == cloneDemo1);
            System.out.println("--------------");
            System.out.println(cloneDemo.cloneChildDemo == cloneDemo1.cloneChildDemo);
            System.out.println(cloneDemo.cloneChildDemo.hashCode());
            System.out.println(cloneDemo1.cloneChildDemo.hashCode());
        }
    /*
    辅助
    辅助
    1349393271
    1338668845
    false
    --------------
    false
    159413332
    1028214719
    */
    

    (3)使用建议:

    针对浅拷贝,我们不应该实现Cloneable接口,而应该使用拷贝构造器或者拷贝工厂。
    而对于深拷贝,还是推荐使用 clone() 方法,这样只需要每个类自己维护自己即可,而无需关心内部其他的对象中,其他的参数是否也需要 clone() 。

    三、探讨hashcode与equals的设计使用:

    我们先来看来两段代码:

    (1)不重写hashcode与equals:

    public class Person {
        private String name;
    
        public Person(String name) {
            this.name = name;
        }
        public static void main(String[]args){
            HashMap hm = new HashMap();
            Person person = new Person ("辅助");
            Person person1 = new Person ("辅助");
            hm.put(person , "a");
            hm.put(person1 , "b");
            System.out.println(hm.size());
        }
    }
    //输出size为2
    
    }
    
    

    (2)重写hashcode与equals:

    public class Person {
        private String name;
    
        public Person(String name) {
            this.name = name;
        }
    
        @Override
        public int hashCode(){
            return Objects.hash(name);
        }
        @Override
        public boolean equals(Object obj) {
            if(this == obj){
                return true;
            }
            if(obj instanceof Person){
                Person person=(Person)obj;
                if(this.name.equals(person.name)){
                    return true;
                }
            }
            return false;
        }
            public static void main(String[]args){
            HashMap hm = new HashMap();
            Person person = new Person ("辅助");
            Person person1 = new Person ("辅助");
            hm.put(person , "a");
            hm.put(person1 , "b");
            System.out.println(hm.size());
        }
    }
    //输出是1
    

    从上面可以看出,我们重写hashCode与equals方法时,注意自己的设计,取得不好只是会影响容器的效率以及影响到你插入重复key。

    (3)为什么会这样?

    首先,我们阅读源码知道HashMap等相关容器 的设计就是对象的hashCode进行比较,去重设计。
    然后我们思考一下,我们已经可以通过equals方法进行比较了,甚至原始的Objuct可以比较内存地址,那我们为什么还需要一个hashCode的去进行重写?
    试想一下,如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值。然后容器基于这个的hashCode值进行设计运作。这样的话,实际调用equals方法的次数就大大降低了。

    所以就两点原因:

    1)hashCode()方法存在的主要目的就是提高效率。
    2)在集合中判断两个对象相等的条件,其实无论是往集合中存数据,还是从集合中取数据,包括如果控制唯一性等,都是用这个条件判断的。


    结语

    好了,深入Java基础(一)--Object类分析讲完了。是自己大四开始回归博客世界的第一篇,继续总结分享,欢迎各位前来交流。


    更多文章点此处

    相关文章

      网友评论

        本文标题:深入Java基础(一)--Object类分析

        本文链接:https://www.haomeiwen.com/subject/lnubdqtx.html