S-JIS[2008-02-16/2017-09-27] 変更履歴
Javaで日付や時刻を扱うクラスについて。→JDK1.8以降は日付時刻API(java.time)を使う。
|
|
|
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);
TimeUnitは、単位つき時間を表す列挙型。JDK1.5で追加された。[2008-07-30]
例 | 説明 | 参考 |
---|---|---|
TimeUnit.SECONDS.toMillis(秒) |
秒をミリ秒に変換する。 | Timer#schedule() |
TimeUnit.MILLISECONDS.toSeconds(ミリ秒) |
ミリ秒を秒に変換する。 | 秒単位の経過時間 |
TimeUnit.SECONDS.sleep(秒) |
秒単位でスリープする。 | Thread.sleep() |
TimeUnit.NANOSECONDS.sleep(ナノ秒) |
ナノ秒単位でスリープする。 |
Java9で、日付時刻APIのChronoUnitに変換するメソッドが追加された。[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で日時を保持するクラス。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の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());
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 = 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つのインスタンスを複数スレッドから呼び出すから。
CalendarはDateの加工・演算を行うクラス。
年・月・日・時・分・秒を個別に設定したり取得したり、その単位で加算したり減算したりすることが出来る。
(Dateはタイムゾーンを保持していないが)Calendarはタイムゾーンを保持する。[2016-05-30](→JDK1.8のZonedDateTime)
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 = 〜; 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); }