S-JIS[2008-02-16/2017-09-27] 変更履歴

Java 日付時刻・カレンダー

Javaで日付や時刻を扱うクラスについて。→JDK1.8以降は日付時刻API(java.time)を使う。


java.lang.System

Systemクラスに、現在時刻(UTC)に当たる値をlong型の整数で返すメソッドがある。[2008-07-05]

メソッド名 説明
currentTimeMillis() 単位はミリ秒。
ただし精度は10ミリ秒程度(10ミリ秒間隔の数値しか返さない。OS依存であり、WindowsXPだと16ミリ秒くらいな気がする)。
nanoTime() 単位はナノ秒。JDK1.5以降。
時間間隔(経過時間)を測るのにしか使えない。
	long s = System.nanoTime();
	〜
	long e = System.nanoTime();
	System.out.printf("経過時間:%dナノ秒%n", e - s);

java.util.concurrent.TimeUnit

TimeUnitは、単位つき時間を表す列挙型。JDK1.5で追加された。[2008-07-30]

説明 参考
TimeUnit.SECONDS.toMillis(秒) 秒をミリ秒に変換する。 Timer#schedule()
TimeUnit.MILLISECONDS.toSeconds(ミリ秒) ミリ秒を秒に変換する。 秒単位の経過時間
TimeUnit.SECONDS.sleep(秒) 秒単位でスリープする。 Thread.sleep()
TimeUnit.NANOSECONDS.sleep(ナノ秒) ナノ秒単位でスリープする。

Java9で、日付時刻APIChronoUnitに変換するメソッドが追加された。[2017-09-27]

import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
	ChronoUnit seconds = TimeUnit.SECONDS.toChronoUnit(); // ChronoUnit.SECONDS
	TimeUnit seconds = TimeUnit.of(ChronoUnit.SECONDS); // TimeUnit.SECONDS

java.util.Date

java.util.Dateは、Javaで日時を保持するクラス。Date(日付)という名前だけど、時刻も保持できる。タイムゾーンは保持されない(UTCである)。 (→JDK1.8のLocalDateTime[/2016-05-30]

import java.util.Date;
	Date now = new Date();	//現在日時でDateを作成
	Date now = new Date(System.currentTimeMillis()); //←実態はこれ

Dateクラスには年月日や年月日時分秒を引数にとるコンストラクターもある(あった)が、現在はdeprecatedになっており、CalendarクラスDateFormatクラスを使って特定の日時を作成するのが推奨されている。

また、年・月・日・時刻を直接指定したり取得したりするメソッドもある(あった)が、現在はCalendarクラスを使うことが推奨されている。


toString()で、保持している日時を文字列にして出力することが出来る。
ただし出力される内容は、日本人から見るとあまり見やすくない。整形はDateFormatクラスを使う。
しかしちょっとしたデバッグで出力したいなら、toString()で充分。

	System.out.println(now);
Sat Feb 16 01:48:45 JST 2008

java.util.Date自身はタイムゾーンを保持していない(UTCである)。[2016-05-30]
しかし、toString()ではデフォルトタイムゾーンを取得して、そのタイムゾーンの値が出力される。(上記の例でJSTと表示されているのは、日本のパソコンで実行したから)
非推奨にはなっているが、toGMTString()を使うと、GMT(=UTC)の値が出力される。


java.sql.Date

java.sql.DateはJavaのDB関連の標準クラスなので、DBを扱っているとよく出てくる。
java.sql.Dateはjava.util.Dateを継承しているので、java.util.Dateとして扱うことも出来る。

クラス名はDateなのでjava.util.Dateとまぎらわしいので要注意。
特にどちらをimportしているのかをよく気にする必要がある。(2つ両方を同時にimportすることは出来ない

import java.sql.Date;
	Date sqlNow = new Date(System.currentTimeMillis());
	java.util.Date utilDate = sqlNow; //java.util.Dateから派生しているので、キャストも無しでも代入できる
	Date sqlDate = new Date(utilDate.getTime());

java.text.DateFormat

DateFormatは日付の書式を扱うクラス。
Dateを整形して文字列に変換したり、文字列からDateインスタンスを作ったり出来る。 (→JDK1.8のDateTimeFormatter

具体的にはSimpleDateFormat書式文字を指定してインスタンスを作り、parse()format()を使って変換する。

文字列→Date(エラー時はParseExceptionが発生):

	DateFormat df = new SimpleDateFormat("yyyy/MM/dd");
	Date date = df.parse("2008/02/16");
//	Date date = df.parse("2008/2/1");	//これくらいならエラーにならず正常に変換される
//	Date date = df.parse("2008:02:16");	//ParseExceptionが発生する

DateFormat#setLenient(false)を呼ぶと厳密にマッチするかチェックされるようになり、曖昧な状態はエラーになる。[2009-02-04]

文字列→Date(エラー時はエラー位置を返す):

	DateFormat df = new SimpleDateFormat("yyyy/MM/dd");
	ParsePosition pos = new ParsePosition(0);
	Date date = df.parse("2008/02/16", pos);
	System.out.println(pos.getErrorIndex());	//正常終了ならエラー位置は-1
	pos = new ParsePosition(0);
	date = df.parse("2008:02/16", pos);
	System.out.println(pos.getErrorIndex());	//エラー時はエラーのあった位置(この例だと4)

Date→文字列

	Date date = new Date();
	DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
	System.out.println(df.format(date));

※書式のパターン文字は、SimpleDateFormatのJavadocに詳細が載っている。


SimpleDateFormat(DateFormat)はタイムゾーンを保持している。[2016-05-30]
タイムゾーンを変えたい場合はsetTimeZone()を使う。

	Date date = new Date();
	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
	System.out.println(sdf.format(date));

Dateを文字列に変換(format)するときは、(Date自身はタイムゾーンを保持していないので)DateFormatが保持しているタイムゾーン向けに変換される。

文字列からDateに変換(parse)するときは、その文字列はDateFormatが保持しているタイムゾーンの日時として扱われる。


なお、SimpleDateFormatはMTセーフではないので、マルチスレッドで使う場合には注意。 (DateFormatに限らず、他のFormat系クラスも)
参考: AKIMOTO, HirokiさんのSimpleDateFormat はスレッドセーフではない
ちなみにここではDate#toString()も危ないと書いてあるが、もしかしてJDK1.3の話じゃなかろうか。
JDK1.4ではSimpleDateFormatを使う際にsynchronizedしてるから大丈夫なんじゃないかなぁ?
JDK1.5や1.6ではDateFormatを使わずStringBuilderを使って直接文字列を生成しているから大丈夫だろう。

ちなみにSimpleDateFormatは、synchronizedを使って同期化するのと毎回newで作るのとでは、同期化した方が効率がいいっぽい。[/2008-05-12]
以下のようなスレッドを複数並行で実行し、時間を計測してみた。(WindowsXP、JDK1.6)

並行数 バッチパターン1 バッチパターン2 バッチパターン3 バッチパターン2’ バッチパターン1’
  全体で共通のDateFormatを生成
同期あり(同期が必要)
ループ内で毎回DateFormatを生成
同期なし(同期は不要)
ThreadLocalでDateFormatを生成
同期なし(同期は不要)[2008-07-10]
ループ外でDateFormatを生成
同期なし(同期は不要)
スレッド専用のDateFormatを生成
同期なし(同期は不要)
 
class Worker1 extends Thread {




  private static final DateFormat df
    = new SimpleDateFormat("yyyy/MM/dd");



  @Override
  public void run() {


    for (int i = 0; i < 10000; i++) {


      synchronized (df) {
        try {
          df.parse("2008/04/26");
        } catch (ParseException e) {
          e.printStackTrace();
        }
      }

    }
  }

}
class Worker2 extends Thread {









  @Override
  public void run() {


    for (int i = 0; i < 10000; i++) {
      DateFormat df
        = new SimpleDateFormat("yyyy/MM/dd");

      try {
        df.parse("2008/04/26");
      } catch (ParseException e) {
        e.printStackTrace();
      }


    }
  }

}
class Worker3 extends Thread {

  private static final ThreadLocal<DateFormat>
    dflocal = new ThreadLocal<DateFormat>() {
      @Override
      protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy/MM/dd");
      }
    };

  @Override
  public void run() {


    for (int i = 0; i < 10000; i++) {
      DateFormat df = dflocal.get();


      try {
        df.parse("2008/04/26");
      } catch (ParseException e) {
        e.printStackTrace();
      }


    }
  }

} 
class Worker2_ extends Thread {









  @Override
  public void run() {
    DateFormat df
      = new SimpleDateFormat("yyyy/MM/dd");
    for (int i = 0; i < 10000; i++) {



      try {
        df.parse("2008/04/26");
      } catch (ParseException e) {
        e.printStackTrace();
      }


    }
  }

}
class Worker1_ extends Thread {




  private final DateFormat df
    = new SimpleDateFormat("yyyy/MM/dd");



  @Override
  public void run() {


    for (int i = 0; i < 10000; i++) {



        try {
          df.parse("2008/04/26");
        } catch (ParseException e) {
          e.printStackTrace();
        }


    }
  }

}
2 約310ms 約600ms 約260ms 約260ms 約210ms
3 約390ms 約750ms 約300ms 約300ms 約250ms
10 約1060ms 約2030ms 660〜900ms 660〜900ms 580〜600ms

時間測定は、start()してからjoin()が終わるまでの時間。ワーカースレッドをインスタンス化する時間は含まない。
つまりバッチパターン1やバッチパターン1’でDateFormatをインスタンス化する時間は時間計測に含まれていない。

大量に同時処理が走れば、パターン1では排他されて待ちが発生するのに対し、パターン2ではそれは無いはず。なのに、パターン1の方が早い。
つまり排他処理(synchronized)SimpleDateFormatインスタンス生成ほどの負荷はかかっていないと考えられる。
(SimpleDateFormatのコンストラクターは デフォルトロカール取得等の色々な処理を行っているので、けっこう重いようだ)
(synchronizedブロックの中の処理に時間がかかるようなら 結果も入れ替わるかもしれないが、少なくともSimpleDateFormat#parse()はそんなに重くは無いようだ)
new SimpleDateFormat()にかかる時間とsynchronizedにかかる時間の比較

パターン2’は、パターン2に対し、スレッドのループの前に一度だけスレッド毎のSimpleDateFormatを生成し、そのスレッド内ではそれを使い回す(したがって排他が不要な)パターン。
SimpleDateFormatの生成は一度しか行わないし、同期をとる必要も無いので最も高速。(パターン1’はクラス生成時の初期化でSimpleDateFormatをインスタンス化しており、計測時間にそれが含まれていないだけで、理屈は同じ)
パターン3は、ThreadLocalクラスを使った、スレッド毎にインスタンスを保有する仕組み。初期化はスレッド毎に一度しか行われない。すなわち、動作としてはパターン2’と同等であり、実行速度もほとんど同じ!
バッチではThreadLocalを使わなくてもスレッド毎に変数を保持できる(パターン1’)が、ウェブ(サーブレット)ならとても有効だろう。[2008-07-10]

ウェブ(サーブレット)ではループするという処理はあまり無いと思うので、以下のようなパターンになると思う。
(バッチに例えれば、下記のdoGet()がループで呼ばれるイメージ)

ウェブパターン1 ウェブパターン2 ウェブパターン3
全体で共通のDateFormatを生成
同期あり(同期が必要)
処理内でDateFormatを生成
同期なし(同期は不要)
ThreadLocalでDateFormatを生成
同期なし(同期は不要)[2008-07-10]
class Servlet1 extends HttpServlet {




  private final DateFormat df
    = new SimpleDateFormat("yyyy/MM/dd");



  @Override
  protected void doGet(〜) {


    synchronized (df) {
      try {
        df.parse("2008/04/26");
      } catch (ParseException e) {
        e.printStackTrace();
      }
    }
  }

}
class Servlet2 extends HttpServlet {









  @Override
  protected void doGet(〜) {
    DateFormat df
      = new SimpleDateFormat("yyyy/MM/dd");

    try {
      df.parse("2008/04/26");
    } catch (ParseException e) {
      e.printStackTrace();
    }

  }

}
class Servlet3 extends HttpServlet {

  private final ThreadLocal<DateFormat>
    dflocal = new ThreadLocal<DateFormat>() {
      @Override
      protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy/MM/dd");
      }
    };

  @Override
  protected void doGet(〜) {

    DateFormat df = dflocal.get();

    try {
      df.parse("2008/04/26");
    } catch (ParseException e) {
      e.printStackTrace();
    }

  }

} 
バッチパターン1に相当 バッチパターン2に相当 バッチパターン3に相当

該当するバッチパターンの実行時間を考えれば、ウェブパターン3が最も実行効率が良いと思われる。次いでパターン1。[/2008-07-10]
(ウェブパターン1や3ならDateFormatインスタンスが破棄されないのでGCにも影響しないし)

ウェブパターン1のdfは、バッチと違ってstatic変数ではないが、排他する必要がある。なぜなら、サーブレットでは1つのインスタンスを複数スレッドから呼び出すから。


java.util.Calendar

CalendarDateの加工・演算を行うクラス。
年・月・日・時・分・秒を個別に設定したり取得したり、その単位で加算したり減算したりすることが出来る。
(Dateはタイムゾーンを保持していないが)Calendarはタイムゾーンを保持する。[2016-05-30](→JDK1.8のZonedDateTime


Calendarインスタンスの生成

Calendarは抽象クラスなので、インスタンスを生成するにはgetInstance()を呼び出す。

	Calendar cal = Calendar.getInstance();	//現在日時を保持したカレンダー

具体的には、Calendar#getInstance()はデフォルトではGregorianCalendarインスタンスを返す。

ただしJDK1.6では、デフォルトロカールが日本になっている場合はJapaneseImperialCalendarを返す。これは和暦を扱えるらしい。
このクラスはpublicじゃないのでJavadocは生成されてないみたいだけど、ソースを見るとautherは日本人っぽい。 さすが。
でもデフォルトのデフォルトロカールはJapaneseImperialCalendarを返すような条件になっていないので、一番簡単なのはロカールを自分で指定してやることかな。

//×	Locale loc = Locale.JAPAN;
//×	Locale loc = Locale.JAPANESE;
	Locale loc = new Locale("ja", "JP", "JP");
	cal = Calendar.getInstance(loc);
	System.out.println(cal.getClass());

GregorianCalendarのコンストラクターを直接呼び出してインスタンスを生成することも出来る。

	Calendar cal = new GregorianCalendar(year, month - 1, day);
	Calendar cal = new GregorianCalendar(year, month - 1, day, hour, minute, second);
	Calendar cal = new GregorianCalendar(year, month - 1, day, hour, minute, second, msec);

タイムゾーンを指定するには以下のようにする。[2016-05-30]

	Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Asia/Tokyo"));

Dateとの変換

Dateとの変換は以下の様にする。

	Date date = 〜;
	cal.setTime(date);
	Date date = cal.getTime();

DateクラスなのにsetTime()・getTime()とは、これ如何に?

なお、ミリ秒を表す整数(long型)を介して扱うことも出来る。

	cal.setTimeInMillis(date.getTime());
	cal.setTimeInMillis(System.currentTimeMillis());
	Date date = new Date(cal.getTimeInMillis());

※Calendarはタイムゾーンを保持しているが、Dateは保持していない(UTC扱い)。ミリ秒のlong値もUTC相当。これらを扱うときは、Calendar内のタイムゾーンに/から変換される。[2016-05-30]


カレンダーの設定・取得

Calendarの個々の要素(年月日時分秒)だけを書き換えたり取得したりすることが出来る。

	int year = cal.get(Calendar.YEAR);
	cal.set(Calendar.YEAR, year);

指定する定数

ただし月(MONTH)だけは、何故か0〜11で扱うようになっている。(0が1月、11が12月)

	int month = cal.get(Calendar.MONTH) + 1;
	cal.set(Calendar.MONTH, month - 1);

※値の設定/取得(特にDATE(DAY_OF_MONTH)やHOUR(HOUR_OF_DAY))では、タイムゾーンが考慮される。[2016-05-30]


ある程度まとめてセットするメソッドもある。

	cal.set(year, month - 1, day, hour, min, sec);

月末日・月初日

ある月の月末日を取得するには、getActualMaximum()メソッドを使う。[2008-02-17]

	Calendar cal = new GregorianCalendar(2008, 2 - 1, 15);
	int max = cal.getActualMaximum(Calendar.DATE);
	cal.set(Calendar.DATE, max);

getMaximum()というメソッドもあるが、これはその暦で取りうる最大の値を返す。
すなわちグレゴリオ暦(GregorianCalendar)ではgetMaximum(DATE)は常に31を返す。
getActualMaximum()はうるう年の2月なら29、そうでない2月なら28、小の月は30を返してくれる。

同様にgetActualMinimum()やgetMinimum()というメソッドもあるが、こちらは日の場合は常に1を返すので、わざわざ使わなくてもいいと思う。

ちなみにgetMaximum(YEAR)の値は292278994(2億9千万)だった。
つまりこのJREを使い続ける場合、西暦2億9千万年問題が発生するわけだ(爆)


カレンダーの増減

Calendarの個々の要素(年月日時分秒)を基準に、値を増減させることが出来る。

月を基準に増減させるのは、以下のようになる。

	cal.add(Calendar.MONTH, +1);	//1ヶ月増やす
	cal.add(Calendar.MONTH, -1);	//1ヶ月減らす

12月から1ヶ月進めると年が1つ増え、月は1月になる。
逆に1月から1ヶ月戻すと年が1つ減り、月は12月になる。

add()では、基準にしたカレンダーフィールドより小さいフィールドは基本的に変わらない。
つまり月を基準にした場合、基本的に日(やそれ以下の時分秒)は変わらない。
ただし増減した結果、存在しない日になった場合(つまり5/31が4月になるような場合)、存在する末日になる(5/31→4/30)。

//3月から月を-1した例(2008年はうるう年)
2008-03-27 → 2008-02-27
2008-03-28 → 2008-02-28
2008-03-29 → 2008-02-29
2008-03-30 → 2008-02-29
2008-03-31 → 2008-02-29

add()でなくroll()を使うと、基準にしたカレンダーフィールドより大きいフィールドも変わらない。
例えば月を基準にした場合、12月から1増やすと、年は変わらずに1月に変わる。日についてはadd()と同じ。

	cal.roll(Calendar.MONTH, true);	//月だけ1ヶ月増やす
	cal.roll(Calendar.MONTH, false);	//月だけ1ヶ月減らす
	cal.roll(Calendar.MONTH, +1);	//月だけ1ヶ月増やす
	cal.roll(Calendar.MONTH, -1);	//月だけ1ヶ月減らす

しかしGregorianCalendar#roll()は、うるう年に関してバグがある。
Bug ID: 5014535 - incorrect rolling from leap-years

2/29(うるう年)から“うるう年でない年”へYEAR単位でroll()すると、本来は2/28になるべきなのに3/1になってしまう。

	Calendar cal = new GregorianCalendar(2008, 2 - 1, 29);
	System.out.println(cal.getTime());
	cal.roll(Calendar.YEAR, true);
	System.out.println(cal.getTime());
Fri Feb 29 00:00:00 JST 2008
Sun Mar 01 00:00:00 JST 2009

ちなみにadd(YEAR)はバグっていない。

	Calendar cal = new GregorianCalendar(2008, 2 - 1, 29);
	System.out.println(cal.getTime());
	cal.add(Calendar.YEAR, 1);
	System.out.println(cal.getTime());
Fri Feb 29 00:00:00 JST 2008
Sat Feb 28 00:00:00 JST 2009

これは2004年(JRE1.4.2)に報告されたバグで、試してみるとJRE1.4.0も同じだし、JRE1.5も1.6も直っていなかった。放置されてるんだろうか。
でも確かにroll(YEAR)なんて使わない(add(YEAR)で代替可能、というよりadd()を使うのが素直だよな)ので、どーでもいいような気がしてきた(苦笑)


タイムゾーン

Date#toString()で表示される時刻は、日本のPCで実行した場合は日本時間で表示されると思う。[2010-05-27]
これは、デフォルトのタイムゾーンが日本になっている為。
タイムゾーンを変えてやると、表示内容も変わる。 (Date自身はタイムゾーンを保持していないが、Date#toString()はデフォルトタイムゾーンを使っている。[2016-05-30]

→JDK1.8のZoneId


現在のタイムゾーンの確認方法

import java.util.TimeZone;
	TimeZone defaultZone = TimeZone.getDefault();
	System.out.println(defaultZone);
	System.out.println(defaultZone.getRawOffset() / (1000 * 60 * 60));

↓実行例

sun.util.calendar.ZoneInfo[id="Asia/Tokyo",offset=32400000,dstSavings=0,useDaylight=false,transitions=10,lastRule=null]
9

オフセットの32400000ミリ秒は、9時間。


デフォルトタイムゾーンの設定方法

	Date date = new Date();	//現在日時
	System.out.println("デフォルト:" + date);

	TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
	System.out.println("アメリカ :" + date);

	TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
	System.out.println("GMT    :" + date);

	TimeZone.setDefault(defaultZone);
	System.out.println("元に戻した:" + date);

↓実行例

デフォルト:Fri May 28 00:55:36 JST 2010
アメリカ :Thu May 27 08:55:36 PDT 2010
GMT    :Thu May 27 15:55:36 GMT 2010
元に戻した:Fri May 28 00:55:36 JST 2010

Date#toString()内部でデフォルトのタイムゾーンを参照している為、同一のDateインスタンスでもtoString()の出力表現が変わってくる。
(Dateインスタンス内部で保持している時刻(基準時点からの経過時間)は変わるわけではない)


また、JavaVM起動時のVM引数に以下のように指定することで、デフォルトのタイムゾーンを設定することが出来る。

> java -Duser.timezone=America/Los_Angeles jp.hishidama.example.TimeZoneExample

タイムゾーンに指定できるIDの一覧は、以下のようにして取得する。

	String[] ids = TimeZone.getAvailableIDs();
	for (String id : ids) {
		System.out.println(id);
	}

Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま