Java での SimpleDateFormat の詳細な使用法

日常の開発では時間を頻繁に使用しますが、Java コードで時間を取得する方法はたくさんあります。ただし、取得した時刻の形式は方法によって異なるため、必要な形式で時刻を表示するには書式設定ツールが必要です。

最も一般的な方法は、SimpleDateFormat クラスを使用することです。比較的簡単な機能を備えているように見えるクラスですが、使い方を誤ると重大な問題を引き起こす可能性があります。

Alibaba Java 開発マニュアルには、次のような明確な規定があります。

次に、この記事では SimpleDateFormat の使用法と原理に焦点を当て、正しい姿勢で使用する方法を分析します。

SimpleDateFormat の使用法

SimpleDateFormat は、日付の書式設定と解析のために Java によって提供されるツール クラスです。書式設定 (日付 -> テキスト)、解析 (テキスト -> 日付)、および正規化が可能です。SimpleDateFormat を使用すると、ユーザー定義の日付/時刻形式パターンを選択できます。

JavaではSimpleDateFormatのformatメソッドを使用してDate型をString型に変換し、出力形式を指定できます。

// Date转String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);

上記のコードの変換結果は 2018-11-25 13:00:00 となり、日付と時刻の形式は「日付と時刻のパターン」文字列で指定されます。別の形式に変換したい場合は、別の時間モードを指定するだけです。

Java では、SimpleDateFormat の parse メソッドを使用して、String 型を Date 型に変換できます。

// String转Data
System.out.println(sdf.parse(dataStr));

日時パターンの表現方法

SimpleDateFormat を使用する場合は、時間要素を文字で記述し、それらを目的の日付と時刻のパターンに組み立てる必要があります。よく使用される時間要素と文字の対応表は次のとおりです。

![-w717][1]

通常、パターン文字は繰り返され、その数によって正確な表現が決まります。次の表に、一般的に使用される出力形式を示します。

![-w535][2]

異なるタイムゾーンでの出力時間

タイムゾーンは、同じ時間の定義を使用する地球上の領域です。昔、人々は太陽の位置(時角)を観察して時刻を決めていたため、経度が異なる場所では異なる時刻(地方時)が存在していました。1863 年に、タイムゾーンの概念が初めて使用されました。タイム ゾーンは、地域の標準時間を設定することで、この問題の一部を解決します。

世界の国々は地球上の異なる位置にあるため、特に東西に長い国では、日の出と日の入りの時刻も異なるはずです。このようなずれは時差ボケとして知られています。

現在、世界は 24 のタイムゾーンに分かれています。実際には、1 つの国または 1 つの省が同時に 2 つ以上のタイムゾーンにまたがることが多く、管理上の便宜を考慮して 1 つの国または 1 つの省がグループ化されることがよくあります。したがって、時差は南北の直線で厳密に分けられるのではなく、自然条件によって決まります。たとえば、中国は広大な国土を持ち、ほぼ 5 つのタイムゾーンにまたがっていますが、利便性と使いやすさを考慮して、実際には東 8 タイムゾーンの標準時、つまり北京時間のみが使用されています。

タイムゾーンが異なると、同じ国の別の都市でも時間が異なる場合があるため、Javaで時刻を取得する場合はタイムゾーンに注意する必要があります。

デフォルトでは、指定しない場合、日付を作成するときに現在のコンピューターが存在するタイムゾーンがデフォルトのタイムゾーンとして使用されます。そのため、これを使用するだけで中国の現在時刻を取得できますnew Date()

では、Java コードで異なるタイムゾーンの時刻を取得するにはどうすればよいでしょうか? SimpleDateFormat はこの機能を実現できます。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));

上記のコードの変換結果は次のようになります: 2018-11-24 21:00:00 。中国の時間は11月25日の13時で、ロサンゼルスの時間は中国の北京時間より16時間遅れています(これは冬時間や夏時間にも関係するので詳細は割愛します)。

興味があれば、米国ニューヨーク (America/New_York) の時刻を印刷してみることもできます。ニューヨーク時間は2018-11-25 00:00:00です。ニューヨーク時間は中国の北京時間より13時間進んでいます。

もちろん、他のタイムゾーンを表示する方法はこれだけではありませんが、この記事では主に SimpleDateFormat を紹介し、その他の方法は当面紹介しません。

SimpleDateFormat のスレッド セーフ

SimpleDateFormat は一般的に使用され、一般にアプリケーション内の時刻表示モードは同じであるため、多くの人は次の方法で SimpleDateFormat を定義します。

public class Main {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime()));
    }
}

この定義方法には大きなセキュリティ上のリスクがあります。

問題の再現

コードの一部を見てみましょう。次のコードは、スレッド プールを使用して時間出力を実行します。

   /** * @author Hollis */ 
   public class Main {

    /**
     * 定义一个全局的SimpleDateFormat
     */
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 使用ThreadFactoryBuilder定义一个线程池
     */
    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    /**
     * 定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行
     */
    private static CountDownLatch countDownLatch = new CountDownLatch(100);

    public static void main(String[] args) {
        //定义一个线程安全的HashSet
        Set<String> dates = Collections.synchronizedSet(new HashSet<String>());
        for (int i = 0; i < 100; i++) {
            //获取当前时间
            Calendar calendar = Calendar.getInstance();
            int finalI = i;
            pool.execute(() -> {
                    //时间增加
                    calendar.add(Calendar.DATE, finalI);
                    //通过simpleDateFormat把时间转换成字符串
                    String dateString = simpleDateFormat.format(calendar.getTime());
                    //把字符串放入Set中
                    dates.add(dateString);
                    //countDown
                    countDownLatch.countDown();
            });
        }
        //阻塞,直到countDown数量为0
        countDownLatch.await();
        //输出去重后的时间个数
        System.out.println(dates.size());
    }
}

上記のコードは実際には比較的シンプルで理解しやすいです。これは、100 回ループし、ループするたびに現在時刻に日数を加算し (この日数はループ数によって変わります)、すべての日付を重複排除機能を備えたスレッドセーフな Set入れますを選択し、Set 内の要素の数を出力します。

意図的に書いた上記の例は少し複雑ですが、ほぼすべてにコメントを追加しました。これには、[スレッド プールの作成][3]、[CountDownLatch][4]、ラムダ式、スレッドセーフな HashSet およびその他の知識が含まれます。興味のある友達は一人ずつ調べてみましょう。

通常の状況では、上記のコードの出力は 100 になるはずです。ただし、実際の実行結果は 100 未満の数値になります。

その理由は、SimpleDateFormat が非スレッド セーフ クラスとして、複数のスレッドで共有変数として使用され、スレッド セーフの問題が発生するためです。

この点については、『Alibaba Java Development Manual - Concurrent Processing』の第 1 章、セクション 6 にも明確な記述があります。

そこで、その理由と解決方法を見てみましょう。

スレッドが安全でない理由

上記のコードを通じて、同時シナリオで SimpleDateFormat を使用するとスレッド セーフティの問題が発生することがわかりました。実際、JDK ドキュメントには、SimpleDateFormat はマルチスレッド シナリオでは使用すべきではないと明確に記載されています。

日付形式は同期されません。スレッドごとに個別の形式インスタンスを作成することをお勧めします。複数のスレッドが同時にフォーマットにアクセスする場合は、外部で同期する必要があります。

では、なぜこのような問題が発生するのか、SimpleDateFormat の最下層はどのように実現されているのかを分析してみましょう。

SimpleDateFormat クラスの format メソッドの実装に従って調べてみましょう。

![][5]

SimpleDateFormat の format メソッドは、メンバー変数カレンダーを使用して、実行中の時間を節約します。実はこれが問題の核心なのです。

SimpleDateFormat を宣言するときに静的定義を使用するためです。この SimpleDateFormat は共有変数となり、SimpleDateFormat のカレンダーには複数のスレッドからアクセスできます。

スレッド 1 が実行を終了しcalendar.setTime、時刻を 2018-11-11 に設定したとします。実行が完了する前に、スレッド 2 が再度実行されcalendar.setTime、時刻が 2018-12-12 に変更されます。この時点では、スレッド 1 は実行を継続しており、calendar.getTime取得される時刻はスレッド 2 が変更された後のものです。

format メソッドに加えて、SimpleDateFormat の parse メソッドにも同じ問題があります。

したがって、SimpleDateFormat を共有変数として使用しないでください。

の解き方

SimpleDateFormatの問題点とその原因を紹介しましたが、この問題を解決する方法はあるのでしょうか?

解決策は多数ありますが、ここではより一般的に使用される 3 つの方法を紹介します。

ローカル変数を使用する

for (int i = 0; i < 100; i++) {
    //获取当前时间
    Calendar calendar = Calendar.getInstance();
    int finalI = i;
    pool.execute(() -> {
        // SimpleDateFormat声明成局部变量
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //时间增加
        calendar.add(Calendar.DATE, finalI);
        //通过simpleDateFormat把时间转换成字符串
        String dateString = simpleDateFormat.format(calendar.getTime());
        //把字符串放入Set中
        dates.add(dateString);
        //countDown
        countDownLatch.countDown();
    });
}

SimpleDateFormat はローカル変数になるため、複数のスレッドから同時にアクセスされることはなく、スレッドの安全性の問題が回避されます。

同期ロックを追加する

ローカル変数への変更に加えて、共有変数をロックするという、より馴染みのある別の方法もあります。

for (int i = 0; i < 100; i++) {
    //获取当前时间
    Calendar calendar = Calendar.getInstance();
    int finalI = i;
    pool.execute(() -> {
        //加锁
        synchronized (simpleDateFormat) {
            //时间增加
            calendar.add(Calendar.DATE, finalI);
            //通过simpleDateFormat把时间转换成字符串
            String dateString = simpleDateFormat.format(calendar.getTime());
            //把字符串放入Set中
            dates.add(dateString);
            //countDown
            countDownLatch.countDown();
        }
    });
}

ロックすると、複数のスレッドがキューに入れられ、順番に実行されます。同時実行によって引き起こされるスレッドの安全性の問題を回避します。

実際、上記のコードにはまだ改善の余地があります。つまり、ロックの粒度をより小さく設定し、simpleDateFormat.formatこのより効率的になります。

スレッドローカルを使用する

3 番目の方法は、ThreadLocal を使用することです。ThreadLocal では、各スレッドが単一の SimpleDateFormat オブジェクトを確実に取得できるため、当然、競合の問題は発生しません。

/**
 * 使用ThreadLocal定义一个全局的SimpleDateFormat
 */
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

//用法
String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());

ThreadLocal を使用した実装は、実際にはキャッシュの考え方に少し似ており、各スレッドには排他的なオブジェクトがあり、頻繁なオブジェクトの作成やマルチスレッドの競合が回避されます。

もちろん、上記のコードにも改善の余地があります。つまり、SimpleDateFormat の作成プロセスを遅延読み込みに変更することができます。ここでは詳細には触れません。

DateTimeFormatter を使用する

Java8 アプリケーションの場合は、スレッドセーフな書式設定ツール クラスである SimpleDateFormat の代わりに DateTimeFormatter を使用できます。公式ドキュメントに記載されているように、このクラスはシンプルで美しく強力な不変のスレッドセーフです。

//解析日期
String dateStr= "2016年10月25日";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate date= LocalDate.parse(dateStr, formatter);

//日期转换为字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now .format(format);
System.out.println(nowStr);

要約する

この記事では、SimpleDateFormat の使い方を紹介します。SimpleDateFormat は主に String と Date 間の変換を行うことができ、時刻を異なるタイムゾーンに変換して出力することもできます。同時に、SimpleDateFormat は同時シナリオでのスレッドの安全性を保証できないため、開発者はその安全性を確保する必要があるとも述べられています。

主な方法は、ローカル変数に変更する、synchronized を使用してロックする、Threadlocal を使用してスレッドごとに個別のスレッドを作成する、などです。

この記事を通じて、SimpleDateFormat をより便利に使用できるようになれば幸いです。

おすすめ

転載: blog.csdn.net/zy_dreamer/article/details/132307231