Java Language
シリアライゼーション
サーチ…
前書き
Javaは、オブジェクトの直列化と呼ばれるメカニズムを提供します。このメカニズムでは、オブジェクトは、オブジェクトのデータと、オブジェクトのタイプおよびオブジェクトに格納されているデータのタイプに関する情報を含む一連のバイトとして表現できます。
シリアライズされたオブジェクトがファイルに書き込まれた後、ファイルから読み込み、デシリアライズすることができます。つまり、オブジェクトを表すタイプ情報とバイトとそのデータを使用してメモリ内のオブジェクトを再作成できます。
Javaでの基本的なシリアライゼーション
シリアライゼーションとは
シリアライゼーションは、オブジェクトの状態(参照を含む)を一連のバイトに変換するプロセスであり、将来のある時点でこれらのバイトをライブオブジェクトに再構築するプロセスです。シリアル化は、オブジェクトを永続化する場合に使用されます。 Java RMIは、クライアントからサーバへのメソッド呼び出しの引数として、またはメソッド呼び出しからの戻り値として、またはリモートメソッドによってスローされる例外として、JVM間でオブジェクトを渡すためにも使用されます。一般に、JVMの存続期間を超えてオブジェクトが存在するようにするには、直列化が使用されます。
java.io.Serializable
はマーカーインターフェイスです(本文はありません)。これは、Javaクラスをシリアライズ可能としてマークするために使用されます。
各シリアライズクラスでシリアライズランタイム関連付けバージョン番号は、呼び出さserialVersionUID
シリアライズされたオブジェクトの送信側と受信側のシリアルに対して互換性があり、そのオブジェクトのクラスをロードしたことを確認するために、 デ -serialization中に使用されます、。受信側が、対応する送信側のクラスと異なるserialVersionUID
を持つオブジェクトのクラスをロードした場合、非直列化によりInvalidClassException
が発生します。直列化可能クラスは、独自の宣言できるserialVersionUID
というフィールド宣言によって明示的にserialVersionUID
でなければならないstatic, final,
およびタイプのlong
:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
クラスを直列化に適格にする方法
オブジェクトを永続化するには、それぞれのクラスがjava.io.Serializable
インタフェースを実装する必要があります。
import java.io.Serializable;
public class SerialClass implements Serializable {
private static final long serialVersionUID = 1L;
private Date currentTime;
public SerialClass() {
currentTime = Calendar.getInstance().getTime();
}
public Date getCurrentTime() {
return currentTime;
}
}
オブジェクトをファイルに書き込む方法
このオブジェクトをファイルシステムに書き込む必要があります。この目的でjava.io.ObjectOutputStream
を使用します。
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
public class PersistSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = new SerialClass(); //We will write this object to file system.
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
out.writeObject(time); //Write byte stream to file system.
out.close();
} catch(IOException ex){
ex.printStackTrace();
}
}
}
直列化された状態からオブジェクトを再作成する方法
格納されたオブジェクトは、後でjava.io.ObjectInputStream
を使用してファイルシステムから読み取ることができます。
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.java.lang.ClassNotFoundException;
public class ReadSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = null;
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
time = (SerialClass)in.readObject();
in.close();
} catch(IOException ex){
ex.printStackTrace();
} catch(ClassNotFoundException cnfe){
cnfe.printStackTrace();
}
// print out restored time
System.out.println("Restored time: " + time.getTime());
}
}
シリアライズされたクラスはバイナリ形式です。クラス定義が変更された場合、直列化復元は問題になることがあります。詳しくは、Java Serialization SpecificationのSerializable Objectsのバージョンの章を参照してください。
オブジェクトをシリアライズすると、それがルートであるオブジェクトグラフ全体がシリアル化され、循環グラフの存在下で正しく動作します。 reset()
メソッドは、すでにシリアル化されているオブジェクトについてObjectOutputStream
が忘れるようにします。
Gsonによるシリアライゼーション
Gsonによるシリアライズは簡単で、正しいJSONを出力します。
public class Employe {
private String firstName;
private String lastName;
private int age;
private BigDecimal salary;
private List<String> skills;
//getters and setters
}
(シリアライゼーション)
//Skills
List<String> skills = new LinkedList<String>();
skills.add("leadership");
skills.add("Java Experience");
//Employe
Employe obj = new Employe();
obj.setFirstName("Christian");
obj.setLastName("Lusardi");
obj.setAge(25);
obj.setSalary(new BigDecimal("10000"));
obj.setSkills(skills);
//Serialization process
Gson gson = new Gson();
String json = gson.toJson(obj); //{"firstName":"Christian","lastName":"Lusardi","age":25,"salary":10000,"skills":["leadership","Java Experience"]}
循環参照を使用してオブジェクトをシリアル化することはできません。循環参照が無限に再帰するためです。
(脱直列化)
//it's very simple...
//Assuming that json is the previous String object....
Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj
Jackson 2のシリアライズ
以下は、オブジェクトを対応するJSON文字列にシリアライズする方法を示す実装です。
class Test {
private int idx;
private String name;
public int getIdx() {
return idx;
}
public void setIdx(int idx) {
this.idx = idx;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
シリアライゼーション:
Test test = new Test();
test.setIdx(1);
test.setName("abc");
ObjectMapper mapper = new ObjectMapper();
String jsonString;
try {
jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(test);
System.out.println(jsonString);
} catch (JsonProcessingException ex) {
// Handle Exception
}
出力:
{
"idx" : 1,
"name" : "abc"
}
あなたがそれを必要としない場合は、デフォルトプリティプリンターを省略することができます。
ここで使用される依存関係は次のとおりです。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.3</version>
</dependency>
カスタムシリアル化
この例では、初期化時に引数として渡される2つの整数の範囲の乱数を生成してコンソールに出力するクラスを作成します。
public class SimpleRangeRandom implements Runnable {
private int min;
private int max;
private Thread thread;
public SimpleRangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
private void WriteObject(ObjectOutputStreamout) throws IO Exception;
private void ReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
このクラスをシリアライズ可能にしたい場合は、いくつかの問題があります。スレッドは、シリアライズ可能ではない特定のシステムレベルのクラスの1つです。したがって、スレッドを一時的に宣言する必要があります。これを行うことで、このクラスのオブジェクトをシリアライズすることができますが、引き続き問題が発生します。コンストラクタで見ることができるように、私たちのランダム化プログラムの最小値と最大値を設定し、その後にランダム値を生成して出力するスレッドを開始します。したがって、 readObject()を呼び出して永続オブジェクトを復元するとき、コンストラクタは新しいオブジェクトの作成がないので再実行されません。その場合、クラス内に2つのメソッドを用意してカスタムシリアル化を開発する必要があります。これらのメソッドは次のとおりです。
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
したがって、私たちの実装をreadObject()に追加することで、スレッドを開始して開始することができます:
class RangeRandom implements Serializable, Runnable {
private int min;
private int max;
private transient Thread thread;
//transient should be any field that either cannot be serialized e.g Thread or any field you do not want serialized
public RangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
thread = new Thread(this);
thread.start();
}
}
私たちの例の主なものは次のとおりです。
public class Main {
public static void main(String[] args) {
System.out.println("Hello");
RangeRandom rangeRandom = new RangeRandom(1,10);
FileOutputStream fos = null;
ObjectOutputStream out = null;
try
{
fos = new FileOutputStream("test");
out = new ObjectOutputStream(fos);
out.writeObject(rangeRandom);
out.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
RangeRandom rangeRandom2 = null;
FileInputStream fis = null;
ObjectInputStream in = null;
try
{
fis = new FileInputStream("test");
in = new ObjectInputStream(fis);
rangeRandom2 = (RangeRandom)in.readObject();
in.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
catch(ClassNotFoundException ex)
{
ex.printStackTrace();
}
}
}
メインを実行すると、RangeRandomインスタンスごとに2つのスレッドが実行されていることがわかります。これは、 Thread.start()メソッドがコンストラクタとreadObject()の両方にあるためです。
バージョン管理とserialVersionUID
java.io.Serializable
インタフェースを実装してクラスをシリアライズ可能にすると、コンパイラはlong
型のserialVersionUID
というstatic final
フィールドを探します。クラスがこのフィールドを明示的に宣言していない場合、コンパイラはそのようなフィールドを1つ作成し、 serialVersionUID
インプリメンテーション依存の計算の値から割り当てます。この計算は、クラスのさまざまな側面に依存し、Sunによって与えられたオブジェクト直列化仕様に従います。しかし、この値は、すべてのコンパイラ実装で同じであることが保証されているわけではありません。
この値は、シリアライゼーションに対するクラスの互換性をチェックするために使用され、保存されたオブジェクトのデシリアライズ中に行われます。直列化ランタイムがいることを確認しserialVersionUID
、デシリアライズされたデータから読み取られ、 serialVersionUID
クラスで宣言まったく同じです。そうでない場合は、 InvalidClassException
スローします。
このフィールドの値のデフォルト計算に頼るのではなく、シリアライズ可能にするすべてのクラスでlong型の静的なfinalフィールドを明示的に宣言して初期化し、 'serialVersionUID'という名前を明示的に宣言して初期化することを強くお勧めしますバージョン管理を使用します。 'serialVersionUID'の計算は非常に敏感で、コンパイラの実装ごとに異なる場合があります。したがって、直列化プロセスの送信側と受信側で異なるコンパイラ実装を使用したため、同じクラスの場合でもInvalidClassException
取得することがあります。
public class Example implements Serializable {
static final long serialVersionUID = 1L /*or some other value*/;
//...
}
serialVersionUID
が同じであれば、Java Serializationは異なるバージョンのクラスを扱うことができます。互換性のある変更と互換性のない変更があります。
互換性のある変更
- フィールドの追加:再構成されるクラスにストリーム内に存在しないフィールドがある場合、オブジェクトのフィールドはそのタイプのデフォルト値に初期化されます。クラス固有の初期化が必要な場合、クラスはreadObjectメソッドを提供して、フィールドを非デフォルト値に初期化できます。
- クラスの追加:ストリームには、ストリーム内の各オブジェクトのタイプ階層が含まれます。ストリーム内のこの階層を現在のクラスと比較すると、追加のクラスを検出できます。ストリーム内にオブジェクトを初期化する情報がないため、クラスのフィールドはデフォルト値に初期化されます。
- クラスの削除:ストリーム内のクラス階層を現在のクラスのクラス階層と比較することで、クラスが削除されたことを検出できます。この場合、そのクラスに対応するフィールドとオブジェクトがストリームから読み込まれます。プリミティブフィールドは破棄されますが、削除されたクラスによって参照されるオブジェクトはストリームの後半で参照されるため作成されます。ストリームがガベージコレクションまたはリセットされると、ガベージコレクションされます。
- writeObject / readObjectメソッドの追加:ストリームを読み取るバージョンにこれらのメソッドがある場合、通常のようにreadObjectがデフォルトのシリアル化によってストリームに書き込まれた必要なデータを読み取ることが期待されます。任意のデータを読み込む前に、defaultReadObjectを最初に呼び出す必要があります。 writeObjectメソッドは通常どおり、defaultWriteObjectを呼び出して必要なデータを書き込んだ後、オプションのデータを書き込むことができます。
- java.io.Serializableの追加:これは型の追加と同じです。このクラスのストリームには値が存在しないため、フィールドはデフォルト値に初期化されます。非直列化可能なクラスのサブクラス化のサポートでは、クラスのスーパータイプに引数なしのコンストラクタがあり、クラス自体がデフォルト値に初期化されている必要があります。引数なしのコンストラクタが使用できない場合、InvalidClassExceptionがスローされます。
- フィールドへのアクセスの変更:アクセス修飾子public、package、protected、およびprivateは、フィールドに値を割り当てるシリアライズ機能に影響を与えません。
- フィールドをstaticからnonstaticまたはtransientからnontransientに変更する:デフォルトのシリアル化を使用してシリアライズ可能なフィールドを計算する場合、この変更はクラスにフィールドを追加することと同じです。新しいフィールドはストリームに書き込まれますが、シリアル化では静的フィールドまたは一時フィールドに値が割り当てられないため、以前のクラスでは値が無視されます。
互換性のない変更
- フィールドの削除:クラス内でフィールドが削除された場合、書き込まれたストリームにはその値は含まれません。以前のクラスによってストリームが読み取られると、ストリームに値がないため、フィールドの値はデフォルト値に設定されます。しかし、このデフォルト値は、契約を履行するための以前のバージョンの能力を損なう可能性があります。
- クラスを階層の上または下に移動する:ストリームのデータが間違った順序で表示されるため、これは許可されません。
- 非静的フィールドを静的または非過渡フィールドに変更する:デフォルトの直列化に依存する場合、この変更はクラスからフィールドを削除することと同じです。このバージョンのクラスは、そのデータをストリームに書き込まないため、クラスの以前のバージョンでは読み込めません。フィールドを削除するときと同じように、以前のバージョンのフィールドはデフォルト値に初期化され、予期しない方法でクラスが失敗する可能性があります。
- プリミティブフィールドの宣言された型の変更:クラスの各バージョンは宣言された型のデータを書き込みます。ストリームのデータの型がフィールドの型と一致しないため、フィールドを読み取ろうとするクラスの以前のバージョンは失敗します。
- writeObjectメソッドまたはreadObjectメソッドを変更して、既定のフィールドデータの書き込みまたは読み取りを行わないようにするか、以前のバージョンのデータを書き込もうとしないように変更します。デフォルトのフィールドデータは、一貫してストリームに表示されるか、または表示されないようにする必要があります。
- クラスには、使用可能なクラスの実装と互換性のないデータが含まれるため、クラスをSerializableからExternalizableに、またはその逆に変更することは互換性のない変更です。
- ストリームには使用可能なクラスの実装と互換性のないデータが含まれるため、クラスを非enum型からenum型に、またはその逆に変更します。
- SerializableまたはExternalizableのいずれかを削除すると、互換性のない変更になります。これは、書き込まれると、古いバージョンのクラスで必要なフィールドを提供しなくなるためです。
- writeReplaceメソッドまたはreadResolveメソッドをクラスに追加すると、クラスの古いバージョンと互換性のないオブジェクトが生成される場合、互換性がありません。
JacksonとのカスタムJSON逆シリアル化
私たちはJSON形式としてAPIを使い切り、それをPOJOに非整列化します。ジャクソンのorg.codehaus.jackson.map.ObjectMapperは "すぐに"動作し、ほとんどの場合実際には何もしません。しかし、私たちのカスタムニーズを満たすためにカスタムデシリアライザが必要な場合もあります。このチュートリアルでは、独自のカスタムデシリアライザを作成するプロセスについて説明します。
私たちに次のようなエンティティがあるとしましょう。
public class User {
private Long id;
private String name;
private String email;
//getter setter are omitted for clarity
}
そして
public class Program {
private Long id;
private String name;
private User createdBy;
private String contents;
//getter setter are omitted for clarity
}
最初にオブジェクトを直列化/整列化しましょう。
User user = new User();
user.setId(1L);
user.setEmail("[email protected]");
user.setName("Bazlur Rahman");
Program program = new Program();
program.setId(1L);
program.setName("Program @# 1");
program.setCreatedBy(user);
program.setContents("Some contents");
ObjectMapper objectMapper = new ObjectMapper();
最終的なString json = objectMapper.writeValueAsString(プログラム); System.out.println(json);
上記のコードは、次のJSON-
{
"id": 1,
"name": "Program @# 1",
"createdBy": {
"id": 1,
"name": "Bazlur Rahman",
"email": "[email protected]"
},
"contents": "Some contents"
}
今すぐ反対のことを非常に簡単に行うことができます。このJSONがある場合、ObjectMapperを使用して次のようにプログラムオブジェクトに非整列化できます。
これは実際のケースではないとしましょう。 Program
クラスと一致しないAPIとは異なるJSONを使用します。
{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}
JSON文字列を見ると、owenerIdという別のフィールドがあります。
今度はこのJSONをシリアル化したい場合は、例外があります。
例外を回避し、これをシリアル化するには2つの方法があります。
未知のフィールドを無視する
onwerId
無視してonwerId
。 Programクラスに次のアノテーションを追加する
@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}
カスタムデシリアライザの作成
しかし、実際にこのowerId
フィールドが必要な場合があります。これをUser
クラスのIDとして関連づけたいとしましょう。
そのような場合は、カスタムデシリアライザを作成する必要があります。
ご覧のとおり、まずJsonNode
からJsonNode
にアクセスするJsonNode
がありJonsParser
。そして、 get()
メソッドを使ってJsonNode
から情報を簡単に抽出することができます。フィールド名を確認する必要があります。それは正確な名前でなければなりません、スペルミスが例外を引き起こします。
最後に、あなたのProgramDeserializerをObjectMapper
に登録する必要があります。
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Program.class, new ProgramDeserializer());
mapper.registerModule(module);
String newJsonString = "{\"id\":1,\"name\":\"Program @# 1\",\"ownerId\":1,\"contents\":\"Some contents\"}";
final Program program2 = mapper.readValue(newJsonString, Program.class);
また、注釈を使用してデシリアライザを直接登録することもできます。
@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}