(文章目录)
本文简单的介绍了序列化和反序列化的作用、实现方式以及实际的业务场景中如何选择。
一、Java序列化的作用
Java 提供了一种对象序列化的机制。
该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。
整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
二、实现java对象的序列化和反序列化
1、序列化规则
-
java中的序列化时transient变量(这个关键字的作用就是告知JAVA不可以被序列化)和静态变量不会被序列化
-
如果先序列化对象A后序列化B,那么在反序列化的时候一定记着JAVA规定先读到的对象是先被序列化的对象,不要先接收对象B,否则会报错。尤其在使用上面的Externalizable的时候一定要注意读取的先后顺序
-
实现序列化接口的对象并不强制声明唯一的serialVersionUID,是否声明serialVersionUID对于对象序列化的向上向下的兼容性有很大的影响
2、序列化步骤
序列化算法一般会按步骤做如下事情:
- 将对象实例相关类的描述
- 递归地输出类的超类描述直到不再有超类
- 从最顶层的超类开始输出对象实例的实际数据值
- 读取对象的顺序必须与写入的顺序相同
3、注意事项
- 如果有不能被序列化的对象,执行期间就会抛出NotSerializableException异常;
- 序列化时,只对对象的状态进行保存,而不管对象的方法;
- 静态变量不会被序列化,因为所有的对象共享同一份静态变量的值;
- 如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存还原,而且会是递归的方式。(序列化程序会将对象版图上的所有东西储存下来,这样才能让该对象恢复到原来的状态);
- 如果子类实现Serializable接口而父类未实现时,父类不会被序列化,但此时父类必须有个无参构造方法,否则会抛InvalidClassException异常。因为反序列化时会恢复原有子对象的状态,而父类的成员变量也是原有子对象的一部分。由于父类没有实现序列化接口,即使没有显示调用,也会默认执行父类的无参构造函数使变量初始化。
- 如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
三、序列化接口Serializable
1、介绍
对于序列化接口Serializable接口是一个标识接口,它的主要作用就是标识这个对象是可序列化的,jre对象在传输对象的时候会进行相关的封装。 类 ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含序列化和反序列化对象的方法。ObjectOutputStream 类包含很多写方法来写各种数据类型,但是一个特别的方法例外:
public final void writeObject(Object x) throws IOException
上面的方法序列化一个对象,并将它发送到输出流。相似ObjectInputStream 类包含如下反序列化一个对象的方法:
public final Object readObject() throws IOException, ClassNotFoundException
该方法从流中取出下一个对象,并将对象反序列化。它的返回值为Object,因此,你需要将它转换成合适的数据类型。
2、示例
- 该类必须实现 java.io.Serializable 对象
- 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是transient
public class User implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private static String val = "1234";
private Integer id;
private String name;
public transient int SSN;
public int number;
public User() {
}
public void mailCheck() {
System.out.println("Mailing a check to " + name+ " " + name);
}
public class SerializeDemo {
public static void main(String[] args) {
SerializeDemo serializeDemo = new SerializeDemo();
serializeDemo.serialize();
serializeDemo.deSerialize();
}
public void serialize() {
User e = new User();
e.setName("Reyan");
e.SSN = 11122333;
e.number = 101;
try {
FileOutputStream fileOut = new FileOutputStream("user.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(e);
out.close();
fileOut.close();
System.out.println("序列化后的文件保存在:user.ser");
} catch (Exception i) {
i.printStackTrace();
}
}
public void deSerialize() {
FileInputStream fileIn = null;
try {
fileIn = new FileInputStream("user.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
User user = (User) in.readObject();
System.out.println(user.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
四、实现接口Externalizable
1、Externalizable介绍
- writeExternal() 在序列化对象的时候,由于这个类实现了Externalizable 接口,在writeExternal()方法里定义了哪些属性可以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理
- readExternal() 在反序列的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列。 Exterinable的是Serializable的一个扩展。
2、Externalizable示例
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author alanchan
*
*/
public class SerializeDemo2 {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
Operate operate = new Operate();
User person = new User("alan", "123456", "20");
System.out.println("为序列化之前的相关数据如下:\n" + person.toString());
operate.serializable(person);
User newPerson = operate.deSerializable();
System.out.println("-------------------------------------------------------");
System.out.println("序列化之后的相关数据如下:\n" + newPerson.toString());
}
static class Operate {
/**
* 序列化方法
*
* @throws IOException
* @throws FileNotFoundException
*/
public void serializable(User person) throws FileNotFoundException, IOException {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("user.ser"));
outputStream.writeObject(person);
}
/**
* 反序列化的方法
*
* @throws IOException
* @throws FileNotFoundException
* @throws ClassNotFoundException
*/
public User deSerializable() throws FileNotFoundException, IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
return (User) ois.readObject();
}
}
static class User implements Externalizable {
private static final long serialVersionUID = 1L;
String password;
String age;
String userName;
/**
* 需要有一个默认构造器,否则会报no valid constructor异常
*/
public User() {
}
public User(String password, String age, String userName) {
this.password = password;
this.age = age;
this.userName = userName;
}
@Override
public String toString() {
return "User{" + "password='" + password + '\'' + ", age='" + age + '\'' + ", userName='" + userName + '\'' + '}';
}
/**
* 在writeExternal()方法里定义了哪些属性可以序列化,哪些不可以序列化,
* 所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理
*
* @param out
* @throws IOException
*/
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 增加一个新的对象
Date date = new Date();
out.writeObject(userName);
out.writeObject(password);
out.writeObject(date);
}
/**
* 在反序列的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列
*
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 注意这里的接受顺序是有限制的,否则的话会出错的
// 例如上面先write的是A对象的话,那么下面先接受的也一定是A对象...
userName = (String) in.readObject();
password = (String) in.readObject();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = (Date) in.readObject();
System.out.println("反序列化后的日期为:" + sdf.format(date));
}
}
}
五、实现序列化的其它方式
1、把对象包装成JSON字符串传输
这里采用JSON格式同时使用Google的gson进行转义
Gson gson = new Gson();
//转成json
String json = gson.toJson(obj);
//解析成对象,可以强制转换成需要的对象
Object obj= gson.toJson(json);
2、采用Google的ProtoBuf
Protobuf是一个纯粹的展示层协议,可以和各种传输层协议一起使用;Protobuf的文档也非常完善。 但是由于Protobuf产生于Google,所以目前其仅仅支持Java、C#典型应用场景和非应用场景 Protobuf具有广泛的用户基础,空间开销小以及高解析性能是其亮点,非常适合于公司内部的对性能要求高的RPC调用。
由于Protobuf提供了标准的IDL以及对应的编译器,其IDL文件是参与各方的非常强的业务约束,另外,Protobuf与传输层无关,采用HTTP具有良好的跨防火墙的访问属性,所以Protobuf也适用于公司间对性能要求比较高的场景。
由于其解析性能高,序列化后数据量相对少,非常适合应用层对象的持久化场景。
三种方式对比传输同样的数据,google protobuf只有53个字节是最少的。
3、XML&SOAP
XML是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。 SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于XML为序列化和反序列化协议的结构化消息传递协议。
4、Thrift
Thrift是Facebook开源提供的一个高性能,轻量级RPC服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 Thrift并不仅仅是序列化协议,而是一个RPC框架。相对于JSON和XML而言,Thrift在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的RPC解决方案;但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。
5、Avro
Avro提供两种序列化格式:JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美,JSON格式方便测试阶段的调试。
6、其他
hadoop有自己的序列化实现方式。
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.mapreduce.lib.db.DBWritable;
import lombok.Data;
/**
* @author alanchan
* 实现Hadoop序列化接口Writable 从数据库读取/写入数据库的对象应实现DBWritable
*/
@Data
public class User implements Writable, DBWritable {
private int id;
private String userName;
private String password;
private String phone;
private String email;
private String createDay;
@Override
public void write(PreparedStatement ps) throws SQLException {
ps.setInt(1, id);
ps.setString(2, userName);
ps.setString(3, password);
ps.setString(4, phone);
ps.setString(5, email);
ps.setString(6, createDay);
}
@Override
public void readFields(ResultSet rs) throws SQLException {
this.id = rs.getInt(1);
this.userName = rs.getString(2);
this.password = rs.getString(3);
this.phone = rs.getString(4);
this.email = rs.getString(5);
// this.createDay = rs.getDate(6);
this.createDay = rs.getString(6);
}
@Override
public void write(DataOutput out) throws IOException {
out.writeInt(id);
out.writeUTF(userName);
out.writeUTF(password);
out.writeUTF(phone);
out.writeUTF(email);
out.writeUTF(createDay);
}
@Override
public void readFields(DataInput in) throws IOException {
id = in.readInt();
userName = in.readUTF();
password = in.readUTF();
phone = in.readUTF();
email = in.readUTF();
createDay = in.readUTF();
}
}
六、序列化实现推荐
以上描述的几种序列化和反序列化协议都各自具有相应的特点,适用于不同的场景。
- 对于公司间的系统调用,如果性能要求在100ms以上的服务,建议基于XML的SOAP协议
- 基于Web browser的Ajax,以及Mobile app与服务端之间的通讯,建议JSON协议。
- 对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的运用场景,建议JSON协议
- 对于调试环境比较恶劣的场景,建议JSON或XML
- 当对性能和简洁性有极高要求的场景,推荐Protobuf、Thrift、Avro
- 对于T级别的数据的持久化应用场景,推荐Protobuf或Avro
- 动态语言为主的应用场景,推荐Avro
- 以静态类型语言为主的应用场景,推荐Protobuf
- 如果需要提供一个完整的RPC解决方案,推荐Thrift
- 如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,推荐Protobuf
以上,简单的介绍了序列化和反序列化的作用、实现方式以及实际的业务场景中如何选择。