zoukankan      html  css  js  c++  java
  • java基础——序列化

    为什么需要序列化

     我们知道,java程序在运行时,对象是在对上创建的,如果程序停止了,那么这个对象也不复存在了。当我们需要将对象存储在硬盘上时,就需要序列化的技术了。

    序列化就是一种将对象转换成字节序列的过程。反序列化就是将字节序列代表的对象恢复成原来的样子。通过序列化与反序列化,我们可以实现进程间的通信。

    序列化使用场景:

    1、持久化存储某个对象。

    2、进程间的通信(包括网络中传输对象)。

    序列化的实现方式

    如果要序列化某个对象,那么这个类应该实现 Serializable 接口或者 Externalizable 接口之一。

    实现 Serializable  接口

    Serializable 接口是一个空接口,里面没有任何的方法,只要一个对象实现了该接口,那么这个对象就可以被序列化。

    序列化步骤:

    1、创建 ObjectOutputStream 输出流对象, 指定对象字节流的输出位置。

    2、通过 writeObject() 将对象的字节流写入文件。

    3、关闭输出流。

    反序列化步骤:

    1、创建一个 ObjectInputStream 输入流,指定需要读取的文件。

    2、通过 readObject() 获取已经序列化的对象。

    package javaIO;
    
    import java.io.Serializable;
    
    //创建一个 Student 类实现 Serializable 接口
    public class Student implements Serializable{
        
        private String name;
        private int num;
        
        //只写了带参的构造函数, 没有提供无参的构造函数
        public Student(String name, int num) {
            super();
            this.name = name;
            this.num = num;
            System.out.println("反序列化是否调用了构造函数?");
        }
        
        
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getNum() {
            return num;
        }
        public void setNum(int num) {
            this.num = num;
        }
    
        @Override
        public String toString() {
            return "Student [name=" + name + ", num=" + num + "]";
        }
    }
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    public class TestSerializable {
        public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
            
            // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\javatest\student.txt"));
            
            Student student = new Student("Zz-feng", 1001);
            
            //通过 writeObject() 将对象的字节流写入文件
            out.writeObject(student);
            
            //关闭输出流
            out.close();
            
            // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\javatest\student.txt"));
            
            //通过 readObject() 将以序列化的对象恢复
            Object object = in.readObject();
            
            System.out.println(object);  //输出  Student [name=Zz-feng, num=1001]
            
            //关闭输入流
            in.close();
        }
    }

    通过上面的例子我们看到,在反序列的过程中,并没有调用类的构造函数。反序列化的对象是由 JVM 自己生成的,不需要调用构造方法。

    成员变量是引用的序列化

    如果一个可序列化的类的成员不是基本类型,也不是String类型,而是一个引用类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。

    public class Person{
        //省略相关属性与方法
    }
    public class Teacher implements Serializable {
    
        private String name;
        private Person person;
    
        public Teacher(String name, Person person) {
            this.name = name;
            this.person = person;
        }
    
         public static void main(String[] args) throws Exception {
            try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
                Person person = new Person("路飞", 20);
                Teacher teacher = new Teacher("雷利", person);
                oos.writeObject(teacher);
            }
        }
    }

    我们看到程序直接报错,因为Person类的对象是不可序列化的,这导致了Teacher的对象不可序列化。

    类的静态变量不会被序列化。

    同一对象序列化多次

    同一对象序列化多次,会将这个对象序列化多次吗?答案是否定的。

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    public class TestSerializable {
        public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
            
            // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\javatest\student.txt"));
            
            Student s1 = new Student("Zz-feng", "1001");
            Student s2 = new Student("mytest", "1002");
            Student s3 = new Student("mytest", "1002");
            
            //通过 writeObject() 将对象的字节流写入文件
            out.writeObject(s1);
            out.writeObject(s2);
            out.writeObject(s3);
            out.writeObject(s1);
            out.writeObject(s2);
            out.writeObject(s3);
            
            //关闭输出流
            out.close();
            
            // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\javatest\student.txt"));
            
            //通过 readObject() 将以序列化的对象恢复
            Object read_s1 = in.readObject();
            Object read_s2 = in.readObject();
            Object read_s3 = in.readObject();
            Object read_s11 = in.readObject();
            Object read_s22 = in.readObject();
            Object read_s33 = in.readObject();
            
            System.out.println(read_s1 == read_s2);        // false
            System.out.println(read_s2 == read_s3);        // false
            
            System.out.println(read_s1 == read_s33);    // false        
            System.out.println(read_s2 == read_s33);    // false
            
            System.out.println(read_s1 == read_s11);    // true
            System.out.println(read_s2 == read_s22);    // true
            
            //关闭输入流
            in.close();
        }
    }

    从输出结果来看,同一个对象只会序列化一次,并不是每次调用 writeObject() 方法都会序列化一个对象。

    序列化过程

    1、如果一个对象已经被序列化过,那么这个对象会保存一个序列化编码号,并将序列化编号一起保存在输出的序列化文件中。

    2、当程序试图序列化一个对象时,首先会检查该对象是否已经存在序列化编号,如果有,则直接输出编号到序列化文件中。否则进行序列化。

    序列化存在的问题

    根据序列的过程,如果一个对象已经被序列化了,那么再次调用 writeObject() 方法并不会再次序列化该对象。如果该对象的属性是可以 set() 等方式修改的,那么反序列化得到的对象并不会显示被修改的内容。

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    public class TestSerializable {
        public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
            
            // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\javatest\student.txt"));
            
            Student s1 = new Student("Zz-feng", "1001");
            Student s2 = new Student("mytest", "1002");
            
            //通过 writeObject() 将对象的字节流写入文件
            out.writeObject(s1);
            
            s1.setName("newName");
            s1.setNum("newNum");
            
            out.writeObject(s1);
            out.writeObject(s1);
            
            //关闭输出流
            out.close();
            
            // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\javatest\student.txt"));
            
            //通过 readObject() 将以序列化的对象恢复
            Object read_s1 = in.readObject();
            Object read_s11 = in.readObject();
            
            System.out.println(read_s1);    // Student [name=Zz-feng, num=1001]
            System.out.println(read_s11);    // Student [name=Zz-feng, num=1001]
            
            //关闭输入流
            in.close();
        }
    }

    自定义序列化

    有些时候,我们有这样的需求,某些属性不需要序列化。使用 transient 关键字选择不需要序列化的字段。

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    public class TestSerializable {
        public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
            
            // 创建 ObjectOutputStream 输出流对象, 传入一个 FileOutputStream 对象, 指定对象字节流的输出位置
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\javatest\student.txt"));
            
            Student s1 = new Student("Zz-feng", "1001", 18, "香波地岛");
            
            //通过 writeObject() 将对象的字节流写入文件
            out.writeObject(s1);
            
            out.writeObject(s1);
            
            //关闭输出流
            out.close();
            
            // 创建 ObjectOutputStream 输出流对象, 传入一个 FileInputStream 对象, 指定需要反序列化的文件
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\javatest\student.txt"));
            
            //通过 readObject() 将以序列化的对象恢复
            Student read_s1 = (Student)in.readObject();
            
            System.out.println(read_s1);               // Student [name=Zz-feng, num=1001]
            System.out.println(read_s1.getAge());      // 0
            System.out.println(read_s1.getAddress());  // null
            
            //关闭输入流
            in.close();
        }
    }

    通过上面的代码我们发现了一个问题,尽管 transient  可以决定哪些内容是不需要序列化的,但在反序列化的过程中,JVM会忽视那些没有被序列化的部分,这样我们并不能完整的获得这个对象。对于 transient  修饰的成员变量会赋默认值。

    对象的某些信息可能是敏感信息,比如银行卡号等,如果直接序列化可能存在信息泄露的风险,如果使用 transient  不进行序列化,那么反序列化时又得不到这个对象的完整信息。为了解决这些问题,我们可以重写以下三个方法,从而对序列化的信息进行加密,防止信息泄露:

    1、private  void  writeObject(ObjectOutputStream  out)     //序列化时会调用此方法

    2、private  void  readObject(ObjectInputStream  in)      //反序列化时会调用此方法

    3、private  void  readObjectNoData()     //当序列化的类版本和反序列化的类版本不同时,或者 ObjectInputStream 流被修改时,会调用此方法。

    Serializable 接口是一个空接口,上面的三个方法都不是必需的,但一般我们会重写前面两个。

    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    //创建一个 Student 类实现 Serializable 接口
    public class Student implements Serializable{
        
        private String name;
        private String num;
        private transient int age;
        private transient String address;
        
        
        //自定义序列化
        private void writeObject(ObjectOutputStream out) throws IOException {
            
            //只序列化以下3个成员变量
            out.writeObject(name);
            out.writeInt(age);
            
            //写入反序后的信息,当然我们也可以使用其他加密方式。这样别人打开文件,看到的就不是真正的信息,更安全。
            out.writeObject(new StringBuffer(address).reverse());
        }
    
        //自定义反序列化。注意:read()的顺序要和write()的顺序一致。比如说序列化时写的顺序是name、age、address,反序列化时读的顺序也要是name、age、address
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            
            //readObject()返回的是Object,要强制类型转换
            this.name= (String)in.readObject();      
            this.name=(String)in.readObject();
            
            //反序才得到真正的信息
            StringBuffer pwd=(StringBuffer)in.readObject();
            this.address=pwd.reverse().toString();
        }
    }
    public class Test {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            Student s1=new Student ("张三", "1234", 18, "空岛");
    
            //序列化
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./obj.txt"));
            //调用我们自定义的writeObject()方法
            out.writeObject(s1);
            out.close();
    
            //反序列化
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("./obj.txt"));
            //调用自定义的readObject()方法
            Student s = (Student )in.readObject();     
            in.close()
    
            //测试
            System.out.println(s.getAge());    //18
            System.out.println(s.getName());   //张三
            System.out.println(s.getAddress());   //空岛
        }
    } 

    除了上面的方法外,我们还可以使用 private/default/protected/public  Object  writeReplace(){......} 或者 private/default/protected/public  Object  writeReplace(){......}。这两个方法返回一个 Object 对象,重写该方法,可以做一些格式化处理。

    //implements Serializable
    class User implements Serializable{
        private int id;
        private String name;
        private String password;
    
        public User(int id,String name,String password){
            this.id=id;
            this.name=name;
            this.password=password;
        }
    
        //......
    
        private Object writeReplace(){
            String info="请编号为"+id+"的客户"+name+"到1号柜台办理业务。";
            return info;
        }
    }
    //implements Serializable
    class User implements Serializable{
        private int id;
        private String name;
        private String password;
        //......其他成员变量
    
        public User(int id,String name,String password){
            this.id=id;
            this.name=name;
            this.password=password;
        }
    
        //...........
    
        //用指定的对象替换掉反序列化读取的对象
        private Object readResolve(){
            String info="请编号为"+id+"的客户"+name+"到1号柜台办理业务。";
            return info;
        }
    
    }

    writeReplace:在序列化时,会先调用此方法,再调用 writeObject 方法。此方法可将任意对象代替目标序列化对象。

    readResolve:在反序列化读取对象后,会自动调用此方法,将读取的对象替换为指定的对象。反序列化出来的对象被立即丢弃。此方法在readeObject后调用。常用来反序列化单例类,保证单例的唯一性。

    两者只是作用的时间点不同,可以联合使用。

    Externalizable:强制自定义序列化

    除了实现 Serializable 接口外,还可以通过 Externalizable 接口实现序列化。该接口中有两个方法:

    public interface Externalizable extends java.io.Serializable {
         void writeExternal(ObjectOutput out) throws IOException;    //调用writeObject()时,会自动调用此方法来序列化对象  
         void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;    //调用readObject()时,会自动调用此方法来反序列化
    }
    //implements Externalizable
    class User implements Externalizable{
        private int id;
        private String name;
        private String password;
        //......其他成员变量
    
        //必须要有无参的构造函数
        public User(){
    
        }
    
        public User(int id,String name,String password){
            this.id=id;
            this.name=name;
            this.password=password;
        }
    
        public int getId() {
            return id;
        }
    
        public String getName() {
            return name;
        }
    
        public String getPassword() {
            return password;
        }
    
        //自定义序列化
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeInt(id);
            out.writeObject(name);
            out.writeObject(password);
        }
    
        //自定义反序列化。注意读的顺序要和写的顺序一致。
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            this.id=in.readInt();
            this.name=(String)in.readObject();
            this.password=(String)in.readObject();
        }
    }

    Externalizable 接口不同于 Serializable 接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供 pulic 的无参构造器,因为在反序列化的时候需要反射创建对象。

    两种序列化对比

    实现Serializable接口实现Externalizable接口
    系统自动存储必要的信息,有没有无参构造函数都行 程序员决定存储哪些信息,不需要有无参构造函数
    Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 必须实现接口内的两个方法
    性能略差 性能略好

    虽然Externalizable接口带来了一定的性能提升,但变成复杂度也提高了,所以一般通过实现Serializable接口进行序列化。

    版本控制——serialVersionUID

    在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。

    一个类实现了 Serializable 接口,如果我们没有显示的指定版本号,那么 JVM 会给这个类指定一个默认的 serialVersionUID,默认样式为:

    private static final long serialVersionUID = 1L;

    当然,我们也可以显示的指定版本号:

    private static final long serialVersionUID = 123456789L;

    serialVersionUID 主要是为了解决版本问题,如果一个类的方法或字段有所改动,当我们设置了 serialVersionUID 后,在反序列化是就会报出异常。如果我们没有设置serialVersionUID,由于默认的 serialVersionUID 的值是一样的,所以在反序列化过程中,缺少的字段会默认空值,多余的字段会被丢弃。

    使用 serialVersionUID 的情形:

    1、如果只是修改了方法,反序列化不容影响,则无需修改版本号;

    2、如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;

    3、如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

    参考资料

    java序列化,看这篇就够了

    Java 自定义序列化、反序列化

    serialVersionUID的作用

  • 相关阅读:
    jquery sortable 在ie中拖动的后tabs不显示解决了
    光标是停在文本框文字的最后 ie
    PetShop安装
    Python小题目 针对快速教程
    vb升级到vb.net的一些文章(downmoon收集自msdn)
    [算法 笔记]大根堆
    [More Effective C++ 学习笔记]异常
    [More Effective C++ 学习笔记]效率
    [TCP IP详解:学习笔记]TCP定时器
    【Linux 编程】pthead_cond_t 的使用
  • 原文地址:https://www.cnblogs.com/Zz-feng/p/13340530.html
Copyright © 2011-2022 走看看