やあ!今日のレッスンでは、Java でのシリアル化と逆シリアル化について説明します。簡単な例から始めます。あなたがコンピューター ゲーム開発者であると想像してください。90 年代に育ち、その時代のゲーム コンソールを覚えている人なら、おそらく、今日では当たり前のことである、ゲームの保存とロードの機能がゲーム コンソールに欠けていたことをご存知でしょう :) そうでない場合は、想像してみてください。これらの能力がなければ、今日のゲームは駄目になってしまうのではないかと心配です。とにかく、ゲームを「保存」および「ロード」することは何を意味するのでしょうか? さて、私たちは日常の意味を理解しています。中断した場所からゲームを続けたいのです。これを行うには、後でゲームをロードするために使用する特定の「チェック ポイント」を作成します。しかし、カジュアルなゲーマーではなくプログラマーにとって、それは何を意味するのでしょうか? 答えは簡単です。プログラムの状態を保存するからです。ストラテジー ゲームでスペインをプレイしているとします。ゲームには状態があります。つまり、全員がどの領土を持っているか、全員がどれだけのリソースを持っているか、どのような同盟が存在し、誰と交戦しているか、誰と戦争しているかなどです。データを復元してゲームを続行するには、この情報、つまりプログラムの状態を何らかの方法で保存する必要があります。たまたま、 Java におけるシリアル化は、オブジェクトの状態をバイトのシーケンスとして保存するプロセスです。 Java における逆シリアル化は、これらのバイトからオブジェクトを復元するプロセスです。任意の Java オブジェクトをバイト シーケンスに変換できます。なぜ私たちはこれが必要なのですか?プログラムはそれ自体では存在しないと繰り返し述べてきました。ほとんどの場合、それらは相互にやり取りしたり、データを交換したりします。これにはバイト形式が便利で効率的です。たとえば、SavedGameのオブジェクトを変換できます。クラスをバイトのシーケンスに変換し、これらのバイトをネットワーク経由で別のコンピュータに転送し、別のコンピュータでこれらのバイトを Java オブジェクトに変換し直します。難しそうですよね?これをすべて実現するのは難しいようです: / 幸いなことに、そうではありません。:) Java では、Serializableインターフェイスがシリアル化プロセスを担当します。このインターフェイスは非常にシンプルです。これを使用するために 1 つのメソッドを実装する必要はありません。ゲームを保存するためのクラスがいかに単純であるかを見てください。
import java.io.Serializable;
import java.util.Arrays;
public class SavedGame implements Serializable {
private static final long serialVersionUID = 1L;
private String[] territoryInfo;
private String[] resourceInfo;
private String[] diplomacyInfo;
public SavedGame(String[] territoryInfo, String[] resourceInfo, String[] diplomacyInfo){
this.territoryInfo = territoryInfo;
this.resourceInfo = resourceInfo;
this.diplomacyInfo = diplomacyInfo;
}
public String[] getTerritoryInfo() {
return territoryInfo;
}
public void setTerritoryInfo(String[] territoryInfo) {
this.territoryInfo = territoryInfo;
}
public String[] getResourceInfo() {
return resourceInfo;
}
public void setResourceInfo(String[] resourceInfo) {
this.resourceInfo = resourceInfo;
}
public String[] getDiplomacyInfo() {
return diplomacyInfo;
}
public void setDiplomacyInfo(String[] diplomacyInfo) {
this.diplomacyInfo = diplomacyInfo;
}
@Override
public String toString() {
return "SavedGame{" +
"territoryInfo=" + Arrays.toString(territoryInfo) +
", resourceInfo=" + Arrays.toString(resourceInfo) +
", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
'}';
}
}
3 つの配列は領土、リソース、外交に関する情報を担当し、Serializable インターフェイスは Java マシンに「このクラスのオブジェクトをシリアル化できればすべて問題ありません」と伝えます。インターフェイスが 1 つも存在しないインターフェイスは奇妙に見えます :/ なぜ必要なのでしょうか? その質問に対する答えは上にあります。Java マシンに必要な情報を提供するためにのみ必要です。過去のレッスンで、マーカー インターフェイスについて簡単に説明しました。これらは、将来 Java マシンに役立つ追加情報をクラスにマークするだけの特別な情報インターフェイスです。実装する必要があるメソッドはありません。これがSerializableです— そのようなインターフェイスの 1 つです。ここでもう 1 つの重要な点があります。なぜ必要なのでしょうか。クラスで定義したprivate staticfinallongserialVersionUID変数? このフィールドには、シリアル化されたクラスの一意のバージョン識別子が含まれます。Serializableインターフェイスを実装するすべてのクラスにはバージョン識別子があります。これは、クラスの内容、つまりフィールドとその宣言順序、およびメソッドとその宣言順序に基づいて決定されます。また、クラス内のフィールドのタイプやフィールドの数を変更すると、バージョン識別子は即座に変更されます。クラスがシリアル化されるときに、serialVersionUIDも書き込まれます。逆シリアル化しようとすると、つまりバイト シーケンスからオブジェクトを復元しようとすると、serialVersionUIDの値がSerialVersionUIDの値と比較されます。私たちのプログラムのクラスの。値が一致しない場合は、java.io.InvalidClassException がスローされます。以下にその例を見ていきます。このような状況を回避するには、クラスのバージョン識別子を手動で設定するだけです。この例では、単純に 1 になります (他の任意の数値を使用できます)。さて、 SavedGameオブジェクトをシリアル化して、何が起こるかを見てみましょう。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Main {
public static void main(String[] args) throws IOException {
// Create our object
String[] territoryInfo = {"Spain has 6 provinces", "Russia has 10 provinces", "France has 8 provinces"};
String[] resourceInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a neutral position"};
SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);
// Create 2 streams to serialize the object and save it to a file
FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
// Save the game to a file
objectOutputStream.writeObject(savedGame);
// Close the stream and release resources
objectOutputStream.close();
}
}
ご覧のとおり、FileOutputStreamとObjectOutputStreamという 2 つのストリームを作成しました。1 つ目はファイルにデータを書き込む方法を認識し、2 つ目はオブジェクトをバイトに変換します。new BufferedReader(new InputStreamReader(...))など、同様のネストされた構造を前のレッスンですでに見てきたので、怖がらせる必要はありません :) この 2 つのストリームのチェーンを作成することで、両方のタスクを実行します。SavedGameオブジェクトをバイトのシーケンスに変換し、 writeObject()メソッドを使用してファイルに保存します。ちなみに、私たちは何が得られたかさえ見ていませんでした。ファイルを見てみましょう。 *注意: 事前にファイルを作成する必要はありません。指定された名前のファイルが存在しない場合は、自動的に作成されます*。 その内容は次のとおりです 。文字列; зТVзй{G xp t pФранция РІРѕСЋРµСã СЃ Россией, Р˜СЃРїР°РЅРёС Џ Р・аняла РїРѕР・ицию РЅРμР№СãралиС、 етаuq ~ t "РЈ Р˜СЃРїР°РЅРёРё 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золо таt !РЈ Франции 90 Р・РѕР »РѕС‚Р°uq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций ああ、ああ:( 私たちのプログラムは動作しなかったようです:( 実際には動作しました。単純なオブジェクトやテキストではなく、一連のバイトをファイルに送信したことを覚えていますか? そうですね、これがこのバイト シーケンスです。次のようになります:) これは保存されたゲームです! 元のオブジェクトを復元したい場合、つまり中断したところからゲームを開始して続行したい場合は、逆のプロセス、つまりデシリアライズが必要です。
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
SavedGame savedGame = (SavedGame) objectInputStream.readObject();
System.out.println(savedGame);
}
}
そして結果がこれです! SavedGame{territoryInfo=[スペインには 6 州、ロシアには 10 州、フランスには 8 州]、resourceInfo=[スペインには 100 ゴールド、ロシアには 80 ゴールド、フランスには 90 ゴールド]、外交Info=[フランスはロシアと戦争中、スペインは中立の立場を取りました]} すばらしいですね!まずゲームの状態をファイルに保存し、次にそのファイルから復元することができました。次に、同じことを実行してみますが、SavedGameクラスからバージョン識別子を削除します。両方のクラスを書き直すことはありません。それらのコードは同じになります。SavedGameクラスからプライベートの静的な最終的な長い SerialVersionUIDを削除するだけです。シリアル化後のオブジェクトは次のとおりです。 зн sr SavedGameі€MіuОm`` [外交Infot [Ljava/lang/String;[ resourceInfoq ~ [territoryInfoq ~ xpur [Ljava.lang.String; зТVзй{G xp t pФранция РІРѕСЋРµСã СЃ Р РѕСЃСЃРёРμР№、 Р˜СЃРїР°РЅРёСЏ Р・аняла РїРѕР・ицию РЅРμР№С、ралиС、РμС、Р°uq ~ t "РЈ Р˜СЃРїР°РЅРёРё 100 золотР°t РЈ Р РѕСЃСЃРёРё 80 Р・олоСãР°t !РЈ Франции 90 Р・олоСãР°uq ~ t &Р Ј Р˜СЃРїР°РЅРёРё 6 провинцийt %РЈ РРѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 РїСЂРѕРІРёР ЅС†РёР№ しかし、逆シリアル化しようとすると何が起こるかを見てください: InvalidClassException: local class incompatibility: stream classdescserialVersionUID = -196410440475012755, local classserialVersionUID = -6675950253085108747 ところで、重要なことを見逃していました。明らかに、文字列とプリミティブは簡単にシリアル化されます。Java には確かにこれのための組み込みメカニズムがいくつかあります。しかし、シリアル化可能なクラスにプリミティブではなく、他のオブジェクトへの参照があるフィールドがある場合はどうなるでしょうか? たとえば、SavedGameクラスと連携するために、個別のTerritoryInfo、ResourceInfo、およびDiplomacyInfoクラスを作成してみましょう。
public class TerritoryInfo {
private String info;
public TerritoryInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "TerritoryInfo{" +
"info='" + info + '\'' +
'}';
}
}
public class ResourceInfo {
private String info;
public ResourceInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "ResourceInfo{" +
"info='" + info + '\'' +
'}';
}
}
public class DiplomacyInfo {
private String info;
public DiplomacyInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "DiplomacyInfo{" +
"info='" + info + '\'' +
'}';
}
}
ここで、次の質問に直面します。SavedGameクラスをシリアル化したい場合、これらのクラスはすべてSerializableでなければなりませんか?
import java.io.Serializable;
import java.util.Arrays;
public class SavedGame implements Serializable {
private TerritoryInfo territoryInfo;
private ResourceInfo resourceInfo;
private DiplomacyInfo diplomacyInfo;
public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
this.territoryInfo = territoryInfo;
this.resourceInfo = resourceInfo;
this.diplomacyInfo = diplomacyInfo;
}
public TerritoryInfo getTerritoryInfo() {
return territoryInfo;
}
public void setTerritoryInfo(TerritoryInfo territoryInfo) {
this.territoryInfo = territoryInfo;
}
public ResourceInfo getResourceInfo() {
return resourceInfo;
}
public void setResourceInfo(ResourceInfo resourceInfo) {
this.resourceInfo = resourceInfo;
}
public DiplomacyInfo getDiplomacyInfo() {
return diplomacyInfo;
}
public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
this.diplomacyInfo = diplomacyInfo;
}
@Override
public String toString() {
return "SavedGame{" +
"territoryInfo=" + territoryInfo +
", resourceInfo=" + resourceInfo +
", diplomacyInfo=" + diplomacyInfo +
'}';
}
}
よし、それでは!テストしてみよう!現時点では、すべてをそのままにして、SavedGameオブジェクトをシリアル化してみます。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Main {
public static void main(String[] args) throws IOException {
// Create our object
TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
ResourceInfo resourceInfo = new ResourceInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
DiplomacyInfo diplomacyInfo = new DiplomacyInfo("France is at war with Russia, Spain has taken a neutral position");
SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(savedGame);
objectOutputStream.close();
}
}
結果: スレッド「メイン」での例外 java.io.NotSerializableException: DiplomacyInfo うまくいきませんでした。これが私たちの質問に対する答えです。オブジェクトがシリアル化されると、そのインスタンス変数によって参照されるすべてのオブジェクトがシリアル化されます。また、それらのオブジェクトが他のオブジェクトも参照している場合、それらもシリアル化されます。そして永遠に続きます。このチェーン内のすべてのクラスはSerializableである必要があります。そうでない場合は、クラスをシリアル化することができず、例外がスローされます。ちなみに、これは将来的に問題を引き起こす可能性があります。たとえば、シリアル化中にクラスの一部が必要ない場合はどうすればよいでしょうか? あるいは、 TerritoryInfoクラスをライブラリの一部として「継承を通じて」取得した場合はどうなるでしょうか? そしてさらにそうでないと仮定してください』したがって、それを変更することはできません。これは、 TerritoryInfoフィールドをSavedGameクラスに追加できないことを意味します。追加すると、 SavedGameクラス全体がシリアル化できなくなるからです。それは問題です: / Java では、この種の問題はtransientキーワードによって解決されます。このキーワードをクラスのフィールドに追加すると、そのフィールドはシリアル化されません。SavedGameクラスのフィールドの 1 つをtransientにして、単一のオブジェクトをシリアル化して復元してみましょう。
import java.io.Serializable;
public class SavedGame implements Serializable {
private transient TerritoryInfo territoryInfo;
private ResourceInfo resourceInfo;
private DiplomacyInfo diplomacyInfo;
public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
this.territoryInfo = territoryInfo;
this.resourceInfo = resourceInfo;
this.diplomacyInfo = diplomacyInfo;
}
// ...getters, setters, toString()...
}
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Main {
public static void main(String[] args) throws IOException {
// Create our object
TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
ResourceInfo resourceInfo = new ResourceInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
DiplomacyInfo diplomacyInfo = new DiplomacyInfo("France is at war with Russia, Spain has taken a neutral position");
SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(savedGame);
objectOutputStream.close();
}
}
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
SavedGame savedGame = (SavedGame) objectInputStream.readObject();
System.out.println(savedGame);
objectInputStream.close();
}
}
結果は次のとおりです: SavedGame{territoryInfo=null, resourceInfo=ResourceInfo{info='スペインは 100 ゴールド、ロシアは 80 ゴールド、フランスは 90 ゴールド'}、diplomacyInfo=DiplomacyInfo{info='フランスはロシア、スペインと戦争中ですは中立の立場をとりました'}} つまり、一時フィールドにどのような値が割り当てられるかという質問に対する答えが得られました。デフォルト値が割り当てられます。オブジェクトの場合、これはnullです。このトピックに関する優れた章は、書籍「Head-First Java」で読むことができます。注目してください:)
GO TO FULL VERSION