Movatterモバイル変換


[0]ホーム

URL:


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;
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.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());

java.text.DateFormat

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());//正常終了ならエラー位置は-1
pos = newParsePosition(0);date = df.parse("2008:02/16", pos);System.out.println(pos.getErrorIndex());//エラー時はエラーのあった位置(この例だと4)

Date→文字列

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約2030ms660〜900ms660〜900ms580〜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つのインスタンスを複数スレッドから呼び出すから。


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 = 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 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);}

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


[8]ページ先頭

©2009-2025 Movatter.jp