やあ!今日のレッスンでは、Java でのシリアル化と逆シリアル化について説明します。簡単な例から始めます。あなたがコンピューター ゲームを作成したとします。90 年代に育ち、その時代のゲーム コンソールを覚えているなら、おそらく、ゲームを保存したりロードしたりする機能、つまり今日私たちが当然と思っている機能がゲーム コンソールに欠けていたことをご存じでしょう :) そうでない場合は、想像してみてください。
今では、これらの能力のないゲームは駄目になるのではないかと思います。そもそも、ゲームを「保存」したり「ロード」したりすることは何を意味するのでしょうか? まあ、普通の意味はわかります。中断したところからゲームを続けたいということです。これを行うには、一種の「チェック ポイント」を作成し、それを使用してゲームをロードします。しかし、カジュアルなゲーマーではなくプログラマーにとって、これは何を意味するのでしょうか? 答えは簡単です。「私たちは」。Strategium でスペインとしてプレイしているとします。ゲームには状態があります。誰がどの領土を所有しているか、誰がどれだけのリソースを持っているか、誰が誰と同盟を結んでいるか、誰が誰と戦争中であるかなどです。将来復元してゲームを続行するには、この情報、つまりプログラムの状態を何らかの方法で保存する必要があります。まさにこれがシリアル化と逆シリアル化の目的だからです。 シリアル化は、オブジェクトの状態をバイトのシーケンスに保存するプロセスです。 逆シリアル化これらのバイトからオブジェクトを復元するプロセスです。任意の Java オブジェクトをバイト シーケンスに変換できます。なぜそれが必要なのでしょうか? プログラムはそれ自体では存在しない、ということを何度も述べてきました。ほとんどの場合、他のプログラムと対話したり、データを交換したりします。また、バイト シーケンスは便利で効率的な形式です。たとえば、
Java では、この種の問題は

SavedGame
オブジェクトをバイトのシーケンスに変換し、これらのバイトをネットワーク経由で別のコンピュータに送信し、2 番目のコンピュータでこれらのバイトを Java オブジェクトに戻すことができます。難しそうですよね?そして、このプロセスの実装は面倒に思えます :/ 幸いなことに、そうではありません。:) Java では、Serializable
インターフェイスはシリアル化プロセスを担当します。このインターフェイスは非常にシンプルです。これを使用するために単一のメソッドを実装する必要はありません。ゲーム保存クラスは次のように単純に見えます。
import java.io.Serializable;
import java.util.Arrays;
public class SavedGame implements Serializable {
private static final long serialVersionUID = 1L;
private String[] territoriesInfo;
private String[] resourcesInfo;
private String[] diplomacyInfo;
public SavedGame(String[] territoriesInfo, String[] resourcesInfo, String[] diplomacyInfo){
this.territoriesInfo = territoriesInfo;
this.resourcesInfo = resourcesInfo;
this.diplomacyInfo = diplomacyInfo;
}
public String[] getTerritoriesInfo() {
return territoriesInfo;
}
public void setTerritoriesInfo(String[] territoriesInfo) {
this.territoriesInfo = territoriesInfo;
}
public String[] getResourcesInfo() {
return resourcesInfo;
}
public void setResourcesInfo(String[] resourcesInfo) {
this.resourcesInfo = resourcesInfo;
}
public String[] getDiplomacyInfo() {
return diplomacyInfo;
}
public void setDiplomacyInfo(String[] diplomacyInfo) {
this.diplomacyInfo = diplomacyInfo;
}
@Override
public String toString() {
return "SavedGame{" +
"territoriesInfo=" + Arrays.toString(territoriesInfo) +
", resourcesInfo=" + Arrays.toString(resourcesInfo) +
", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
'}';
}
}
3 つの配列は、領土、資源、外交に関する情報を担当します。Serializable インターフェイスは Java 仮想マシンに「すべて問題ありません。必要に応じて、このクラスのオブジェクトをシリアル化できます」と伝えます。インターフェイスが 1 つも存在しないインターフェイスは奇妙に見えます :/ なぜ必要なのでしょうか? この質問に対する答えは上で見ることができます。これは、Java 仮想マシンに必要な情報を提供するだけの役割を果たします。以前のレッスンの 1 つで、マーカー インターフェイスについて簡単に説明しました。これらは、将来 Java マシンに役立つ追加情報をクラスにマークするだけの特別な情報インターフェイスです。実装する必要があるメソッドはありません。Serializable
はそれらのインターフェイスの 1 つです。private static final long serialVersionUID
もう 1 つの重要な点:クラスで定義した変数がなぜ必要なのでしょうか? なぜ必要なのでしょうか? このフィールドには、シリアル化されたクラスのバージョンの一意の識別子が含まれます。インターフェイスを実装するクラスには識別子Serializable
がありますversion
。これは、クラスの内容 (フィールド、宣言された順序、メソッドなど) に基づいて計算されます。クラス内のフィールドのタイプやフィールドの数を変更すると、バージョン識別子はすぐに変更されます。 。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[] resourcesInfo = {"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, resourcesInfo, 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 free resources
objectOutputStream.close();
}
}
ご覧のとおり、FileOutputStream
と の2 つのストリームを作成しましたObjectOutputStream
。1 つ目はデータをファイルに書き込むことができ、2 つ目はオブジェクトをバイトに変換します。同様の「ネストされた」構造 (例: ) については、前のレッスンですでに見たので、怖がる必要はありません :) このような 2 つのストリームの「チェーン」を作成することで、両方のタスクを実行します。オブジェクトをセットにnew BufferedReader(new InputStreamReader(...))
変換します。SavedGame
のバイト数を取得し、 メソッドを使用してファイルに保存しますwriteObject()
。ちなみに、私たちは何が得られたかさえ見ていませんでした。ファイルを見てみましょう。 *注意: 事前にファイルを作成する必要はありません。その名前のファイルが存在しない場合は、自動的に作成されます*。 その内容は次のとおりです。
¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранция воюет СЃ Россией, Рспания заняла позицию нейтралитетаuq ~ t "РЈ Рспании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Рспании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
ああ :( 私たちのプログラムは動作しなかったようです :( 実際、動作しました。単なるオブジェクトやテキストではなく、一連のバイトをファイルに送信したことを覚えていますか? そうですね、これがその内容です。バイトのセットは次のようになります:) これは保存されたゲームです! 元のオブジェクトを復元したい場合、つまり中断したところからゲームを開始して続行したい場合は、逆のプロセスが必要です: deserialization。場合:
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{territoriesInfo=["Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces], resourcesInfo=[Spain has 100 gold, Russia has 80 gold, France has 90 gold], diplomacyInfo=[France is at war with Russia, Spain has taken a neutral position]}
素晴らしい!まずゲームの状態をファイルに保存し、次にそのファイルから復元することができました。次に、クラスのバージョン識別子を使用せずに同じことを実行してみましょうSavedGame
。両方のクラスを書き直すことはありません。コードは同じままですが、クラスprivate static final long serialVersionUID
から削除しますSavedGame
。 シリアル化後のオブジェクトは次のとおりです。
¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ 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 incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
これは上で述べたまさに例外です。ところで、重要なことを見逃していました。文字列とプリミティブが簡単にシリアル化できることは理にかなっています。おそらく、Java にはこれを行うための何らかの組み込みメカニズムが備わっているでしょう。しかし、serializable
クラスにプリミティブではなく、他のオブジェクトへの参照があるフィールドがある場合はどうなるでしょうか? たとえば、クラスと連携するためにTerritoriesInfo
、ResourcesInfo
およびクラスを個別に作成してみましょう。 DiplomacyInfo
SavedGame
public class TerritoriesInfo {
private String info;
public TerritoriesInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "TerritoriesInfo{" +
"info='" + info + '\'' +
'}';
}
}
public class ResourcesInfo {
private String info;
public ResourcesInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "ResourcesInfo{" +
"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 + '\'' +
'}';
}
}
ここで疑問が生じます:変更したクラスをシリアル化したい場合、これらすべてのクラスが必要なのでしょうか?Serializable
SavedGame
import java.io.Serializable;
import java.util.Arrays;
public class SavedGame implements Serializable {
private TerritoriesInfo territoriesInfo;
private ResourcesInfo resourcesInfo;
private DiplomacyInfo diplomacyInfo;
public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
this.territoriesInfo = territoriesInfo;
this.resourcesInfo = resourcesInfo;
this.diplomacyInfo = diplomacyInfo;
}
public TerritoriesInfo getTerritoriesInfo() {
return territoriesInfo;
}
public void setTerritoriesInfo(TerritoriesInfo territoriesInfo) {
this.territoriesInfo = territoriesInfo;
}
public ResourcesInfo getResourcesInfo() {
return resourcesInfo;
}
public void setResourcesInfo(ResourcesInfo resourcesInfo) {
this.resourcesInfo = resourcesInfo;
}
public DiplomacyInfo getDiplomacyInfo() {
return diplomacyInfo;
}
public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
this.diplomacyInfo = diplomacyInfo;
}
@Override
public String toString() {
return "SavedGame{" +
"territoriesInfo=" + territoriesInfo +
", resourcesInfo=" + resourcesInfo +
", 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(territoriesInfo, resourcesInfo, diplomacyInfo);
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(savedGame);
objectOutputStream.close();
}
}
結果:
Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo
うまくいきませんでした!基本的に、それが私たちの質問に対する答えです。オブジェクトがシリアル化されると、そのインスタンス変数によって参照されるすべてのオブジェクトがシリアル化されます。 また、それらのオブジェクトが他のオブジェクトも参照している場合、それらもシリアル化されます。など、無限に続きます。このチェーン内のすべてのクラスは でなければなりませんSerializable
。そうしないと、クラスをシリアル化できず、例外がスローされます。ちなみに、これは将来的に問題を引き起こす可能性があります。たとえば、シリアル化するときにクラスの一部が必要ない場合はどうすればよいでしょうか? あるいは、たとえば、TerritoryInfo
クラスがサードパーティのライブラリの一部として提供された場合はどうなるでしょうか。さらに、そうではなくSerializable
、したがってそれを変更できないと仮定します。TerritoryInfo
フィールドを追加できないことがわかりました。SavedGame
なぜなら、そうするとクラス全体がSavedGame
シリアル化できなくなるからです。それは問題です :/ 
transient
キーワードを使用して解決されます。このキーワードをクラスのフィールドに追加すると、そのフィールドはシリアル化されません。クラスのインスタンス フィールドの1 つを一時的なものにしてみましょうSavedGame
。次に、1 つのオブジェクトをシリアル化して復元します。
import java.io.Serializable;
public class SavedGame implements Serializable {
private transient TerritoriesInfo territoriesInfo;
private ResourcesInfo resourcesInfo;
private DiplomacyInfo diplomacyInfo;
public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
this.territoriesInfo = territoriesInfo;
this.resourcesInfo = resourcesInfo;
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(territoriesInfo, resourcesInfo, 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{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='Spain has 100 gold, Russia has 80 gold, France has 90 gold'}, diplomacyInfo=DiplomacyInfo{info='France is at war with Russia, Spain has taken a neutral position'}}
さらに、フィールドにどのような値が割り当てられるかについての質問に対する答えも得られましたtransient
。デフォルト値が割り当てられます。オブジェクトの場合、これは ですnull
。数分の時間に余裕があるときに、連載に関するこの優れた記事を読むことができます。インターフェイスについても説明しますExternalizable
。これについては次のレッスンで説明します。さらに、書籍『Head-First Java』には、このトピックに関する章があります。ちょっと注意してください:)
GO TO FULL VERSION