についてserialVersionUID
。このフィールドは一体何のためにあるのでしょうか? 設定しないとどうなりますか? なぜ「Alibaba Java 開発マニュアル」には次のような規定があるのでしょうか。
![-w934][4]
背景知識
直列化可能と外部化可能
クラスはjava.io.Serializable
インターフェイスを実装して、シリアル化機能を有効にします。**このインターフェイスを実装していないクラスは、シリアル化または逆シリアル化できません。** 直列化可能なクラスのすべてのサブタイプは、それ自体直列化可能です。
読者がソース コードを読んだ場合はSerializable
、それが何も含まれていない単なる空のインターフェイスであることがわかるでしょう。**シリアル化可能なインターフェイスにはメソッドやフィールドがなく、シリアル化可能なセマンティクスを識別するためにのみ使用されます。**ただし、クラスがこのインターフェイスを実装しておらず、シリアル化したい場合は、java.io.NotSerializableException
例外がスローされます。
このインターフェイスを実装するメソッドのみがシリアル化および逆シリアル化できるようにするにはどうすればよいでしょうか?
その理由は、シリアル化プロセス中に次のコードが実行されるためです。
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
Enum
シリアル化操作を行う際には、シリアル化するクラスが、型であるArray
かどうかを判断しSerializable
、そうでない場合は直接スローしますNotSerializableException
。
インターフェイスはJava でも提供されておりExternalizable
、シリアル化機能を提供するために実装することもできます。
Externalizable
から継承されSerializable
、このインターフェイスではwriteExternal()
との 2 つの抽象メソッドが定義されていますreadExternal()
。
Externalizable
シリアル化と逆シリアル化にインターフェイスを使用する場合、開発者はメソッドwriteExternal()
をオーバーライドする必要がありますreadExternal()
。それ以外の場合は、すべての変数の値がデフォルト値になります。
一時的な
transient
キーワードの機能は、変数のシリアル化を制御することです。変数宣言の前にこのキーワードを追加すると、変数がファイルにシリアル化されるのを防ぐことができます。逆シリアル化された後、変数の値は次のような初期値に設定されます。transient
int型は0、オブジェクト型はnullです。
カスタムシリアル化戦略
writeObject
シリアル化プロセス中に、およびメソッドがシリアル化されたクラスで定義されている場合、仮想マシンはreadObject
オブジェクト クラスのwriteObject
およびメソッドを呼び出してreadObject
、ユーザー定義のシリアル化と逆シリアル化を実行しようとします。
そのようなメソッドがない場合、デフォルトの呼び出しは のObjectOutputStream
メソッドdefaultWriteObject
と のObjectInputStream
メソッドですdefaultReadObject
。
ユーザー定義writeObject
およびreadObject
メソッドを使用すると、ユーザーはシリアル化プロセス中にシリアル化された値を動的に変更するなど、シリアル化プロセスを制御できます。
writeObject
したがって、いくつかの特殊なフィールドに対してシリアル化戦略を定義する必要がある場合は、一時的な変更を使用し、の実装readObject
など、 および メソッドを自分で書き直すことを検討できます。java.util.ArrayList
String、Integer などのシリアル化インターフェイスを実装する Java クラスをランダムにいくつか見つけます。詳細を確認できます。つまり、これらのクラスの実装に加えて、![][5 ] も定義されていSerializable
ますserialVersionUID
。
それで、それは正確に何ですかserialVersionUID
?なぜそのようなフィールドを設定するのでしょうか?
シリアルバージョンUIDとは何ですか
シリアル化は、オブジェクトの状態情報を保存または送信できる形式に変換するプロセスです。Java オブジェクトは JVM のヒープ メモリに格納されることは誰もが知っています。つまり、JVM ヒープが存在しない場合、オブジェクトは消滅します。
シリアル化は、JVM が停止している場合でもオブジェクトを保存できるソリューションを提供します。私たちが普段使っているUディスクと同じです。Java オブジェクトを、ファイルに保存するなど、保存または送信できる形式 (バイナリ ストリームなど) にシリアル化します。このようにして、オブジェクトが再度必要になると、バイナリ ストリームがファイルから読み取られ、オブジェクトはバイナリ ストリームから逆シリアル化されます。
仮想マシンが逆シリアル化を許可するかどうかは、クラスパスと関数コードが一貫しているかどうかだけでなく、2 つのクラスのシリアル化 ID が一貫しているかどうかにも依存します。このいわゆるシリアル化 ID は、コードで定義されていますserialVersionUID
。
SerialVersionUID が変更されるとどうなるか
例を挙げて、serialVersionUID
変更するとどうなるかを見てみましょう。
public class SerializableDemo1 {
public static void main(String[] args) {
//Initializes The Object
User1 user = new User1();
user.setName("hollis");
//Write Obj to File
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(oos);
}
}
}
class User1 implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
まず上記のコードを実行し、User1 オブジェクトをファイルに書き込みます。次に、User1 クラスを変更し、serialVersionUID
の値を変更します2L
。
class User1 implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
次に、次のコードを実行して、ファイル内のオブジェクトを逆シリアル化します。
public class SerializableDemo2 {
public static void main(String[] args) {
//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1) ois.readObject();
System.out.println(newUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
実行結果は以下の通りです。
java.io.InvalidClassException: com.hollis.User1; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
上記のコードは をスローし、矛盾をjava.io.InvalidClassException
指摘していることがわかります。serialVersionUID
これは、逆シリアル化するときに、JVM が受信バイト ストリームを対応するserialVersionUID
ローカル エンティティ クラスとserialVersionUID
比較するためです。それらが同じである場合、一貫性があるとみなされ、逆シリアル化できます。そうでない場合は、バージョンの不一致に対するシーケンス例外が発生します。InvalidCastException
。
このため、「Alibaba Java 開発マニュアル」では、互換性アップグレード中、クラスを変更する場合はクラスを変更しないように規定されていますserialVersionUID
。完全に互換性のない 2 つのバージョンでない限り。したがって、serialVersionUID
実際にはバージョンの整合性を検証しています。
興味があれば、各バージョンの JDK コードを参照してください。これらの下位互換性のあるクラスはserialVersionUID
変更されていません。たとえば、String クラスはserialVersionUID
常に です-6849794470754667710L
。
ただし、著者は、この仕様は実際にはより厳格になる可能性がある、つまり次のような規制になる可能性があると考えています。
クラスがSerializable
インターフェイスを実装する場合、変数を手動で追加しprivate static final long serialVersionUID
、初期値を設定する必要があります。
なぜserialVersionUIDを明示的に指定する必要があるのですか
クラス内で明示的に定義しなかった場合にserialVersionUID
何が起こるかを見てください。
上記のデモ コードを変更してみます。まず、次のクラスを使用して、このクラスでは定義されていないオブジェクトを定義し、serialVersionUID
それをファイルに書き込みます。
class User1 implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
次に、User1 クラスを変更し、プロパティを追加します。ファイルから読み取って逆シリアル化しようとしています。
class User1 implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
結果:java.io.InvalidClassException: com.hollis.User1; local class incompatible: stream classdesc serialVersionUID = -2986778152837257883, local class serialVersionUID = 7961728318907695402
もう一度、とというInvalidClassException
2 つの違いを捨てて指摘します。serialVersionUID
-2986778152837257883
7961728318907695402
ここから、システムが自動的に 1 つを追加したことがわかりますserialVersionUID
。
したがって、クラスを実装したらSerializable
、明示的に定義することをお勧めしますserialVersionUID
。そうしないと、クラスを変更するときに例外が発生します。
serialVersionUID
表示生成方法は 2 つあります。1
つはデフォルトの 1L です。private static final long serialVersionUID = 1L;
もう 1 つは、クラス名、インターフェイス名、メンバーのメソッドと属性などに従って 64 ビットのハッシュ フィールドを生成します。たとえば、次のとおりです。
private static final long serialVersionUID = xxxxL;
後者の方法は、後で紹介する IDE を使用して生成できます。
背後にある原則
その理由を知るために、ソース コードを見て、serialVersionUID
変更時に例外がスローされる理由を分析してみましょう。明示的な定義がない場合、serialVersionUID
デフォルトはどこから来るのでしょうか?
コードの量を簡素化するために、逆シリアル化の呼び出しチェーンは次のようになります。
ObjectInputStream.readObject -> readObject0 -> readOrdinaryObject -> readClassDesc -> readNonProxyDesc -> ObjectStreamClass.initNonProxy
のinitNonProxy
キーコードは次のとおりです。
![][6]
逆シリアル化プロセス中にserialVersionUID
比較が行われ、それらが等しくないことが判明した場合は、例外が直接スローされます。
getSerialVersionUID
このメソッドを詳しく見てみましょう。
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}
定義されていない場合serialVersionUID
、computeDefaultSUID
メソッドが呼び出されてデフォルトのメソッドが生成されますserialVersionUID
。
これは、上記 2 つの問題の根本を見つけるものでもあり、実際、コード内では厳密な検証が行われています。
アイデアのヒント
定義を忘れないようにするためにserialVersionUID
、Intellij IDEA の設定を調整できます。Serializable
インターフェイスの実装後、定義がない場合serialVersionUID
、IDEA (Eclipse と同じ) は次のプロンプトを表示します: ![][7]
ワンクリックで生成できます。
![][8]
もちろん、この構成はデフォルトでは有効になりません。IDEA で手動で設定する必要があります。
![][9]
図の 3 の部分 (serialVersionUID 設定のない Serializable クラス) にチェックを入れて保存します。
要約する
serialVersionUID
バージョンの一貫性を検証するために使用されます。serialVersionUID
したがって、互換性のアップグレードを行う場合は、クラスの値を変更しないでください。
クラスが Serializable インターフェイスを実装している場合は、それを忘れずに定義する必要がありますserialVersionUID
。そうしないと例外が発生します。IDE でこれを設定すると、プロンプトを表示できるようになり、ワンクリックですぐに生成できますserialVersionUID
。
例外が発生する理由は、デシリアライズ処理中に検証が行われるためで、明確に定義されていない場合はクラスのプロパティに応じて自動的に例外が生成されます。