Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

Serializableの意味わかりますか?

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

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 SerializableserialVersionUID = 1L;の意味わかりますか?
「わかりません!」っていう人向けに書いた記事です。「わかるよ!」って人はこの記事を読む必要ないです。

まずは概念から

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

image.png
図はこの記事から抜粋

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

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

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

上記TestServletHttpServletを extends しており、HttpServletGenericServletを extends、GenericServletSerializableを implements しているので、TestServletSerializableです。

image.png

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

image.png

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

image.png

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

image.png

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

実際にSerialize/Deserializeさせてみる

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

image.png

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

image.png

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;
    }
}
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クラスは、先ほどのものと中身は同じものを別のフォルダにコピーして使用しています。

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

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

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.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()の中)。

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

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

image.png

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

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

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

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

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

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

serialVersionUIDの変更方法

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

image.png

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

transientでSerialize/Deserializeの対象外とする

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

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を設定します。

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

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

おわりに

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

以上。

参考

公式

Serialize:シリアライズ(直列化)とDeserialize:デシリアライズ

Serializeとセッション

serialVersionUID

Tomcatのセッション管理

taumax
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away