以前セッション管理の方法をメモリからDBに変更した際に、シリアライズ周りでエラーが起きたのでそれの原因と対策についてまとめる。
前提
Javaではデータの初期化に下記のような方法がある。
List<String> animals = new ArrayList<>() {
{
add("いぬ");
add("ねこ");
add("たぬき");
}
};
これは匿名クラスとインスタンスイニシャライザを使って初期化を行うという方法である。
通常Listの初期化にはList.of()
, Arrays.asList()
などを使うが、これはimmutableなListを作成するため、後から要素の追加などが出来ない。
しかし、匿名クラスとインスタンスイニシャライザを使えばArrayListのサブクラスとしてインスタンスを作成するため、その後の要素の追加が可能になる。
起きたこと
Serializableを実装したSchoolオブジェクトをシリアライズしようとしたエラーが発生した。
// シリアライズ可能なクラス
public class School implements Serializable {
private List<String> studentNames;
public List<String> getStudentNames() {
return studentNames;
}
public void setStudentNames(final List<String> studentNames) {
this.studentNames = studentNames;
}
}
public class Service {
public void process() {
// 匿名クラス + インスタンスイニシャライザを使ってリストを初期化
final List<String> studentNames = new ArrayList<>() {
{
add("ひろし");
add("みか");
add("たくろう");
add("たけし");
}
};
final School school = new School();
school.setStudentNames(studentNames);
try (FileOutputStream fos = new FileOutputStream("school.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
// Schoolのシリアライズ
oos.writeObject(school);
} catch (final IOException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(final String[] args) {
final Service service = new Service();
service.process();
}
}
java.io.NotSerializableException: Service
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1553)
at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1510)
at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1433)
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1179)
at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1553)
at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1510)
at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1433)
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1179)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:349)
at Service.process(Service.java:22)
at Main.main(Main.java:4)
原因
NotSerializableException
はシリアライズしようとしたオブジェクトがSerializableを実装(implements)していないときに投げられる例外で、今回の場合だとServiceクラスがSerializableを実装していないためエラーとなった。
そもそもシリアライズしようとしているオブジェクトはSchoolなのになぜServiceが出てくるのか?
これが今回のポイントで、インスタンスメソッド内で匿名クラスを使うとその匿名クラスはコンパイル時に親クラスの情報を持つようになる。
実際にコンパイル後のクラスの情報を見てみる。
$ cd bin
$ ls
Main.class School.class 'Service$1.class' Service.class
コンパイルすると匿名クラスも一つのclassファイルとして生成されていて、今回の場合だとService$1.class
がそれにあたる。
javapコマンドで逆アセンブルすることでclassファイルのフィールドやメソッドを確認することができる。
$ javap -p 'Service$1.class'
Compiled from "Service.java"
class Service$1 extends java.util.ArrayList<java.lang.String> {
final Service this$0;
Service$1(Service);
}
すると匿名クラスのService$1.class
はフィールドにServiceを持っていることがわかる。
これでSchoolオブジェクトをシリアライズしようとしたときにServiceオブジェクトが登場することがわかった。
対策
匿名クラスを使ったデータの初期化は行わない
今回の例でもただインスタンスの作成とデータの初期化を同時やりたかっただけなので、普通にadd
やaddAll
などをすればよいし、List.of()
, Arrays.asList()
を引数に渡してインスタンス生成するやり方でも良い。
final List<String> studentNames = new ArrayList<>();
studentNames.add("ひろし");
studentNames.add("みか");
studentNames.add("たくろう");
studentNames.add("たけし");
final List<String> studentNames = new ArrayList<>(List.of("ひろし", "みか", "たくろう", "たけし"));
自分の書いているコードの意味をしっかりと理解する
ArrayListの初期化の方法を検索すると、今回の匿名クラスとインスタンスイニシャライザを使って初期化する方法を紹介している記事は多数見つかる。
そのコードを理解しないままに使ってしまうと、思わぬところでエラーとなってしまうため、この構文が何をしているものなのか、それを使った上で問題はないのかをしっかり理解した上で使うようにする。