序列化与反序列化

Posted zenghi-home

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了序列化与反序列化相关的知识,希望对你有一定的参考价值。

一、序列化和反序列化的概念

把对象转换为字节序列的过程称为对象的序列化
把字节序列恢复为对象的过程称为对象的反序列化
对象的序列化主要有两种用途:

  1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2. 在网络上传送对象的字节序列。

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,保存在物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

二、JDK类库中的序列化API

Java中序列化与反序列化用到两个对象流:

  1. java.io.ObjectOutputStream:对象输出流,它的writeObject(Object obj)方法可对参数指定的Object对象进行序列化,把得到的字节序列写到一个目标输出流中。
  2. java.io.ObjectInputStream:对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

对于上面两个对象流,还需要两个流:

  • 保存到到本地硬盘
    1. java.io.FileOutputStream:对象序列化为字节输出到本地硬盘
    2. java.io.FileInputStream:从本地硬盘读取字节反序列化
  • 网络传输
    1. java.io.ByteArrayOutputStream:将对象序列化为字节
    2. java.io.ByteArrayInputStream:从字节反序列化为对象

Java实现序列化与反序列化提供有两个接口:

  1. Serializable接口:实现Serializable接口的类可以 采用默认的序列化方式
  2. Externalizable接口:实现Externalizable接口的类完全由自身来控制序列化的行为

只有实现了Serializable和Externalizable接口的类的对象才能被序列化。

对象序列化步骤:

  1. 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2. 通过对象输出流的writeObject()方法写对象。

对象反序列化的步骤如下:

  1. 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  2. 通过对象输入流的readObject()方法读取对象。

实现Serializable的序列化与反序列化

对象序列化保存在本地硬盘

public class UserInfoTest {
    /**
     * 从内存序列化到本地
     */
    @Test
    public void serialize() throws IOException {
        UserInfo userInfo = new UserInfo();
        userInfo.setName("张三");
        userInfo.setAge(23);

        //序列化到本地
        FileOutputStream fileOutputStream = new FileOutputStream("./aa.txt");
        ObjectOutputStream os = new ObjectOutputStream(fileOutputStream);
        os.writeObject(userInfo);
        System.out.println("序列化成功");
    }

    /**
     * 从本地反序列化到内存
     */
    @Test
    public void deserialize() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("./aa.txt");
        ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
        UserInfo userInfo = (UserInfo) inputStream.readObject();
        System.out.println("反序列化成功:" + userInfo);
    }
}
@Data
class UserInfo implements Serializable {
    private String name;
    private Integer age;
}

网络传输(使用RabbitMQ测试)

/**
 * @author Hayson
 * @date 2018/11/13 09:08
 * @description RabbitMQ测试序列化对象
 */
public class Send {
    private final static String QUEUE_NAME = "test_serializable";
    private final static String HOST = "192.168.239.134";
    private final static Integer PORT = 5672;
    private final static String USERNAME = "admin";
    private final static String PASSWORD = "admin";

    public static void main(String[] args) throws IOException, TimeoutException {
        Send();     //发送消息
        Recv();     //接收消息
    }
    /**
     * 对象序列化为字节数组,发送字节数组消息
     * @throws IOException
     * @throws TimeoutException
     */
    private static void Send() throws IOException, TimeoutException {
        Connection connection = getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(QUEUE_NAME, BuiltinExchangeType.DIRECT);
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.queueBind(QUEUE_NAME, QUEUE_NAME, QUEUE_NAME);

        UserInfo userInfo = new UserInfo();
        userInfo.setUserName("张三");
        userInfo.setPassWord("23");

        //对象序列化为字节数组
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(512);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(userInfo);
        byte[] userInfoBytes = outputStream.toByteArray();

        channel.basicPublish(QUEUE_NAME, QUEUE_NAME, null, userInfoBytes);
        System.out.println("发送对象字节大小 = " + userInfoBytes.length);

        objectOutputStream.close();
        connection.close();
    }
    /**
     * 接收字节数组消息,反序列化为对象
     * @throws IOException
     * @throws TimeoutException
     */
    private static void Recv() throws IOException, TimeoutException {
        Connection connection = getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, 
                                       Envelope envelope, 
                                       AMQP.BasicProperties properties, 
                                       byte[] body) throws IOException {
                // 把接收到的字节数组反序列化为对象
                ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
                ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
                UserInfo userInfo = null;
                try {
                    userInfo = (UserInfo) objectInputStream.readObject();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }finally{
                    objectInputStream.close();
                }
                System.out.println(userInfo);
            }
        };
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
    /**
     * 获取Connection连接
     * @return
     * @throws IOException
     * @throws TimeoutException
     */
    private static Connection getConnection() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST);
        factory.setPort(PORT);
        factory.setUsername(USERNAME);
        factory.setPassword(PASSWORD);

        return factory.newConnection();
    }
}
@Data
class UserInfo implements Serializable{
    private String name;
    private Integer age;
}

上面运行的序列化和反序列化都没有问题,但是如果我们序列化了对象到硬盘,而项目在后期再原来的UserInfo类的基础上添加成员变量private String address;

@Data
class UserInfo implements Serializable{
    private String name;
    private Integer age;
    private String address;//新添加成员变量
}

添加新成员变量后,再从本地反序列化,则会有以下异常(截取部分):

java.io.InvalidClassException: com.hayson.config.UserInfo; local class incompatible: stream classdesc serialVersionUID = 6288102273809508157, local class serialVersionUID = -4025030802652115000

出现异常原因:

serialVersionUID是用于记录class文件的版本信息的,serialVersionUID这个数字是JVM(JAVA虚拟机)通过一个类的类名、成员、包名、工程名算出的一个数字。而这时候序列化文件中记录的serialVersionUID与项目中的不一致,即找不到对应的类来反序列化。

serialVersionUID

如果序列化与反序列化的时候可能会修改类的成员,那么最好一开始就给这个类指定一个serialVersionUID,如果一类已经指定的serialVersionUID,然后
在序列化与反序列化的时候,jvm都不会再自己算这个 classserialVersionUID了。

去掉刚才添加的成员变量;,并且在User类中指定一个serialVersionUID

   @Data
    private static class UserInfo implements Serializable {
        private static final long serialVersionUID = 7078078434151067638L;
        private String name;
        private Integer age;
        //private String address;
    }

重新序列化到aa.txt文件中,然后再在javaBean中添加private String address;新成员变量(将上面的注释去掉),再执行反序列化操作,此时运行不再出现上面的异常情况,并且执行反序列化结果与javaBean中的成员变量增加不影响结果。

如果在UserInfo类中再添加成员变量,而这个变量为一个class ,如Address,那么Address类也必须要实现Serializable接口。

@Data
private static class UserInfo implements Serializable {
    private static final long serialVersionUID = 7078078434151067638L;
    private String name;
    private Integer age;
    private Address address;    //新增成员变量是一个类,这个类必须实现Serializable
}
class Address implements Serializable{
    private String country;
    private String city;
}

transient关键字

transient关键字,当你不想要某些字段序列化,可以用transient关键字修饰

@Data
private static class UserInfo implements Serializable {
    private static final long serialVersionUID = 7078078434151067638L;
    private String name;
    private Integer age;
    transient String address;//新添加成员变量,有transient关键字,不序列化该字段
}



以上是关于序列化与反序列化的主要内容,如果未能解决你的问题,请参考以下文章

GolangGo 语言 JSON 的序列化与反序列化实践

GolangGo 语言 JSON 的序列化与反序列化实践

GolangGo 语言 JSON 的序列化与反序列化实践

Serializable详解:代码验证Java序列化与反序列化

php中序列化与反序列化

Java对象操作流:序列化与反序列化