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;
Datenow = 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;
DatesqlNow = newDate(System.currentTimeMillis());
java.util.DateutilDate =sqlNow;//java.util.Dateから派生しているので、キャストも無しでも代入できるDate sqlDate = newDate(utilDate.getTime());
DateFormatは日付の書式を扱うクラス。
Dateを整形して文字列に変換したり、文字列からDateインスタンスを作ったり出来る。(→JDK1.8のDateTimeFormatter)
具体的にはSimpleDateFormatに書式文字を指定してインスタンスを作り、parse()やformat()を使って変換する。
文字列→Date(エラー時はParseExceptionが発生):
DateFormat df = newSimpleDateFormat("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 = newSimpleDateFormat("yyyy/MM/dd");ParsePosition pos = newParsePosition(0);Date date = df.parse("2008/02/16", pos);System.out.println(pos.getErrorIndex());//正常終了ならエラー位置は-1pos = newParsePosition(0);date = df.parse("2008:02/16", pos);System.out.println(pos.getErrorIndex());//エラー時はエラーのあった位置(この例だと4)Date date = new Date();DateFormat df = newSimpleDateFormat("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 = newSimpleDateFormat("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 extendsThread { privatestatic final DateFormatdf = 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 extendsThread { @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 extendsThread { privatestatic finalThreadLocal<DateFormat>dflocal = newThreadLocal<DateFormat>() { @Override protectedDateFormatinitialValue() { returnnew 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_ extendsThread { @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_ extendsThread { 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 extendsHttpServlet { private final DateFormatdf = new SimpleDateFormat("yyyy/MM/dd"); @Override protected void doGet(〜) {synchronized (df) { try { df.parse("2008/04/26"); } catch (ParseException e) { e.printStackTrace(); }} }} | class Servlet2 extendsHttpServlet { @Override protected void doGet(〜) { DateFormat df = new SimpleDateFormat("yyyy/MM/dd"); try { df.parse("2008/04/26"); } catch (ParseException e) { e.printStackTrace(); } }} | class Servlet3 extendsHttpServlet { private finalThreadLocal<DateFormat>dflocal = newThreadLocal<DateFormat>() { @Override protectedDateFormatinitialValue() { returnnew 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 = newLocale("ja", "JP", "JP");cal = Calendar.getInstance(loc);System.out.println(cal.getClass());GregorianCalendarのコンストラクターを直接呼び出してインスタンスを生成することも出来る。
Calendar cal = newGregorianCalendar(year, month - 1, day);Calendar cal = newGregorianCalendar(year, month - 1, day, hour, minute, second);Calendar cal = newGregorianCalendar(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-272008-03-28 → 2008-02-282008-03-29 → 2008-02-292008-03-30 → 2008-02-292008-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 2008SunMar 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 2008SatFeb 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 = newDate();//現在日時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 2010GMT :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_Angelesjp.hishidama.example.TimeZoneExampleタイムゾーンに指定できるIDの一覧は、以下のようにして取得する。
String[] ids = TimeZone.getAvailableIDs();for (String id : ids) {System.out.println(id);}