この記事では主に Java objectのシリアル化と逆シリアル化について編集者が考えたので、参考として共有します。エディターをフォローして見てみましょう
前回の記事では、バイトストリーム文字ストリームの使用を紹介しましたが、その際、オブジェクト内の各オブジェクトを出力するために DataOutputStream ストリームを使用しました 属性 の値が出力されます。ストリームを 1 つずつ、読み取り時にはその逆を実行します。私たちの意見では、この 動作 は、特にこのオブジェクトに多数の属性値がある場合には非常に面倒です。これに基づいて、Java のオブジェクト直列化メカニズムはこの操作を非常にうまく解決できます。この記事では、Java オブジェクトのシリアル化について簡単に紹介します。主な内容は次のとおりです:
簡潔なコード実装
シリアル化実装の基本アルゴリズム
2 つの特殊な状況
カスタムシリアル化メカニズム
バージョン管理
1. 単純なコード実装
オブジェクトのシリアル化の使用を紹介する前に、以前にオブジェクト タイプのデータをどのように格納したかを見てみましょう。
//简单定义一个Student类 public class Student { private String name; private int age; public Student(){} public Student(String name,int age){ this.name = name; this.age=age; } public void setName(String name){ this.name = name; } public void setAge(int age){ this.age = age; } public String getName(){ return this.name; } public int getAge(){ return this.age; } //重写toString @Override public String toString(){ return ("my name is:"+this.name+" age is:"+this.age); } }
//main方法实现了将对象写入文件并读取出来 public static void main(String[] args) throws IOException{ DataOutputStream dot = new DataOutputStream(new FileOutputStream("hello.txt")); Student stuW = new Student("walker",21); //将此对象写入到文件中 dot.writeUTF(stuW.getName()); dot.writeInt(stuW.getAge()); dot.close(); //将对象从文件中读出 DataInputStream din = new DataInputStream(new FileInputStream("hello.txt")); Student stuR = new Student(); stuR.setName(din.readUTF()); stuR.setAge(din.readInt()); din.close(); System.out.println(stuR); }
出力結果: my name is:walker age is:21
この種のコード記述は明らかに面倒です。 次に、シリアル化を使用してオブジェクト情報を保存する方法を見てみましょう。
public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); Student stuW = new Student("walker",21); oos.writeObject(stuW); oos.close(); //从文件中读取该对象返回 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Student stuR = (Student)ois.readObject(); System.out.println(stuR); }
ファイルの書き込み時には、writeObject という 1 つのステートメントのみが使用されます。読み取り時には、readObject という 1 つのステートメントのみが使用されます。また、Student の set メソッドと get メソッドは使用されなくなりました。とても簡単なことではありませんか?次に実装の詳細を紹介します。
2. シリアル化の基本アルゴリズム
この仕組みでは、各オブジェクトは固有のシリアル番号に対応しており、各オブジェクトは保存時にもこのシリアル番号に従って対応付けられます。各オブジェクトのシリアル番号を使用して保存および読み取りを行います。まず、オブジェクトをストリームに書き込む場合を例に挙げます。オブジェクトの基本情報は、初めて検出されたときにストリームに保存されますが、現在検出されているオブジェクトが保存されている場合は保存されません。この情報を保存し、代わりにこのオブジェクトのシリアル番号を記録します (データを繰り返し保存する必要がないため)。読み取りの場合、ストリーム内で見つかった各オブジェクトは、オブジェクトのシリアル番号が読み取られると、直接出力されます。
オブジェクトをシリアル化可能にしたい場合は、インターフェース java.io.Serializable; を実装する必要があります。これはマークインターフェースであり、メソッドを実装する必要はありません。 ObjectOutputStream ストリームは、オブジェクト情報をバイトに変換できるストリームです。コンストラクター は次のとおりです:
public ObjectOutputStream(OutputStream out)
つまり、すべてのバイト ストリームをパラメーターとして渡すことができ、すべてのバイト操作と互換性があります。 writeObject メソッドと readObject メソッドは、シリアル化オブジェクトと逆シリアル化オブジェクトを実装するためにこのストリームで定義されています。もちろん、これら 2 つのメソッドをクラスに実装することでシリアル化メカニズムをカスタマイズすることもできます。これについては後で詳しく説明します。ここでは、シリアル化メカニズム全体を理解するだけで済みます。すべてのオブジェクト データのコピーが 1 つだけ保存され、同じオブジェクトが再度表示される場合は、対応するシリアル番号のみが保存されます。以下では、2 つの特別な状況を通じて、彼の基本的なアルゴリズムを直観的に体験していきます。
3. 2 つの特別な例
まず最初の例を見てみましょう:
public class Student implements Serializable { String name; int age; Teacher t; //另外一个对象类型 public Student(){} public Student(String name,int age,Teacher t){ this.name = name; this.age=age; this.t = t; } public void setName(String name){this.name = name;} public void setAge(int age){this.age = age;} public void setT(Teacher t){this.t = t;} public String getName(){return this.name;} public int getAge(){return this.age;} public Teacher getT(){return this.t;} } public class Teacher implements Serializable { String name; public Teacher(String name){ this.name = name; } } public static void main(String[] args) throws IOException, ClassNotFoundException { Teacher t = new Teacher("li"); Student stu1 = new Student("walker",21,t); Student stu2 = new Student("yam",22,t); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); oos.writeObject(stu1); oos.writeObject(stu2); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Student stuR1 = (Student)ois.readObject(); Student stuR2 = (Student)ois.readObject(); if (stuR1.getT() == stuR2.getT()) System.out.println("相同对象"); }
結果は非常に明白で、同じオブジェクトが出力されます。 main 関数で 2 つの生徒タイプのオブジェクトを定義しましたが、どちらも内部的には同じ教師オブジェクトを参照していました。シリアル化が完了した後、2 つのオブジェクトが逆シリアル化されます。内部の教師オブジェクトが同じインスタンスであるかどうかを比較すると、最初の生徒オブジェクトがシリアル化されたときは t がストリームに書き込まれますが、教師オブジェクトのインスタンスが見つかったときはストリームに書き込まれることがわかります。 2 番目のスチューデント オブジェクトについては、以前に書き込まれたことが判明したため、ストリームには書き込まれず、対応するシーケンス番号のみが参照として保存されます。もちろん、逆シリアル化する場合も原理は同様です。これは、上で紹介したものと同じ基本的なアルゴリズムです。
以下の 2 番目の特別な例を見てください:
public class Student implements Serializable { String name; Teacher t; } public class Teacher implements Serializable { String name; Student stu; } public static void main(String[] args) throws IOException, ClassNotFoundException { Teacher t = new Teacher(); Student s =new Student(); t.name = "walker"; t.stu = s; s.name = "yam"; s.t = t; ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); oos.writeObject(t); oos.writeObject(s); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Teacher tR = (Teacher)ois.readObject(); Student sR = (Student)ois.readObject(); if(tR == sR.t && sR == tR.stu)System.out.println("ok"); }
输出的结果是ok,这个例子可以叫做:循环引用。从结果我们可以看出来,序列化之前两个对象存在的相互的引用关系,经过序列化之后,两者之间的这种引用关系是依然存在的。其实按照我们之前介绍的判断算法来看,首先我们先序列化了teacher对象,因为他内部引用了student的对象,两者都是第一次遇到,所以将两者序列化到流中,然后我们去序列化student对象,发现这个对象以及内部的teacher对象都已经被序列化了,于是只保存对应的序列号。读取的时候根据序列号恢复对象。
四、自定义序列化机制
综上,我们已经介绍完了基本的序列化与反序列化的知识。但是往往我们会有一些特殊的要求,这种默认的序列化机制虽然已经很完善了,但是有些时候还是不能满足我们的需求。所以我们看看如何自定义序列化机制。自定义序列化机制中,我们会使用到一个关键字,它也是我们之前在看源码的时候经常遇到的,transient。将字段声明transient,等于是告诉默认的序列化机制,这个字段你不要给我写到流中去,我会自己处理的。
public class Student implements Serializable { String name; transient int age; public String toString(){ return this.name + ":" + this.age; } } public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); Student stu = new Student(); stu.name = "walker";stu.age = 21; oos.writeObject(stu); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Student stuR = (Student)ois.readObject(); System.out.println(stuR); }
输出结果:walker:0
我们不是给age字段赋初始值了么,怎么会是0呢?正如我们上文所说的一样,被transient修饰的字段不会被写入流中,自然读取出来就没有值,默认是0。下面看看我们怎么自己来序列化这个age。
//改动过的student类,main方法没有改动,大家可以往上看 public class Student implements Serializable { String name; transient int age; private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeInt(25); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); age = ois.readInt(); } public String toString(){ return this.name + ":" + this.age; } }
输出结果:walker:25
结果既不是我么初始化的21,也不是0,而是我们在writeObject方法中写的25。现在我们一点一点看看每个步骤的意义。首先,要想要实现自定义序列化,就需要在该对象定义的类中实现两个方法,writeObject和readObject,而且格式必须和上面贴出来的一样,笔者试过改动方法修饰符,结果导致不能成功序列化。这是因为,Java采用反射机制,检查该对象所在的类中有没有实现这两个方法,没有的话就使用默认的ObjectOutputStream中的这个方法序列化所有字段,如果有的话就执行你自己实现的这个方法。
接下来,看看这两个方法实现的细节,先看writeObject方法,参数是ObjectOutputStream 类型的,这个拿到的是我们在main方法中定义的ObjectOutputStream 对象,要不然它怎么知道该把对象写到那个地方去呢?第一行我们调用的是oos.defaultWriteObject();这个方法实现的功能是,将当前对象中所有没有被transient修饰的字段写入流中,第二条语句我们显式的调用了writeInt方法将age的值写入流中。读取的方法类似,此处不再赘述。
五、版本控制
最后我们来看看,序列化过程的的版本控制问题。在我们将一个对象序列化到流中之后,该对象对应的类的结构改变了,如果此时我们再次从流中将之前保存的对象读取出来,会发生什么?这要分情况来说,如果原类中的字段被删除了,那从流中输出的对应的字段将会被忽略。如果原类中增加了某个字段,那新增的字段的值就是默认值。如果字段的类型发生了改变,抛出异常。在Java中每个类都会有一个记录版本号的变量:static final serivalVersionUID = 115616165165L,此处的值只用于演示并不对应任意某个类。这个版本号是根据该类中的字段等一些属性信息计算出来的,唯一性较高。每次读出的时候都会去比较之前和现在的版本号确认是否发生版本不一致情况,如果版本不一致,就会按照上述的情形分别做处理。
对象的序列化就写完了,如果有什么内容不妥的地方,希望大家指出!
以上がJava オブジェクトのシリアル化と逆シリアル化の詳細なサンプル コードの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。