`
Technoboy
  • 浏览: 156740 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

Java Object Serialization

    博客分类:
  • J2SE
阅读更多
1. Overview
  Java中的序列化就是将Java对象的状态转化为字节序列,以便存储和传输的机制,在未来的某个时间,可以通过字节序列重新构造对象。把Java对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为Java对象的过程称为对象的反序列化。这一切都归功于java.io包下的ObjectInputStream和ObjectOutputStream这两个类。

2. Serializable
  要想实现序列化,类必须实现Serializable接口,这是一个标记接口,没有定义任何方法。如果一个类实现了Serializable接口,那么一旦这个类发布,“改变这个类的实现”的灵活性将大大降低。以下是一个序列化的小例子:
class Message implements Serializable{

	private static final long serialVersionUID = 1L;
	
	private String id;
	
	private String content;
	
	public Message(String id, String content){
		this.id = id;
		this.content = content;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}
	
	public String toString(){
		return "id = " + id + " content = " + content;
	}
}

public class Test{
	
	public static void main(String[] args) {
		serialize();
		deserialize();
	}
	
	private static void serialize(){
		Message message = new Message("1", "serializable test");
		try {
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Message"));
			oos.writeObject(message);
			oos.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("over");
	}
	
	private static void deserialize(){
		try {
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Message"));
			Message message = (Message)ois.readObject();
			System.out.println(message.toString());
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	
}

  需要注意,序列化机制只保存对象的类型信息,属性的类型以及属性值,与方法没有关系。对于静态的变量,序列化机制也是不保存的。因为,静态变量属于类变量,而不是对象变量。而且,并不是所有的Java对象都可以被序列化,例如:Thread,Socket。内部类很少甚至没有实现Serializable接口的。关于容器类的序列化,可以遵循Hashtable的实现方式,即存储键和值的形式,而非一个大的哈希表的数据结构类型。

3. Serial Version UID
  每一个可序列化的类都有一个与之关联的唯一的序列化版本UID。其有两种生成策略,一种是固定的1L(private static final long serialVersionUID = 1L),另一种是依据类名、它实现的接口的名字、以及所有公共和受保护的成员的名字,利用JDK提供的工具随机的生成一个不重复的long类型数据。因此,假如你在已经序列化的类中,添加了新方法或属性,其随机UID的值也许会改变,如果此时在反序列化时,将会抛出java.io.InvalidClassException。因此建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。

4. Serialization Storage Rules
  我们修改上面列子的代码,将message对象写入Message.obj文件中两遍,然后我们查看文件大小,以及从文件中反序列化出两个对象,比较是否相等,代码如下:
public class Test{
	
	public static void main(String[] args) {
		try {
			Message message = new Message("1", "serializable test");
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Message.obj"));
			oos.writeObject(message);
			oos.flush();
			System.out.println(new File("Message.obj").length());
			oos.writeObject(message);
			System.out.println(new File("Message.obj").length());
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Message.obj"));
			Message message1 = (Message)ois.readObject();
			Message message2 = (Message)ois.readObject();
			System.out.println(message1 == message2);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

  输出结果:
115
120
true

  从结果,我们可以看出,对相同对象的第二次序列化后,文件的大小只增加了5字节,且反序列化出来的两个对象相等。原因在于,Java序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的5字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得message1和message2指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。
  我们修改上面main方法中的代码,使第一次序列化id值为2,第二次序列化id值为3,然后反序列化出两个对象,并打印id值:
public static void main(String[] args) {
		try {
			Message message = new Message("1", "serializable test");
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Message.obj"));
			message.setId("2");
			oos.writeObject(message);
			oos.flush();
			System.out.println(new File("Message.obj").length());
                        message.setId("3");
			oos.writeObject(message);
			System.out.println(new File("Message.obj").length());
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Message.obj"));
			Message message1 = (Message)ois.readObject();
			Message message2 = (Message)ois.readObject();
			System.out.println(message1 == message2);
			System.out.println("message1.id = " + message1.getId());
			System.out.println("message2.id = " + message2.getId());
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

  输出结果:
115
120
true
message1.id = 2
message2.id = 2

  结果两次输出,id值都为2。原因就是第一次写入对象后,第二次再试图写的时候,JVM根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用。所以读取时都是第一次保存的对象。因此,我们必须要注意,在同一个对象进行多次序列化到相同文件中时所产生的这个问题。
  
5. Serialization and Inheritance
  假设有如下情形: 一个父类实现了Serializable接口,子类继承父类同时也实现了Serializable接口。那么在反序列化的时候,结果如何呢?请看下面这个小例子:
public class Base implements Serializable{

	private static final long serialVersionUID = 1L;
	
	private int x;
	
	public Base(){
		System.out.println("Base Class : no-arg constructor");
	}
	
	public Base(int x){
		this.x = x;
		System.out.println("Base Class : one-arg constructor");
	}
	
	public int getX(){
		return x;
	}
	
	public String toString(){
		return "x = " + this.x;
	}
}

public class Child extends Base implements Serializable{

	private static final long serialVersionUID = 1L;
	
	private int y;
	
	public Child(){
		System.out.println("Child Class : default no-arg constructor");
	}
	
	public Child(int x, int y){
		super(x);
		this.y = y;
		System.out.println("Child Class : two-args constructor");
	}
	
	public String toString(){
		return "x = " + getX() + " , y = " + this.y;
	}
	
	public static void main(String[] args) {
		try {
			Child child =  new Child(1, 2);
			ObjectOutputStream oos =  new ObjectOutputStream(new FileOutputStream("BaseAndChild"));
			oos.writeObject(child);
			oos.close();
			System.out.println("over");
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("BaseAndChild"));
			Child child2 = (Child)ois.readObject();
			ois.close();
			System.out.println(child2);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

  输出结果为:
Base Class : one-arg constructor
Child Class : two-args constructor
over
x = 1 , y = 2

  从输出的结果,我们可以看出,当父子类同时实现Serializable接口,反序列化时,不调用构造函数,且子类中的x,y值都被正确的设置。当父类没有实现Serializable接口时,输出结果变为:
Base Class : two-args constructor
Child Class : one-arg constructor
over
Base Class : no-arg constructor
x = 0 , y = 2

  说明,在反序列化时,调用了父类的无参构造函数,且子类从父类继承的x也没有被正确的设置。我们可以这样理解,一个Java对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值,所以我们x的取值为0。如果在反序列化时,父类没有提供可访问的,无参的构造函数,将抛出java.io.InvalidClassException异常。

6. Serialization and Proxy
  序列化允许将代理放在流中。看下面这个列子:
public class Product implements Serializable {

	private static final long serialVersionUID = 1L;
	
	private String description;
	
	public Product(String description){
		this.description = description;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}
}

public class ProductProxyFactory implements Serializable, MethodInterceptor{

	private static final long serialVersionUID = 1L;

	public Object intercept(Object obj, Method m, Object[] args,
			MethodProxy mp) throws Throwable {
		
		System.out.println("before interception");
		try {
			return mp.invokeSuper(obj, args);
		} finally {
			System.out.println("after interception");
		}
	}
	
	public static Product getProxy(String description) throws Exception{
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(Product.class);
		enhancer.setCallback(new ProductProxyFactory());
		return (Product)enhancer.create(new Class[]{String.class}, new Object[]{description});
	}
}

public class Serialization {
	
	public static void main(String[] args) throws Exception {
		Product p = ProductProxyFactory.getProxy("first product");
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Product"));
		oos.writeObject(p);
		oos.flush();
		oos.close();
		System.out.println("over");
	}
}

public class Deserialization {
	
	public static void main(String[] args) throws Exception {
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Product"));
		Product p1 = (Product)ois.readObject();
		ois.close();
		System.out.println("deserialize: " + p1.getDescription());
	}
}

先运行Serialization类,然后运行Deserialization类(也就是说在不同的JVM间序列化/反序列化用cglib生成的代理对象),会抛出java.lang.ClassNotFoundException。原因在于,使用cglib生成的代理类是Product子类,而这些子类在不同的JVM间是不同的,因此抛出异常。解决方法是在Product类中加入以下两个方法:
public Object writeReplace() throws ObjectStreamException {
	return new Product(getDescription());
}
public Object readResolve() throws ObjectStreamException {
	return ProductProxyFactory.getProxy(getDescription());
}

  在对代理类进行序列化时,父类(Product类)中的writeReplace()方法会被调用。writeReplace()方法返回的是个Product类的对象,因此实际序列化的对象不是代理类的对象;在对代理类进行反序列化时,父类(Product类)中的readResolve方法会被调用。readResolve方法返回了个新的代理类的对象。
需要注意的是,使用Java 动态代理(Dynamic Proxy)创建代理类有特殊的类标识符,因此在ObjectInputStream对其进行反序列化时,会识别这个特殊的标识符,并调用ObjectInputStream.resolveProxyClass方法对其进行处理,因此会自动返回一个新的代理类的对象,也就是说如果使用动态代理创建代理类,那么不必添加writeReplace和readResolve方法。在另一方面,使用cglib创建的代理类只有普通的类标识符,ObjectInputStream对其进行反序列化时只是调用ObjectInputStream.resolveClass方法对其进行处理,因此需要以上的技巧。
  如果一个类同时改写了这两个方法,以及writeObject()和readObject()方法,那么在序列化时的调用顺序是:
1.writeReplace()
2.writeObject()
3.readObject()
4.readResolve()

7. Externalizable
  Externalizable接口继承自Serializable接口,实现Externalizable接口的类进行序列化时,只保存对象的标识符信息,因此序列化的速度更快,序列化后的字节流更小。Externalizable接口的定义如下:
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

  子类必须提供读取和写出方法的实现。在序列化的时候,通过writerExternal方法序列化对象,通过readExternal方法反序列化一个对象。跟实现了Serializable接口的类不同,在反序列化时,会调用类的无参构造函数,所以该实现类必须提供一个可访问的无参构造函数。当一个类同时实现了Externalizable接口和Serializable接口时,那么实际的序列化形式是由Externalizable接口中声明的方法决定。以下是一个列子:
public class Person implements Externalizable {
	
	private static final long serialVersionUID = 1L;

	private int age;
	
	public Person(){
	}
	
	public Person(int age){
		this.age = age;
	}
	
	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
	public String toString(){
		return "age = " + this.age;
	}
	
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		setAge(in.readInt());

	}

	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeInt(getAge());
	}

	public static void main(String[] args) {
		try {
			Person p = new Person(20);
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Person.obj"));
			oos.writeObject(p);
			oos.close();
			System.out.println("over");
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Person.obj"));
			Person p1 = (Person)ois.readObject();
			System.out.println(p1);
			ois.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

8. Transient关键字
  Transient关键字的作用就是控制变量的序列化,在变量声明前加上transient关键字,可以阻止变量被序列化到文件中,在被反序列化后,transient关键字修饰的变量的值被设置为初始值,如int型为0,对象型为null。以下是一个例子:
public class Person implements Externalizable {
	
	private static final long serialVersionUID = 1L;

	private int age;
	
	private transient String name;
	
	public Person(){
	}
	
	public Person(int age, String name){
		this.age = age;
		this.name = name;
	}
	
	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String toString(){
		return "age = " + this.age + " , name = " + this.name;
	}
	
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		setAge(in.readInt());
	}

	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeInt(getAge());
	}

	public static void main(String[] args) {
		try {
			Person p = new Person(20, "remy");
			ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Person.obj"));
			oos.writeObject(p);
			oos.close();
			System.out.println("over");
			
			//
			ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Person.obj"));
			Person p1 = (Person)ois.readObject();
			System.out.println(p1);
			ois.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

  输出结果:
over
age = 20 , name = null

9. ObjectInputValidation
  我们可以使用ObjectInputValidation接口验证序列化流中的数据是否与最初写到流中的数据一致。我们需要覆盖validateObject()方法,如果调用该方法时发现某处有错误,则抛出一个 InvalidObjectException。
2
1
分享到:
评论

相关推荐

    深入浅析Java Object Serialization与 Hadoop 序列化

    深入浅析Java Object Serialization与 Hadoop 序列化 序列化是指将结构化对象转化为字节流以便在网络上传输或者写到磁盘永久存储的过程。Java 中的序列化是通过实现 Serializable 接口来实现的,而 Hadoop 序列化则...

    J2EE复习题 文件内容很值得看 也很适合考试

    11. **Java Remote Method Invocation (RMI)** 和 **Java Object Serialization**:RMI 允许远程调用对象方法,对象序列化则将Java对象转换为字节流,便于在网络间传输或持久化存储。 12. **Java Servlet API** 和 ...

    shiro 反序列化漏洞综合利用工具 shiro_attack_by J1anfen

    3. 使用安全的序列化库:例如,可以考虑使用 Google 的 `Protocol Buffers` 或 `Java Object Streaming API (Java Object Serialization)` 的替代品,它们提供了更安全的序列化机制。 4. 输入验证:在接收序列化数据...

    Apress.Enterprise.JavaBeans.2.1

    4. **持久化**:JavaBeans 2.1可以通过Java Object Serialization或其他持久化机制保存其状态,以便在以后的会话中恢复。 5. **设计时支持**:在集成开发环境中(IDE),如Eclipse或NetBeans,JavaBeans可以被直观...

    applet与servlet通讯

    2. **Java Object Serialization**:Applet 和 Servlet 之间可以通过序列化和反序列化 Java 对象来进行更复杂的数据交换。Applet 将对象序列化为字节流,发送到 Servlet,Servlet 反序列化后处理数据。 3. **使用 ...

    平:有原则且有效的二进制序列化

    3. **常见的二进制序列化库**:在各种编程语言中,都有流行的二进制序列化库,例如Java的Java Object Serialization,C#的protobuf-net,Python的pickle,Go的protobuf等。 4. **Protocol Buffers (protobuf)**:由...

    FST:快速Java序列化的替代品

    这时,FST(Fast Serialization Toolkit)作为一个高效且JDK兼容的序列化库,提供了更快的速度和更小的内存占用,成为了Java开发者的一个优秀选择。 FST的主要特点包括: 1. **高性能**:FST通过优化的序列化算法...

    Object转xml或xml转Object

    例如,在.NET环境中,可以使用`System.Xml.Serialization.XmlSerializer`类来实现这一功能。同样地,JAXB也提供了`Unmarshaller`进行此操作。 XML是一种标记语言,它的结构清晰,易于人阅读,适合数据的交换和存储...

    Java序列化(Serialization) 机制

    Java序列化机制是Java平台提供的一种标准方法,用于将对象的状态转换为字节流,以便存储在磁盘上,或者在网络中进行传输。这使得Java对象可以在不同的Java虚拟机(JVM)之间交换,这对于分布式应用程序,如远程方法...

    精品-最全的Java代码审计技术资料合集(25份).zip

    Object Serialization Stream Protocol.pdf OFCMS 1.1.4后台存在Freemarker模板命令注入漏洞.docx OWASP Top 10 20134e2d65877248-V1.3.pdf OWASP Top 10 2017 10项最严重的 Web 应用程序安全风险.pdf OWASP代码审计...

    ejb-exemplo2:远程 EJB 计算器

    这通常通过Java Remote Method Invocation (RMI) 和Java Object Serialization来实现。 3. **Session Beans**:ejb-exemplo2可能使用了Session Beans,这是EJB的一种类型,用于表示一次会话或用户交互。Session ...

    Java 9 for Programmers (Deitel Developer Series) 完整高清azw3版

    streams, functional interfaces, object serialization, concurrency, generics, generic collections, database with JDBC™ and JPA, and compelling new Java 9 features, such as the Java Platform Module ...

    msgpack-java-master

    msgpack - MessagePack is an extremely efficient object serialization library. It's like JSON, but very fast and small.

    json序列化jar包

    Most JSON serializers mimic object serialization libraries and try to serialize the entire object graph from the object being turned into JSON. This causes problems when you want a connected object ...

    PyPI 官网下载 | oslo.serialization-2.2.0.tar.gz

    `oslo.serialization`是OpenStack项目的一部分,它提供了一套灵活的数据序列化工具,支持JSON(JavaScript Object Notation)和pickle(Python的内置序列化格式)格式。这个库主要服务于那些需要在不同的系统或进程...

    Java IO, NIO and NIO.2 原版pdf by Friesen

    RandomAccessFile classes along with streams (including object serialization and externalization) and writers/readers. Chapters 6 through 11 focus on NIO. You explore buffers, channels, selectors, ...

    JavaIO.ppt

    1,methods for accessing file, text data, object serialization and internationalization Sequential and Random access 2,Reading and writing of primitive values 3,Applications and applets are ...

    java序列化和反序列化

    Java序列化(Serialization)是一项重要的功能,它可以将对象的状态转化为一系列字节,从而实现对象的持久化存储或在网络上传输。序列化机制使得Java对象能够在不同的平台之间进行传输,并且能够保持其原始状态。 *...

Global site tag (gtag.js) - Google Analytics