Java IO操作——对象序列化(Serializable接口、ObjectOutputStream、以及与Externalizable接口的用法和区别)

学习目标

掌握对象序列化的作用。
掌握Serializable接口的作用。
可以使用ObjectOutputStream进行对象的序列化操作。
可以使用ObjectInputStream进行对象的反序列化操作。
掌握Externalizable接口的作用及与Serializable接口的实现区别。
掌握transient关键字的作用。
可以序列化一组对象。

对象序列化

对象序列化,就是把一个对象变为二进制的数据流的一种方法,通过对象序列化可以方便的实现对象的传输或存储。

如果一个类的对象想被序列化,则对象所在的类必须实现java.io.Serializable接口。此接口的定义如下:
public interface Serializable{ }
一个类不能被无缘无故的被序列化。
但是,在此接口中没有任何一个方法,此接口属于一个标示接口,表示具备了某种能力。例如:现在定义一个类,此类可以被序列化。

定义一个可被序列化的类:
import java.io.Serializable ;
public class Person implements Serializable{
	private String name ;	// 声明name属性,但是此属性不被序列化
	private int age ;		// 声明age属性
	public Person(String name,int age){	// 通过构造设置内容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆写toString()方法
		return "姓名:" + this.name + ";年龄:" + this.age ;
	}
};
以后此类的对象就可以被序列化了。变为二进制byte流。

对象的序列化和反序列化

要想完成对象的输入或输出,还必须依靠对象输出流(ObjectOutputStream)和对象输入流(ObjectInputStream),
使用对象输出流输出序列化对象的步骤,有时也称为序列化,而使用对象输入流读入的过程,有时也称为反序列化。

serialVersionUID

在对象进行序列化或反序列化操作的时候,要考虑JDK版本的问题,如果序列化的JDK版本和反序列化的JDK版本不统一则就有可能造成异常。所以在序列化操作中引入了一个serialVersionUID的常量,可以通过此常量来验证版本的一致性,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现版本不一致的异常。

但是在进行序列化或反序列化操作的时候,对于不同的JDK版本,实际上会出现版本的兼容问题。
import java.io.Serializable ;
public class Person implements Serializable{
	private static final long serialVersionUID = 1L;
	private String name ;	// 声明name属性,但是此属性不被序列化
	private int age ;		// 声明age属性
	public Person(String name,int age){	// 通过构造设置内容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆写toString()方法
		return "姓名:" + this.name + ";年龄:" + this.age ;
	}
};
如果使用开发工具开发,没有编写此代码,则会出现一些安全警告的信息。

对象的序列化及反序列化操作

   对象序列化依靠ObjectOutputStream,对象反序列化依靠ObjectInputStream

 序列化:ObjectOutputStream

对象输出流:ObjectOutputStream
一个对象如果想要进行输出,则必须使用ObjectOutputStream类,此类定义如下:
public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants
 此类的常用方法如下:
1、public ObjectOutputStream(OutputStream out) throws IOException构造方法  传入输出的对象
2、public final void writeObject(Object obj) throws IOException  普通方法 输出对象
此类的使用形式与PrintStream非常的相似,在实例化时也需要传入一个OutputStream的子类对象,之后根据传入的OutputStream子类的对象的不同,输出的位置也不同。
import java.io.File ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
public class SerDemo01{
	public static void main(String args[]) throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定义保存路径
		ObjectOutputStream oos = null ;	// 声明对象输出流
		OutputStream out = new FileOutputStream(f) ;	// 文件输出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Person("张三",30)) ;	// 保存对象
		oos.close() ;	// 关闭
	}
};
序列化的真正内容:所有的对象拥有各自的属性,但是所有的方法都是公共的,所以序列化对象的时候实际上序列化的就是属性。

反序列化:ObjectInputStream

对象输入流:ObjectInputStream
使用ObjectInputStream可以直接把被序列化好的对象反序列化回来。
ObjectInputStream的定义如下:
public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants
此类的常用方法如下:
1、public ObjectInputStream(InputStream in) throws IOException 构造方法 构造输入对象。
2、public final Object readObject() throws IOException, ClassNotFoundException普通方法 从指定位置读取对象。
此类也是InputStream的子类,与PrintStream类的使用类似,此类同样需要接收InputStream类的实例才可以被实例化。
import java.io.File ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
public class SerDemo01{
	public static void main(String args[]) throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定义保存路径
		ObjectOutputStream oos = null ;	// 声明对象输出流
		OutputStream out = new FileOutputStream(f) ;	// 文件输出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Person("张三",30)) ;	// 保存对象
		oos.close() ;	// 关闭
	}
};

问题:
  如果一个类实现了Serializable接口,则肯定此类可以被序列化下来,那么也就意味着此类多了一项功能,那么让所有的类都实现此接口是不是更好?
  其实并不是这样,因为JDK是不断升级的,现在的Serialzable接口中没有任何定义,一旦JDK升级后在里面定义了类必须实现的方法,那么对所有类的维护就会变得更加困难,额外增加了很多代价。

Externalizable接口

被Serializable接口声明的类其对象的内容都将被序列化,如果现在用户希望可以自己指定序列化的内容,则可以让一个类实现Externalizable接口,此接口的定义如下:
public interface Externalizable extends Serializable{
    public void writeExternal(ObjectOutput out) throws IOException;// 写入
    public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;// 读取
}
利用此接口修改之前的程序:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person implements Externalizable{
	private static final long serialVersionUID = 1L;
	private String name ;	// 声明name属性,但是此属性不被序列化
	private int age ;		// 声明age属性
	public Person(String name,int age){	// 通过构造设置内容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆写toString()方法
		return "姓名:" + this.name + ";年龄:" + this.age ;
	}
	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		// TODO Auto-generated method stub
		  this.name = (String)in.readObject();
		  this.age = in.readInt();
	}
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		// TODO Auto-generated method stub
		out.writeObject(this.name); // 保存姓名属性
		out.writeInt(this.age);
	}

};
为了方便测试,现在将序列化及反序列化操作弄成方法调用的形式。
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo03{
	public static void main(String args[]) throws Exception{
//		ser() ;
		dser() ;
	}
	public static void ser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定义保存路径
		ObjectOutputStream oos = null ;	// 声明对象输出流
		OutputStream out = new FileOutputStream(f) ;	// 文件输出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Person("张三",30)) ;	// 保存对象
		oos.close() ;	// 关闭
	}
	public static void dser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定义保存路径
		ObjectInputStream ois = null ;	// 声明对象输入流
		InputStream input = new FileInputStream(f) ;	// 文件输入流
		ois = new ObjectInputStream(input) ;	// 实例化对象输入流
		Object obj = ois.readObject() ;	// 读取对象
		ois.close() ;	// 关闭
		System.out.println(obj) ;
	}
};
以上程序执行的时候出现了一个错误:

在使用Externalizable接口的时候需要在被序列化的类中定义一个无参构造,因为此接口在进行反序列化的时候,会先使用类中的无参构造方法为其进行实例化,之后再将内容分别设置到属性之中,再次修改Person类如下:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person implements Externalizable{
	private static final long serialVersionUID = 1L;
	private String name ;	// 声明name属性,但是此属性不被序列化
	private int age ;		// 声明age属性
	public Person(){}; 
	public Person(String name,int age){	// 通过构造设置内容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆写toString()方法
		return "姓名:" + this.name + ";年龄:" + this.age ;
	}
	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		// TODO Auto-generated method stub
		  this.name = (String)in.readObject();
		  this.age = in.readInt();
	}
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		// TODO Auto-generated method stub
		out.writeObject(this.name); // 保存姓名属性
		out.writeInt(this.age);
	}

};

Externalizable 接口与 Serializable接口实现序列化的区别


在开发中使用Serializable接口是最多的。而Externalizable接口基本上是不会出现的。

transient关键字

当使用Serializable接口实现序列化操作时,如果一个对象中的某个属性不希望被序列化的话,则可以使用transient关键字进行声明。
import java.io.Serializable ;
public class Person implements Serializable{
	private static final long serialVersionUID = 1L;
	private transient String name ;	// 声明name属性,但是此属性不被序列化
	private int age ;		// 声明age属性
	public Person(String name,int age){	// 通过构造设置内容
		this.name = name ;
		this.age = age ;
	}
	public String toString(){	// 覆写toString()方法
		return "姓名:" + this.name + ";年龄:" + this.age ;
	}
};
操作代码:
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo04{
	public static void main(String args[]) throws Exception{
		ser() ;
		dser() ;
	}
	public static void ser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定义保存路径
		ObjectOutputStream oos = null ;	// 声明对象输出流
		OutputStream out = new FileOutputStream(f) ;	// 文件输出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Person("张三",30)) ;	// 保存对象
		oos.close() ;	// 关闭
	}
	public static void dser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定义保存路径
		ObjectInputStream ois = null ;	// 声明对象输入流
		InputStream input = new FileInputStream(f) ;	// 文件输入流
		ois = new ObjectInputStream(input) ;	// 实例化对象输入流
		Object obj = ois.readObject() ;	// 读取对象
		ois.close() ;	// 关闭
		System.out.println(obj) ;
	}
};

transient+Serializable接口完全可以取代Externalizable接口的功能。

序列化一组对象

对象输出时只提供了一个对象的输出操作(writeObject(Object obj)),并没有提供多个对象的输出,所以如果现在要同时序列化多个对象的时候就可以使用对象数组进行操作,因为数组属于引用数据类型,所以可以直接使用Object类型进行接收。

如果要保存多个对象,则最好使用对象数组的形式完成。
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo05{
	public static void main(String args[]) throws Exception{
		Person per[] = {new Person("张三",30),new Person("李四",31),
			new Person("王五",32)} ;
		ser(per) ;
		Object o[] = (Object[])dser() ;
		for(int i=0;i<o.length;i++){
			Person p = (Person)o[i] ;
			System.out.println(p) ;
		}
	}
	public static void ser(Object obj[]) throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定义保存路径
		ObjectOutputStream oos = null ;	// 声明对象输出流
		OutputStream out = new FileOutputStream(f) ;	// 文件输出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(obj) ;	// 保存对象
		oos.close() ;	// 关闭
	}
	public static Object[] dser() throws Exception {
		File f = new File("D:" + File.separator + "test.txt") ;	// 定义保存路径
		ObjectInputStream ois = null ;	// 声明对象输入流
		InputStream input = new FileInputStream(f) ;	// 文件输入流
		ois = new ObjectInputStream(input) ;	// 实例化对象输入流
		Object obj[] = (Object[])ois.readObject() ;	// 读取对象
		ois.close() ;	// 关闭
		return obj ;
	}
};

问题:保存的数据有限,所以为了解决这样的问题,JAVA中引入了类集框架解决数组的存储限制问题。

关于复杂类型的类操作如下:

import java.io.Serializable ;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Person implements Serializable{
	private static final long serialVersionUID = 1L;
	private  String name ;	// 声明name属性,但是此属性不被序列化
	private int age ;		// 声明age属性
	public Person(String name,int age){	// 通过构造设置内容
		this.name = name ;
		this.age = 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;
	}

	public String toString(){	// 覆写toString()方法
		return "姓名:" + this.name + ";年龄:" + this.age ;
	}
};
class Book implements Serializable{
	private static final long serialVersionUID = 1L;
	private String bookName;
	private float price;
	public Book(String bookName,float price){
		this.bookName = bookName;
		this.price = price;
	}
	public String toString() {
		return "书名:"+this.bookName+"; 价格:"+this.price;
	}
}
class Student extends Person{
	private static final long serialVersionUID = 1L;
	private int score ;		// 声明学分属性
	private List<Book> books = new ArrayList<Book>();
	public Student(String name,int age,int score,Book ...books){	// 通过构造设置内容
	    super(name, age); //调用父类构造方法
		this.score = score ;
		Collections.addAll(this.books, books);
	}
	public String toString(){	// 覆写toString()方法
		return super.toString()+"; 学分"+this.score +"\n"+this.books;
	}
};


操作:
import java.io.File ;
import java.io.IOException ;
import java.io.FileOutputStream ;
import java.io.OutputStream ;
import java.io.ObjectOutputStream ;
import java.io.FileInputStream ;
import java.io.InputStream ;
import java.io.ObjectInputStream ;
public class SerDemo04{
	public static void main(String args[]) throws Exception{
		ser() ;
		dser() ;
	}
	public static void ser() throws Exception {
		File f = new File("/Users/liuxun/Downloads/test.txt") ;	// 定义保存路径
		ObjectOutputStream oos = null ;	// 声明对象输出流
		OutputStream out = new FileOutputStream(f) ;	// 文件输出流
		oos = new ObjectOutputStream(out) ;
		oos.writeObject(new Student("张三",30,80,new Book("一万个为什么",23.5f))) ;	// 保存对象
		oos.close() ;	// 关闭
	}
	public static void dser() throws Exception {
		File f = new File("/Users/liuxun/Downloads/test.txt") ;	// 定义保存路径
		ObjectInputStream ois = null ;	// 声明对象输入流
		InputStream input = new FileInputStream(f) ;	// 文件输入流
		ois = new ObjectInputStream(input) ;	// 实例化对象输入流
		Object obj = ois.readObject() ;	// 读取对象
		ois.close() ;	// 关闭
		System.out.println(obj) ;
	}
};


总结:
1、对象序列化的作用:对象序列化不一定都向文件中保存,也有可能面向于其他的输入或输出。
2、被序列化的对象的类必须实现Serializable接口,如果某个属性不希望被保存下来,则可以使用transient关键字声明。
3、关于嵌套或继承的复杂类,则所有涉及的类都直接实现Serializable接口即可。







你可能感兴趣的:(Java IO操作——对象序列化(Serializable接口、ObjectOutputStream、以及与Externalizable接口的用法和区别))