Javaでのシリアル化。 それほど単純ではない





シリアル化は、オブジェクトを一連のバイトに変換し、そこから完全に復元できるプロセスです。 なぜこれが必要なのですか? 実際には、通常のプログラムの実行では、プログラムの起動から終了までのオブジェクトの最大寿命がわかっています。 シリアル化により、このフレームワークを拡張し、プログラムの起動時にもオブジェクトに「生命を与える」ことができます。



すべてに追加のボーナスは、クロスプラットフォームの保存です。 使用しているオペレーティングシステムに関係なく、シリアル化はオブジェクトをバイトストリームに変換し、任意のOSで復元できます。 ネットワーク経由でオブジェクトを転送する必要がある場合は、オブジェクトをシリアル化してファイルに保存し、ネットワーク経由で受信者に転送できます。 彼は受け取ったオブジェクトを回復できます。 また、シリアル化を使用すると、オペレーティングシステムが異なる可能性のある異なるマシン上にあるメソッド(Java RMI)をリモートで呼び出し、呼び出し元のJavaプロセスのマシン上にあるかのように操作できます。



シリアル化メカニズムの実装は非常に簡単です。 クラスは、 Serializableインターフェイスを実装する必要があります。 このインターフェイスはメソッドを持たない識別子ですが、このクラスのオブジェクトをシリアル化できることをjvmに伝えます。 シリアル化メカニズムは基本的な入出力システムに接続されており、オブジェクトをバイトストリームに変換して実行するため、出力ストリームOutputStreamを作成し、 ObjectOutputStreamにパッケージ化してwriteObject()メソッドを呼び出す必要があります。 オブジェクトを復元するには、 InputStreamObjectInputStreamにパックし、 readObject()メソッドを呼び出す必要があります



直列化中に、直列化可能なオブジェクトとともに、オブジェクトグラフが保存されます。 つまり これに関連するすべてのオブジェクト、他のクラスのオブジェクトもシリアル化されます。



クラスPersonのオブジェクトをシリアル化する例を考えてみましょう。



import java.io.*; class Home implements Serializable { private String home; public Home(String home) { this.home = home; } public String getHome() { return home; } } public class Person implements Serializable { private String name; private int countOfNiva; private String fatherName; private Home home; public Person(String name, int countOfNiva, String fatherName, Home home) { this.name = name; this.countOfNiva = countOfNiva; this.fatherName = fatherName; this.home = home; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", countOfNiva=" + countOfNiva + ", fatherName='" + fatherName + '\'' + ", home=" + home + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Home home = new Home("Vishnevaia 1"); Person igor = new Person("Igor", 2, "Raphael", home); Person renat = new Person("Renat", 2, "Raphael", home); //      ObjectOutputStream ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("person.out")); objectOutputStream.writeObject(igor); objectOutputStream.writeObject(renat); objectOutputStream.close(); //       ObjectInputStream ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream("person.out")); Person igorRestored = (Person) objectInputStream.readObject(); Person renatRestored = (Person) objectInputStream.readObject(); objectInputStream.close(); //    ByteArrayOutputStream ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream2.writeObject(igor); objectOutputStream2.writeObject(renat); objectOutputStream2.flush(); //    ByteArrayInputStream ObjectInputStream objectInputStream2 = new ObjectInputStream( new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Person igorRestoredFromByte = (Person) objectInputStream2.readObject(); Person renatRestoredFromByte = (Person) objectInputStream2.readObject(); objectInputStream2.close(); System.out.println("Before Serialize: " + "\n" + igor + "\n" + renat); System.out.println("After Restored From Byte: " + "\n" + igorRestoredFromByte + "\n" + renatRestoredFromByte); System.out.println("After Restored: " + "\n" + igorRestored + "\n" + renatRestored); } }
      
      





結論:



 Before Serialize: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@355da254} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@355da254} After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
      
      





この例では、 Personオブジェクトをシリアル化するときに、オブジェクトのグラフがシリアル化されることを示すために、 Homeクラスが作成されます。 HomeクラスはSerializableインターフェイスも実装する必要があります。そうでない場合、 java.io.NotSerializableExceptionが発生します。 この例では、 ByteArrayOutputStreamクラスを使用したシリアル化についても説明しています。



プログラムの実行結果から興味深い結論を導き出すことができます。 シリアル化前に同じオブジェクトへの参照があったオブジェクトを復元する場合、このオブジェクトは一度だけ復元されます。 これは、回復後のオブジェクト内の同じリンクで確認できます。



 After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae}
      
      





ただし、2つの出力ストリーム( ObjectInputStreamByteArrayOutputStreamがある )で記録する場合、ストリームの1つで以前に作成されたという事実にもかかわらず、 ホームオブジェクトが再作成されることもわかります。 これは、2つのストリームで受信したホームオブジェクトの異なるアドレスで見られます。 1つの出力ストリームでシリアル化してからオブジェクトを復元すると、不要な重複なしにオブジェクトのネットワーク全体を復元できることが保証されます。 もちろん、プログラムの実行中にオブジェクトの状態が変わる場合がありますが、これはプログラマーの良心です。



問題



この例は、オブジェクトを復元するときにClassNotFoundExceptionが発生する可能性があることも示しています。 この理由は何ですか? 実際、 Personクラスのオブジェクトをファイルに簡単にシリアル化し、ネットワークを介して友人に転送できます。友人は、 Personクラスが存在しない別のアプリケーションでオブジェクトを復元できます。



そのシリアル化。 作り方



自分でシリアル化を管理したい場合はどうしますか? たとえば、オブジェクトにはユーザーのユーザー名とパスワードが保存されます。 ネットワーク上でさらに転送するには、シリアル化する必要があります。 この場合、パスワードを渡すことは非常に信頼できません。 この問題を解決するには? 2つの方法があります。 まず、 transientキーワードを使用します。 第二に、 Serializableの関心を実現する代わりにその拡張機能であるExternalizableインターフェイスを使用します。 それらを比較するための最初と2番目の方法の例を考えてみましょう。



最初の方法-トランジェントを使用したシリアル化



 import java.io.*; public class Logon implements Serializable { private String login; private transient String password; public Logon(String login, String password) { this.login = login; this.password = password; } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } }
      
      





結論:



 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'}
      
      





2番目の方法-Externalizableインターフェースの実装によるシリアル化



 import java.io.*; public class Logon implements Externalizable { private String login; private String password; public Logon() { } public Logon(String login, String password) { this.login = login; this.password = password; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(login); } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { login = (String) in.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } }
      
      





結論:



 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'}
      
      





目を引く2つのオプションの最初の違いは、コードのサイズです。 Externalizableインターフェースを実装する場合、 writeExternal()およびreadExternal()の 2つのメソッドをオーバーライドする必要があります。 writeExternal()メソッドでは、どのフィールドをシリアル化するか、およびreadExternal()でどのように読み取るかを示します。 transientという単語を使用する場合、シリアル化する必要のないフィールドを明示的に示します。 また、2番目のメソッドでは、デフォルトコンストラクター、さらにパブリックコンストラクターを明示的に作成しました。 なぜこれが行われるのですか? このコンストラクタなしでコードを実行してみましょう。 そして出力を見てください:



 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} Exception in thread "main" java.io.InvalidClassException: Logon; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at Logon.main(Logon.java:45)
      
      





java.io.InvalidClassException例外が発生しました。 この理由は何ですか? スタックトレースに沿って進むと、 ObjectStreamClassクラスのコンストラクターに次の行があることがわかります。



  if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl);
      
      





Externalizableインターフェースの場合、 getExternalizableConstructor()コンストラクターメソッドが呼び出されその内部でReflectionを介して、オブジェクトを復元するクラスのデフォルトコンストラクターを取得しようとします。 見つからない場合、またはパブリックでない場合、例外が発生します。 この状況を回避するには、次のようにします。クラスでコンストラクターを明示的に作成せずに、セッターを使用してフィールドを埋め、ゲッターで値を取得します。 次に、クラスをコンパイルすると、デフォルトのコンストラクターが作成され、 getExternalizableConstructor()で使用できます。 Serializableの場合getSerializableConstructor()メソッドはObjectクラスのコンストラクターを取得し、そこから必要なクラスを探します。見つからない場合はClassNotFoundExceptionを取得しますSerializableExternalizableの主な違いは、前者がオブジェクトリカバリを作成するためにコンストラクタを必要としないことです。 バイトから完全に回復するだけです。 2つ目は、復元中に、宣言ポイントでコンストラクターを使用してオブジェクトが最初に作成され、次に、シリアル化中に受信したバイトからのフィールドの値がオブジェクトに書き込まれます。 個人的に、私は最初の方法を好みます、それははるかに簡単です。 さらに、シリアル化動作を設定する必要がある場合でも、 Externalizableを使用することも、 writeObject()およびreadObject()メソッドをオーバーライドせずに)追加してSerializableを実装することもできません。 しかし、彼らが「働く」ためには、彼らの署名を厳密に守らなければなりません。



 import java.io.*; public class Talda implements Serializable { private String name; private String description; public Talda(String name, String description) { this.name = name; this.description = description; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); System.out.println("Our writeObject"); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); System.out.println("Our readObject"); } @Override public String toString() { return "Talda{" + "name='" + name + '\'' + ", description='" + description + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Talda partizanka = new Talda("Partizanka", "Viiiski"); System.out.println("Before: \n" + partizanka); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Talda.out")); out.writeObject(partizanka); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Talda.out")); partizanka = (Talda) in.readObject(); System.out.println("After: \n" + partizanka); } }
      
      





結論:



 Before: Talda{name='Partizanka', description='Viiiski'} Our writeObject Our readObject After: Talda{name='Partizanka', description='Viiiski'}
      
      





追加したメソッド内で、 defaultWriteObject()およびdefaultReadObject()が呼び出されます。 追加したメソッドがなくても動作するかのように、デフォルトのシリアライゼーションを担当します。



実際、これは氷山の一角にすぎません。シリアル化のメカニズムを詳しく調べ続けると、高い確率でさらに多くのニュアンスを見つけることができます。「シリアル化...はそれほど単純ではありません。」



All Articles