Java基础22:Cloneable和Serializable接口

一、Cloneable接口

在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同的新对象B,并且此后对B任何改动都不会影响到A中的值。也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。在Java语言中,用简单的赋值语句是不能满足这种需求的,需要使用clone。

clone:它允许在堆中克隆出一块和原对象一样的对象,并将这个对象的地址赋予新的引用。简而言之,克隆就是快速构造一个和已有对象相同的副本。

Java中的类要实现clone功能必须实现java.lang.Cloneable接口。Cloneable接口属于合法标志性接口(接口内不含有任何方法),只有实现这个接口后,然后在类中重写Object中的clone()方法,然后通过类的实例对象调用clone()方法才能克隆成功,否则就会抛出CloneNotSupportedException异常。

Java中所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone(),这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用;二是拷贝对象与用 new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。

如果一个类重写了 Object 内定义的 clone()方法 ,需要同时实现 Cloneable 接口(虽然这个接口内并没有定义 clone() 方法),否则会抛出异常,也就是说, Cloneable接口只是个合法调用 clone() 的标识(marker-interface)。

class CloneClass implements Cloneable
{
  public int aInt;

   //重写Object中的clone方法,并声明为public
  public Object clone()
  {
   CloneClass o = null;
   try
    {
     o = (CloneClass)super.clone();//调用父类Object的clone()方法
   }
     catch(CloneNotSupportedException e)
    {
      e.printStackTrace();
   }
   return o;
 }
}

1、有三个值得注意的地方:

a>为了实现clone功能,CloneClass类实现了Cloneable接口,这个接口属于java.lang 包,java.lang包已经被缺省的导入类中,所以不需要写成java.lang.Cloneable;

b>重写java.lang.Object.clone()方法,Object类中的clone()方法是一个protected属性的方法。这也意味着如果要应用clone()方法,必须继承Object类,在 Java中所有的类是缺省继承Object类的,也就不用关心这点了。

这里有一个疑问,Object中的clone方法是一个空的方法,那么他是如何判断类是否实现了cloneable接口呢?

原因在于这个方法中有一个native关键字修饰。native修饰的方法都是空的方法,但是这些方法都是有实现体的(这里也就间接说明了native关键字不能与abstract同时使用。因为abstract修饰的方法与java的接口中的方法类似,他显式的说明了修饰的方法,在当前是没有实现体的,abstract的方法的实现体都由子类重写),只不过native方法调用的实现体,都是非java代码编写的(调用的是在jvm中编写的C的接口),每一个native方法在jvm中都有一个同名的实现体,native方法在逻辑上的判断都是由实现体实现的,另外这种native修饰的方法对返回类型,异常控制等都没有约束。

 由此可见,这里判断是否实现cloneable接口,是在调用jvm中的实现体时进行判断的。

总结:Object类的clone()方法是一个native方法,native方法的效率一般来说都是远高于java中的非native方法。这也解释了为 什么要用Object中clone()方法而不是先new一个对象,然后把原始对象中的信息赋到新对象中,虽然这也实现了clone功能,但因为这个实例的创建过程十分复杂,在执行过程中会消耗大量的时间,所以导致效率较低。

c>为了让其它类能调用这个clone 类的clone()方法,重载之后要把clone()方法的属性设置为public。

2、深入理解深度克隆与浅度克隆

首先,在Java中创建对象的方式有四种:

        一种是new,通过new关键字在堆中为对象开辟空间,在执行new时,首先会看所要创建的对象的类型,知道了类型,才能知道需 要给这个对象分配多大的内存区域,分配内存后,调用对象的构造函数,填充对象中各个变量的值,将对象初始化,然后通过构造方法返回对象的地址;

      另一种是clone,clone也是首先分配内存,这里分配的内存与调用clone方法对象的内存相同,然后将源对象中各个变量的值,填充到新的对象中,填充完成后,clone方法返回一个新的地址,这个新地址的对象与源对象相同,只是地址不同。

另外还有输入输出流,反射构造对象等

深度克隆和浅度克隆,这东西虽然平常不怎么用,但是了解一下还是有必要的。Object中的克隆方法是浅度克隆,JDK规定了克隆需要满足的一些条件,简要总结一下就是:对某个对象进行克隆,对象的的成员变量如果包括引用类型或者数组,那么克隆的时候其实是不会把这些对象也带着复制到克隆出来的对象里面的,只是复制一个引用,这个引用指向被克隆对象的成员对象,但是基本数据类型是会跟着被带到克隆对象里面去的。而深度可能就是把对象的所有属性都统统复制一份新的到目标对象里面去。

如下图所示:

 

Java实现深度克隆的简单方法使用Java的流,先将对象序列化,然后序列化回对象,其中的限制为克隆的对象必须实现Serializable接口.。

Java实现深度克隆的简单方法


二、Serializable接口

Serializable接口中一个成员函数或者成员变量也没有,这个接口的作用就是实现序列化,那什么是序列化?

1、序列化

Java提供了一种保存对象状态的机制,那就是序列化。

对象的寿命通常随着生成该对象的程序的终止而终止,而有时候需要把在内存中的各种对象的状态(也就是实例变量,不是方法)保存下来,并且可以在需要时再将对象恢复。

Java 序列化技术可以将一个对象的状态写入一个Byte 流里(序列化),并且可以从其它地方把该Byte 流里的数据读出来(反序列化)。

2、什么时候需要序列化

想把内存中的对象状态保存到一个文件中或者数据库中时候;
想把对象通过网络进行传播的时候。

3、如何序列化

只要一个类实现Serializable接口,那么这个类就可以序列化了。

举个栗子:

class Person implements Serializable
{   
    //一会就说这个是做什么的
    private static final long serialVersionUID = 1L; 

    String name;
    int age;
    public Person(String name,int age){
        this.name = name;
        this.age = age;
    }   
    public String toString(){
        return "name:"+name+"\tage:"+age;
    }
}

通过ObjectOutputStream 的writeObject()方法把这个类的对象写到一个地方(文件),再通过ObjectInputStream 的readObject()方法把这个对象读出来。

File file = new File("file"+File.separator+"out.txt");

    /*
     * 1、序列化
     */
    FileOutputStream fos = null;
    try {
        fos = new FileOutputStream(file);
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(fos);
            Person person = new Person("tom", 22);
            // 调用 person的 tostring() 方法
            System.out.println(person);
            //写入对象
            oos.writeObject(person);            
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                oos.close();
            } catch (IOException e) {
                System.out.println("oos关闭失败:"+e.getMessage());
            }
        }
    } catch (FileNotFoundException e) {
        System.out.println("找不到文件:"+e.getMessage());
    } finally{
        try {
            fos.close();
        } catch (IOException e) {
            System.out.println("fos关闭失败:"+e.getMessage());
        }
    }

   /*
     *2、反序列化
     */
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(file);
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(fis);
            try {
                Person person = (Person)ois.readObject();   //读出对象
                System.out.println(person);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } 
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                ois.close();
            } catch (IOException e) {
                System.out.println("ois关闭失败:"+e.getMessage());
            }
        }
    } catch (FileNotFoundException e) {
        System.out.println("找不到文件:"+e.getMessage());
    } finally{
        try {
            fis.close();
        } catch (IOException e) {
            System.out.println("fis关闭失败:"+e.getMessage());
        }
    }

运行结果:

name:tom    age:22
name:tom    age:22

结果完全一样;如果把Person类中的implements Serializable 去掉,Person类就不能序列化了,此时再运行上述程序,就会报java.io.NotSerializableException异常。

4、serialVersionUID

注意到上面程序中有一个 serialVersionUID ,实现了Serializable接口之后,Eclipse就会提示你增加一个 serialVersionUID,虽然不加的话上述程序依然能够正常运行。

序列化 ID 在 Eclipse 下提供了两种生成策略

一个是固定的 1L
一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具,根据类名、接口名、成员方法及属性等来生成)
上面程序中,输出对象和读入对象使用的是同一个Person类。

如果是通过网络传输的话,如果Person类的serialVersionUID不一致,那么反序列化就不能正常进行。例如在客户端A中Person类的serialVersionUID=1L,而在客户端B中Person类的serialVersionUID=2L, 那么就不能重构这个Person对象。

试图重构就会报java.io.InvalidClassException异常,因为这两个类的版本不一致,local class incompatible,重构就会出现错误。如果没有特殊需求的话,使用用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。

5、transient关键字

经常在实现了 Serializable接口的类中能看见transient关键字。 transient关键字的作用是:阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。

当某些变量不想被序列化,同是又不适合使用static关键字声明,那么此时就需要用transient关键字来声明该变量。

例如用 transient关键字 修饰name变量

class Person implements Serializable{   

    private static final long serialVersionUID = 1L;

    transient String name;
    int age;

    public Person(String name,int age){
        this.name = name;
        this.age = age;
    }   
    public String toString(){
        return "name:"+name+"\tage:"+age;
    }
}

在反序列化视图重构对象的时候,作用与static变量一样, 输出结果为:

name:null   age:22

在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

注:对于某些类型的属性,其状态是瞬时的,这样的属性是无法保存其状态的。例如一个线程属性或需要访问IO、本地资源、网络资源等的属性,对于这些字段,我们必须用transient关键字标明,否则编译器将报措。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java架构何哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值