1. 程式人生 > >一篇文章概括 Java Date Time 的使用

一篇文章概括 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 天)。