こんにちは!今日の講義では、Java のシリアル化と逆シリアル化について説明します。簡単な例から始めましょう。あなたがコンピューター ゲームの作成者だとしましょう。90 年代に育ち、当時のゲーム コンソールを覚えている人なら、おそらく今日のゲーム コンソールには、ゲームの保存とロードという明らかな機能が欠けていたことをご存知でしょう :) もしそうでないなら…想像してみてください。今日、そのようなオプションのないゲームは失敗する運命にあると思います。実際、ゲームの「セーブ」と「ロード」とは何でしょうか? まあ、通常の意味では、前回中断したところからゲームを続けたいということは理解できます。これを行うには、一種の「チェックポイント」を作成し、それを使用してゲームをロードします。しかし、これは日常的な意味ではなく、「プログラマー」の意味で何を意味するのでしょうか? 答えは簡単です。プログラムの状態を保存するからです。スペイン向けの戦略ゲームをプレイしているとします。ゲームには状態があります。誰がどの領土を所有し、誰がどれだけの資源を持っているか、誰が誰と同盟を結んでいるか、逆に誰が戦争中かなどです。この情報、つまりプログラムの状態は、後でデータを復元してゲームを続行するために、何らかの方法で保存する必要があります。これはまさに、シリアル化と逆シリアル化のメカニズムが使用される目的です。 シリアル化は、オブジェクトの状態をバイトのシーケンスに格納するプロセスです。 逆シリアル化は、これらのバイトからオブジェクトを再構築するプロセスです。Java オブジェクトはすべてバイトのシーケンスに変換されます。それはなんのためですか?プログラムはそれ自体では存在しないと何度も述べてきました。ほとんどの場合、それらは相互に対話し、データを交換します。これにはバイト形式が便利で効率的です。たとえば、クラス オブジェクト(保存されたゲーム) を一連のバイトに変換し、そのバイトをネットワーク経由で別のコンピュータに転送し、2 台目のコンピュータでそれらのバイトを Java オブジェクトに戻すことができます。聞き取りにくいですよね?どうやら、このプロセスを組織するのは簡単ではないようです :/ 幸いなことに、そうではありません。:) Java では、 Serializableインターフェイスがシリアル化プロセスを担当します。このインターフェイスは非常にシンプルです。これを使用するために単一のメソッドを実装する必要はありません。ゲーム保存クラスは次のようになります。
SavedGame
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 マシンに「すべて問題ありません。どちらかというと、このクラスのオブジェクトはシリアル化できます。」と伝えます。メソッドを持たないインターフェイスは奇妙に見えます :/ なぜ必要なのでしょうか? この質問に対する答えは上記の通りです。必要な情報を Java マシンに提供するだけです。以前の講義の 1 つで、マーカー インターフェイスについて簡単に説明しました。これらは、将来 Java マシンに役立つ追加情報をクラスにマークするだけの特別な情報インターフェイスです。実装する必要のあるメソッドはありません。したがって、Serializable はそのようなインターフェイスの 1 つです。private static final long serialVersionUID
もう 1 つの重要な点は、クラスで定義した変数です。なぜ必要なのでしょうか? このフィールドには、シリアル化されたクラスの一意のバージョン識別子が含まれます。Serializable インターフェイスを実装するクラスにはバージョン識別子があります。これは、クラスの内容 (フィールド、宣言順序、メソッド) に基づいて計算されます。また、クラス内のフィールドのタイプやフィールドの数を変更すると、バージョン識別子は即座に変更されます。クラスがシリアル化されるときに、serialVersionUID も書き込まれます。逆シリアル化、つまり一連のバイトからオブジェクトを復元しようとすると、その値がプログラム内のクラスのserialVersionUID
値と比較されます。serialVersionUID
値が一致しない場合は、java.io.InvalidClassException がスローされます。以下にその例を見ていきます。このような状況を避けるために、このバージョン ID をクラスに手動で設定するだけです。この例では、単純に 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 position of neutrality"};
SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, diplomacyInfo);
//create 2 threads 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 game to file
objectOutputStream.writeObject(savedGame);
// close the stream and release resources
objectOutputStream.close();
}
}
ご覧のとおり、FileOutputStream
と の2 つのスレッドを作成しましたObjectOutputStream
。1 つ目はデータをファイルに書き込むことができ、2 つ目はオブジェクトをバイトに変換できます。たとえば、new BufferedReader(new InputStreamReader(...))
以前の講義で同様の「ネストされた」構造をすでに見たので、怖がらせる必要はありません :) このような 2 つのスレッドの「チェーン」を作成することで、両方のタスクを実行します。オブジェクトをSavedGame
バイトのセットに変換します。そして、メソッドを使用してファイルに保存しますwriteObject()
。ちなみに、私たちは何が得られたかさえ確認していませんでした。ファイルを見てみましょう。 *注意: ファイルを事前に作成する必要はありません。その名前のファイルが存在しない場合は、自動的に作成されます* そして、その内容は次のとおりです。 зн sr SavedGame [外交情報 [Ljava/lang/String;[ resourceInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранцвоюеСã SЃ R РѕСЃСЃРёРµ Р№、Р˜С ЃРїР° РЅРёСЏ Р・Р°Ряла РїРѕР・ицию РЅРμР№СãралиСãРµСãР°uq ~ t "РЈ Р˜СЃРї ании 100 золотаt Р J R РѕСЃСЃРёРё 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{territoriesInfo=[スペインには 6 州、ロシアには 10 州、フランスには 8 州]、resourcesInfo=[スペインには 100 ゴールド、ロシアには 80 ゴールド、フランスには 90 ゴールド]、外交Info=[フランスはロシアと戦争中、スペインは中立の立場を占めています]} すごいですね!まずゲームの状態をファイルに保存し、次にそのファイルから復元することができました。次に、同じことを試してみましょう。ただし、SavedGame
クラスからバージョン識別子を削除します。両方のクラスを書き直すことはせず、それらのコードは同じになり、単に をSavedGame
削除しますprivate static final long serialVersionUID
。シリアル化後の私たちのオブジェクトは次のとおりです: ¬нSrsavedgame'uuurm‰[diplomacyinfot [ljava/lang/string; [RestoriNfoq〜 [ljava.lang.String;¡ ã СЃ Россией, Р˜СЃРїР°РЅРЏР・аняла RїРѕР・ицию нейСã С ЂР°Р»РёСãРµСãР°uq ~ t "РЈ Р˜СЃРї ании 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золоСãР°t !РЈ Франции 90 золотаuq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 R їСЂРѕРІРёРЅС† РёР№t %РЈ Р РѕСЃСЃРёРё 10 провнинцийt &РЈ Р¤С ЂР°РЅС†РёРё 8 проввинцинА そして、逆シリアル化しようとすると、次のことが起こりました: InvalidClassException: local class incompatibility : stream classdescserialVersionUID = - 196410440475012755, ローカル クラス SerialVersionUID = -6675950253085108747 これは、上で説明したのと同じ例外です。これについては、学生の 1 人の記事で詳しく読むことができます。ところで、重要な点が 1 つ抜けていました。文字列とプリミティブが異なることは明らかです。簡単にシリアル化できます。Java には確かにいくつかの機能があり、そのための組み込みメカニズムがあります。しかしserializable
、 -class にプリミティブとしてではなく、他のオブジェクトへの参照として表現されるフィールドがある場合はどうなるでしょうか? たとえば、 class を操作するため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 + '\'' +
'}';
}
}
しかし、ここで疑問が生じます。変更されたクラスをシリアル化したい場合、これらのクラスはすべてシリアル化可能でなければなりませんか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
TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
ResourcesInfo resourcesInfo = new ResourcesInfo("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 position of neutrality");
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();
}
}
結果: スレッド「メイン」java.io.NotSerializableException での例外: DiplomacyInfo が 失敗しました。実際、これが私たちの質問に対する答えです。オブジェクトをシリアル化すると、そのインスタンス変数で参照されるすべてのオブジェクトがシリアル化されます。また、それらのオブジェクトが 3 番目のオブジェクトも参照している場合、それらもシリアル化されます。など、無限に続きます。このチェーン内のすべてのクラスはシリアル化可能でなければなりません。そうでない場合はシリアル化できず、例外がスローされます。ちなみに、これは将来的に問題を引き起こす可能性があります。たとえば、シリアル化中にクラスの一部が必要ない場合はどうすればよいでしょうか? あるいは、たとえば、TerritoryInfo
プログラム内のクラスをライブラリの一部として継承したとします。ただし、シリアル化可能ではないため、変更することはできません。クラス全体がシリアル化できなくなるため、クラスTerritoryInfo
にフィールドを追加SavedGame
できないことがわかりました。SavedGame
問題:/ この種の問題は、Java ではキーワードを使用して解決されますtransient
。このキーワードをクラス フィールドに追加すると、このフィールドの値はシリアル化されません。クラスのフィールドの 1 つを作成してみましょうSavedGame transient
。その後、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
TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
ResourcesInfo resourcesInfo = new ResourcesInfo("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 position of neutrality");
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, resourceInfo=ResourcesInfo{info='スペインは 100 ゴールド、ロシアは 80 ゴールド、フランスは 90 ゴールド'}、diplomacyInfo=DiplomacyInfo{info='フランスはロシアと戦争中です、スペインは中立の立場をとりました'}} 同時に、transient
-フィールドにどのような値が割り当てられるかという質問に対する回答も受け取りました。デフォルト値が割り当てられます。オブジェクトの場合、これは ですnull
。暇なときに、連載に関するこの優れた記事を読むことができます。また、インターフェイスについても説明しますExternalizable
。これについては、次の講義で説明します。さらに、書籍「Head-First Java」にはこのトピックに関する章があるので、注目してください:)
GO TO FULL VERSION