了解Java序列化和反序列化

什么是java对象序列化

Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。

##为什么需要序列化和反序列化

  • 一是利用序列化实现数据的持久化,通过序列化可以把数据永久的保存下来,二是利用序列化实现远程通信,即在网络上传送对象的字节序列

##序列化的基本方法

1. 简单demo:序列化类仅实现java.io.Serializable接口

Person类,实现了Serializable接口,它包含三个字段:name,String类型;age,Integer类型;gender,Gender类型。另外,还重写该类的toString()方法,以方便打印Person实例中的内容。 避免代码太长,没有写getter和setter方法


public class Person implements Serializable {  

private String name = null;  

private Integer age = null;  

private Gender gender = null;  

public Person() {  
    System.out.println("none-arg constructor");  
}  

public Person(String name, Integer age, Gender gender) {  
    System.out.println("arg constructor");  
    this.name = name;  
    this.age = age;  
    this.gender = gender;  
}   
@Override 
public String toString() {  
    return "[" + name + ", " + age + ", " + gender + "]";  
}  
} 

SimpleSerial,是一个简单的序列化程序,它先将一个Person对象保存到文件person.out中,然后再从该文件中读出被存储的Person对象,并打印该对象。


public class SimpleSerial {  
public static void main(String[] args) throws Exception {  
    File file = new File("person.out");  

    ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));  
    Person person = new Person("John", 101, Gender.MALE);  
    oout.writeObject(person);  
    oout.close();  

    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));  
    Object newPerson = oin.readObject(); // 没有强制转换到Person类型  
    oin.close();  
    System.out.println(newPerson);  
}  
} 

上述程序的输出的结果为:

arg constructor
[John, 31, MALE]

2.transient关键字
当某个字段被声明为transient后,默认序列化机制就会忽略该字段。此处将Person类中的age字段声明为transient,如下所示


public class Person implements Serializable {  
...  
transient private Integer age = null;  
...  
} 

再执行SimpleSerial应用程序,出入如下:

arg constructor
[John, null, MALE]

可见,age字段声明了transient后,不被序列化

3.被序列化类实现了Serializable接口并且定义了readObject和writeObject
对于上述已被声明为transitive的字段age,除了将transitive关键字去掉之外,是否还有其它方法能使它再次可被序列化?方法之一就是在Person类中添加两个方法:writeObject()与readObject(),定义了之后ObjectOutputStream调用Person对象的writeObject的方法进行序列化。ObjectInputStream会调用Person对象的readObject的方法进行反序列化
如下所示:


public class Person implements Serializable {

transient private Integer age = null;

private void writeObject(ObjectOutputStream out) throws IOException {  
    out.defaultWriteObject();  
    out.writeInt(age);  
}  

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
    in.defaultReadObject();  
    age = in.readInt();  
}  
} 

4.被序列化类实现了Externalizable接口
无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。JDK中提供了另一个序列化接口–Externalizable。Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。更改Person的代码:


public class Person implements Externalizable {  

private String name = null;  

transient private Integer age = null;  

private Gender gender = null;  

public Person() {  
    System.out.println("none-arg constructor");  
}  

public Person(String name, Integer age, Gender gender) {  
    System.out.println("arg constructor");  
    this.name = name;  
    this.age = age;  
    this.gender = gender;  
}  

private void writeObject(ObjectOutputStream out) throws IOException {  
    out.defaultWriteObject();  
    out.writeInt(age);  
}  

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
    in.defaultReadObject();  
    age = in.readInt();  
}  

@Override 
public void writeExternal(ObjectOutput out) throws IOException {  

}  

@Override 
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  

}  
...  
} 

执行之后的结果:

arg constructor
none-arg constructor
[null, null, null]

由于writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空.另外,使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。

序列化ID问题
实现序列化接口的对象不强制声明唯一的serialVersionUID,是否声明serialVersionUID对于对象的序列化向上向下兼容性有很大的影响

  • 对象被被序列化保存,在fan序列化的时候,增加或者减少字段,没有唯一serialVersionUID会反序列化失败。

如果没有明确指定serialVersionUID,序列化的时候会根据字段和特定的算法生成一个serialVersionUID,当属性有变化时这个id发生了变化,所以反序列化的时候
就会失败。抛出“本地classd的唯一id和流中class的唯一id不匹配”。

  • 进行两个客户端之间的通过网络传递对象输入,则两边需要定义一样的serialVersionUID进行序列化和反序列化

简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
当实现java.io.Serializable接口的实体(类)没有显式地定义一个名为serialVersionUID,类型为long的变量时,Java序列化机制会根据编译的class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID 。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。

##实现序列化的其他方法
Json序列化-jackson
以下是序列化对象User类


public class User {
private String name;
private Integer age;
private Date birthday;
private String email;

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

public Integer getAge() {
    return age;
}
public void setAge(Integer age) {
    this.age = age;
}

public Date getBirthday() {
    return birthday;
}
public void setBirthday(Date birthday) {
    this.birthday = birthday;
}

public String getEmail() {
    return email;
}
public void setEmail(String email) {
    this.email = email;
}
} 

Json序列化


import com.fasterxml.jackson.databind.ObjectMapper;  
public class JacksonDemo {  
public static void main(String[] args) throws ParseException, IOException {  
    User user = new User();  
    user.setName("小民");   
    user.setEmail("xiaomin@sina.com");  
    user.setAge(20);  
      
    SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd");  
    user.setBirthday(dateformat.parse("1996-10-01"));         
     
    ObjectMapper mapper = new ObjectMapper();  
      
    //User类转JSON  
    //输出结果:{"name":"小民","age":20,"birthday":844099200000,"email":"xiaomin@sina.com"}  
    String json = mapper.writeValueAsString(user);  
    System.out.println(json);  
      
    //Java集合转JSON  
    //输出结果:[{"name":"小民","age":20,"birthday":844099200000,"email":"xiaomin@sina.com"}]  
    List<User> users = new ArrayList<User>();  
    users.add(user);  
    String jsonlist = mapper.writeValueAsString(users);  
    System.out.println(jsonlist);  
}  
}  

Json序列化-fastjson
对于上述的User类,使用fastjson进行序列化,序列化方法如下,可以自己去尝试下


     //序列化
    String text = JSON.toJSONString(u);
    
    //反序列化
    User user = JSON.parseObject(text, User.class);

ProtoBuff序列化

ProtocolBuffer是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化。适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
优点:跨语言;序列化后数据占用空间比JSON小,JSON有一定的格式,在数据量上还有可以压缩的空间。
缺点:它以二进制的方式存储,无法直接读取编辑,除非你有 .proto 定义,否则无法直接读出 Protobuffer的任何内容。
其与thrift的对比:两者语法类似,都支持版本向后兼容和向前兼容,thrift侧重点是构建跨语言的可伸缩的服务,支持的语言多,同时提供了全套RPC解决方案,可以很方便的直接构建服务,不需要做太多其他的工作。 Protobuffer主要是一种序列化机制,在数据序列化上进行性能比较,Protobuffer相对较好。

百度研发的jprotobuf框架将Google原始的protobuf进行了封装,对其进行简化,仅提供序列化和反序列化方法。其实用上也比较简洁,通过对JavaBean中的字段进行注解就行,不需要撰写.proto文件和实用编译器将其生成.java文件,百度的jprotobuf都替我们做了这些事情了。有兴趣的可以大家可以自己去网上看一下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值