java的Object类源码详解

java的Object类源码详解

类的定义

public class Object {

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

    public final native Class<?> getClass();

    public native int hashCode();

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

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

}

  Object 类属于 java.lang 包,此包下的所有类在使用时无需手动导入,系统会在程序编译期间自动导入。Object 类是所有类的基类,当一个类没有直接继承某个类时,默认继承Object类,也就是说任何类都直接或间接继承此类,Object 类中能访问的方法在所有类中都可以调用。

  我们知道类构造器是创建Java对象的途径之一,通过new 关键字调用构造器完成对象的实例化,还能通过构造器对对象进行相应的初始化。一个类必须要有一个构造器的存在,如果没有显示声明,那么系统会默认创造一个无参构造器,在JDK的Object类源码中,是看不到构造器的,系统会自动添加一个无参构造器。我们可以通过:

   Object obj = new Object();构造一个Object类的对象。

      registerNatives 方法,一个类定义了本地方法后,想要调用操作系统的实现,必须还要装载本地库,但是我们发现在 Object.class 类中具有很多本地方法,但是却没有看到本地库的载入代码。而且这是用 private 关键字声明的,在类外面根本调用不了,看到上面的代码,这就明白了吧。静态代码块就是一个类在初始化过程中必定会执行的内容,所以在类加载的时候是会执行该方法的,通过该方法来注册本地方法。

 通常很多面试题都会问 equals() 方法和 == 运算符的区别,== 运算符用于比较基本类型的值是否相同,或者比较两个对象的引用是否相等,而 equals 用于比较两个对象是否相等,这样说可能比较宽泛,两个对象如何才是相等的呢?这个标尺该如何定?

  可以看到,在 Object 类中,== 运算符和 equals 方法是等价的,都是比较两个对象的引用是否相等,从另一方面来讲,如果两个对象的引用相等,那么这两个对象一定是相等的。对于我们自定义的一个对象,如果不重写 equals 方法,那么在比较对象的时候就是调用 Object 类的 equals 方法,也就是用 == 运算符比较两个对象。

  在Java规范中,对 equals 方法的使用必须遵循以下几个原则:

  ①、自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。

  ②、对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。 

  ③、传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。

  ④、一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改

  ⑤、对于任何非空引用值 x,x.equals(null) 都应返回 false。

  下面我们自定义一个 Person 类,然后重写其equals 方法,比较两个 Person 对象:

 public class Person {
     private String pname;
     private int page;
 
     public Person(){}
 
     public Person(String pname,int page){
         this.pname = pname;
         this.page = page;
     }
     public int getPage() {
         return page;
     }
 
     public void setPage(int page) {
         this.page = page;
     }
 
     public String getPname() {
         return pname;
     }
 
     public void setPname(String pname) {
         this.pname = pname;
     }
     @Override
     public boolean equals(Object obj) {
         if(this == obj){//引用相等那么两个对象当然相等
             return true;
         }
         if(obj == null || !(obj instanceof  Person)){//对象为空或者不是Person类的实例
             return false;
         }
         Person otherPerson = (Person)obj;
         if(otherPerson.getPname().equals(this.getPname()) && otherPerson.getPage()==this.getPage()){
             return true;
         }
         return false;
     }
 
     public static void main(String[] args) {
         Person p1 = new Person("Tom",21);
         Person p2 = new Person("Marry",20);
         System.out.println(p1==p2);//false
         System.out.println(p1.equals(p2));//false
 
         Person p3 = new Person("Tom",21);
         System.out.println(p1.equals(p3));//true
     }
 
 }

  通过重写 equals 方法,我们自定义两个对象相等的标尺为Person对象的两个属性都相等,则对象相等,否则不相等。

  这时候有个Person 类的子类 Man,也重写了 equals 方法:

 public class Man extends Person{
     private String sex;
 
     public Man(String pname,int page,String sex){
         super(pname,page);
         this.sex = sex;
     }
     @Override
     public boolean equals(Object obj) {
         if(!super.equals(obj)){
             return false;
         }
         if(obj == null || !(obj instanceof  Man)){//对象为空或者不是Person类的实例
             return false;
         }
         Man man = (Man) obj;
         return sex.equals(man.sex);
     }
 
     public static void main(String[] args) {
         Person p = new Person("Tom",22);
         Man m = new Man("Tom",22,"男");
 
         System.out.println(p.equals(m));//true
         System.out.println(m.equals(p));//false
     }
 }

  通过打印结果我们发现 person.equals(man)得到的结果是 true,而man.equals(person)得到的结果却是false,这显然是不正确的。

  问题出现在 instanceof 关键字上,关于 instanceof 关键字的用法

  Man 是 Person 的子类,person instanceof Man 结果当然是false。这违反了我们上面说的对称性。

  实际上用 instanceof 关键字是做不到对称性的要求的。这里推荐做法是用 getClass()方法取代 instanceof 运算符。getClass() 关键字也是 Object 类中的一个方法,作用是返回一个对象的运行时类。

  那么 Person 类中的 equals 方法为:

    public boolean equals(Object obj) {
        if(this == obj){//引用相等那么两个对象当然相等
            return true;
        }
        if(obj == null || (getClass() != obj.getClass())){//对象为空或者不是Person类的实例
            return false;
        }
        Person otherPerson = (Person)obj;
        if(otherPerson.getPname().equals(this.getPname()) && otherPerson.getPage()==this.getPage()){
            return true;
        }
        return false;
    }

  打印结果 person.equals(man)得到的结果是 false,man.equals(person)得到的结果也是false,满足对称性。

  注意:使用 getClass 不是绝对的,要根据情况而定,毕竟定义对象是否相等的标准是由程序员自己定义的。而且使用 getClass 不符合多态的定义,比如 AbstractSet 抽象类,它有两个子类 TreeSet 和 HashSet,他们分别使用不同的算法实现查找集合的操作,但无论集合采用哪种方式实现,都需要拥有对两个集合进行比较的功能,如果使用 getClass 实现equals方法的重写,那么就不能在两个不同子类的对象进行相等的比较。而且集合类比较特殊,其子类是不需要自定义相等的概念的。

  所以什么时候使用 instanceof 运算符,什么时候使用 getClass() 有如下建议:

  ①、如果子类能够拥有自己的相等概念,则对称性需求将强制采用 getClass 进行检测。

  ②、如果有超类决定相等的概念,那么就可以使用 instanceof 进行检测,这样可以在不同的子类的对象之间进行相等的比较。

  下面给出一个完美的 equals 方法的建议:

  1、显示参数命名为 otherObject,稍后会将它转换成另一个叫做 other 的变量。

  2、判断比较的两个对象引用是否相等,如果引用相等那么表示是同一个对象,那么当然相等

  3、如果 otherObject 为 null,直接返回false,表示不相等

  4、比较 this 和 otherObject 是否是同一个类:如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测;如果所有的子类都有统一的定义,那么使用 instanceof 检测

  5、将 otherObject 转换成对应的类类型变量

  6、最后对对象的属性进行比较。使用 == 比较基本类型,使用 equals 比较对象。如果都相等则返回true,否则返回false。注意如果是在子类中定义equals,则要包含 super.equals(other)

  下面我们给出 Person 类中完整的 equals 方法的书写:

      @Override
      public boolean equals(Object otherObject) {
          //1、判断比较的两个对象引用是否相等,如果引用相等那么表示是同一个对象,那么当然相等
         if(this == otherObject){
              return true;
          }
         //2、如果 otherObject 为 null,直接返回false,表示不相等
         if(otherObject == null ){//对象为空或者不是Person类的实例
             return false;
         }
         //3、比较 this 和 otherObject 是否是同一个类(注意下面两个只能使用一种)
         //3.1:如果 equals 的语义在每个子类中所有改变,就使用 getClass 检测
         if(this.getClass() != otherObject.getClass()){
             return false;
         }
         //3.2:如果所有的子类都有统一的定义,那么使用 instanceof 检测
         if(!(otherObject instanceof Person)){
             return false;
         }
 
         //4、将 otherObject 转换成对应的类类型变量
         Person other = (Person) otherObject;
 
         //5、最后对对象的属性进行比较。使用 == 比较基本类型,使用 equals 比较对象。如果都相等则返回true,否则返回false
         //   使用 Objects 工具类的 equals 方法防止比较的两个对象有一个为 null而报错,因为 null.equals() 是会抛异常的
         return Objects.equals(this.pname,other.pname) && this.page == other.page;
 
         //6、注意如果是在子类中定义equals,则要包含 super.equals(other)
         //return super.equals(other) && Objects.equals(this.pname,other.pname) && this.page == other.page;
 
     }

请注意,无论何时重写此方法,通常都必须重写hashCode方法,以维护hashCode方法的一般约定,该方法声明相等对象必须具有相同的哈希代码。

getClass()在 Object 类中如下,作用是返回对象的运行时类。

public class Son extends Parent{}

@Test
public void testClass(){
    Parent p = new Son();
    System.out.println(p.getClass());
    System.out.println(Parent.class);
}

  打印结果:

  

  结论:class 是一个类的属性,能获取该类编译时的类对象,而 getClass() 是一个类的方法,它是获取该类运行时的类对象。

  还有一个需要大家注意的是,虽然Object类中getClass() 方法声明是:public final native Class<?> getClass();返回的是一个 Class<?>,但是如下是能通过编译的:

Class<? extends String> c = "".getClass();

  也就是说类型为T的变量getClass方法的返回值类型其实是Class<? extends T>而非getClass方法声明中的Class<?>。

        hashCode方法也是native方法,作用是返回对象的散列码,是 int 类型的数值。

  那么这个方法存在的意义是什么呢?

  我们知道在Java 中有几种集合类,比如 List,Set,还有 Map,List集合一般是存放的元素是有序可重复的,Set 存放的元素则是无序不可重复的,而 Map 集合存放的是键值对。

  前面我们说过判断一个元素是否相等可以通过 equals 方法,没增加一个元素,那么我们就通过 equals 方法判断集合中的每一个元素是否重复,但是如果集合中有10000个元素了,但我们新加入一个元素时,那就需要进行10000次equals方法的调用,这显然效率很低。

  于是,Java 的集合设计者就采用了 哈希表 来实现。关于哈希表的数据结构我有过介绍。哈希算法也称为散列算法,是将数据依特定算法产生的结果直接指定到一个地址上。这个结果就是由 hashCode 方法产生。这样一来,当集合要添加新的元素时,先调用这个元素的 hashCode 方法,就一下子能定位到它应该放置的物理位置上。

  ①、如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;

  ②、如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了;

  ③、不相同的话,也就是发生了Hash key相同导致冲突的情况,那么就在这个Hash key的地方产生一个链表,将所有产生相同HashCode的对象放到这个单链表上去,串在一起(很少出现)。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

  这里有 A,B,C,D四个对象,分别通过 hashCode 方法产生了三个值,注意 A 和 B 对象调用 hashCode 产生的值是相同的,即 A.hashCode() = B.hashCode() = 0x001,发生了哈希冲突,这时候由于最先是插入了 A,在插入的B的时候,我们发现 B 是要插入到 A 所在的位置,而 A 已经插入了,这时候就通过调用 equals 方法判断 A 和 B 是否相同,如果相同就不插入 B,如果不同则将 B 插入到 A 后面的位置。所以对于 equals 方法和 hashCode 方法有如下要求:

  一、hashCode 要求

  ①、在程序运行时期间,只要对象的(字段的)变化不会影响equals方法的决策结果,那么,在这个期间,无论调用多少次hashCode,都必须返回同一个散列码。

  ②、通过equals调用返回true 的2个对象的hashCode一定一样。

  ③、通过equasl返回false 的2个对象的散列码不需要不同,也就是他们的hashCode方法的返回值允许出现相同的情况。

  因此我们可以得到如下推论:

  两个对象相等,其 hashCode 一定相同;

  两个对象不相等,其 hashCode 有可能相同;

  hashCode 相同的两个对象,不一定相等;

  hashCode 不相同的两个对象,一定不相等;

   这四个推论通过上图可以更好的理解。

  可能会有人疑问,对于不能重复的集合,为什么不直接通过 hashCode 对于每个元素都产生唯一的值,如果重复就是相同的值,这样不就不需要调用 equals 方法来判断是否相同了吗?
  实际上对于元素不是很多的情况下,直接通过 hashCode 产生唯一的索引值,通过这个索引值能直接找到元素,而且还能判断是否相同。比如数据库存储的数据,ID 是有序排列的,我们能通过 ID 直接找到某个元素,如果新插入的元素 ID 已经有了,那就表示是重复数据,这是很完美的办法。但现实是存储的元素很难有这样的 ID 关键字,也就很难这种实现 hashCode 的唯一算法,再者就算能实现,但是产生的 hashCode 码是非常大的,这会大的超过 Java 所能表示的范围,很占内存空间,所以也是不予考虑的。

  二、hashCode 编写指导:

  ①、不同对象的hash码应该尽量不同,避免hash冲突,也就是算法获得的元素要尽量均匀分布。

  ②、hash 值是一个 int 类型,在Java中占用 4 个字节,也就是 232 次方,要避免溢出。

  在 JDK 的 Integer类,Float 类,String 类等都重写了 hashCode 方法,我们自定义对象也可以参考这些类来写。

  下面是 JDK String 类的hashCode 源码:

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

  再次提醒大家,对于 Map 集合,我们可以选取Java中的基本类型,还有引用类型 String 作为 key,因为它们都按照规范重写了 equals 方法和 hashCode 方法。但是如果你用自定义对象作为 key,那么一定要覆写 equals 方法和 hashCode 方法,不然会有意想不到的错误产生。

     toString方法,getClass().getName()是返回对象的全类名(包含包名),Integer.toHexString(hashCode()) 是以16进制无符号整数形式返回此哈希码的字符串表示形式。

  打印某个对象时,默认是调用 toString 方法,比如 System.out.println(person),等价于 System.out.println(person.toString())

  
    protected native Object clone() throws CloneNotSupportedException;

    public final native void notify();

    public final native void notifyAll();

    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);
    }

    protected void finalize() throws Throwable { }

        Java的深拷贝与浅拷贝,简单来说就是创建一个和已知对象一模一样的对象。

        创建对象的5种方式

  ①、通过 new 关键字

  这是最常用的一种方式,通过 new 关键字调用类的有参或无参构造方法来创建对象。比如 Object obj = new Object();

  ②、通过 Class 类的 newInstance() 方法

  这种默认是调用类的无参构造方法创建对象。比如 Person p2 = (Person) Class.forName("com.ys.test.Person").newInstance();

  ③、通过 Constructor 类的 newInstance 方法

  这和第二种方法类时,都是通过反射来实现。通过 java.lang.relect.Constructor 类的 newInstance() 方法指定某个构造器来创建对象。

  Person p3 = (Person) Person.class.getConstructors()[0].newInstance();

  实际上第二种方法利用 Class 的 newInstance() 方法创建对象,其内部调用还是 Constructor 的 newInstance() 方法。

  ④、利用 Clone 方法

  Clone 是 Object 类中的一个方法,通过 对象A.clone() 方法会创建一个内容和对象 A 一模一样的对象 B,clone 克隆,顾名思义就是创建一个一模一样的对象出来。

  Person p4 = (Person) p3.clone();

clone方法的作用就是复制对象,产生一个新的对象。那么这个新的对象和原对象是什么关系呢?

 在 Java 中数据类型可以分为两大类:基本类型和引用类型。

  基本类型也称为值类型,分别是字符类型 char,布尔类型 boolean以及数值类型 byte、short、int、long、float、double。

  引用类型则包括类、接口、数组、枚举等。

  Java 将内存空间分为堆和栈。基本类型直接在栈中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆中,通过栈中的引用指向堆中存放的数据。

  上图定义的 a 和 b 都是基本类型,其值是直接存放在栈中的;而 c 和 d 是 String 声明的,这是一个引用类型,引用地址是存放在 栈中,然后指向堆的内存空间。

  下面 d = c;这条语句表示将 c 的引用赋值给 d,那么 c 和 d 将指向同一块堆内存空间。

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

public class Person implements Cloneable{
    public String pname;
    public int page;
    public Address address;
    public Person() {}
    
    public Person(String pname,int page){
        this.pname = pname;
        this.page = page;
        this.address = new Address();
    }
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Address {
    private String provices;
    private String city;
    public void setAddress(String provices,String city){
        this.provices = provices;
        this.city = city;
    }
    @Override
    public String toString() {
        return "Address [provices=" + provices + ", city=" + city + "]";
    }
    
}

  这是一个我们要进行赋值的原始类 Person。下面我们产生一个 Person 对象,并调用其 clone 方法复制一个新的对象。

  注意:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。

@Test
public void testShallowClone() throws Exception{
    Person p1 = new Person("zhangsan",21);
    p1.setAddress("湖北省", "武汉市");
    Person p2 = (Person) p1.clone();
    System.out.println("p1:"+p1);
    System.out.println("p1.getPname:"+p1.getPname().hashCode());
    
    System.out.println("p2:"+p2);
    System.out.println("p2.getPname:"+p2.getPname().hashCode());
    
    p1.display("p1");
    p2.display("p2");
    p2.setAddress("湖北省", "荆州市");
    System.out.println("将复制之后的对象地址修改:");
    p1.display("p1");
    p2.display("p2");
@Test
public void testShallowClone() throws Exception{
    Person p1 = new Person("zhangsan",21);
    p1.setAddress("湖北省", "武汉市");
    Person p2 = (Person) p1.clone();
    System.out.println("p1:"+p1);
    System.out.println("p1.getPname:"+p1.getPname().hashCode());
    
    System.out.println("p2:"+p2);
    System.out.println("p2.getPname:"+p2.getPname().hashCode());
    
    p1.display("p1");
    p2.display("p2");
    p2.setAddress("湖北省", "荆州市");
    System.out.println("将复制之后的对象地址修改:");
    p1.display("p1");
    p2.display("p2");
}

  打印结果为:

  

  首先看原始类 Person 实现 Cloneable 接口,并且覆写 clone 方法,它还有三个属性,一个引用类型 String定义的 pname,一个基本类型 int定义的 page,还有一个引用类型 Address ,这是一个自定义类,这个类也包含两个属性 pprovices 和 city 。

  接着看测试内容,首先我们创建一个Person 类的对象 p1,其pname 为zhangsan,page为21,地址类 Address 两个属性为 湖北省和武汉市。接着我们调用 clone() 方法复制另一个对象 p2,接着打印这两个对象的内容。

  从第 1 行和第 3 行打印结果:

  p1:com.ys.test.Person@349319f9

  p2:com.ys.test.Person@258e4566

  可以看出这是两个不同的对象。

  从第 5 行和第 6 行打印的对象内容看,原对象 p1 和克隆出来的对象 p2 内容完全相同。

  代码中我们只是更改了克隆对象 p2 的属性 Address 为湖北省荆州市(原对象 p1 是湖北省武汉市) ,但是从第 7 行和第 8 行打印结果来看,原对象 p1 和克隆对象 p2 的 Address 属性都被修改了。

  也就是说对象 Person 的属性 Address,经过 clone 之后,其实只是复制了其引用,他们指向的还是同一块堆内存空间,当修改其中一个对象的属性 Address,另一个也会跟着变化。

  深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。

  深拷贝的原理我们知道了,就是要让原始对象和克隆之后的对象所具有的引用类型属性不是指向同一块堆内存,这里有两种实现思路。

  ①、让每个引用类型属性内部都重写clone() 方法

  既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,但是String类型有点特殊,后面会详细讲解),我们在 Address 类内部也重写 clone 方法。如下:

  Address.class:

 public class Address implements Cloneable{
     private String provices;
     private String city;
     public void setAddress(String provices,String city){
         this.provices = provices;
         this.city = city;
     }
     @Override
     public String toString() {
         return "Address [provices=" + provices + ", city=" + city + "]";
     }
     @Override
     protected Object clone() throws CloneNotSupportedException {
         return super.clone();
     }
     
 }

  Person.class 的 clone() 方法:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        p.address = (Address) address.clone();
        return p;
    }

  测试还是和上面一样,我们会发现更改了p2对象的Address属性,p1 对象的 Address 属性并没有变化。

  但是这种做法有个弊端,这里我们Person 类只有一个 Address 引用类型,而 Address 类没有,所以我们只用重写 Address 类的clone 方法,但是如果 Address 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。

  ②、利用序列化

  序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。

  注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。

//深度拷贝
public Object deepClone() throws Exception{
    // 序列化
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);

    oos.writeObject(this);

    // 反序列化
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);

    return ois.readObject();
}

   因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。

/* 通过序列化实现深拷贝 */
public class DeepCopyBySerialization {
    public static void main(String[] args) throws IOException, ClassNotFoundException  {
        Age a=new Age(20);
        Student stu1=new Student("摇头耶稣",a,175);
        //通过序列化方法实现深拷贝
        ByteArrayOutputStream bos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(bos);
        oos.writeObject(stu1);
        oos.flush();
        ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        Student stu2=(Student)ois.readObject();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        System.out.println();
        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("大傻子");
        //改变age这个引用类型的成员变量的值
        a.setAge(99);
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}
class Age implements Serializable{
}
class Student implements Serializable{
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;
    //构造方法,其中一个参数为另一个类的对象
    public Student(String name,Age a,int length) {
        this.name=name;
        this.aage=a;
        this.length=length;
    }
}

运行结果为:

姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175
姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: 摇头耶稣, 年龄为: 20, 长度是: 175

可以通过很简洁的代码即可完美实现深拷贝。不过要注意的是,如果某个属性被transient修饰,那么该属性就无法被拷贝了。

  ⑤、反序列化

  序列化是把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。

为什么要做序列化?

  1、在分布式系统中,此时需要把对象在网络上传输,就得把对象数据转换为二进制形式,需要共享的数据的 JavaBean 对象,都得做序列化。

  2、服务器钝化:如果服务器发现某些对象好久没活动了,那么服务器就会把这些内存中的对象持久化在本地磁盘文件中(Java对象转换为二进制文件);如果服务器发现某些对象需要活动时,先去内存中寻找,找不到再去磁盘文件中反序列化我们的对象数据,恢复成 Java 对象。这样能节省服务器内存。

Java 怎么进行序列化?

  1、需要做序列化的对象的类,必须实现序列化接口:Java.lang.Serializable 接口(这是一个标志接口,没有任何抽象方法),Java 中大多数类都实现了该接口,比如:String,Integer

  2、底层会判断,如果当前对象是 Serializable 的实例,才允许做序列化,Java对象 instanceof Serializable 来判断。

  3、在 Java 中使用对象流来完成序列化和反序列化

    ObjectOutputStream:通过 writeObject()方法做序列化操作

    ObjectInputStream:通过 readObject() 方法做反序列化操作

 第一步:创建一个 JavaBean 对象

public class Person implements Serializable{
    private String name;
    private int age;
     
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
} 

第二步:使用 ObjectOutputStream 对象实现序列化

//在根目录下新建一个 io 的文件夹
        OutputStream op = new FileOutputStream("io"+File.separator+"a.txt");
        ObjectOutputStream ops = new ObjectOutputStream(op);
        ops.writeObject(new Person("vae",1));
         
        ops.close();

  我们打开 a.txt 文件,发现里面的内容乱码,注意这不需要我们来看懂,这是二进制文件,计算机能读懂就行了。

错误一:如果新建的 Person 对象没有实现 Serializable 接口,那么上面的操作会报错:

    

第三步:使用ObjectInputStream 对象实现反序列化

  反序列化的对象必须要提供该对象的字节码文件.class

InputStream in = new FileInputStream("io"+File.separator+"a.txt");
        ObjectInputStream os = new ObjectInputStream(in);
        byte[] buffer = new byte[10];
        int len = -1;
        Person p = (Person) os.readObject();
        System.out.println(p);  //Person [name=vae, age=1]
        os.close();

问题1:如果某些数据不需要做序列化,比如密码,比如上面的年龄?

解决办法:在字段面前加上 transient

private String name;//需要序列化
    transient private int age;//不需要序列化

  那么我们在反序列化的时候,打印出来的就是Person [name=vae, age=0],整型数据默认值为 0 

问题2:序列化版本问题,在完成序列化操作后,由于项目的升级或修改,可能我们会对序列化对象进行修改,比如增加某个字段,那么我们在进行反序列化就会报错:

解决办法:在 JavaBean 对象中增加一个 serialVersionUID 字段,用来固定这个版本,无论我们怎么修改,版本都是一致的,就能进行反序列化了

private static final long serialVersionUID = 8656128222714547171L;

       finalize方法是Object提供的的实例方法,该方法用于垃圾回收,一般由 JVM 自动调用,一般不需要程序员去手动调用该方法。使用规则如下:

当对象不再被任何对象引用时,GC会调用该对象的finalize()方法
finalize()是Object的方法,子类可以覆盖这个方法来做一些系统资源的释放或者数据的清理
可以在finalize()让这个对象再次被引用,避免被GC回收;但是最常用的目的还是做cleanup
Java不保证这个finalize()一定被执行;但是保证调用finalize的线程没有持有任何user-visible同步锁。
在finalize里面抛出的异常会被忽略,同时方法终止。
当finalize被调用之后,JVM会再一次检测这个对象是否能被存活的线程访问得到,如果不是,则清除该对象。也就是finalize只能被调用一次;也就是说,覆盖了finalize方法的对象需要经过两个GC周期才能被清除。

     notify()/notifyAll()/wait(),这是用于多线程之间的通信方法。

关于让线程等待和唤醒线程的方法,如下:(这是 Object 类中的方法)

  

wait():执行该方法的线程对象,释放同步锁,JVM会把该线程放到等待池中,等待其他线程唤醒该线程

notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待(注意锁池和等待池的区别)

notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待。

注意:上述方法只能被同步监听锁对象来调用,这也是为啥wait() 和 notify()方法都在 Object 对象中,因为同步监听锁可以是任意对象,只不过必须是需要同步线程的共同对象即可,否则别的对象调用会报错:        java.lang.IllegalMonitorStateException

假设 A 线程和 B 线程同时操作一个 X 对象,A,B 线程可以通过 X 对象的 wait() 和 notify() 方法来进行通信,流程如下:

①、当线程 A 执行 X 对象的同步方法时,A 线程持有 X 对象的 锁,B线程在 X 对象的锁池中等待

②、A线程在同步方法中执行 X.wait() 方法时,A线程释放 X 对象的锁,进入 X 对象的等待池中

③、在 X 对象的锁池中等待锁的 B 线程获得 X 对象的锁,执行 X 的另一个同步方法

④、B 线程在同步方法中执行 X.notify() 方法,JVM 把 A 线程从等待池中移动到 X 对象的锁池中,等待获取锁

⑤、B 线程执行完同步方法,释放锁,等待获取锁的 A 线程获得锁,继续执行同步方法

生产者和消费者模型为:

    生产者Producer 生产某个对象(共享资源),放在缓冲池中,然后消费者从缓冲池中取出这个对象。也就是生产者生产一个,消费者取出一个。这样进行循环。

 第一步:我们先创建共享资源的类 Person,它有两个方法,一个生产对象,一个消费对象

public class Person {
    private String name;
    private int age;
     
    //表示共享资源对象是否为空,如果为 true,表示需要生产,如果为 false,则有数据了,不要生产
    private boolean isEmpty = true;
    /**
     * 生产数据
     * @param name
     * @param age
     */
    public synchronized void push(String name,int age){
        try {
            //不能用 if,因为可能有多个线程
            while(!isEmpty){//进入到while语句内,说明 isEmpty==false,那么表示有数据了,不能生产,必须要等待消费者消费
                this.wait();//导致当前线程等待,进入等待池中,只能被其他线程唤醒
            }
             
            //-------生产数据开始-------
            this.name = name;
            //延时代码
            Thread.sleep(10);
            this.age = age;
            //-------生产数据结束-------
            isEmpty = false;//设置 isEmpty 为 false,表示已经有数据了
            this.notifyAll();//生产完毕,唤醒所有消费者
        } catch (Exception e) {
            e.printStackTrace();
        }
         
    }
    /**
     * 取数据,消费数据
     * @return
     */
    public synchronized void pop(){
        try {
            //不能用 if,因为可能有多个线程
            while(isEmpty){//进入 while 代码块,表示 isEmpty==true,表示为空,等待生产者生产数据,消费者要进入等待池中
                this.wait();//消费者线程等待
            }
            //-------消费开始-------
            Thread.sleep(10);
            System.out.println(this.name+"---"+this.age);
            //-------消费结束------
            isEmpty = true;//设置 isEmpty为true,表示需要生产者生产对象
            this.notifyAll();//消费完毕,唤醒所有生产者
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  第二步:创建生产者线程,并在 run() 方法中生产50个对象

public class Producer implements Runnable{
    //共享资源对象
    Person p = null;
    public Producer(Person p){
        this.p = p;
    }
    @Override
    public void run() {
        //生产对象
        for(int i = 0 ; i < 50 ; i++){
            //如果是偶数,那么生产对象 Tom--11;如果是奇数,则生产对象 Marry--21
            if(i%2==0){
                p.push("Tom", 11);
            }else{
                p.push("Marry", 21);
            }
        }
    }
}

  第三步:创建消费者线程,并在 run() 方法中消费50个对象

public class Consumer implements Runnable{
    //共享资源对象
    Person p = null;
    public Consumer(Person p) {
        this.p = p;
    }
     
    @Override
    public void run() {
        for(int i = 0 ; i < 50 ; i++){
            //消费对象
            p.pop();
        }
    }
}

  由于我们的模型是生产一个,马上消费一个,那期望的结果便是 Tom---11,Marry--21,Tom---11,Mary---21......   连续这样交替出现50次。

发布了370 篇原创文章 · 获赞 88 · 访问量 29万+

猜你喜欢

转载自blog.csdn.net/qq_35029061/article/details/100220554