Java-对象序列化

对象序列化的作用

  • Java平台允许我们在内存中创建可复用的Java对象。
  • 一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长
  • 在现实应用中,可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。
  • Java对象序列化就能够帮助我们实现该功能。

  • 使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。
  • 必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量
  • 由此可知,对象序列化不会关注类中的静态变量
  • 除了在持久化对象时会用到对象序列化之外,当使用RPC(包括Java标准RMI远程方法调用),或在网络中传递对象时,都会用到对象序列化。
  • Java序列化API为处理对象序列化提供了一个标准机制

序列化接口

  • 在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化
  • java.io.Serializable是一个标识接口,即意味着它仅仅是为了说明类的可序列化属性,接口没有包含任何需要子类实现的抽象方法

对象序列化与反序列化

  • 将对象的状态信息保存到流中的操作,称为序列化。
  • 可以使用Java提供的工具ObjectOutputStream . writeObject (Serializable obj )来完成
  • 从流中读取对状态信息的操作称为反序列化,可以使用Java提供的工具ObjectInputStream.readObject ()来完成

  • 为什么一个类实现了Serializable接口,它就可以被序列化呢?从ObjectOutputStream类的源代码片段可以看出它是如何来使用这个标识接口的(源代码1177行)

4、

一个简单的序列化程序

先将一个Person对象保存到缓存中,然后再从该缓存中读出被存储的Person对象,并打印该对象

  • Person类
    import java.io.Serializable;
    //要序列化Person创建的对象,需要实现Serializable接口
    public class Person implements Serializable {
        private String name;
        private int age;
        private String address;
        public Person() {
        }
        public Person(String name, int age, String address) {
            this.name = name;
            this.age = age;
            this.address = address;
        }
        public String getName() {return name;}
        public void setName(String name) {this.name = name;}
        public int getAge() {return age;}
        public void setAge(int age) {this.age = age;}
        public String getAddress() {return address;}
        public void setAddress(String address) {this.address = address;}
        @Override
        public String toString() {
            return "Person{" +"name='" + name + '\'' +", age=" + age +", address='" + address + '\'' +'}';
        }
    }
  • 例1
    import java.io.*;
    public class SerializableTest {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            /*
             把Person对象序列化的ByteArrayOutputStream字节数组输出流中
             对象序列化使用ObjectOutputStream
            */
            ObjectOutputStream oos = null;//实例化ObjectOutputStream
            Person p = null;
            byte[] buffer = null;//定义变量
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();//实例化ByteArrayOutputStream
                oos = new ObjectOutputStream(baos);
                p = new Person("Yi", 21, "重庆");
                oos.writeObject(p);//ObjectOutputStream 将Person对象写入流中
                buffer = baos.toByteArray();//把ByteArrayOutputStream 对象返回一个byte数组
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            /*
            ByteArrayInputStream字节数组输入流中反序列化成Person对象
            反序列化成Person对象使用ObjectInputStream流
            */
            ObjectInputStream ois = null;//实例化ObjectInputStream对象
            Person p1 = null;
            try {
                ByteArrayInputStream bais = new ByteArrayInputStream(buffer);//实例化ByteArrayInputStream,传入buffer参数
                ois = new ObjectInputStream(bais);
                p1 = (Person) ois.readObject();//readObject 从流中读取数据转换成Person
                System.out.println(p1);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(p == p1);//p1和p2是两个对象
        }
    }
    /*
    Person{name='Yi', age=21, address='重庆'}
    false
    */

5、从上例的运行结果可以看出的要点:

  • 对于Serializable反序列化后的对象,不需要调用构造方法重新构造,对象完全以它存储的二进制位作为基础来构造,而不调用构造方法

6、对象序列化过程不仅仅保存单个对象,还能追踪对象内所包含的所有引用,并保存那些对象(这些对象也需实现了Serializable接口)

7、序列前的对象与序列化后的对象是深复制反序列化还原后的对象地址与原来的的地址不同,但是内容是一样的,而且对象中包含的引用也相同。换句话说,通过序列化操作,我们可以实现对任何可Serializable对象的”深度复制“,这意味着复制的是整个对象网,而不仅仅是基本对象及其引用。对于同一流的对象,他们的地址是相同,说明他们是同一个对象,但是与其他流的对象地址却不相同。也就说,只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个。

8、上面提到,如果仅仅只是让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制

9、使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大

一个较复杂的序列化程序

  • Student类
    import java.io.Serializable;
    public class Student implements Serializable {
        private String no;
        private double score;
        private Person person;
        public Student(String no, double score, Person person) {
            this.no = no;
            this.score = score;
            this.person = person;
        }
        public String getNo() {return no;}
        public void setNo(String no) {this.no = no;}
        public double getScore() {return score;}
        public void setScore(double score) {this.score = score;}
        public Person getPerson() {return person;}
        public void setPerson(Person person) {this.person = person;}
        @Override
        public String toString() {
            return "Student{" + "no='" + no + '\'' +", score=" + score +", person=" + person + '}';
        }
    }
  • 例2
    import com.tjetc.IOUtils;
    import java.io.*;
    public class SerializableStudentTest {
        public static void main(String[] args) throws ClassNotFoundException {
            try {
                writeObjYoFile();
                readObjFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //把对象序列化到文件中
        private static void writeObjYoFile() throws IOException {
            FileOutputStream fos = new FileOutputStream("student.dat");//创建FileOutputStream文件字节输出流
            ObjectOutputStream oos = new ObjectOutputStream(fos);//实例化ObjectOutputStream
            Person p = new Person("Jun", 26, "深圳");
            Student s = new Student("001", 99, p);
            oos.writeObject(s);//ObjectOutputStream 将Person对象写入流中
            IOUtils.close(oos);
            IOUtils.close(fos);
        }
        //从文件中反序列化成对象
        private static void readObjFile() throws IOException, ClassNotFoundException {
            FileInputStream fis = new FileInputStream("student.dat");//创建FileInputStream文件字节输入流
            ObjectInputStream ois = new ObjectInputStream(fis);//实例化ObjectInputStream对象
            Student s1 = (Student) ois.readObject();//readObject 从流中读取数据转换成Person
            System.out.println(s1);
            IOUtils.close(ois);
            IOUtils.close(fis);
        }
    }
    /*Student{no='001', score=99.0, person=Person{name='Jun', age=26, address='深圳'}}*/
  • 自己封装的IOUtils类
    import java.io.*;
    public class IOUtils {
        public static void close(OutputStream o) {
            if (o != null) {
                try {
                    o.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void close(InputStream i) {
            if (i != null) {
                try {
                    i.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void close(Writer w) {
            if (w != null) {
                try {
                    w.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void close(Reader r) {
            if (r != null) {
                try {
                    r.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

10、在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程

 

11、

transient

当某个字段被声明为transient后,默认序列化机制就会忽略该字段

  • 例子
    import java.io.Serializable;
    public class Person1 implements Serializable {
        private String name;
        private int age;
        //字段被声明为transient后,默认序列化机制就会忽略该字段
        private transient String address;
        public String getName() {return name;
        }
        public void setName(String name) {this.name = name;
        }
        public int getAge() {return age;
        }
        public void setAge(int age) {this.age = age;
        }
        public String getAddress() {return address;
        }
        public void setAddress(String address) {this.address = address;
        }
        public Person1(String name, int age, String address) {
            this.name = name;
            this.age = age;
            this.address = address;
        }
        @Override
        public String toString() {return "[Person:"+name+","+age+","+address+"]";
        }
    }
    import java.io.*;
    public class SerializableTest1 {
        public static void main(String[] args) throws Exception {
            example1();
        }
        /**
         * 把对象序列化写入硬盘方式,在反序列化出来
         *
         * @throws IOException
         * @throws ClassNotFoundException
         */
        public static void example1() throws IOException, ClassNotFoundException {
            // 对象的序列化
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person1.dat"));
            Person1 person = new Person1("Tom", 40, "China");
            oos.writeObject(person);
            oos.close();
            System.out.println(person);
            // 对象的反序列化
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person1.dat"));
            Person1 person1 = (Person1) ois.readObject();
            System.out.println(person1);
            System.out.println(person == person1);
        }
    }
    /*
    [Person:Tom,40,China]
    [Person:Tom,40,null]
    false
    * */

12、

writeObject()与readObject()

对于上述已被声明为transient的字段address,除了将transient关键字去掉之外,是否还有其它方法能使它再次可被序列化?

  • 方法之一就是在Person类中添加两个方法:writeObject()与readObject()
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    public class Person1 implements Serializable {
        private String name;
        private int age;
        private transient String address;
        //不用transient来修饰,用 writeObject()与readObject()这二个方法实现序列化
        private void writeObject(ObjectOutputStream out) throws IOException {
            //默认的序列化方法
            out.defaultWriteObject();
            //主动序列化
            out.writeUTF(address);
        }
        private void readObject(ObjectInputStream in) throws IOException,
                ClassNotFoundException {
            //默认的反序列化方法
            in.defaultReadObject();
            //主动反序列化,赋值成员变量address
            address = in.readUTF();
        }
        public String getName() {return name;}
        public void setName(String name) {this.name = name;}
        public int getAge() {return age;}
        public void setAge(int age) {this.age = age;}
        public String getAddress() {return address;}
        public void setAddress(String address) {this.address = address; }
        public Person1(String name, int age, String address) {
            this.name = name;
            this.age = age;
            this.address = address;
        }
        @Override
        public String toString() {
            return "[Person:"+name+","+age+","+address+"]";
        }
    }
    /*运行SerializableTest1后:
    [Person:Tom,40,China]
    [Person:Tom,40,China]
    false
    * */

13、注意:刚才说的是添加方法而不是“覆盖”或者“实现”,因为这两个方法不是基类Object也不是接口Serializable中的方法

  • 一旦对象被序列化或者反序列还原,就会自动地分别调用者两个方法。也就是说,只要我们提供了这两个方法,就会使用它们而不是默认的序列化机制,这个两个方法必须在类内部自己实现。
  • 两个方法其实是private类型。也就是说这两个方法仅能被这个类的其他成员调用,但其实我们没有在这个类的其他的方法中调用这两个方法。
  • 是ObjectOutputStream和ObjectInputStream对象的writeObject和readObject()方法分别调用者两个方法(通过过反射机制来访问类的私有方法),在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,利用反射来搜索是否有writeObject()方法。
  • 如果有,就会跳过正常的序列化过程,转而调用这个它的writeObject()方法,readObject方法处理方式也一样writeObject()内部可以通过ObjectOutputStream.defaultWriteObject()来执行默认的writeObject()(非transient字段由这个方法保存),同样的,在类readObject内部,可以通过ObjectInputStream.defalutReadObject()来执行默认的readObject()方法

14、

serialVersionUID

在Java中,软件的兼容性是一个大问题,尤其在使用到对象串行性的时候,那么在某一个对象已经被串行化了,可是这个对象又被修改后重新部署了(如在原来的类添加一个新属性),那么在这种情况下, 用老软件来读取新文件格式虽然不是什么难事,但是有可能丢失一些信息。

  • serialVersionUID来解决这些问题,新增的serialVersionUID必须定义成下面这种形式:
    static final long serialVersionUID=-12345678L;
  • idea一键生成 serialVersionUID

     

  • 在实现Serializable接口的类上alt+enter就可以一键生成serialVersionUID

15、如果我们不显式提供serialVersionUID的值,则Java会根据以下几个属性 进行自动计算:

  • 类的名字;属性字段的名字;方法的名字;已实现的接口

         

  • 改动上述任意一项内容(无论是增加或删除),都会引起编码值变化,从而引起类似的异常警报。
  • 这个数字序列称为“串行化版本统一标识符”(serial version universal identifier),简称UID。
  • 解决这个问题的办法是在类里面新增一个域serialVersionUID,强制类仍旧使用原来的UID

16、无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化

17、

Externalizable

      JDK中还提供了另一个序列化接口:

  • 使用该接口之后,之前基于Serializable接口的序列化机制就将失效。
  • 对象将按照我们自定义的方式进行序列化或反序列化,这对于一些信息敏感应用或对序列化反序列化性能要求较高来说非常重要

18、Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由我们自己完成,另外使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。由于这个原因,实现Externalizable接口的必须要提供一个无参的构造器,且它的访问权限为public

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
//序列化接口Externalizable
public class Person3 extends Person implements Externalizable {
    private String name;
    private int age;
    private String address;
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    public int getAge() { return age; }
    public void setAge(int age) {this.age = age; }
    public String getAddress() {return address;}
    public void setAddress(String address) {this.address = address;}
    public Person3(String name, int age, String address) {
        System.out.println("Person构造方法");
        this.name = name;
        this.age = age;
        this.address = address;
    }
    //无参的构造方法要有
    public Person3() {
    }
    //重写序列化接口Externalizable接口中的方法来自定义序列化
    @Override
    public void readExternal(ObjectInput arg0) throws IOException,
            ClassNotFoundException {
        System.out.println("自定义序列化过程");
        name = arg0.readUTF();
        //可以序列化进去,这里也可以选择不读,那反序列化出来的就是0默认值
        age = arg0.readInt();
        address = arg0.readUTF();
    }
    @Override
    public void writeExternal(ObjectOutput arg0) throws IOException {
        System.out.println("自定义反序列化过程");
        arg0.writeUTF(name+"ext");
        arg0.writeInt(age + 10);
        arg0.writeUTF(address + "ext");
    }
    @Override
    public String toString() {
        return "[Person:" + name + "," + age + "," + address + "]";
    }
}
import java.io.*;
public class SerializableTest3 {
    public static void main(String[] args) throws Exception {
        example2();
    }
    /**
     *     * 把对象序列化写入数组方式,在反序列化出来
     *     * @throws IOException
     *     * @throws ClassNotFoundException
     *    
     */
    public static void example2() throws IOException, ClassNotFoundException {
        // 对象的序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        Person3 person = new Person3("Jerry", 30, "Beijing");
        oos.writeObject(person);
        byte[] buffer = baos.toByteArray();
        oos.close();
        baos.close();
        // 对象的反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(
                buffer));
        Person3 Person3 = (Person3) ois.readObject();
        System.out.println(Person3);
        System.out.println(person);
        System.out.println(person == Person3);
    }
}
/*
Person构造方法
自定义反序列化过程
自定义序列化过程
[Person:Jerryext,40,Beijingext]
[Person:Jerry,30,Beijing]
false
*/

posted @   carat9588  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示