Javaベースの【Javaのシーケンス化と逆シーケンス化を深く分析する】

37337 ワード

シーケンス化はJavaオブジェクトの永続化の手段であり、ネットワーク転送、RMIなどのシーンに一般的に応用されている.この文章では、Javaのシーケンス化の原理とシーケンス化戦略をどのようにカスタマイズするかを深く説明する.
Javaオブジェクトのシーケンス化の原理
☕10.なぜシーケンス化するのか
一般的に、Javaプラットフォームで作成したオブジェクトは、JVMの実行時にのみ存在します.つまり、これらのオブジェクトのライフサイクルはJVMのライフサイクルよりも長くはありません.しかし、現実的なアプリケーションでは、JVMが停止した後に指定したオブジェクトを保存(永続化)し、保存されたオブジェクトを将来再読み込みする必要がある場合があります.そのため、このようなニーズを実現するために、私たちのシーケンス化機能があります.
Javaオブジェクトを使用してオブジェクトをシーケンス化して保存すると、オブジェクトの状態が1バイトに保存され、将来的には、これらのバイトがオブジェクトに組み立てられます.オブジェクトのシーケンス化は、クラス内の静的変数を保存することなく、オブジェクトの「ステータス」、すなわちそのメンバー変数を保存することに注意してください.
オブジェクトを永続化するときにオブジェクトのシーケンス化が使用されるほか、RMI(Romote Method Invocation)を使用したり、ネットワークでオブジェクトを転送したりするときにオブジェクトのシーケンス化が使用され、Javaシーケンス化APIはオブジェクトのシーケンス化を処理するための基準メカニズムを提供し、このAPIは簡単で使いやすい.
☕10.Javaオブジェクトのシーケンス化と逆シーケンス方式
Javaでは、クラスがjava.io.Serializableインタフェースを実装している限り、シーケンス化できます.demoコードを例に挙げます.
シーケンス化用のPersonクラスを作成
public class Person implements Serializable {
    private String name;
    private transient int age;
    private static final long serialVersionUID = -6849794470754667710L;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    @Override
    public String toString() {
        return "Person { " +
                "name = '" + this.name + "', " +
                "age = " + this.age + " }";
    }
}

Personのシーケンス化と逆シーケンス化
public static void main(String[] args) {
    //            
    Person person = new Person();
    person.setName("Andy");
    person.setAge(24);
    System.out.println(person);

    //            
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person"));
        oos.writeObject(person);
    } catch (IOException e) {
        e.printStackTrace();
    } 

    //            
    File file = new File("person");
    try {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person"));
        Person newPerson = (Person) ois.readObject();
        System.out.println(newPerson);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

>>>>>
Person { name = 'Andy', age = 24 }
Person { name = 'Andy', age = 0 }

次に、オブジェクトのシーケンス化と逆シーケンス化に関する重要な知識について説明します.
  • クラスはjava.io.Serialzableインタフェースが実装されている場合にのみシーケンス化できます.
  • オブジェクトのシーケンス化および逆シーケンス化は、ObjectOutputStreamおよびObjectInputStreamによって行われる.
  • 仮想マシンが逆シーケンス化を許可するかどうかは、クラスパスと機能コードが一致するかどうかだけでなく、2つのクラスのシーケンス化ID(private static final long serialVersionUID)が一致するかどうかにも依存します.
  • シーケンス化静的変数は保存されません.シーケンス化はオブジェクトの状態を保存し、静的変数はクラスの状態に属するため、シーケンス化は静的変数を保存しません.
  • 親オブジェクトもシーケンス化するには、親にもSerializableインタフェースを実装させる必要があります.
  • Transientキーワードの役割は、変数のシーケンス化を制御することです.この変数がファイルにシーケンス化されないようにします.逆シーケンス化された後、transient変数の値はint型が0、オブジェクト型がnullのような初期値に設定される.

  • ヒント:サーバ側がクライアントにシーケンス化オブジェクトデータを送信する場合、オブジェクトの中にはパスワード文字列などの敏感なデータがあり、このパスワードフィールドをシーケンス化する際に暗号化したいが、クライアントが復号化された鍵を持っていれば、クライアントが逆シーケンス化する場合にのみパスワードを読み取ることができる.これにより、シーケンス化されたオブジェクトのデータセキュリティがある程度保証されます.
    ☕10.シリアル化ストレージ・ルール
    Javaシーケンス化メカニズムは、ディスク領域を節約するために、特定のストレージルールを有し、ファイルに書き込まれたものが同じオブジェクトである場合、オブジェクトの内容を格納するのではなく、参照といくつかの制御情報の空間を再格納するだけで、逆シーケンス化時に、リード関係を回復し、したがって、同じオブジェクトを複数回書き込み逆シーケンス化して得られるオブジェクトは、同じオブジェクトへの参照である.このストレージ・ルールは、ストレージ・スペースを大幅に節約します.
    たとえば、次のコードの例です.
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("result.obj"));
    Test test = new Test();
    test.num = 1;
    oos.writeObject(test);
    oos.flush();
    test.num = 2;
    oos.writeObject(test);
    oos.close();
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("result.obj"));
    Test t1 = (Test) ois.readObject();
    Test t2 = (Test) ois.readObject();
    System.out.println("t1.num = " + t1.num);
    System.out.println("t2.num = " + t2.num);
    
    >>>>>
    t1.num = 1
    t2.num = 1
    

    この例の目的はtestオブジェクトをresult.objファイルに2回保存し、1回書き込み後にオブジェクト属性値を変更して2回目を保存し、result.objから2つのオブジェクトを順次読み出し、2つのオブジェクトのnum属性値を出力することです.ケースコードの目的は,本来,一度にオブジェクトを転送して前後の状態を修正することである.
    その結果、2つの出力はいずれも1である.なぜなら、1回目の書き込み以降、2回目の書き込みを試みた場合、仮想マシンは参照関係によって同じオブジェクトがすでにファイルに書き込まれていることを知っているため、2回目の書き込みの参照のみを保存するので、読み取り時は1回目の保存対象となる.そのため、同じファイルでwriteObjectを複数回使用する場合は、この問題に特に注意する必要があります.
    カスタムシーケンス化と逆シーケンス化
    ArrayListでは、カスタムシーケンス化と逆シーケンス化の方法について説明します.まず、ArrayListのシーケンス化と逆シーケンス化の例を書きましょう.
    public static void main(String[] args) {
    	//       ArrayList  
        List<String> strList = new ArrayList<>();
        strList.add("Alpha");
        strList.add("Beta");
        strList.add("Gamma");
        System.out.println("origin list: " + strList);
    
        //    ArrayList  
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("stringList"));
            oos.writeObject(strList);
        } catch (IOException e) {
            e.printStackTrace();
        }
    
        //         ArrayList  
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("stringList"));
            List<String> newStrList = (ArrayList<String>)ois.readObject();
            System.out.println("new list: " + newStrList);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    >>>>>
    origin list: [Alpha, Beta, Gamma]
    new list: [Alpha, Beta, Gamma]
    

    コードの結果は予想通りです.では、ArrayListのソースコードを見てみましょう.
    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
        private static final long serialVersionUID = 8683452581122892189L;
        transient Object[] elementData; // non-private to simplify nested class access
        private int size;
    }
    

    ソースコードから、ArrayListは確かにSerializableインタフェースを実現していることがわかります.これは、シーケンス化および逆シーケンス化操作が可能であることを示しています.しかし、データを格納するelementData配列はtransientであるため、シーケンス化の過程でデータは保存されないはずだったが、なぜ保持されたのだろうか.答えは:ArrayListはwriteObjectとreadObjectという方法を定義しています.
    WriteObjectとreadObjectを紹介する前に、重要な知識点を理解する必要があります.
  • シーケンス化中、シーケンス化されたクラスにwriteObjectメソッドとreadObjectメソッドが定義されている場合、仮想機会はオブジェクトクラスのwriteObjectメソッドとreadObjectメソッドを呼び出し、ユーザー定義のシーケンス化と逆シーケンス化を行います*このようなメソッドがない場合、デフォルト呼び出しは、ObjectOutputStreamのdefaultWriteObjectメソッドおよびObjectInputStreamのdefaultReadObjectメソッド
  • です.
  • ユーザによってカスタマイズされたwriteObjectおよびreadObjectメソッドは、シーケンス化のプロセスをユーザに制御することを可能にすることができ、例えば、シーケンス化のプロセス中にシーケンス化の数値
  • を動的に変更することができる.
    では、この2つの方法の具体的な実装ソースを見てみましょう.
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
    
        // Write out size as capacity for behavioral compatibility with clone()
        s.writeInt(size);
    
        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
    
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    
    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
        
        elementData = EMPTY_ELEMENTDATA;
    
        // Read in size, and any hidden stuff
        s.defaultReadObject();
    
        // Read in capacity
        s.readInt(); // ignored
    
        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);
    
            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }
    

    では、なぜArrayListはこのような方法でシーケンス化を実現するのでしょうか.
    ArrayListは実際には動的配列であり,満タンになるたびに設定された長さ値が自動的に増加し,配列が自動的に長さを100に設定し,実際には1つの要素しか置かないと99個のnull要素がシーケンス化されることを知っている.したがって,最適化ストレージとシーケンス化の際にこれだけのnullを同時にシーケンス化しないために,ArrayListは要素配列をtransientに設定し,writeObjectとreadObjectメソッドを書き換えることでその要素を保持する.
    書き換えたwriteObjectとreadObjectがどのように呼び出されたかを分析します.
    オブジェクトのシーケンス化プロセスは、ObjectOutputStreamとObjectInputputput Streamによって実現されます.ObjectOutputStreamを例にとると、ObjectOutputStreamのシーケンス化にはwriteObjectメソッドが使用されます.では、ソースコードに入ってみましょう.紙面に限られています.ここでは、writeObjectメソッドを得る呼び出しスタックを一歩追跡します.
    writeObject >> writeObjecto >> writeOrdinaryObject >> writeSerialData >> invokeWriteObject
    invokeWriteObjectのソースコードを見てみましょう.
    void invokeWriteObject(Object obj, ObjectOutputStream out)
            throws IOException, UnsupportedOperationException{
        if (writeObjectMethod != null) {
            try {
                writeObjectMethod.invoke(obj, new Object[]{ out });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }
    

    ここでwriteObjectMethod.invoke(obj,new Object[]{out})が鍵であり、writeObjectMethodメソッドを反射的に呼び出すことで、このwriteObjectMethodを公式に説明する.
    class-defined writeObject method, or null if none
    したがって、クラスのwriteObjectメソッドとreadObjectメソッドは、Object OutputStreamのwriteObjectメソッドとObject InputStreamのreadObjectメソッドを使用すると、反射的に呼び出されます.
    まとめ:
  • クラスをシーケンス化するには、Serializableインタフェースを実装する必要があります.そうでない場合、NotSerializableException例外が放出されます.これは、シーケンス化操作中にタイプがチェックされ、シーケンス化されたクラスがEnum、Array、Serializableタイプのいずれかに属する必要があるためです.
  • 変数宣言の前にtransientキーワードを付けることで、この変数がファイルにシーケンス化されることを阻止することができる
  • .
  • クラスにwriteObjectおよびreadObjectメソッドを追加することにより、カスタムシーケンス化方式
  • を実現することができる.