Java序列化总结--基础篇

一.什么是Java序列化:

         所谓的序列化就是说:将那些实现了Serializable接口的对象转化为一个字节序列,并以后能将这个字节序列完全的恢复为原来的对象。被序列化后的对象可以通过网络流传播、亦或是存入到本地的文件当中。实现对象的持久化。


二.序列化的实现:

         将要被序列化的对象必须要实现Serializable接口,然后使用一个输出流(eg.FileOutputStream)来构造一个ObjectOutputStream(对象流),接着使用ObjectOutputStream中的writeObject(Object obj)方法就可以将参数为obj的对象写出。如果需要写入,则使用输入流。

         看一个摘自<Thinkingin Java>上的Demo~

package demo1;

import java.io.Serializable;

public class Data implements Serializable {

	private int n;
	
	public Data(int n){
		this.n=n;
	}
	

	public String toString(){
		return Integer.toString(n);
	}

}

package demo1;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Random;

public class Worm implements Serializable {
	
	private static Random rand = new Random(47);
	
	private Data[] data = {
		new Data(rand.nextInt(10)),
		new Data(rand.nextInt(10)),
		new Data(rand.nextInt(10))
	};
	
	private Worm next;
	
	private char c;
	
	public Worm(int i,char x){
		System.out.println(" worm construtor "+i);
		c=x;
		if(--i>0)
			next=new Worm(i,(char)(x+1));
	}
	
	public Worm(){
		System.out.println("default construcotr..");
	}
	
	public String toString(){
		StringBuffer str = new StringBuffer();
		str.append(c);
		str.append("(");
		for(Data d:data){
			str.append(d);
		}
		str.append(")");
		if(next!=null)
			str.append(next);
		return str.toString();
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Worm w = new Worm(6,'a');
		System.out.println(" w = "+w);
		try {
			ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("C:"+File.separator+"worm.out"));
			os.writeObject("Worm storage...");
			os.writeObject(w);
			os.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			ObjectInputStream osi = new ObjectInputStream(new FileInputStream("C:"+File.separator+"worm.out"));
			String s = (String)osi.readObject();
			Worm w2 =(Worm)osi.readObject();
			osi.close();
			System.out.println( s +"  w2 "+w2);
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		

}

根据运行结果我们可以发现,被还原后的对象却是包含了原对象中所有的连接。注意:在还原的过程中,没有调用任何的构造器,整个对象都是从InputStream流中恢复而来的。

         我不知道大家注意到一个地方没有,我们在写入的时候,显示写入了一个String,然后写入了一个Worm:当我们从流中读取时,可能并不知道开始写入的确切类型,假设我们都转为了String,编译时期,没有报错,但是运行期报错:


根据所报的错误,我们可以知道JVM是知道每次读出来的数据类型的,但是,假设下面的场景:一个类Wrom被序列化并通过流传输到另一个电脑上,此时此电脑上没有Worm的定义,那该怎么办?

看Demo,我们另开一个Project~ ,然后直接读取第一个文件中写入到C盘的worm.out,运行之后直接发现报错:

<p><u><span style="color:navy;">java.lang.ClassNotFoundException</span></u><span style="color:red;">:demo1.Worm</span></p>

所以说,除非保证JVM可以找到先关的class文件,否则会出错。

3、序列化的控制

当我们对某个对象进行控制的时候,有时候我们不希望其序列化某些敏感信息,比如private中password属性,如果自动序列化以后人们可以通过读取文件或者拦截网络传输的方式来访问它。

         为了解决这个问题,有两个方法,一是使用Transient关键字,另一个是实现Enternaliable接口。先看Transient关键字;

         Method1~:

package transient关键字;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Date;

public class Logon implements Serializable{
	
	private Date date = new Date();
	
	private String username;
	
	private transient String password;
	
	public Logon(String username,String password){
		this.username=username;
		this.password=password;
	}
	
	public String toString(){
		return "logon info is "+username+" data is "+date+" password is  "+password;
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Logon a = new Logon("Hulk","hello~~");
		System.out.println("logon a is "+a);
		try {
			ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("C:"+File.separator+"logon.out"));
			o.writeObject(a);
			o.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		try {
			ObjectInputStream in = new ObjectInputStream(
					new FileInputStream("C:"+File.separator+"logon.out"));
			System.out.println("this is read from logon.out");
			try {
				Logon b = (Logon)in.readObject();
				System.out.println(" logon is "+b);
			} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

结果:

logon a is logon info is Hulk data is Tue Mar 24 19:29:07 CST 2015 password is  hello~~
this is read from logon.out
 logon is logon info is Hulk data is Tue Mar 24 19:29:07 CST 2015 password is  null

根据上面的demo,我们发现加了transient关键字的的属性,被置为了null,而且发现date属性是从磁盘恢复的而不是新建的~~。

         Method2~:

         除了仅仅使用transient关键字,我们还可以添加writeObject和readObject这两个函数:这样一来一旦对象被序列化或者被反序列化还原,就会自动的分别调用这两个方法,也就是说只要实现了这两个方法,就会使用它们而不是默认的序列化机制。

package 自定义实现序列化;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SerialCtl implements Serializable {
	
	private String a;
	private transient String b;
	public SerialCtl(String a,String b){
		this.a="Not Transient "+a;
		this.b="Transient "+b;
	}
	public String toString(){
		return a+"\n"+b;
	}
	//注意,方法是private的
	private void writeObject(ObjectOutputStream stream) throws IOException{
		System.out.println(" executing myself writeObject...");
	//	stream.defaultWriteObject();
		stream.writeObject(b);
	}
	
	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException{
		System.out.println(" executing myself readObject...");
	//	stream.defaultReadObject();
		b=(String)stream.readObject();
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		SerialCtl sc = new SerialCtl("Test1","Test2");
		System.out.println("before \n"+sc);
		ByteArrayOutputStream buf = new ByteArrayOutputStream();
		
		try {
			ObjectOutputStream o = new ObjectOutputStream(buf);
			o.writeObject(sc);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			ObjectInputStream in = new ObjectInputStream(
					new ByteArrayInputStream(buf.toByteArray())
					);
			try {
				SerialCtl sc1 = (SerialCtl)in.readObject();
				System.out.println(sc1);
			} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

注意一下,它们被申明了 private ,所以它们只能被这个类的其他成员调用。然而,实际上我们并没有从这个类的其他方法中调用它们,而是 ObjectInputStream 以及 ObjectOutpurStream 在调用它们 ~ 【利用反射调用】:在调用 ObjectInputStream.readObject ()时,会检查你所传递的 Serialiazable 对象,看他是否实现了自己的 readObject() ,如果这样,就跳过正常的序列化过程,并且调用他的 readObject ()

结果:

before 
Not Transient Test1
Transient Test2
 executing myself writeObject...
 executing myself readObject...
Not Transient Test1
Transient Test2

我们可以根据结果看到如下信息:

         1.自己实现的writeObject()方法以及readObject()方法的确被调用了。

         2.加了transient关键字的属性,在自己重新的实现的方法中保存了下来,说明默认的序列化机制被破坏。

         3.defaultWriteObject()方法的作用是使用默认的机制去写入对象的非transient部分,如果删除,结果如下:

before 
Not Transient Test1
Transient Test2
 executing myself writeObject...
 executing myself readObject...
null
Transient Test2

4.上面阐述了序列化的基本特点,下来看看一些其他的特性:

1、序列化ID问题.

看下面的 demo~

项目A中有这样一个class

 

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;


public class A implements Serializable {
	
	 private static final long serialVersionUID = 1L; 

	 private String name; 
	
	 public String getName() 
	 { 
		 return name; 
	 } 
	
	 public void setName(String name) 
	 { 
		 this.name = name; 
	 } 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		A a = new A();
		a.setName("mz");
		try {
			ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("C:"+File.separatorChar+"a.out"));
			o.writeObject(a);
			o.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

}

项目B中有这样一个class


public class A implements Serializable {
	
	 private static final long serialVersionUID = 2L; 

	 private String name; 

	 
	 public String getName() 
	 { 
		 return name; 
	 } 
	
	 public void setName(String name) 
	 { 
		 this.name = name; 
	 } 
	 
	 public String tosString(){
		 return " name is "+name;
	 }
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		try {
			ObjectInputStream in = new ObjectInputStream(new 
					FileInputStream("C:"+File.separator+"a.out"));
			try {
				A a = (A)in.readObject();
				System.out.println(a);
				in.close();
			} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
 }
	}

}


<p><span style="color:black;">我们将</span><span style="color:black;">ProtectA</span><span style="color:black;">中的</span><span style="color:black;">class</span><span style="color:black;">写入到磁盘,在</span><span style="color:black;">ProtectB</span><span style="color:black;">中从磁盘读出来,看下效果:</span></p><p align="left"><u><span style="color:navy;">java.io.InvalidClassException</span></u><span style="color:red;">: A; local class incompatible: stream classdesc serialVersionUID = 1,local class serialVersionUID = 2</span></p><p align="left"><span style="color:red;">    </span>atjava.io.ObjectStreamClass.initNonProxy(Unknown Source)</p><p align="left"><span style="color:red;">    </span>atjava.io.ObjectInputStream.readNonProxyDesc(Unknown Source)</p><p align="left"><span style="color:red;">    </span>atjava.io.ObjectInputStream.readClassDesc(Unknown Source)</p><p align="left"><span style="color:red;">    </span>atjava.io.ObjectInputStream.readOrdinaryObject(Unknown Source)</p><p align="left"><span style="color:red;">    </span>atjava.io.ObjectInputStream.reIadObject0(Unknown Source)</p><p align="left"><span style="color:red;">    </span>atjava.io.ObjectnputStream.readObject(Unknown Source)</p><p align="left"><span style="color:red;">    </span>atA.main(<u><span style="color:navy;">A.java:34</span></u><span style="color:red;">)</span></p><p><span style="color:black;">这是为什么呢?</span></p><p><em><strong>虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 
</strong></em></p><p><em><strong>private static final long serialVersionUID =1L)</strong></em>。虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。</p><p>如果将将第二A中的序列改为1L,即可执行成功。</p>


如果两个class对象都不添加serialVersionUID,看下面例子:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;


public class A implements Serializable {
	

	 private String name; 
	
	 public String getName() 
	 { 
		 return name; 
	 } 
	
	 public void setName(String name) 
	 { 
		 this.name = name; 
	 } 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		A a = new A();
		a.setName("mz");
		try {
			ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("C:"+File.separatorChar+"a.out"));
			o.writeObject(a);
			o.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

}

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.Serializable;


public class A implements Serializable {
	
	 private String name; 

	 private int age;
	 
	 public String getName() 
	 { 
		 return name; 
	 } 
	
	 public void setName(String name) 
	 { 
		 this.name = name; 
	 } 
	 
	 public String tosString(){
		 return " name is "+name+" age is "+age;
	 }
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		try {
			ObjectInputStream in = new ObjectInputStream(new 
					FileInputStream("C:"+File.separator+"a.out"));
			try {
				A a = (A)in.readObject();
				System.out.println(a);
				in.close();
			} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

第二个项目中的A比第一个多了一个age属性,此时我们读取第一个项目中的文件,发现报如下错误:

 java.io.InvalidClassException: A; local classincompatible: stream classdesc serialVersionUID = -6543253821653298239, localclass serialVersionUID = 7168241804858576228

    atjava.io.ObjtStreamClass.initNonProxy(Unknown Source)

    atjava.io.ObjectInputStream.readNonProxyDesc(Unknown Source)

    atjava.io.ObjectInputStream.readClassDesc(Unknown Source)

    atjava.io.ObjectInputStream.readOrdinaryObject(Unknown Source)

    atjava.io.ObjectInputStream.readObject0(Unknown Source)

    atjava.io.ObjectInputStream.readObject(Unknown Source)

    atA.main(A.java:38)

说明不指定UID,一旦对象的属性改变,其UID也会改变。

如同设置了UID,改变了对象的属性,则不会报错,缺省的属性为默认值(int为0,对象为null)


2.类的静态变量

public class Test implements Serializable {
	
	private static final long serialVersionUID = 1L;

	public static int staticVar = 5;
	/**
	 * @param args
	 * @throws IOException 
	 * @throws FileNotFoundException 
	 * @throws ClassNotFoundException 
	 */
	public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
		// TODO Auto-generated method stub
		ObjectOutputStream out = new ObjectOutputStream(
				new FileOutputStream("result.obj"));
		out.writeObject(new Test());
		out.close();

		//序列化后修改为10
		Test.staticVar = 10;

		ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
				"result.obj"));
		Test t = (Test) oin.readObject();
		oin.close();
		
		//再读取,通过t.staticVar打印新的值
		System.out.println(t.staticVar);
	}

}

根据结果可以看到,最后的结果居然是10,而不是保存之前的5.。这是因为之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量

3.父类的序列化

说到这问题,明显可能性有四种情况:

         ①父类未序列化,子类序列化了。

         ②父类未序列化,子类未序列化

         ③父类序列化,子类未序列化

         ④父类序列化,子类序列化

         针对以上四种可能,其实只有第一种问题值得探究~ 第三种子类继承了父类,间接实现了序列化。讨论下,第一种情况:

public class Dad {

	
	private String name;
	
	public Dad(String name){
		this.name=name;
	}
	
	//public Dad(){}
	
	public String getNames(){
		return name;
	}
	
	public String toString(){
		return "dad name is "+name;
	}
	
}

public class Son extends Dad implements Serializable {

	
	private String school;
	
	public Son(String name,String school) {
		super(name);
		// TODO Auto-generated constructor stub
		this.school=school;
	}
	
	
	public String toString(){
		return "School is "+school+"  dad name is";
	}
	/**
	 * @param args
	 * @throws IOException 
	 * @throws FileNotFoundException 
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Son s = new Son("ss","ts");
		System.out.println(" Serializable ..."+s);
		ObjectOutputStream out = null;
		try {
			out = new ObjectOutputStream(new FileOutputStream("C:"+File.separator+"ddd.out"));
			out.writeObject(s);
			out.close();
		} catch (Exception e) {
			// TODO: handle exception
		}
		ObjectInputStream in = null;
		try {
			in = new ObjectInputStream(new FileInputStream("C:"+File.separator+"ddd.out"));
			Son ss = (Son)in.readObject();
		
			in.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

运行会发现报如下异常:

java.io.InvalidClassException: 序列化继承问题.Son; no valid constructor

    atjava.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(UnknownSource)

    atjava.io.ObjectStreamClass.checkDeserialize(Unknown Source)

    atjava.io.ObjectInputStream.readOrdinaryObject(Unknown Source)

    atjava.io.ObjectInputStream.readObject0(Unknown Source)

    atjava.io.ObjectInputStream.readObject(Unknown Source)

    at序列化继承问题.Son.main(Son.java:49)

添加默认无惨构造后:

public class Dad {

	
	private String name;
	
	public Dad(String name){
		this.name=name;
	}
	
	public Dad(){}
	
	public String getNames(){
		return name;
	}
	
	public String toString(){
		return "dad name is "+name;
	}
	
}

4.包含一个对象

如果一个序列化对象中,包含了一个对象没有实现序列化接口,那么,如果直接序列化此对象会报错:

public class bag {
	
	private String name;
	
	private int price;
	
	public bag(){}
	
	public bag(String name,int price){
		this.name=name;
		this.price=price;
	}
	
	public String toString(){
		return " name is "+name+"  price is "+price;
	}

}

package 包含一个对象;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import 序列化继承问题.Son;

public class stu implements Serializable {
		
	private String name;
	
	private bag b = new bag("nike",300);
	
	public stu(String name){
		this.name=name;
	}
	
	private void writeObject(ObjectOutputStream stream) throws IOException{
		stream.defaultWriteObject();
		stream.writeObject(b);
	}
	
	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException{
		stream.defaultReadObject();
		bag b = (bag)stream.readObject();
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		stu s = new stu("huang");
		ObjectOutputStream out = null;
		try {
			out = new ObjectOutputStream(new FileOutputStream("C:"+File.separator+"bag.out"));
			out.writeObject(s);
			out.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		ObjectInputStream in = null;
		try {
			in = new ObjectInputStream(new FileInputStream("C:"+File.separator+"bag.out"));
			stu ss = (stu)in.readObject();
		
			in.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

运行后,报如下错误:

java.io.NotSerializableException: 包含一个对象.bag

    atjava.io.ObjectOutputStream.writeObject0(Unknown Source)

    atjava.io.ObjectOutputStream.defaultWriteFields(Unknown Source)

    atjava.io.ObjectOutputStream.writeSerialData(Unknown Source)

    atjava.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source)

    atjava.io.ObjectOutputStream.writeObject0(Unknown Source)

    atjava.io.ObjectOutputStream.writeObject(Unknown Source)

    at包含一个对象.stu.main(stu.java:30)

java.io.WriteAbortedException: writing aborted; java.io.NotSerializableException: 包含一个对象.bag

    atjava.io.ObjectInputStream.readObject0(Unknown Source)

    atjava.io.ObjectInputStream.defaultReadFields(Unknown Source)

    atjava.io.ObjectInputStream.readSerialData(Unknown Source)

    atjava.io.ObjectInputStream.readOrdinaryObject(Unknown Source)

    atjava.io.ObjectInputStream.readObject0(Unknown Source)

    atjava.io.ObjectInputStream.readObject(Unknown Source)

    at包含一个对象.stu.main(stu.java:38)

Caused by: java.io.NotSerializableException: 包含一个对象.bag

    atjava.io.ObjectOutputStream.writeObject0(Unknown Source)

    atjava.io.ObjectOutputStream.defaultWriteFields(Unknown Source)i

    atjava.io.ObjectOutputStream.writeSerialData(Unknown Source)

    atjava.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source)

    atjava.io.ObjectOutputStream.writeObject0(Unknown Source)

    atjava.io.ObjectOutputStream.writeObject(Unknown Source)

    at包含一个对象.stu.main(stu.java:30)

当我们直接给被包含的类实现序列化接口,正常运行~



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值