JavaのSerializableを実際にアプリケーションを動かしながら理解する

# Serializableの意味わかりますか？

Java でWebアプリケーションを作ると、以下のような JavaBeans クラスを作ることがあると思います。

```java:JavaBeans.java
import java.io.Serializable;

public class JavaBeans implements Serializable {

    private static final long serialVersionUID = 1L;

    private String aaa = null;

    public String getAaa() {
        return aaa;
    }

    public void setAaa(String aaa) {
        this.aaa = aaa;
    }
}
```

この`implements Serializable`と`serialVersionUID = 1L;`の意味わかりますか？
「わかりません！」っていう人向けに書いた記事です。「わかるよ！」って人はこの記事を読む必要ないです。

# まずは概念から
Javaのインスタンスをバイト配列として出力することをSerialize（シリアライズ）と言い、その逆をDeserializeと言います。下の図がわかりやすいのですが、ファイルやメモリ、データベースなどにインスタンスを保存することをSerializeと言っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/2dd7eb88-2a0d-42eb-7919-b1e578d4b303.png)
図は[この記事](https://data-flair.training/blogs/serialization-and-deserialization-in-java/)から抜粋

なので`implements Serializable`と宣言するということは、「このインスタンスはdiskなどに保存することができます！」って宣言していることになります。

ちょっと個人的に作ったWebアプリケーションがあるので、それで実験してみます。

```java:TestServlet.java
public class TestServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    // ・・・
}
```

上記`TestServlet`は`HttpServlet`を extends しており、`HttpServlet`は`GenericServlet`を extends、`GenericServlet`は`Serializable`を implements しているので、`TestServlet`は`Serializable`です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/631d23eb-4b3e-7492-5acf-9c19cf8c9b8f.png)


このWebアプリケーションのサーバ(Tomcat)を起動します。デプロイ先のlocalhostフォルダ直下にはまだ何もありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/38bd98ea-043d-3f3c-c076-7ff4ea5f3ab3.png)

この後、`TestServlet`が動くようにブラウザを開くか何かして、サーバーを停止します。すると、デプロイ先のlocalhostフォルダ直下に`SESSIONS.ser`という謎のファイルが出来上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/68c523a0-a467-4874-f8b0-164ac1f57030.png)

再度サーバーを起動します。すると、`SESSIONS.ser`は消えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/65331cec-c7c8-0f43-e73c-62cb95dbf7c7.png)

`SESSIONS.ser`はTomcatがセッション情報をSerializeしたものです。これのおかげで、サーバが再起動されてもセッション情報は失われず、再起動後にアクセスすることで再起動前の情報を復元(Deserialize)することができます。逆に言えば、この仕様のために、セッションに格納するオブジェクトはSerializableをimplementsしている必要があります。
一見便利そうなんですけど、これ、再起動前後でプログラムが変更された場合にどうなるかわかりますか？

# 実際にSerialize/Deserializeさせてみる
下図のような構成のアプリケーションを作り、実際にSerialize/Deserializeさせてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/1aa70be5-3d2b-15bc-c275-8718f3a32f32.png)


まずSerializeする方です。任意のフォルダに`SampleBean.java`と、それをSerializeする`WriteObject.java`を作ります。WriteObject.javaはSampleBeanインスタンスを生成し、2つのプロパティに100を設定してそれをSerializeしています。WriteObjectを実行することで`sample.ser`を作ります。sample.serはSampleBeanインスタンスをSerializeしたものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/857dd56a-ab99-dfc3-3c6b-f3ab56c2c536.png)


```java:SampleBean.java
import java.io.Serializable;

public class SampleBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private double propertyD;
    private int propertyE;

    public double getPropertyD() {
        return propertyD;
    }

    public void setPropertyD(double propertyD) {
        this.propertyD = propertyD;
    }

    public int getPropertyE() {
        return propertyE;
    }

    public void setPropertyE(int propertyE) {
        this.propertyE = propertyE;
    }
}
```


```java:WriteObject.java
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class WriteObject {
    public static void main (String args[]) {
        SampleBean sample = new SampleBean();
        sample.setPropertyD(100);
        sample.setPropertyE(100);

        try {
            FileOutputStream fout = new FileOutputStream("sample.ser");
            ObjectOutputStream oos = new ObjectOutputStream(fout);
            oos.writeObject(sample);
            oos.close();
            System.out.println("Done");
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}
```


次にDeserializeする方です。
上記で生成した`sample.ser`を読み込むクラス(JspTest.java)を作ります。
JspTest.javaで使用しているSampleBeanクラスは、先ほどのものと中身は同じものを別のフォルダにコピーして使用しています。

```java:JspTest.java
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/JspTest")
public class JspTest extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        SampleBean sample = null;
        List<SampleBean> list = new ArrayList<>();
        try {
            FileInputStream fin = new FileInputStream("c:\\sample.ser");
            ObjectInputStream ois = new ObjectInputStream(fin);
            sample = (SampleBean) ois.readObject();
            ois.close();
            list.add(sample);
        } catch(Exception ex) {
            ex.printStackTrace();
        }

        request.setAttribute("samplelist", list);
        String path = "/WEB-INF/jsp/jsptest.jsp";
        RequestDispatcher disp = request.getRequestDispatcher(path);
        disp.forward(request, response);
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}
```

それを表示するjsp

```jsp:jsptest.jsp
<%@ page language="java" contentType="text/html;charset=Windows-31J"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ page isELIgnored="false" %>

<html><body>
<table border="1">
  <thead>
    <tr>
      <th>プロパティD</th>
      <th>プロパティE</th>
    </tr>
  </thead>
  <tbody>
    <c:forEach items="${samplelist}" var="item">
      <tr>
        <td><c:out value="${item.propertyD}" /></td>
        <td><c:out value="${item.propertyE}" /></td>
      </tr>
    </c:forEach>
  <tbody>
</table>
</body></html>
```

実行結果（ブラウザ表示）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/fa3fc0f8-be9b-e26b-2438-3a6c98fbee83.png)

SerializeされたインスタンスをDeserializeしてそれをブラウザに表示することができました。
これがSerialize/Deserializeです。

# serialVersionUIDとは？
次は`serialVersionUID`です。クラスに implements Serializable と書き、serialVersionUID を宣言しなかった場合、`The serializable class XXXX does not declare a static final serialVersionUID field of type long`というワーニングが出てきます。
`serialVersionUID`とは、インスタンスのバージョンです。と言われてもあまりピンとこないと思うので、もっと具体的に理解するために、少し実験をしてみます。
先ほどSerializeした時に使用したSampleBean.javaと、それをDeserializeした時に使用したSampleBean.javaのserialVersionUIDを不一致にさせてみます。
JspTest.javaが使用するSampleBean.java（Deserializeする方）を`serialVersionUID = 2L;`に変更します。SerializeされたSampleBean.javaは`serialVersionUID = 1L;`のままです。
変更した後、ブラウザをF5で更新すると以下のようなエラーが出力されます。


```java:エラー内容
java.io.InvalidClassException: SampleBean; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
	at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:715)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1997)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1866)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2159)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1685)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:499)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:457)
	at JspTest.doGet(JspTest.java:27)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:832)
```

`InvalidClassException`というエラーが発生し、その原因はSampleBeanのserialVersionUID が不一致だということがわかります。
逆にserialVersionUID は同じ値だけど中身が違う場合はどうなるでしょうか？
Serializeする方のSampleBeanは変更せず、Deserializeする方だけ以下のように、getterの中で×100してみます（getPropertyE()の中）。

```java:SampleBean.java（Deserialize側のみ変更）
import java.io.Serializable;

public class SampleBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private double propertyD;
    private int propertyE;

    public double getPropertyD() {
        return propertyD;
    }

    public void setPropertyD(double propertyD) {
        this.propertyD = propertyD;
    }

    public int getPropertyE() {
        return propertyE * 100;    // getterの中で×100してみる
    }

    public void setPropertyE(int propertyE) {
        this.propertyE = propertyE;
    }
}
```

実行結果（ブラウザ表示）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/2ef7c891-5ca7-7c46-dea5-e9efbb689055.png)

エラーは発生せず、×100した値が表示されていることがわかります。正常終了はしたものの、<font color='red'>この挙動は本当にあなたが想定した挙動ですか？</font>Serializeした時とDeserializeした時でプログラムの中身が変更されていた場合の挙動を考慮したことがありますか？
Deserializeで復元しているのは、インスタンス変数の値です。Serializeした時に、propertyDにはdouble型の100、propertyEにはint型の100を設定しているので、Deserializeではその値を復元しています。setter/getterの中で何をしているかなんて気にしていません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/d8956c95-4087-7919-193e-9aa887c08cd4.png)

なので、例えばsetterの中に、引数の値が10以上の場合はエラーとするようなチェックを入れたとしても、それを無視した値が復元されます。

```java:SampleBean.java
import java.io.Serializable;

public class SampleBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private double propertyD;
    private int propertyE;

    public double getPropertyD() {
        return propertyD;
    }

    public void setPropertyD(double propertyD) {
        // 引数の値が10以上の場合はエラー
        if (propertyD > 10) {
            throw new IllegalArgumentException();
        }
        this.propertyD = propertyD;
    }

    public int getPropertyE() {
        return propertyE;
    }

    public void setPropertyE(int propertyE) {
        this.propertyE = propertyE;
    }
}
```


実行結果（ブラウザ表示）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/3d59f132-7217-aff1-6dd5-dba663014926.png)

setterで10以上ならエラーというチェックを入れたにも関わらず、特にエラーもなく表示できてしまいます。
この記事の最初の方で、セッション情報がSerialize/Deserializeされているという話をしました。Webサーバがセッション情報をSerializeするのは以下のような場合です。

 - サーバを停止した時
 - セッションを格納する領域（メモリ）が満杯になった時

Deserializeするのはこの逆のタイミングです。SerializeされてからDeserializeされるまでの間にプログラムが変更されていたとしても、その変更内容を無視してDeserializeされてしまいます。それが困るようであればserialVersionUIDを変更する必要があります。

Serialize/DeserializeのJavaの仕様は以下にまとめられています。
[5.6 直列化に影響する型変更](https://docs.oracle.com/javase/jp/6/platform/serialization/spec/version.html#6678)
特に意識してほしいのは「5.6.2 互換性のある変更」の方で、Javaの仕様として互換性がある（＝Deserialize可能）だとしても、実際に使用しているそのシステムにおいて互換性があるかどうかはまた別の話です。Javaの仕様として互換性がある場合に、それは本当にDeserializeさせて大丈夫なのか？改めて画面を操作させないと変なことが起こるようなのであれば、serialVersionUIDを変更してエラーを発生させた方が安全な場合もあると思います。

# serialVersionUIDの変更方法

自分の手で変更することも可能ですが、IDEに生成させることも可能です。Eclipseであれば以下のような手順になります。
（IntelliJ IDEAを使用している方は[こちら](https://qiita.com/tenten0213/items/94721d0e3005c79fbbc5)）
クラスから`private static final long serialVersionUID = 1L;`をいったん消します。するとクラスでワーニングが出るのでそこにカーソルを合わせる（下図）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/779b5fba-a508-5d52-f0e6-d1581c7fc1b6.png)

`private static final long serialVersionUID = 2779728212248637579L;`というserialVersionUIDが生成されました。この値はクラスの構造が変わると生成される値も変更されます。クラスの構造とは、インスタンス変数の追加や変数名の変更、メソッドの追加、・・・などです。

# transientでSerialize/Deserializeの対象外とする
インスタンス変数にtransient修飾子を付与することで、Serialize/Deserializeの対象外とすることができます。試しに以下のようにpropertyEにtransient修飾子を付与してみます。

```java:SampleBean.java
import java.io.Serializable;

public class SampleBean implements Serializable {

    private static final long serialVersionUID = 4798282072280430232L;
    private double propertyD;
    private transient int propertyE;  // transientを付与

    public double getPropertyD() {
        return propertyD;
    }

    public void setPropertyD(double propertyD) {
        this.propertyD = propertyD;
    }

    public int getPropertyE() {
        return propertyE;
    }

    public void setPropertyE(int propertyE) {
        this.propertyE = propertyE;
    }
}
```

Serializeする際には100を設定します。

```java:WriteObject.java
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class WriteObject {
    public static void main (String args[]) {
        SampleBean sample = new SampleBean();
        sample.setPropertyD(100);
        sample.setPropertyE(100);

        try {
            FileOutputStream fout = new FileOutputStream("sample.ser");
            ObjectOutputStream oos = new ObjectOutputStream(fout);
            oos.writeObject(sample);
            oos.close();
            System.out.println("Done");
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}
```

ここで生成したsample.serを読み込ませてブラウザ表示した結果が以下です。

実行結果（ブラウザ表示）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254168/e96f8737-c104-9ff6-0cf6-0523da54f383.png)

propertyEにtransientを付けたことで、Serializeの際に設定した100という値がDeserializeした時には設定されていないことがわかります。0が表示されているのはintのデフォルト値が0だからです。
ちなみにtransientを付けるとIDEが生成するserialVersionUID の値も変わります。

# おわりに
ざっとこんな感じです。私もよくわかっていない部分がありますが、私の周囲でこのあたりのことをちゃんと理解した上で`implements Serializable`とか`serialVersionUID = 1L`を書いている人があまりいないような気がしたので書いてみました。この記事を読んで少しでも多くの方が、Serializeの意味をちゃんと理解した上で`implements Serializable`とか`serialVersionUID = 1L`と書いてくれようになってくれたらいいなと思います。

以上。

# 参考

公式

 - [5.6 直列化に影響する型変更](https://docs.oracle.com/javase/jp/6/platform/serialization/spec/version.html#6678)
 - [システムアーキテクチャー](https://docs.oracle.com/javase/jp/6/platform/serialization/spec/serial-arch.html#4188)
 - [直列化可能オブジェクトのバージョン管理](https://docs.oracle.com/javase/jp/6/platform/serialization/spec/version.html)


Serialize：シリアライズ（直列化）とDeserialize：デシリアライズ

 - [直列化（serialize：シリアライズ）](https://www.ne.jp/asahi/hishidama/home/tech/java/serial.html)
 - [Serialization and Deserialization in Java](https://data-flair.training/blogs/serialization-and-deserialization-in-java/)
 - [Serializable について](https://qiita.com/NBT/items/9f76c9fd1c7a90506658)
 - [Javaのシリアライズ(直列化)メモ](https://qiita.com/Sekky0905/items/b3c6776d10f183d8fc89)
 - [難解なSerializableという仕様について俺が知っていること、というか俺の理解](http://daisuke-m.hatenablog.com/entry/20100414/1271228333)
 - [Javaとシリアライズと互換性](https://kazuhira-r.hatenablog.com/entry/20130929/1380443535)

Serializeとセッション

 - [3.1.14 セッションリカバリ機能を利用する場合のWebアプリケーション作成方法](https://software.fujitsu.com/jp/manual/manualfiles/m140003/b1ws1025/04z200/b1025-00-03-01-14.html)
 - [セッションに保存されるオブジェクトがシリアライズ可能でない](https://ameblo.jp/togoe1204/entry-10010472956.html)
 - [sessionに登録するObjectはSerializableしなければならない](https://qiita.com/esgr_dxx/items/346b0e08ffeece9df1de)
 - [[JAVA] フェイルオーバー時や再起動時に NullPointerException や Cannot get property ‘xxxxxx’ on null object](https://huge.mints.ne.jp/11/2013/it_technique/groovy/grails/512/)

serialVersionUID

 - [serialVersionUIDを理解する](https://www.codeflow.site/ja/article/java-best-practices__understand-the-serialversionuid)
 - [Eclipse JavaのシリアルバージョンUIDを自動で生成する方法](https://itsakura.com/eclipse-java-serializable)
 - [IntelliJ IDEAでserialVersionUIDを生成する](https://qiita.com/tenten0213/items/94721d0e3005c79fbbc5)


Tomcatのセッション管理

 - [Tomcat のセッション永続化について](https://qiita.com/shintaness/items/73d176c0a4f369608898)
 - [（作業中）tomcat再開 その44 session manager](https://ameblo.jp/dagyah/entry-12397201041.html)