java序列化之writeObject 和readObject

什麼是序列化和反序列化?

序列化:將對象轉化爲字節的過程稱爲序列化過程。
反序列化:將字節轉化爲對象的過程稱爲反序列化。

序列化主要應用於網絡傳輸和數據存儲的場景。在java中,只有類實現了java.io.serializable接口,該類才能被序列化。
示例Demo1.java:

package com.example.demo;

import java.io.*;

public class Demo1 {
    public static class Person implements Serializable {
        private static final long serialVersionUID = 233858934995755239L;
        private String firstName;
        private String lastName;

        public Person(String firstName,String lastName){
            System.out.println("Init Person...");
            this.firstName = firstName;
            this.lastName = lastName;
        }
        public String toString() {
            return String.format("Pearson.toString(): firstName=%s,lastName=%s", firstName, lastName);
        }
    }

    public static void main(String[] args) {
        String firstName="Li",LastName="Kelly";
        Person person  = new Person(firstName, LastName);
        System.out.println("序列化前:"+person.toString());
        ObjectOutputStream outStream;
        ObjectInputStream inStream;
        String filePath = "D:/demo/person.obj";
        try {
            //創建一個ObjectOutputStream輸出流
            outStream = new ObjectOutputStream(new FileOutputStream(filePath));
            //將對象序列化到文件filePath
            outStream.writeObject(person);

            inStream = new ObjectInputStream(new FileInputStream(filePath));
            Person readObject = (Person)inStream.readObject();
            System.out.println("反序列化後:"+readObject.toString());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

輸出:

Init Person...
序列化前:Pearson.toString(): firstName=Li,lastName=Kelly

反序列化後:Pearson.toString(): firstName=Li,lastName=Kelly

可見對象反序列化,並不沒有調用類的構造方法(反序列化沒有輸出Init Person...)。

 

注:

如果一個可序列化的類的成員不是基本類型,也不是String類型,那這個引用類型也必須是可序列化的,也即繼承serializable,否則,會導致此類不能序列化。

比如上面的代碼中,我們給Person類加一個Dog對象,這個Dog沒有繼承serializable。

package com.example.demo;

import java.io.*;

public class Demo1 {
    public static class Dog{

    }
    public static class Person implements Serializable {
        private static final long serialVersionUID = 233858934995755239L;
        private String firstName;
        private String lastName;
        private Dog dog;

        public Person(String firstName,String lastName, Dog dog){
            System.out.println("Init Person...");
            this.firstName = firstName;
            this.lastName = lastName;
            this.dog = dog;
        }
        public String toString() {
            return String.format("Pearson.toString(): firstName=%s,lastName=%s", firstName, lastName);
        }
    }

    public static void main(String[] args) {
        String firstName="Li",LastName="Kelly";
        Dog dog= new Dog();
        Person person  = new Person(firstName, LastName,dog);
        System.out.println("序列化前:"+person.toString());
        ObjectOutputStream outStream;
        ObjectInputStream inStream;
        String filePath = "D:/demo/person.obj";
        try {
            //創建一個ObjectOutputStream輸出流
            outStream = new ObjectOutputStream(new FileOutputStream(filePath));
            //將對象序列化到文件filePath
            outStream.writeObject(person);

            inStream = new ObjectInputStream(new FileInputStream(filePath));
            Person readObject = (Person)inStream.readObject();
            System.out.println("反序列化後:"+readObject.toString());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

此時執行代碼會報java.io.NotSerializableException: com.example.demo.Demo1$Dog的錯誤。讓Dog繼承serializable則不會報錯。

問:

①同一個對象序列化多次,會將這個對象序列化多次嗎?否。java序列化同一對象,並不會將此對象序列化多次得到多個對象。

            outStream = new ObjectOutputStream(new FileOutputStream(filePath));
            //將對象序列化到文件filePath
            outStream.writeObject(person);
            outStream.writeObject(person);

            inStream = new ObjectInputStream(new FileInputStream(filePath));
            Person readObject1 = (Person)inStream.readObject();
            Person readObject2 = (Person)inStream.readObject();
            System.out.println(readObject1==readObject2);
            System.out.println("反序列化後:"+readObject1.toString());

output:

Init Person...
序列化前:Pearson.toString(): firstName=Li,lastName=Kelly
true
序列化後:Pearson.toString(): firstName=Li,lastName=Kelly

ReadObject1和ReadObject2是同一對象。

②有些時候,我們有這樣的需求,某些屬性不需要序列化。怎麼做?使用transient關鍵字選擇不需要序列化的字段。

示例:

package com.example.demo;

import java.io.*;

public class Demo1 {
    public static class Dog implements Serializable{

    }
    public static class Person implements Serializable {
        private static final long serialVersionUID = 233858934995755239L;
        private String firstName;
        private transient String lastName;
        private transient int age;
        private Dog dog;

        public Person(String firstName,String lastName, Dog dog, int age){
            System.out.println("Init Person...");
            this.firstName = firstName;
            this.lastName = lastName;
            this.dog = dog;
            this.age = age;
        }
        public String toString() {
            return String.format("Pearson.toString(): firstName=%s,lastName=%s,age=%s", firstName, lastName,age);
        }
    }

    public static void main(String[] args) {
        String firstName="Li",LastName="Kelly";
        Dog dog= new Dog();
        Person person  = new Person(firstName, LastName,dog,23);
        System.out.println("序列化前:"+person.toString());
        ObjectOutputStream outStream;
        ObjectInputStream inStream;
        String filePath = "D:/demo/person.obj";
        try {
            //創建一個ObjectOutputStream輸出流
            outStream = new ObjectOutputStream(new FileOutputStream(filePath));
            //將對象序列化到文件filePath
            outStream.writeObject(person);

            inStream = new ObjectInputStream(new FileInputStream(filePath));
            Person readObject = (Person)inStream.readObject();
            System.out.println("反序列化後:"+readObject.toString());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

輸出:

Init Person...
序列化前:Pearson.toString(): firstName=Li,lastName=Kelly,age=23
反序列化後:Pearson.toString(): firstName=Li,lastName=null,age=0

從輸出我們看到,使用transient修飾的屬性(此處是lastName、age),java序列化時,會忽略掉此類字段。

反序列化出的對象,被transient修飾的屬性是默認值。對於引用類型,值是null;基本類型,值是0;boolean類型,值是false。

③使用transient雖然簡單,但是將此屬性完全隔離在了序列化之外。有沒有一種機制,可以定製對象序列化的方式?

答:有。通過重寫writeObject與readObject方法,可以自己選擇哪些屬性需要序列化, 哪些屬性不需要。如果writeObject使用某種規則序列化,則相應的readObject需要相反的規則反序列化,以便能正確反序列化出對象。

示例:

package com.example.demo;

import java.io.*;

public class Demo1 {
    public static class Dog implements Serializable{

    }
    public static class Person implements Serializable {
        private static final long serialVersionUID = 233858934995755239L;
        private String firstName;
        private transient String lastName;
        private transient int age;
        private Dog dog;

        public Person(String firstName,String lastName, Dog dog, int age){
            System.out.println("Init Person...");
            this.firstName = firstName;
            this.lastName = lastName;
            this.dog = dog;
            this.age = age;
        }
        public String toString() {
            return String.format("Pearson.toString(): firstName=%s,lastName=%s,age=%s", firstName, lastName,age);
        }
        private void writeObject(ObjectOutputStream out) throws IOException {
            //將名字反轉寫入二進制流
            out.writeObject(new StringBuffer(this.lastName).reverse());
            out.writeInt(age);
        }

        private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
            //將讀出的字符串反轉恢復回來
            this.lastName = ((StringBuffer)ins.readObject()).reverse().toString();
            this.age = ins.readInt();
        }
    }

    public static void main(String[] args) {
        String firstName="Li",lastName="Kelly";
        Dog dog= new Dog();
        Person person  = new Person(firstName, lastName, dog,23);
        System.out.println("序列化前:"+person.toString());
        ObjectOutputStream outStream;
        ObjectInputStream inStream;
        String filePath = "D:/demo/person.obj";
        try {
            //創建一個ObjectOutputStream輸出流
            outStream = new ObjectOutputStream(new FileOutputStream(filePath));
            //將對象序列化到文件filePath
            outStream.writeObject(person);

            inStream = new ObjectInputStream(new FileInputStream(filePath));
            Person readObject = (Person)inStream.readObject();
            System.out.println("反序列化後:"+readObject.toString());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

在上面的例子中,我們給Person類增加了writeObject和readObject方法。

private void writeObject(ObjectOutputStream out) throws IOException {
            //將名字反轉寫入二進制流
            out.writeObject(new StringBuffer(this.lastName).reverse());
            out.writeInt(age);
        }

        private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
            //將讀出的字符串反轉恢復回來
            this.lastName = ((StringBuffer)ins.readObject()).reverse().toString();
            this.age = ins.readInt();
        }

輸出:

Init Person...
序列化前:Pearson.toString(): firstName=Li,lastName=Kelly,age=23
反序列化後:Pearson.toString(): firstName=null,lastName=Kelly,age=23

在writeObject裏,首先out.writeObject(new StringBuffer(this.lastName).reverse());這句的作用是將名字反轉寫入二進制流。我們打開D:/demo/person.obj文件,看下lastName的形式是不是反轉的。

person.obj文件是亂碼的,但是我們仍然可以看到我們想要的,Kelly變成了yllek。

writeObject第二句是out.writeInt(age);age屬性我們加了transient關鍵字,理論上這個屬性應該不會被序列化。然而我們在這裏加上了這行代碼,所以age又可以被序列化。

除了這個,我們還可以看到writeObject和readObject都是private方法,但是它卻被外部類(ObjectOutputStream和ObjectInputStream)調用,並且它們既不存在與Java.lang.Object,也沒有在Serializeable中聲明,那它們是如何被ObjectOutputStream和ObjectInputStream調用的呢?
答:利用反射機制。ObjectOutputStream和ObjectInputStream使用了反射來尋找是否聲明瞭這兩個方法。因爲它們使用getPrivateMethod,所以這些方法不得不被聲明爲priate以至於供ObjectOutputStream來使用。 

注:Write的順序和read的順序需要對應。譬如writeObject有多個字段都用writeInt寫入流中,那麼readint需要按照順序將其賦值。

例:

private void writeObject(ObjectOutputStream out) throws IOException {
            //將名字反轉寫入二進制流
            out.writeObject(new StringBuffer(this.lastName).reverse());
            out.writeInt(age);
            out.writeObject(this.firstName);
        }

        private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
            //將讀出的字符串反轉恢復回來
            this.lastName = ((StringBuffer)ins.readObject()).reverse().toString();
            this.firstName =(String) ins.readObject();
            this.age = ins.readInt();
        }

此時代碼報錯:

java.io.OptionalDataException
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1585)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章