一篇文章概括 Java Date Time 的使用

本文目的:掌握 Java 中日期和時間常用 API 的使用。
參考:Jakob Jenkov的英文教程Java Date Time TutorialJavaDoc

概覽

Java 8 新增 API

Java 8 部分新類 描述
Instant 表示時間線上的某一瞬間,用秒和納秒錶示。
Duration 表示時間差,用秒和納秒錶示。
LocalDate 表示沒有時區信息的日期,例如生日、法定假日等。
LocalTime 表示沒有時區信息的一天中的本地時間。
LocalDateTime 表示沒有時區信息的日期和時間
ZonedDateTime 表示日期和時間,包括時區信息
DateTimeFormatter 將日期時間對象格式化爲字符串。

衆所周知,在 Java 8 中添加了一個全新的日期時間 API 位於 java.time 包中,主要變化是,自1970年1月1日以來,日期和時間現在不再由單個毫秒數表示,而是由1970年1月1日以來的秒數和納秒數表示。
秒數既可以是正的,也可以是負的,用 long 表示。納秒數始終爲正,由 int 表示。

Java 7 具有以下日期和時間類和方法:

Java 7 日期時間常用類/方法 描述
System.currentTimeMillis() 自1970年1月1日起以毫秒爲單位返回當前日期和時間的靜態方法
java.util.Date 表示日期和時間的類。這個類中的大多數方法都是不推薦的。
java.sql.Date 表示日期的類。這個date類與JDBC一起使用。
java.sql.Timestamp 表示日期和時間的類。這個date和time類與JDBC一起使用。
java.util.Calendar 日曆類的基類。 有方法做日期和時間算術,比如將日期或月份添加到另一個日期。
java.util.GregorianCalendar 一個 Calendar 類的子類。代表公曆,在今天的西方世界大部分地區使用。擁有 Calendar 中的所有做日期和時間算術的方法。
java.util.TimeZone 一個表示時區的類,在跨時區執行日曆計算時非常有用。

應該使用所有這些類中的哪一個取決於想要做什麼,如果你需要做簡單的計時, System.currentTimeMillis() 方法就可以了。

  • 如果只需要一個對象來保存日期,例如作爲簡單域模型對象中的屬性,則可以使用 java.util.Date 類。
  • 如果需要讀取和寫入數據庫的日期和時間,則使用 java.sql.Date 和 java.sql.Timestamp 類。
  • 如果您需要進行日期計算,例如將日期或月份添加到另一個日期,或者查看工作日(星期一,星期二等)這些給定日期,或者轉換時區之間的日期和時間,請使用 java.util.Calendar 和 java .util.GregorianCalendar 類。

System.currentTimeMillis()

currenttimemillis() 靜態方法以毫秒爲單位返回自1970年1月1日以來的時間。返回的值是long。這裏有一個例子:

long timeNow = System.currentTimeMillis();

這個返回值可以用來初始化 java.util.Date, java.sql.Date, java.sql.Timestamp 和 java.util.GregorianCalendar 對象,它還可以用於在程序中測量時間。

currenttimemillis() 方法的粒度大於 1 毫秒,這取決於操作系統,還可能更大,許多操作系統以幾十毫秒爲單位測量時間。如果需要更精確的計時,請使用 System.nanoTime() ,但是這個方法返回的時間是從任意一個時刻計算的,甚至有可能是負數,所以不能用於初始化日期時間對象,只適合用於計算兩個時間點的時間差。

java.util.Date

用來表示日期,包含年月日時分秒 ,目前該類中的大多數方法都不贊成使用了,一般用 Calendar 類來代替它,但還是有必要簡單瞭解一下。
下面是一些使用例子:

Date dateNow = new Date(); // 使用當前日期和時間創建

Date 類的默認構造器,源碼是這樣的:

public Date() {
        this(System.currentTimeMillis());
}

也可以使用一個 long 型的有參構造函數:

Date date = new Date(long);

Date 類還有一個 getTime() 實例方法,這個方法的返回值就是 new Date(long) 時指定的 long 參數。

從 Java 8 開始,新增了和 Instant 互相轉換的方法,關於 Instant 請參考本文下部分,這裏瞭解就行:

static Date from(Instant instant);
Instant toInstant();

java.sql.Date

此類是上述 java.util.Date 類的子類,所以它繼承了 java.util.Date 的所有方法和字段。一般在 JDBC API 中使用它,比如可以在 PreparedStatement 上設置日期,或者從 ResultSet 獲取日期,

和 java.util.Date 最大的區別就是它只記日期,不記時間,即只有年月日,如果構造的時候包含了時間信息,那麼時間信息會被捨棄,如果要記時間,需要用到 java.sql.Timestamp 類。

java.sql.Timestamp

此類也繼承了 java.util.Date,包含的信息有年月日時分秒納秒,是的,它還擴展了納秒,一個使用示例如下:

long time = System.currentTimeMillis();
java.sql.Timestamp timestamp = new java.sql.Timestamp(time);

timestamp.setNanos(123456);
int nanos = timestamp.getNanos(); // nanos = 123456

java.util.Calendar 和 GregorianCalendar

Calendar 抽象類用於執行日期和時間換算,無法使用構造器實例化它,原因是世界上有不止一個日曆。
但是其提供了一個 getInstance() 方法,可以獲取對應當前時間的 Calendar 對象:

Calendar rightNow = Calendar.getInstance();

getInstance() 方法底層是如下這樣實現的:

public static Calendar getInstance() {
        return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}

沒錯,很容易想到,此方法還有重載的,可以提供部分指定初始化參數的版本,如下:

getInstance(TimeZone zone);
getInstance(Locale aLocale);
getInstance(TimeZone zone, Locale aLocale);

此外,一般可以通過其子類 GregorianCalendar 來訪問日期時間信息,一個例子如下:

Calendar calendar = new GregorianCalendar();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH); 
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); // 一月 Jan = 0, 不是 1
int dayOfWeek  = calendar.get(Calendar.DAY_OF_WEEK);
int weekOfYear = calendar.get(Calendar.WEEK_OF_YEAR);
int weekOfMonth= calendar.get(Calendar.WEEK_OF_MONTH);

int hour = calendar.get(Calendar.HOUR);        // 12 小時制
int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY); // 24 小時制
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int millisecond= calendar.get(Calendar.MILLISECOND);

calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 11); // 11 = december,十二月
calendar.set(Calendar.DAY_OF_MONTH, 24); // 聖誕節

年月日等的加減

Calendar calendar = new GregorianCalendar();
// 加 1 天
calendar.add(Calendar.DAY_OF_MONTH, 1);
// 當第二個參數爲負數時,表示減,下面就是減 1 天
calendar.add(Calendar.DAY_OF_MONTH, -1);

Calendar/Date/String 的互相轉換

// Calendar to Date
Calendar calendar = Calendar.getInstance();
java.util.Date date = calendar.getTime();

// Date to Calendar
calendar.setTime(new java.util.Date());

// Calendar to String
Calendar calendat = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = sdf.format(calendar.getTime());

// String to Calendar
String str = "2018-12-3";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse(str);
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);

// Date to String
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = sdf.format(new Date());

// String to Date
String str = "2018-12-3";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date dateParse = sdf.parse(str);

Calendar 容易犯錯的地方

  1. Calendar 類的 MONTH 字段不是往常的從 1 到 12 。而是從 0 到 11 ,其中 0 是一月,11 是十二月。
  2. 一週的某一天是從 1 到 7 表示,這點不出意料,但是一週的第一天是星期日而不是星期一,這意味着 1 =星期日,2 =星期一,7 =星期六。
  3. 如果需要進行復雜的日期和時間計算,最好在官方JavaDoc中閱讀java.util.Calendar的類文檔。 類文檔包含有關類的特定行爲的更多詳細信息。 例如,如果將日期設置爲 2018 年 1 月 34 日,那麼實際日期是什麼?

java.util.TimeZone

TimeZone 是一個表示時區的類,在跨時區執行日曆計算時非常有用,一般和 Calendar 一起使用。

注意:在 Java 8 日期時間 API 中,時區由 java.time.ZoneId 類表示。 如果使用的是 Java 8 日期時間類(如 ZonedDateTime 類)的話,則只需要使用 ZoneId 類就行了。 如果使用的是 Calendar (來自Java 7和更早的日期時間API),那麼仍然可以使用 java.util.TimeZone 類。

從 Calendar 中獲取TimeZone

Calendar calendar = new GregorianCalendar();
// 從 Calendar 獲取時區
TimeZone timeZone = calendar.getTimeZone();

// 爲 Calendar 設置時區
calendar.setTimeZone(timeZone);

創建 TimeZone 對象

// 獲取默認時區對象
TimeZone timeZone = TimeZone.getDefault();
// 獲取指定時區對象
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone timeZone = TimeZone.getTimeZone("Europe/Copenhagen");

TimeZone.getTimeZone() 方法的參數可以是一個 zone ID ,可以查看 JavaDoc 獲取全部 ID 。

注意:如果 getTimeZone(String zoneID);方法的 zoneID 設置錯誤(不匹配系統支持的任意值),比如 “Asiannn/Shanghai”,那也不會拋出任何異常,而是默默地設置 zoneID 爲 GMT0 ,即格林威治時間。

時區的名稱、ID和偏移量

我們可以查看給定時區的顯示名稱、ID和時間偏移量,如下所示

TimeZone timeZone = TimeZone.getDefault();
System.out.println(timeZone.getDisplayName());
System.out.println(timeZone.getID());
System.out.println(timeZone.getOffset(System.currentTimeMillis()));

以上代碼將輸出:

中國標準時間
Asia/Shanghai
28800000

getOffset() 方法以 int 類型返回該時區在指定日期的 UTC 偏移量(毫秒)。上例中的 28800000 毫秒,也就是 8 h ,我們在東八區(+8)。

在時區之間轉換

TimeZone timeZoneCN = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone timeZone0 = TimeZone.getTimeZone("Etc/GMT0");

Calendar calendar = new GregorianCalendar();

calendar.setTimeZone(timeZoneCN);
long timeCN = calendar.getTimeInMillis();
System.out.println(calendar.getTimeZone().getDisplayName());
System.out.println("timeCN = " + timeCN);
System.out.println("hour = " + calendar.get(Calendar.HOUR_OF_DAY));

calendar.setTimeZone(timeZone0);
System.out.println(calendar.getTimeZone().getDisplayName());
long time0 = calendar.getTimeInMillis();
System.out.println("time0 = " + time0);
System.out.println("hour = " + calendar.get(Calendar.HOUR_OF_DAY));

以上程序將會輸出如下:

中國標準時間
timeCN = 1543850448183
hour = 23
格林威治時間
time0 = 1543850448183
hour = 15

可以看到,以毫秒爲單位的時間在兩個時區是相同的,但是已從23點變成15點鐘了,因爲中國標準時間比格林威治時間快 8 小時,如此,我們設置不同時區獲取對應時區的正確時間,這樣就實現的換算的目的。

使用 SimpleDateFormat 解析和格式化日期

java.text.SimpleDateFormat 類可以解析字符串中的日期,也可以格式化字符串中的日期,本文將展示幾個例子:

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

String dateString = format.format(new Date());
Date date = format.parse ("2018-12-03");  

作爲參數傳遞給 SimpleDateFormat 類的字符串是一種模式(模板),用於說明如何解析和格式化日期。 在上面的示例中使用了模式“yyyy-MM-dd”,表示年份 4 位數(yyyy),月份 2 位數(MM)和日期 2 位數(dd)的表示形式,"2018-12-03"中使用‘-’分割是因爲在模式中也是用‘-’分割字母的。

以下是常見模式字母列表,具體請看 JavaDoc :

y   = year   (yy or yyyy)
M   = month  (MM)
d   = day in month (dd)
h   = hour (0-12)  (hh)
H   = hour (0-23)  (HH)
m   = minute in hour (mm)
s   = seconds (ss)
S   = milliseconds (SSS)
z   = time zone  text        (e.g. Pacific Standard Time...)
Z   = time zone, time offset (e.g. -0800)

//一下是一些示例:
yyyy-MM-dd HH:mm:ss  (2018-12-3 23:59:59)
HH:mm:ss.SSS (23:59.59.999)
yyyy-MM-dd HH:mm:ss.SSS   (2009-12-31 23:59:59.999)
yyyy-MM-dd HH:mm:ss.SSS Z   (2009-12-31 23:59:59.999 +0100)       

如果指定 “dd” 來解析new SimpleDateFormat("yyyy-MM-dd");那麼天數肯定被表示爲 2 位,比如 3 號就是 03。
如果是指定 “d” 來解析new SimpleDateFormat("yyyy-MM-d"); 那麼天數優先是 1 位,比如 3 號就是 3, 如果超出 1 位,那會自動擴展爲 2 位,比如 13 號,那麼就是 13 。

Instant 表示某一瞬間

Java .time.Instant 類表示時間線上的一個特定時刻,被定義爲自原點起的偏移量,原點是1970年1月1日00點格林,也就是格林尼治時間。 時間以每天 86400 秒爲單位,從原點向前移動。

Java.time 這個包是線程安全的,並且和其他大部分類一樣,是不可變類。Instant 也不例外。

使用 Instant 類的工廠方法之一創建實例。例如,要創建一個表示當前時刻的時間點,可以調用 instance .now() ,如下所示:

Instant now = Instant.now();

Instant 對象包含秒和納秒,來表示其包含的時間, 自紀元以來的秒數是上完提到的自原點以來的秒數。 納秒是 Instant 的一部分,不到一秒鐘。分別可以通過如下 2 個方法獲取:

long getEpochSecond();
int getNano();

Instant 運算

Instant類還有幾種方法可用於相對於Instant進行計算。 這些方法中的一些(不是全部)是:

  • plusSeconds()
  • plusMillis()
  • plusNanos()
  • minusSeconds()
  • minusMillis()
  • minusNanos()

一個例子如下:

Instant now = Instant.now(); // 現在這一瞬間

Instant later = now.plusSeconds(3); // 3 秒後的瞬間
Instant earlier = now.minusSeconds(3); // 3 秒前的瞬間

因爲 Instant 是不可變的,所以上面的計算方法,是返回一個代表計算結果的新的 Instant 對象。

Duration 表示時間間隔

java.time.Duration 表示兩個 Instant 之間的一段時間,Duration 實例是不可變的,因此一旦創建它,就不能更改它的值。但可以基於一個 Duration 對象創建新的 Duration 對象。

創建 Duration 對象

可以使用 Duration 類的工廠方法之一創建 Duration 對象,有 between()/ofDays()/ofSeconds()/from() 等方法,但其底層都是調用了同一個構造方法,其源碼如下:

 private Duration(long seconds, int nanos) {
        super();
        this.seconds = seconds;
        this.nanos = nanos;
    }

下面是一個使用 between() 方法創建的示例:

Instant first = Instant.now();
// 其他耗時操作
Instant second = Instant.now();
Duration duration = Duration.between(first, second);

訪問 Duration 對象的時間信息

從上述構造器源碼可知,Duration 在內部維護兩個值:

  • final int nanos;
  • final long seconds;

請注意沒有單獨的毫秒部分,只有納秒和秒。但可以可以將整個時間間隔 Duration 轉換爲其他時間單位,如納秒、分鐘、小時或天:

  • long toNanos()
  • long toMillis()
  • long toMinutes()
  • long toHours()
  • long toDays()

toNanos() 與 getNano() 的不同之處在於 getNano() 僅返回持續時間小於一秒的部分(即整個時間段中不到 1 秒的那部分)。 toNanos() 方法返回的是轉換爲納秒的整個時間段(即秒部分轉成納秒+納秒部分)。

沒有 toSeconds() 方法,因爲 getSeconds() 方法已經可以獲取 Duration 的秒部分。

Duration 的計算

Duration 類包含一組可用於基於 Duration 對象執行計算的方法。其中一些方法是:

  • Duration plus(Duration duration)
  • Duration plusNanos(long)
  • Duration plusMillis(long)
  • Duration plusSeconds(long)
  • Duration plusMinutes(long)
  • Duration plusHours(long)
  • Duration plusDays(long)
  • Duration minusXxx(long) 上面所有對應 minus 方法

這些方法的使用大同小異,一下是一個例子:

Duration start = ... 
Duration added = start.plusDays(3); // 加 3 天
Duration subtracted = start.minusDays(3); // 減 3 天

同樣,爲了使Duration對象保持不可變,所有計算方法都返回表示計算結果的新的 Duration 對象。

LocalDate 表示本地日期

java.time.LocalDate 表示本地日期,沒有時區信息。當地的日期可以是生日或法定假日等,與一年中的某一天有關,和一天中的某一時間無關。這個類對象也是不可變的,計算操作會返回一個新的 LocalDate 對象。
下面是一個創建 LocalDate 對象的例子:

LocalDate localDate1 = LocalDate.now();
LocalDate localDate2 = LocalDate.of(2018, 11, 11);

還有很多方法可以創建 LocalDate 對象,我列出一部分下面,具體的請查看 JavaDoc 。

訪問 LocalDate 中的日期信息

LocalDate 中一共有 3 個日期信息字段,分別是:

  • final int year;
  • final short month;
  • final short day;

對應一些獲取信息的方法:

  • int getYear()
  • Month getMonth()
  • int getDayOfMonth()
  • int getDayOfYear()
  • DayOfWeek getDayOfWeek()

LocalDate 計算

  • LocalDate plusYears(long yearsToAdd)
  • LocalDate plusMonths(long monthsToAdd)
  • LocalDate plusWeeks(long weeksToAdd)
  • LocalDate plusDays(long daysToAdd)
  • LocalDate minusXxx(long xxxToSubtract) 對應上面 plus 方法的 minus 版本

下面是一個例子:

LocalDate localDate = LocalDate.of(2018, 12, 12);

LocalDate localDate1 = localDate.plusYears(3); // 加 3 年
LocalDate localDate2 = localDate.minusYears(3);

LocalTime 表示本地時間

java.time.LocalTime 表示沒有任何時區信息的特定時間,例如,上午 10 點。同樣,這是一個不可變類。
下面是一個創建 LocalTime 對象的例子:

LocalTime localTime1 = LocalTime.now();
LocalTime localTime2 = LocalTime.of(21, 30, 59, 11001);

LocalTime 內部維護了 4 個變量維護時間信息:

  • final byte hour;
  • final byte minute;
  • final byte second;
  • final int nano;

也包含了必要的計算時間的方法,例如 LocalTime plusHours(long hoursToAdd); 其他的和 LocalDate 大同小異,就不展開講了。

LocalDateTime 表示本地日期和時間

java.time.LocalDateTime 類表示沒有任何時區信息的本地日期和時間,同樣是不可變類。

查看其源碼發現其內部就是維護了一個 LocalDate 對象和一個 LocalTime 對象來表示日期時間信息。

final LocalDate date;
final LocalTime time;

所以完全可以把它看成是 LocalDate 和 LocalTime 的結合。
下面是一個創建 LocalDateTime 對象的例子:

LocalDateTime localDateTime1 = LocalDateTime.now();
LocalDateTime localDateTime2 =LocalDateTime.of(2018, 11, 11, 10, 55, 36, 123);

上面第二行代碼使用 of() 工廠方法創建對象,其參數分別對應年月日時分秒納秒。

其他獲取日期時間信息和計算請參考 LocalDate 和 LocalTime 的。

ZonedDateTime 表示帶有時區信息的日期和時間

java.time.ZonedDateTime 可以用來代表世界上某個特定事件的開始,比如會議、火箭發射等等。
它同樣是不可變類,下面是一個創建此類對象的例子:

ZonedDateTime zonedDateTime = ZonedDateTime.now();
ZoneId zoneId = ZoneId.of("UTC+1");
ZonedDateTime zonedDateTime2 = ZonedDateTime.of(2015, 11, 30, 23, 45, 59, 1234, zoneId);

時區

時區由 ZoneId 類表示,如前面的示例所示。可以使用 ZoneId.now() 方法創建 ZoneId 對象。也可以使用 of() 方法指定時區信息,下面是一個例子:

ZoneId zoneId1 = ZoneId.of("UTC+1");
ZoneId zoneId2 = ZoneId.of("Europe/Paris");

傳遞給 of() 方法的參數是要爲其創建 ZoneId 的時區的ID。在上面的例子中,ID 是“UTC+1”,它是 UTC (格林威治)時間的偏移量。另外也可以直接指定具體的時區 ID 字符串,這在本文開頭有介紹。

ZonedDateTime 相比 LocalDateTime 只是多了地區信息,其內部維護了下面這 3 個變量來表示日期信息和地區:

  • final LocalDateTime dateTime;
  • final ZoneOffset offset;
  • final ZoneId zone;

所以其他的方法如獲取日期時間信息和計算時間,請參考上述。

DateTimeFormatter

java.time.DateTimeFormatter 類用於解析和格式化用 Java 8 日期時間 API 中的類表示的日期。

預定義 DateTimeFormatter 對象

DateTimeFormatter 類包含一組預定義的(常量)實例,這些實例可以解析和格式化來自標準日期格式的日期。這省去了爲 DateTimeFormatter 定義日期格式的麻煩。包含的部分預定義實例如下:

BASIC_ISO_DATE

ISO_LOCAL_DATE
ISO_LOCAL_TIME
ISO_LOCAL_DATE_TIME

ISO_OFFSET_DATE

ISO_ZONED_DATE_TIME

這些預定義的 DateTimeFormatter 實例中的每一個都預先配置爲格式化和解析不同格式的日期。 這裏不解釋所有這些預定義的 DateTimeFormatter 實例。 可以在 JavaDoc 中查看。

格式化 Date 的例子

DateTimeFormatter formatter = DateTimeFormatter.BASIC_ISO_DATE;

String formattedDate = formatter.format(LocalDate.now());
System.out.println(formattedDate); // 20181204

String formattedZonedDate = formatter.format(ZonedDateTime.now());
System.out.println("formattedZonedDate = " + formattedZonedDate);// 20181204+0800

最後一行輸出 20181204+0800 代表 UTC+8 時區的 2019 年、第 12 個月(12 月)和第 4 天(第 4 天)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章