Java学习(十六):Java序列化和克隆

Serializable接口

序列化:对象的寿命通常随着生成该对象的程序的终止而终止,有时候需要把在内存中的各种对象的状态(也就是实例变量,不是方法)保存下来,并且可以在需要时再将对象恢复。虽然你可以用你自己的各种各样的方法来保存对象的状态。Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。

将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。

整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

序列化的用途

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

类 ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法。

ObjectOutputStream 类包含很多写方法来写各种数据类型,但是一个特别的方法例外:

public final void writeObject(Object x) throws IOException

上面的方法序列化一个对象,并将它发送到输出流。相似的 ObjectInputStream 类包含如下反序列化一个对象的方法:

public final Object readObject()throws IOException, ClassNotFoundException

该方法从流中取出下一个对象,并将对象反序列化。它的返回值为Object,因此,你需要将它转换成合适的数据类型。

如何序列化

只要一个类实现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;
	}
}

请注意,一个类的对象要想序列化成功,必须满足两个条件:

该类必须实现 java.io.Serializable 对象。

该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

如果你想知道一个 Java 标准类是否是可序列化的,请查看该类的文档。检验一个类的实例是否能序列化十分简单, 只需要查看该类有没有实现 java.io.Serializable接口。

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

File file = new File("file"+File.separator+"out.txt");
	
	FileOutputStream fos = null;
	try {
		fos = new FileOutputStream(file);
		ObjectOutputStream oos = null;
		try {
			oos = new ObjectOutputStream(fos);
			Person person = new Person("tom", 22);
			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());
		}
	}
							
	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());
		}
	}

 

这里要注意以下要点:

readObject() 方法中的 try/catch代码块尝试捕获 ClassNotFoundException 异常。对于 JVM 可以反序列化对象,它必须是能够找到字节码的类。如果JVM在反序列化对象的过程中找不到该类,则抛出一个 ClassNotFoundException 异常。如果我把Person类中的implements Serializable 去掉,Person类就不能序列化了。此时再运行上述程序,就会报java.io.NotSerializableException异常。

serialVersionUID

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

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

  • 一个是固定的 1L
  • 一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具,根据类名、接口名、成员方法及属性等来生成)

上面程序中,输出对象和读入对象使用的是同一个Person类。

如果是通过网络传输的话,如果Person类的serialVersionUID不一致,那么反序列化就不能正常进行。例如在客户端A中Person类的serialVersionUID=1L,而在客户端B中Person类的serialVersionUID=2L 那么就不能重构这个Person对象。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。

关于 java 中的序列化与反序列化

关于序列化,常又称为持久化,将其写入磁盘中。

进而对于编码规则来说:

任一一个实体类必须要去实现 Serializable 接口,方便以后将该类持久化,或者将其用于转为字节数组,用于网络传输。

对于一个实体类,不想将所有的属性都进行序列化,有专门的关键字 transient:

private transient String name;

当对该类序列化时,会自动忽略被 transient 修饰的属性。

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

序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

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

序列化中的继承问题

  • 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
  • 一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,要想将父类对象也序列化,就需要让父类也实现Serializable 接口。

总结

序列化给我们提供了一种技术,用于保存对象的变量。以便于传输。

 

Cloneable接口

clone:它允许在堆中克隆出一块和原对象一样的对象,并将这个对象的地址赋予新的引用。 

Java 中 一个类要实现clone功能 必须实现 Cloneable接口,否则在调用 clone() 时会报 CloneNotSupportedException 异常。

Java中所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone(),这个方法将返回Object对象的一个拷贝。 。

浅复制

class CloneClass implements Cloneable{
 public int aInt;
 public Object clone(){
  CloneClass o = null;
  try{
   o = (CloneClass)super.clone();
  }catch(CloneNotSupportedException e){
   e.printStackTrace();
  }
  return o;
 }
}

深复制

public Object clone() throws CloneNotSupportedException{
        Student newStudent = (CloneClass) super.clone();
        newStudent.professor = (Professor) professor.clone();
        return newStudent;
    }

 

有三个值得注意的地方: 

一是为了实现clone功能,CloneClass类实现了Cloneable接口 拷贝对象返回的是一个新对象,而不是一个引用

二是重载了clone()方 法; 

三是在clone()方法中调用了super.clone(),这也意味着无论clone类的继承结构是什么样的,super.clone()直接或 间接调用了java.lang.Object类的clone()方法。

Object类的clone()方法是一个native方法,native方法的效率一般来说都是远高于java中的非native方法。这也解释了为 什么要用Object中clone()方法而不是先new一个对象,然后把原始对象中的信息赋到新对象中,

Object类中的clone()方法还是一个protected属性的方法。这也意味着如果要应用clone()方法,必须继承Object类,重载之后要把clone()方法的属性设置为public。

你可能感兴趣的:(Java基础知识及扩展)