java面试--java序列化及克隆

1、java序列化

序列化介绍:

序列化:将java对象写入字节流;反序列化:从字节流中恢复对象

意义:序列化机制允许将实现序列化的java对象装换位字节序列,这些字节序列可以保存在磁盘,数据库上,或者通过网络传输以达到以后恢复成原来的java对象。序列化机制可以使对象脱离程序的运行而独立存在(脱离JVM的生命周期)

使用场景:

  1. 永久性保存对象。保存对象的字节序列到本地文件的或者数据库中。
  2. 通过序列化,以字节流的形式使对象在网络中记性传递与接收。建议创建的每一个javaBean类都实现Seriallizeable接口
  3. 通过序列化在进程之间传递对象

java实现序列化的方式

1、Seriallizable

seriallizable是一个标记接口,不用于实现任何方法。一旦实现了此接口,该类的对象就是可序列化的对象

a、序列化步骤:

  1. 创建一个ObjectOutputStream输出流
  2. 调用ObjectOutputStream对象的writeObject输出可序列化对象
@SuppressWarnings("unchecked")  
public class Main {  
	public static void main(String[] args) throws IOException {
		Test t=new Test();
		t.setAge(10);
		t.setName("cyc");
		ObjectOutputStream oo=new ObjectOutputStream(new FileOutputStream("D:/test.txt"));
		oo.writeObject(t);
		oo.close();
	} 
}
class Test 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 "Test [name=" + name + ", age=" + age + "]";
	}
	

b、反序列化步骤[反序列化并不会调用构造方法。反序列的对象是由JVM自己生成的对象,不通过构造方法生成]:

  1. 创建一个ObjectInputStream输入流
  2. 调用ObjectInputStrean对象的readObject得到序列化的对象
@SuppressWarnings("unchecked")  
public class Main {  
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		ObjectInputStream oi=new ObjectInputStream(new FileInputStream("D:/test.txt"));
		Test t=(Test) oi.readObject();
		System.out.println(t.toString());
		oi.close();
	} 
}
class Test 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 "Test [name=" + name + ", age=" + age + "]";
	}
	
}

c、成员是引用的序列化:如果一个可序列化的类的成员既不是基本类型也不是String类型,那这个引用类型必须是可以序列化的(实现序列化接口),否则会导致此类不能序列化。

d、同一对象实例化多次的机制:Java序列化同一对象,并不会将此对象序列化多次得到多个对象[只会序列化一次]。在java序列化时,所有保存到磁盘的对象都有一个系列化编号;当程序试图序列化一个对象的时候,会先检查此对象是否已经被序列化过,只有此对象没有被序列化(在一个JVM内),才会将此对象序列化为字节序列输出。

//序列化对象
public class WriteTeacher {
    public static void main(String[] args) throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
            Person person = new Person("路飞", 20);
            Teacher t1 = new Teacher("雷利", person);
            Teacher t2 = new Teacher("红发香克斯", person);
            //依次将4个对象写入输入流
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(person);
            oos.writeObject(t2);
        }
    }
}

//反序列化对象
public class ReadTeacher {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) {
            Teacher t1 = (Teacher) ois.readObject();
            Teacher t2 = (Teacher) ois.readObject();
            Person p = (Person) ois.readObject();
            Teacher t3 = (Teacher) ois.readObject();
            System.out.println(t1 == t2);
            System.out.println(t1.getPerson() == p);
            System.out.println(t2.getPerson() == p);
            System.out.println(t2 == t3);
            System.out.println(t1.getPerson() == t2.getPerson());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//输出结果
//false
//true
//true
//true
//true

 e、序列化算法的潜在问题:由于java序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。

f、 自定义序列化:

  1. 使用transient关键字,transient修饰的变量不会进行序列化。使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
  2. 重写writeObject与readObject方法:通过重写writeObject与readObject方法,可以自己选择哪些属性需要序列化, 哪些属性不需要。如果writeObject使用某种规则序列化,则相应的readObject需要相反的规则反序列化,以便能正确反序列化出对象。这里展示对名字进行反转加密。
    private void writeObject(java.io.ObjectOutputStream out) throws IOException;
    private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
    private void readObjectNoData() throws ObjectStreamException;
    
    
    public class Person implements Serializable {
       private String name;
       private int age;
       //省略构造方法,get及set方法
    
       private void writeObject(ObjectOutputStream out) throws IOException {
           //将名字反转写入二进制流
           out.writeObject(new StringBuffer(this.name).reverse());
           out.writeInt(age);
       }
    
       private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
           //将读出的字符串反转恢复回来
           this.name = ((StringBuffer)ins.readObject()).reverse().toString();
           this.age = ins.readInt();
       }
    }

     

  3. 更彻底的自定义序列化:writeReplace:在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象;readResolve:反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用。,通常用于反序列单例类i,来保证单例类的唯一性。

2、Externalizable:强制自定义序列化

通过实现Externalizable接口,必须实现writeExternal、readExternal方法。

public class ExPerson implements Externalizable {

    private String name;
    private int age;
    //注意,必须加上pulic 无参构造器
    public ExPerson() {
    }

    public ExPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //将name反转后写入二进制流
        StringBuffer reverse = new StringBuffer(name).reverse();
        System.out.println(reverse.toString());
        out.writeObject(reverse);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //将读取的字符串反转后赋值给name实例变量
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        System.out.println(name);
        this.age = in.readInt();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"))) {
            oos.writeObject(new ExPerson("brady", 23));
            ExPerson ep = (ExPerson) ois.readObject();
            System.out.println(ep);
        }
    }
}
//输出结果
//ydarb
//brady
//ExPerson{name='brady', age=23}

 Externalizable不同于Serializable接口,实现此接口必须实现接口中的两个方法,切必须提供无参构造器,因为在反序列化的时候需要反射创建对象。

3、对比

实现Serializable接口实现Externalizable接口
系统自动存储必要的信息程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持必须实现接口内的两个方法
性能略差性能略好

序列化版本号:

反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?所以,java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。

修改serialVersionUID?

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
  • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

序列化总结:

  1. 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 如果想让某个变量不被序列化,使用transient修饰。
  4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  5. 反序列化时必须有序列化对象的class文件。
  6. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  7. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
  8. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  9. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。

 

java对象克隆

首先,java实现克隆需要遵循以下两个规则:必须实现Cloneable接口;实现Cloneable接口的类应该重写clone()方法,重写方法的修饰符必须为public。实际上克隆调用的Object类的clone()方法,clone()是一个本地方法,默认的修饰符为protected。调用Object的clone()的时候,会返回一个复制后的对象,是对所有属性的逐一克隆(引用类型克隆的是引用,值类型克隆的是属性内容)。

protected native Object clone() throws CloneNotSupportedException;

浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象(String类型比较特殊,由于其为final修饰,故也重新创建一个对象);如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

class Address  {
    private String add;

    public String getAdd() {
        return add;
    }

    public void setAdd(String add) {
        this.add = add;
    }

}

class Student implements Cloneable{
    private int number;

    private Address addr;

    public Address getAddr() {
        return addr;
    }

    public void setAddr(Address addr) {
        this.addr = addr;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    @Override
    public Object clone() {
        Student stu = null;
        try{
            stu = (Student)super.clone();
        }catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return stu;
    }
}

在java中,通过覆盖Object类的clone()方法来实现浅克隆。

 

深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

public class Outer implements Serializable{
  private static final long serialVersionUID = 369285298572941L;  //最好是显式声明ID
  public Inner inner;
 //Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化]
  public Outer myclone() {
      Outer outer = null;
      try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
          ByteArrayOutputStream baos = new ByteArrayOutputStream();
          ObjectOutputStream oos = new ObjectOutputStream(baos);
          oos.writeObject(this);
      // 将流序列化成对象
          ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
          ObjectInputStream ois = new ObjectInputStream(bais);
          outer = (Outer) ois.readObject();
      } catch (IOException e) {
          e.printStackTrace();
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
      return outer;
  }
}

class Inner implements Serializable{
  private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
  public String name = "";

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

  @Override
  public String toString() {
      return "Inner的name值为:" + name;
  }
}

 在java中,若要实现深克隆,则可以通过覆盖Object类的clone()方法[要求引用类型属性对于的类实现Cloneable接口并重写clone()],也可以通过序列化(Serialization)等方式

于2020/10/09完成

 

参考链接: 

https://www.cnblogs.com/9dragon/p/10901448.html

https://www.cnblogs.com/Qian123/p/5710533.html

https://baijiahao.baidu.com/s?id=1636492159314232573

https://blog.csdn.net/zhaoheng314/article/details/81985880

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值