JavaRush /Java Blog /Random-JA /そのまま連載化。パート1
articles
レベル 15

そのまま連載化。パート1

Random-JA グループに公開済み
一見すると、シリアル化は簡単なプロセスのように思えます。本当に、これ以上簡単なことはないでしょうか?インターフェイスを実装するクラスを宣言しましたjava.io.Serializable- これで終わりです。クラスを問題なくシリアル化できます。 そのまま連載化。 パート 1 - 1理論的にはこれは真実です。実際には、多くの微妙な点があります。これらは、パフォーマンス、逆シリアル化、クラスの安全性に関連しています。さらに多くの側面を備えています。そのような微妙な点について説明します。この記事は次の部分に分かれています。
  • メカニズムの微妙さ
  • なぜ必要なのでしょうか?Externalizable
  • パフォーマンス
  • しかし一方で
  • データセキュリティ
  • オブジェクトのシリアル化Singleton
最初の部分に進みましょう -

メカニズムの微妙さ

まず、簡単な質問です。オブジェクトをシリアル化可能にする方法は何通りありますか? 実際にやってみると、開発者の 90% 以上がこの質問に (言葉遣いまでは) ほぼ同じように答えています。方法は 1 つだけです。そんな中、二人がいます。誰もが 2 番目の機能を覚えているわけではありませんし、ましてやその機能についてわかりやすいことを言うわけではありません。では、これらの方法とは何でしょうか? 誰もが最初のものを覚えています。これはすでに述べた実装でありjava.io.Serializable、何の努力も必要ありません。2 番目のメソッドもインターフェイスの実装ですが、別のものですjava.io.Externalizable。とは異なりjava.io.Serializable、実装する必要がある 2 つのメソッド (writeExternal(ObjectOutput)と )が含まれていますreadExternal(ObjectInput)。これらのメソッドにはシリアル化/逆シリアル化ロジックが含まれています。 コメント。以下では、実装を標準として、実装を拡張としてシリアル化をSerializable参照する場合がありますExternalizable。別のコメントreadObjectここでは、やの定義などの標準シリアル化制御オプションについては意図的に触れませんwriteObject。これらの方法は多少間違っていると思います。これらのメソッドはインターフェイスでは定義されておらずSerializable、実際には、制限を回避し、標準のシリアル化を柔軟にするための小道具です。Externalizable柔軟性を提供するメソッドが最初から組み込まれています。もう一つ質問しましょう。java.io.Serializable?を使用して、標準のシリアル化は実際にどのように機能しますか? そして、それは Reflection API を通じて機能します。それらの。クラスはフィールドのセットとして解析され、それぞれが出力ストリームに書き込まれます。この操作がパフォーマンスの点で最適ではないことは明らかだと思います。正確な金額は後ほどわかります。前述の 2 つのシリアル化方法には、もう 1 つの大きな違いがあります。つまり、逆シリアル化メカニズム内です。使用すると、Serializable逆シリアル化は次のように行われます。オブジェクトにメモリが割り当てられ、その後、そのフィールドがストリームからの値で埋められます。オブジェクトのコンストラクターは呼び出されません。ここでは、この状況を個別に考える必要があります。さて、私たちのクラスはシリアル化可能です。そして彼の親は?完全にオプションです!さらに、クラスを継承した場合Object、親は間違いなくシリアル化可能ではありません。Objectそして、フィールドについて何も知らないとしても、フィールドは私たち自身の親クラスに存在する可能性があります。彼らはどうなるでしょうか?シリアル化ストリームには入りません。逆シリアル化時にどのような値が取られるのでしょうか? この例を見てみましょう:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
これは透過的です。シリアル化不可能な親クラスとシリアル化可能な子クラスがあります。そして、これが起こります:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
つまり、逆シリアル化中に、親の非シリアル化可能クラスのパラメーターを持たないコンストラクターが呼び出されます。また、そのようなコンストラクターが存在しない場合、逆シリアル化中にエラーが発生します。上で述べたように、子オブジェクトのコンストラクター (逆シリアル化しているオブジェクト) は呼び出されません。標準メカニズムを使用すると、次のように動作しますSerializable。使用するとExternalizable状況が異なります。まず、パラメーターなしでコンストラクターが呼び出され、次に作成されたオブジェクトに対して readExternal メソッドが呼び出され、実際にすべてのデータが読み取られます。したがって、Externalizable インターフェイスを実装するクラスには、パラメーターのないパブリック コンストラクターが必要です。さらに、そのようなクラスのすべての子孫もインターフェイスを実装するとみなされるためExternalizable、パラメータのないコンストラクターも必要です。さらに進んでみましょう。のようなフィールド修飾子がありますtransient。これは、このフィールドをシリアル化すべきではないことを意味します。ただし、あなた自身が理解しているように、この命令は標準のシリアル化メカニズムにのみ影響します。使用する場合、Externalizableこのフィールドをシリアル化したり、減算したりする必要はありません。フィールドが一時的であると宣言されている場合、オブジェクトが逆シリアル化されると、デフォルト値が適用されます。もう一つ、かなり微妙な点。標準のシリアル化では、修飾子を持つフィールドstaticはシリアル化されません。したがって、逆シリアル化後、このフィールドの値は変更されません。もちろん、実装中にExternalizableこのフィールドをわざわざシリアル化および逆シリアル化する人はいませんが、これを行わないことを強くお勧めします。これにより、微妙なエラーが発生する可能性があります。修飾子を含むフィールドは、final通常のフィールドと同様にシリアル化されます。1 つの例外を除いて、外部化可能を使用する場合は逆シリアル化できません。これらはコンストラクターで初期化する必要final-поляがあり、その後、readExternal でこのフィールドの値を変更することは不可能になるためです。したがって、 -field を持つオブジェクトをシリアル化する必要がある場合はfinal、標準のシリアル化を使用するだけで済みます。多くの人が知らないもう一つのポイント。標準のシリアル化では、クラス内でフィールドが宣言される順序が考慮されます。いずれにせよ、これは以前のバージョンの場合であり、Oracle 実装の JVM バージョン 1.6 では、順序は重要ではなくなり、フィールドのタイプと名前が重要になりました。フィールドは一般的に同じままであるにもかかわらず、メソッドの構成は標準メカニズムに影響を与える可能性が非常に高くなります。これを回避するために、次のような仕組みがあります。インターフェイスを実装する各クラスにはSerializable、コンパイル段階でフィールドが 1 つ追加されます。private static final long serialVersionUID。このフィールドには、シリアル化されたクラスの一意のバージョン識別子が含まれます。これは、クラスの内容 (フィールド、その宣言順序、メソッド、その宣言順序) に基づいて計算されます。したがって、クラスが変更されると、このフィールドの値も変更されます。このフィールドは、クラスがシリアル化されるときにストリームに書き込まれます。ちなみに、これはおそらく、static-field がシリアル化される場合に私が知っている唯一のケースです。逆シリアル化中に、このフィールドの値が仮想マシン内のクラスの値と比較されます。値が一致しない場合、次のような例外がスローされます。
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
ただし、バイパスしないにしても、このチェックを欺く方法があります。これは、クラス フィールドのセットとその順序がすでに定義されているが、クラス メソッドが変更される可能性がある場合に便利です。この場合、シリアル化は危険にさらされませんが、標準メカニズムでは、変更されたクラスのバイトコードを使用してデータを逆シリアル化することはできません。しかし、先ほども言いましたが、彼は騙される可能性があります。つまり、クラス内のフィールドを手動で定義しますprivate static final long serialVersionUID。原則として、このフィールドの値は何でも構いません。コードが変更された日付と同じに設定することを好む人もいます。1Lを使用する人もいます。標準値 (内部で計算される値) を取得するには、SDK に含まれる Serialver ユーティリティを使用できます。このように定義すると、フィールドの値が固定されるため、逆シリアル化は常に許可されます。さらに、バージョン 5.0 では、ほぼ次のことがドキュメントに記載されています。デフォルトの計算はクラス構造の詳細に非常に敏感であり、コンパイラの実装によって異なる可能性があるため、すべてのシリアル化可能なクラスでこのフィールドを明示的に宣言することを強くお勧めします。そのため、InvalidClassException予期しない結果が生じます。このフィールドを として宣言することをお勧めしますprivate。それが宣言されているクラスのみを参照します。仕様では修飾子は指定されていませんが。この点について考えてみましょう。次のクラス構造があるとします。
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
言い換えれば、シリアル化できない親から継承されたクラスがあります。このクラスをシリアル化することは可能ですか?そのためには何が必要ですか? 親クラスの変数はどうなるでしょうか? 答えはこれです。はい、Bクラスのインスタンスをシリアル化できます。そのためには何が必要なのでしょうか?ただし、クラスにはAパラメーターのないコンストラクター、publicまたは が必要ですprotected。その後、逆シリアル化中に、すべてのクラス変数がAこのコンストラクターを使用して初期化されます。クラス変数は、Bシリアル化されたデータ ストリームからの値で初期化されます。B理論的には、最初に説明したメソッド -readObjectと、 -をクラス内で定義し、writeObjectその先頭で によるクラス変数の (逆) シリアル化を実行しBin.defaultReadObject/out.defaultWriteObjectその後、使用可能な変数の (逆) シリアル化を実行することが可能です。クラスからA(私たちの場合iPubliciProtectedと同じパッケージ内にあるiPackage場合、これらは と です)。ただし、私の意見では、これには拡張シリアル化を使用する方が良いと思います。次に触れたいのは、複数のオブジェクトのシリアル化です。次のクラス構造があるとします。 BA
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
そのまま連載化。 パート 1 ~ 2クラスのインスタンスをシリアル化するとどうなりますかA? これは class のインスタンスに沿ってドラッグしB、次にクラスは、すべての始まりと同じである、Cinstance への参照を持つインスタンスに沿ってドラッグします。A悪循環と無限再帰? 幸いなことに、そうではありません。次のテストコードを見てみましょう。
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
私たちは何をしているのでしょうか?クラスとのインスタンスを作成しA、それらに相互にリンクを与えてから、それぞれをシリアル化します。次に、それらを逆シリアル化し、一連のチェックを実行します。結果として何が起こるか: BC
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
それで、このテストから何が学べるでしょうか? 初め。逆シリアル化後のオブジェクト参照は、それ以前の参照とは異なります。つまり、シリアル化/逆シリアル化中にオブジェクトがコピーされました。このメソッドは、オブジェクトのクローンを作成するために使用されることがあります。2 番目の結論はより重要です。相互参照を持つ複数のオブジェクトをシリアル化/逆シリアル化する場合、それらの参照は逆シリアル化後も有効なままになります。つまり、シリアル化前に 1 つのオブジェクトを指していた場合、逆シリアル化後も 1 つのオブジェクトを指すことになります。これを確認するための別の小さなテスト:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
クラス オブジェクトBには、クラス オブジェクトへの参照がありますC。シリアル化されると、bclass のインスタンスとともにシリアル化されС、その後 c の同じインスタンスが 3 回シリアル化されます。逆シリアル化後はどうなりますか?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
ご覧のとおり、4 つの逆シリアル化されたオブジェクトはすべて、実際には 1 つのオブジェクトを表しており、そのオブジェクトへの参照は等しいです。連載前と全く同じです。Externalizableもう 1 つの興味深い点は、と を同時に実装するとどうなるかということですSerializable。その質問のように、ゾウとクジラでは誰が誰に勝つでしょうか? を克服しますExternalizable。シリアル化メカニズムは最初にその存在をチェックし、その後でのみその存在をチェックするため、 を実装するクラス B が を実装するクラス A を継承するSerializable場合、クラス B のフィールドはシリアル化されません。最後のポイントは継承です。を実装するクラスから継承する場合、追加のアクションを実行する必要はありません。シリアル化は子クラスにも拡張されます。を実装するクラスから継承する場合は、親クラスの readExternal メソッドと writeExternal メソッドをオーバーライドする必要があります。そうしないと、子クラスのフィールドはシリアル化されません。この場合、親メソッドを忘れずに呼び出す必要があります。そうしないと、親フィールドがシリアル化されません。* * * 詳細についてはおそらく終わりました。しかし、地球規模の問題として、まだ触れていない問題が 1 つあります。つまり - SerializableExternalizableSerializableExternalizable

なぜ外部化可能が必要なのでしょうか?

そもそもなぜ高度なシリアル化が必要なのでしょうか? 答えは簡単です。まず、柔軟性が大幅に向上します。第 2 に、シリアル化されたデータの量に関して大幅なメリットが得られることがよくあります。第三に、パフォーマンスなどの側面があります。これについては以下で説明します。柔軟性があり、すべてが明確に見えます。実際、シリアル化と逆シリアル化のプロセスを必要に応じて制御できるため、クラスの変更から独立できます (先ほど述べたように、クラスの変更は逆シリアル化に大きな影響を与える可能性があります)。したがって、ボリュームの増加について少しお話したいと思います。次のクラスがあるとします。
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
残りは重要ではありません。フィールドは int 型で作成できますが、これは例の効果を高めるだけです。ただし、実際には、フィールドはintパフォーマンス上の理由から型指定される場合があります。いずれにせよ、要点は明らかだ。クラスは日付と時刻を表します。これは主にシリアル化の観点から見て興味深いものです。おそらく最も簡単な方法は、単純なタイムスタンプを保存することでしょう。これは、long 型です。つまり、シリアル化すると 8 バイトかかります。さらに、このアプローチには、コンポーネントを 1 つの値に変換したり、元の値に変換したりするためのメソッドが必要です。– 生産性の低下。このアプローチの利点は、完全にクレイジーな日付が 64 ビットに収まることです。これは大きな安全マージンですが、実際にはほとんどの場合必要ありません。上記のクラスは 2 + 5*1 = 7 バイトを必要とします。さらに、クラスと 6 つのフィールドのオーバーヘッド。このデータを圧縮する方法はありますか? 確かに。秒と分の範囲は 0 ~ 59、つまり それらを表すには、8 ビットではなく 6 ビットで十分です。時間 – 0 ~ 23 (5 ビット)、日 – 0 ~ 30 (5 ビット)、月 – 0 ~ 11 (4 ビット)。年を考慮しないすべての合計 - 26 ビット。int のサイズはまだ 6 ビット残っています。理論的には、場合によっては、これで 1 年間は十分である可能性があります。そうでない場合は、別のバイトを追加すると、データ フィールドのサイズが 14 ビットに増加し、範囲は 0 ~ 16383 になります。実際のアプリケーションではこれで十分です。必要な情報を格納するために必要なデータサイズを合計 5 バイトに削減しました。最大 4 でない場合。欠点は前のケースと同じです。日付をパックして保存する場合は、変換メソッドが必要です。しかし、私はこれをこの方法で実行したいと考えています。つまり、それを別のフィールドに保存し、パッケージ化された形式でシリアル化します。ここで使用するのが理にかなっていますExternalizable:
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
実はそれだけです。シリアル化後、クラスごとにオーバーヘッドが発生し、(6 バイトではなく) 2 つのフィールドと 5 バイトのデータが発生します。これはすでに大幅に優れています。さらなるパッケージ化は専門のライブラリに任せることができます。示された例は非常に単純です。その主な目的は、高度なシリアル化がどのように使用できるかを示すことです。私の意見では、シリアル化されたデータの量が増加する可能性は主な利点とは程遠いです。柔軟性に加えて、主な利点は... (スムーズに次のセクションに進みます...) ソースへのリンク:そのままシリアル化
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION