TECHSCORE BLOG

クラウドCRMを提供するシナジーマーケティングのエンジニアブログです。

SimpleDateFormat と DateTimeFormatter どっちを使ったら良い?

佐藤 淳(サトウ ジュン)
開発のいろんなことやってます。飲み会好きですがお酒はあんまり飲めません。


業務で久しぶりに Java を書くシーンがあったのですが、日付フォーマットする方法がパッと思い出せず、 Web サイトを参考にしても SimpleDateFormat と DateTimeFormatter が混在しているので改めて調べてみました。

調べてみた結果

  • SimpleDateFormat はスレッドセーフでない
  • DateTimeFormatter はスレッドセーフである ※Java8で追加された
  • 基本的には DateTimeFormatter を使用するべき

検証してみる

スレッドセーフを考慮しないとどのような不都合があるのか、実際にコードを書いて検証してみました。 どうせ実装するなら Pure Java でやってみます。

検証コード

実行環境(JDK さえあれば検証できます)

  • WSL2 ( Ubuntu 20.04 )
  • OpenJDK 8

下記 3 ファイルを同階層に作成します。

※以下の実装では、複数のスレッドから、1つのインスタンスに対してフォーマットする関数(format())を呼び出すことを繰り返しています。

// Main.java
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

public class Main {

    public static void main(String[] args) throws Exception {
        testSimpleDateFormat();
        testDateTimeFormatter();
    }

    private static void testSimpleDateFormat() {
        System.out.println("[MAIN][START][SimpleDateFormatTest]");

        // 期待する日付
        List<String> expectedDateStrings = Arrays.asList("1999-12-31", "2000-02-28");
        // フォーマットする日付
        Date date1 = new GregorianCalendar(1999, Calendar.DECEMBER, 31).getTime();
        Date date2 = new GregorianCalendar(2000, Calendar.FEBRUARY, 28).getTime();

        // マルチスレッドで実行するため、テストクラスのインスタンスを2つ生成する
        List<Thread> threads = new ArrayList<Thread>();
        threads.add(new SimpleDateFormatTest(date1, expectedDateStrings));
        threads.add(new SimpleDateFormatTest(date2, expectedDateStrings));

        // マルチスレッド実行
        for (Thread t : threads) {
            t.start();
        }
        for (Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("[MAIN][FINISH][SimpleDateFormatTest]");
    }

    private static void testDateTimeFormatter() {
        System.out.println("[MAIN][START][DateTimeFormatterTest]");

        // 期待する日付
        List<String> expectedDateStrings = Arrays.asList("1999-12-31", "2000-02-28");
        // フォーマットする日付
        LocalDate date1 = LocalDate.of(1999, 12, 31);
        LocalDate date2 = LocalDate.of(2000, 2, 28);

        // マルチスレッドで実行するため、テストクラスのインスタンスを2つ生成する
        List<Thread> threads = new ArrayList<Thread>();
        threads.add(new DateTimeFormatterTest(date1, expectedDateStrings));
        threads.add(new DateTimeFormatterTest(date2, expectedDateStrings));

        // マルチスレッド実行
        for (Thread t : threads) {
            t.start();
        }
        for (Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("[MAIN][FINISH][DateTimeFormatterTest]");
    }
}
// SimpleDateFormatTest.java
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

public class SimpleDateFormatTest extends Thread {

    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    private Date date;
    private List<String> expectedDateStrings;

    public SimpleDateFormatTest(Date date, List<String> expectedDateStrings) {
        this.date = date;
        this.expectedDateStrings = expectedDateStrings;
    }

    public void run() {
        // 日付フォーマット処理を 50 回ループし、期待する日付でない場合に標準出力する
        for (int i = 0; i < 50; i++) {
            String dateString = sdf.format(this.date);
            if (!this.expectedDateStrings.contains(dateString)) {
                System.out.println("[SimpleDateFormatTest] unexpected dateString: " + dateString);
            }
        }
    }
}
// DateTimeFormatterTest.java
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.List;

public class DateTimeFormatterTest extends Thread {

    private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private TemporalAccessor temporalAccessor;
    private List<String> expectedDateStrings;

    public DateTimeFormatterTest(TemporalAccessor temporalAccessor, List<String> expectedDateStrings) {
        this.temporalAccessor = temporalAccessor;
        this.expectedDateStrings = expectedDateStrings;
    }

    public void run() {
        // 日付フォーマット処理を 50 回ループし、期待する日付でない場合に標準出力する
        for (int i = 0; i < 50; i++) {
            String dateString = dtf.format(temporalAccessor);
            if (!this.expectedDateStrings.contains(dateString)) {
                System.out.println("[DateTimeFormatterTest] unexpected dateString: " + dateString);
            }
        }
    }
}

検証コマンド

Pure Java なのでワンライナーでサクッと実行します。

javac *.java && java Main

JDK がなくても Docker を使えばワンライナーでサクッと実行できます。

docker run --rm -v $PWD:/work -w /work openjdk:8 bash -c 'javac *.java && java Main'

検証結果

期待しない日付の場合のみ標準出力されるように実装したので結果は一目瞭然です。 2000-02-31 など、あり得ない日付になってしまうケースが発生することがわかります。 ※実行するたびに結果は変わります。

[MAIN][START][SimpleDateFormatTest]
[SimpleDateFormatTest] unexpected dateString: 2000-12-31
[SimpleDateFormatTest] unexpected dateString: 1999-12-28
[SimpleDateFormatTest] unexpected dateString: 2000-02-31
[SimpleDateFormatTest] unexpected dateString: 1999-02-28
[SimpleDateFormatTest] unexpected dateString: 2000-02-31
[SimpleDateFormatTest] unexpected dateString: 2000-02-31
[SimpleDateFormatTest] unexpected dateString: 1999-12-28
[SimpleDateFormatTest] unexpected dateString: 2000-02-31
[SimpleDateFormatTest] unexpected dateString: 1999-12-28
[SimpleDateFormatTest] unexpected dateString: 2000-12-31
[SimpleDateFormatTest] unexpected dateString: 1999-12-28
[SimpleDateFormatTest] unexpected dateString: 2000-02-31
[SimpleDateFormatTest] unexpected dateString: 1999-12-28
[SimpleDateFormatTest] unexpected dateString: 1999-02-28
[SimpleDateFormatTest] unexpected dateString: 2000-02-31
[MAIN][FINISH][SimpleDateFormatTest]
[MAIN][START][DateTimeFormatterTest]
[MAIN][FINISH][DateTimeFormatterTest]

おわりに

SimpleDateFormat でも static で使用せずに、使うたびにインスタンスを生成すれば問題はありませんが、 基本的には DateTimeFormatter を使用する方が良さそうです。

ちなみに、なぜこんなことが起きるのかというと、 SimpleDateFormat の場合、「年」「月」「日」を順番に StringBuffer で連結していくのですが、 スレッドの割り込みがあると連結する組合せがめちゃくちゃになってしまうからのようです。

シナジーマーケティング株式会社では一緒に働く仲間を募集しています。