Java——日期和時間

七、日期和時間


Java標準庫有兩套處理日期和時間的API:

  • 一套定義在java.util這個包裏面,主要包括DateCalendarTimeZone這幾個類;
  • 一套新的API是在Java 8引入的,定義在java.time這個包裏面,主要包括LocalDateTimeZonedDateTimeZoneId等。

在舊的API中存在有很多問題,所以引入了新的API。但很多時候,我們仍然需要在兩種對象之間進行轉換。

在使用日期和時間時,除非涉及到遺留代碼,否則應該堅持使用新的API。

1、Date 和 Calendar

1.1 Date格式輸出

@Test
public void m0() {
  Date date = new Date();	//獲取當前時間
  DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");	//自定義格式輸出
  System.out.println(sdf.format(date));
}

Date中的 getDate()getDay()getHours()getMinites() 等方法已棄用。

1.2 Calendar

Calendar可以用於獲取並設置年、月、日、時、分、秒,它和Date比,主要多了一個可以做簡單的日期和時間運算的功能。基本用法如下:

@Test
public void m0() {
  // 獲取當前時間:
  Calendar c = Calendar.getInstance();
  int y = c.get(Calendar.YEAR);
  int m = 1 + c.get(Calendar.MONTH);
  int d = c.get(Calendar.DAY_OF_MONTH);
  int w = c.get(Calendar.DAY_OF_WEEK);
  int hh = c.get(Calendar.HOUR_OF_DAY);
  int mm = c.get(Calendar.MINUTE);
  int ss = c.get(Calendar.SECOND);
  int ms = c.get(Calendar.MILLISECOND);
  System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms);
}
//輸出結果
2020-3-10 3 21:10:20.43

Calendar 獲取年月日這些信息變成了get(int field) ,返回的年份不必轉換,返回的月份必須要加1。

返回的星期:,1~7分別表示週日,週一,……,週六。

Calendar 只有一種獲取方式,即Calendar.getInstance() 獲取當地時間。

Calendar 設置一個特定的日期和時間:

@Test
public void m1() {
  // 當前時間:
  Calendar c = Calendar.getInstance();
  // 清除所有:
  c.clear();
  // 設置2019年:
  c.set(Calendar.YEAR, 2019);
  // 設置9月:注意8表示9月:
  c.set(Calendar.MONTH, 8);
  // 設置2日:
  c.set(Calendar.DATE, 2);
  // 設置時間:
  c.set(Calendar.HOUR_OF_DAY, 21);
  c.set(Calendar.MINUTE, 22);
  c.set(Calendar.SECOND, 23);
  //getTime()  可以將Calendar對象轉換爲Date對象
  String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(c.getTime());
  System.out.println(format);	//2019-09-02 21:22:23
}

2、LocalDateTime

從Java 8開始,java.time包提供了新的日期和時間API,主要涉及的類型有:

  • 本地日期和時間:LocalDateTimeLocalDateLocalTime
  • 帶時區的日期和時間:ZonedDateTime
  • 時刻:Instant
  • 時區:ZoneIdZoneOffset
  • 時間間隔:Duration

以及一套新的用於取代SimpleDateFormat的格式化類型DateTimeFormatter

新的API:

  • 嚴格區分了時刻、本地日期、本地時間和帶失去的日期時間,並且對日期和時間的運算更加方便。
  • Month用1~12表示1月到12月;Week用1~7表示週一到週日;
  • 類型幾乎都是不變類型(類似String),不必擔心被修改。

2.1 LocalDateTime

@Test
public void m1() {
  LocalDate d = LocalDate.now(); // 當前日期
  LocalTime t = LocalTime.now(); // 當前時間
  LocalDateTime dt = LocalDateTime.now(); // 當前日期和時間
  System.out.println(d); // 嚴格按照ISO 8601格式打印
  System.out.println(t); // 嚴格按照ISO 8601格式打印
  System.out.println(dt); // 嚴格按照ISO 8601格式打印
}
//輸出結果
2020-03-10
21:48:13.924
2020-03-10T21:48:13.924

2.2 日期和時間 <-> 日期、時間

LocalDateTime dt = LocalDateTime.now(); // 當前日期和時間
LocalDate d = dt.toLocalDate(); // 轉換到當前日期
LocalTime t = dt.toLocalTime(); // 轉換到當前時間
// 指定日期和時間:
LocalDate d2 = LocalDate.of(2019, 11, 30); // 2019-11-30, 注意11=11月
LocalTime t2 = LocalTime.of(15, 16, 17); // 15:16:17
LocalDateTime dt2 = LocalDateTime.of(2019, 11, 30, 15, 16, 17);
LocalDateTime dt3 = LocalDateTime.of(d2, t2);

2.3 String ->LocalDateTime

因爲嚴格按照ISO 8601的格式,所以可以把字符串轉換爲LocalDateTime

LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
LocalDate d = LocalDate.parse("2019-11-19");
LocalTime t = LocalTime.parse("15:16:17");

注意ISO 8601規定的日期和時間分隔符是T。標準格式如下:

  • 日期:yyyy-MM-dd
  • 時間:HH:mm:ss
  • 帶毫秒的時間:HH:mm:ss.SSS
  • 日期和時間:yyyy-MM-dd’T’HH:mm:ss
  • 帶毫秒的日期和時間:yyyy-MM-dd’T’HH:mm:ss.SSS

2.4 DateTimeFormater

如果要自定義輸出的格式,或者要把一個非ISO 8601格式的字符串解析成LocalDateTime,可以使用新的DateTimeFormatter

@Test
public void m1() {
  // 自定義格式化:
  DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
  System.out.println(dtf.format(LocalDateTime.now()));

  // 用自定義格式解析:
  LocalDateTime dt2 = LocalDateTime.parse("2020/03/09 15:16:17", dtf);
  System.out.println(dt2);
}
//輸出結果
2020/03/10 22:02:57
2020-03-09T15:16:17

2.5 LocalDateTime 的計算

LocalDateTime提供了對日期和時間的加減計算方法:

@Test
public void m1() {
  LocalDateTime dt = LocalDateTime.of(2020, 03, 10, 20, 30, 59);
  System.out.println(dt); //2020-03-10T20:30:59
  // 加5天減3小時:
  LocalDateTime dt2 = dt.plusDays(5).minusHours(3);
  System.out.println(dt2); // 2020-03-15T17:30:59
  // 減1月:
  LocalDateTime dt3 = dt2.minusMonths(1);
  System.out.println(dt3); // 2020-02-15T17:30:59
  // 日期變爲15日:
  LocalDateTime dt4 = dt.withDayOfMonth(15);
  System.out.println(dt4); // 2020-03-15T20:30:59
  // 月份變爲9:
  LocalDateTime dt5 = dt2.withMonth(9);
  System.out.println(dt5); // 2020-09-15T17:30:59
}

對日期和時間進行調整則使用withXxx()方法,例如:withHour(15)會把10:11:12變爲15:11:12

  • 調整年:withYear()
  • 調整月:withMonth()
  • 調整日:withDayOfMonth()
  • 調整時:withHour()
  • 調整分:withMinute()
  • 調整秒:withSecond()

調整月時,如果填入錯誤日期,例如32,會報錯。其餘類似。

調整月份時,會相應地調整日期,即把2020-10-31的月份調整爲9時,日期也自動變爲30

2.6 with()方法

LocalDateTime還有一個通用的with()方法允許我們做更復雜的運算。例如:

    @Test
    public void m1() {
        // 本月第一天0:00時刻:
        LocalDateTime firstDay = LocalDate.now().withDayOfMonth(1).atStartOfDay();
        System.out.println(firstDay);   //2020-03-01T00:00

        // 本月最後1天:
        LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
        System.out.println(lastDay);    //2020-03-31

        // 下月第1天:
        LocalDate nextMonthFirstDay = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth());
        System.out.println(nextMonthFirstDay);  //2020-04-01

        // 本月第1個週一:
        LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
        System.out.println(firstWeekday);   //2020-03-02
    }

2.7 日期時間先後判斷

要判斷兩個LocalDateTime的先後,可以使用isBefore()isAfter()方法,對於LocalDateLocalTime類似:

@Test
public void m1() {
  LocalDateTime now = LocalDateTime.now();
  LocalDateTime target = LocalDateTime.of(2019, 11, 19, 8, 15, 0);
  System.out.println(now.isBefore(target));
  System.out.println(LocalDate.now().isBefore(LocalDate.of(2019, 11, 19)));
  System.out.println(LocalTime.now().isAfter(LocalTime.parse("08:15:00")));
}

2.8 Duration和Period

Duration表示兩個時刻之間的時間間隔。另一個類似的Period表示兩個日期之間的天數:

    @Test
    public void m1() {
        LocalDateTime start = LocalDateTime.of(2019, 11, 19, 8, 15, 0);
        LocalDateTime end = LocalDateTime.of(2020, 1, 9, 19, 25, 30);
        Duration d = Duration.between(start, end);
        System.out.println(d); // PT1235H10M30S

        Period p = LocalDate.of(2019, 11, 19).until(LocalDate.of(2020, 1, 9));
        System.out.println(p); // P1M21D
    }

兩個LocalDateTime之間的差值使用Duration表示,類似PT1235H10M30S,表示1235小時10分鐘30秒。

兩個LocalDate之間的差值用Period表示,類似P1M21D,表示1個月21天。

3、ZonedDateTime

當要表示一個帶時區的日期和時間時,就需要用到 ZonedDateTime

可以簡單地把ZonedDateTime理解成LocalDateTimeZoneIdZoneIdjava.time引入的新的時區類。

要創建一個 ZonedDateTime 對象,有以下幾種方法:

/*方法一:通過 `now()` 方法返回當前時間*/
@Test
public void m2() {
  ZonedDateTime zbj = ZonedDateTime.now(); // 默認時區
  ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); //用指定時區獲取當前時間
  System.out.println(zbj);    //2020-03-10T22:42:47.322+08:00[Asia/Shanghai]
  System.out.println(zny);    //2020-03-10T10:42:47.323-04:00[America/New_York]
}

/*方法二:通過給 `LocalDateTime` 附加一個 `ZoneId`*/
@Test
public void m2() {
  LocalDateTime ldt = LocalDateTime.of(2019, 9, 15, 15, 16, 17);
  ZonedDateTime zbj = ldt.atZone(ZoneId.systemDefault());
  ZonedDateTime zny = ldt.atZone(ZoneId.of("America/New_York"));
  System.out.println(zbj);	//2019-09-15T15:16:17+08:00[Asia/Shanghai]
  System.out.println(zny);	//2019-09-15T15:16:17-04:00[America/New_York]
}

方法一中:打印的兩個 ZonedDateTime ,雖然時區不同,但表示的是同一時刻。

方法二中:以這種方式創建的ZonedDateTime,它的日期和時間與LocalDateTime相同,但附加的時區不同,因此是兩個不同的時刻

3.1 時區轉換

withZoneSameInstant() 可以將關聯時區轉化到另一時區,轉化後日期和時間都會相應調整。

/*將北京時間調整爲紐約時間*/
@Test
public void m1() {
  // 以中國時區獲取當前時間:
  ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
  // 轉換爲紐約時間:
  ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));
  System.out.println(zbj);    //2020-03-10T23:33:44.988+08:00[Asia/Shanghai]
  System.out.println(zny);    //2020-03-10T11:33:44.988-04:00[America/New_York]
}

涉及到時區時,千萬不要自己計算時差,否則難以正確處理夏令時。

有了ZonedDateTime ,將其轉化爲本地時間就非常簡單:

ZonedDateTime zdt = ...
LocalDateTime ldt = zdt.toLocalDateTime();

4、Instant

計算機存儲當前的時間,本質上只是一個不斷增長的整數。

Java提供的System.currentTimeMillis()返回的就是以毫秒錶示的當前時間戳。

java.time中當前的時間戳以Instant類型表示,我們用Instant.now()獲取當前時間戳:

@Test
public void m1() {
  Instant now = Instant.now();
  System.out.println(now.getEpochSecond()); // 秒,1583850200
  System.out.println(now.toEpochMilli()); // 毫秒,1583850200151
}

Instant內部只有兩個核心字段:

public final class Instant implements ... {
    private final long seconds;
    private final int nanos;
}

一個是以秒爲單位的時間戳,另一個是更精確的納秒精度。

Instant 附加上一個時區,就可以創建出 ZonedDateTime

// 以指定時間戳創建Instant:
Instant ins = Instant.ofEpochSecond(1568568760);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println(zdt); // 2019-09-16T01:32:40+08:00[Asia/Shanghai]

對於某一個時間戳,給它關聯上指定的ZoneId,就得到了ZonedDateTime,繼而可以獲得了對應時區的LocalDateTime。所以,LocalDateTimeZoneIdInstantZonedDateTimelong都可以互相轉換。

5、最佳實踐

在使用日期和時間時,除非涉及到遺留代碼,否則應該堅持使用新的API。

5.1 舊API ->新API

如果要把舊式的DateCalendar轉換爲新API對象,可以通過toInstant()方法轉換爲Instant對象,再繼續轉換爲ZonedDateTime

// Date -> Instant -> ZonedDateTime:
Date date = new Date();
Instant ins1 = date.toInstant();
ZonedDateTime zdt1 = ins1.atZone(ZoneId.systemDefault());

// Calendar -> Instant -> ZonedDateTime:
Calendar calendar = Calendar.getInstance();
Instant ins2 = calendar.toInstant();
ZonedDateTime zdt2 = ins2.atZone(calendar.getTimeZone().toZoneId());

從上面的代碼還可以看到,舊的TimeZone提供了一個toZoneId(),可以把自己變成新的ZoneId

5.2 新API ->舊API

如果要把新的ZonedDateTime轉換爲舊的API對象,只能藉助long型時間戳做一個“中轉”:

// ZonedDateTime -> long:
ZonedDateTime zdt = ZonedDateTime.now();
long ts = zdt.toEpochSecond() * 1000;

// long -> Date:
Date date = new Date(ts);

// long -> Calendar:
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId()));
calendar.setTimeInMillis(zdt.toEpochSecond() * 1000);

從上面的代碼還可以看到,新的ZoneId轉換爲舊的TimeZone,需要藉助ZoneId.getId()返回的String完成。

5.3 在數據庫中存儲日期和時間

除了舊式的java.util.Date,我們還可以找到另一個java.sql.Date,它繼承自java.util.Date,但會自動忽略所有時間相關信息。

在數據庫中,也存在幾種日期和時間類型:

  • DATETIME:表示日期和時間;
  • DATE:僅表示日期;
  • TIME:僅表示時間;
  • TIMESTAMP:和DATETIME類似,但是數據庫會在創建或者更新記錄的時候同時修改TIMESTAMP

在操作數據庫時,我們需要把數據庫類型與Java類型映射起來。下表是數據庫類型與Java新舊API的映射關係:

數據庫 對應Java類(舊) 對應Java類(新)
DATETIME java.util.Date LocalDateTime
DATE java.sql.Date LocalDate
TIME java.sql.Time LocalTime
TIMESTAMP java.sql.Timestamp LocalDateTime

實際上,在數據庫中,我們需要存儲的最常用的是時刻(Instant),因爲有了時刻信息,就可以根據用戶自己選擇的時區,顯示出正確的本地時間。所以,最好的方法是直接用長整數long表示,在數據庫中存儲爲BIGINT類型。

下面是通過timestampToString 方法,將存儲的long 型時間戳轉化爲不用用戶的本地時間:

public class Main {
    public static void main(String[] args) {
        long ts = 1583856181452L;
        System.out.println(timestampToString(ts, Locale.CHINA, "Asia/Shanghai"));
        System.out.println(timestampToString(ts, Locale.US, "America/New_York"));
    }

    static String timestampToString(long epochMilli, Locale lo, String zoneId) {
        Instant ins = Instant.ofEpochMilli(epochMilli);
        DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT);
        return f.withLocale(lo).format(ZonedDateTime.ofInstant(ins, ZoneId.of(zoneId)));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章