2024/02/19

Java Date Time API

以往處理日期是用 java.util.Date,以及 java.util.Calendar,但 Date 裡面不存在時區概念,只是 Timestamp 的一個 wrapper,Calendar 也很奇怪的將一月份的數值設定為 0,Java 8 以後可以改用 java.time.* API

java.time 這個 package 裡面有這些 class

  • Instant – represents a point in time (timestamp)
  • LocalDate – represents a date (year, month, day)
  • LocalDateTime – same as LocalDate, but includes time with nanosecond precision
  • OffsetDateTime – same as LocalDateTime, but with time zone offset
  • LocalTime – time with nanosecond precision and without date information
  • ZonedDateTime – same as OffsetDateTime, but includes a time zone ID
  • OffsetLocalTime – same as LocalTime, but with time zone offset
  • MonthDay – month and day, without year or time
  • YearMonth – month and year, without day or time
  • Duration – amount of time represented in seconds, minutes and hours. Has nanosecond precision
  • Period – amount of time represented in days, months and years

有 Timezone 的時間

Date 跟 Instant 的概念不同,處理不同時區的做法不同,但 Instant 的做法比較直覺

        Instant nowInstant = Instant.now();
        ZoneId zoneIdTaipei = ZoneId.of("Asia/Taipei");
        ZoneId zoneIdTokyo = ZoneId.of("Asia/Tokyo");
        ZonedDateTime nowZonedDateTimeTaipei = ZonedDateTime.ofInstant(nowInstant, zoneIdTaipei);
        ZonedDateTime nowZonedDateTimeTokyo = ZonedDateTime.ofInstant(nowInstant, zoneIdTokyo);
        System.out.println("nowZonedDateTimeTaipei="+nowZonedDateTimeTaipei);
        System.out.println("nowZonedDateTimeTokyo="+nowZonedDateTimeTokyo);
        // 跟想像的一樣,將現在的時間放到不同時區,可得到不同時區的正確時間
//        nowZonedDateTimeTaipei=2023-06-14T14:42:44.873547+08:00[Asia/Taipei]
//        nowZonedDateTimeTokyo=2023-06-14T15:42:44.873547+09:00[Asia/Tokyo]

        Date nowDate = new Date();
        System.out.println("nowDate="+nowDate);
        TimeZone tz = TimeZone.getTimeZone("Asia/Tokyo");
        Calendar calendar = Calendar.getInstance(tz);
        calendar.setTime(nowDate);
        Date nowDate2 = calendar.getTime();
        System.out.println("nowDate2="+nowDate2);
        // 因為 Date 沒有時區的概念,是儲存 GMT 1970/1/1 00:00:00 以後所經過的 ms 數值
        // 但列印 Date 時,會自動根據執行程式的時區,轉換為該時區的時間
        // 把現在時間 Date 放到 Tokyo 時區,取回新的 Date 以後,結果兩個 Date 很奇怪的結果是一樣的
//        nowDate=Wed Jun 14 14:42:44 CST 2023
//        nowDate2=Wed Jun 14 14:42:44 CST 2023

        // 要搭配 SimpleDateFormat
        SimpleDateFormat sdfTaipei = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdfTaipei.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
        SimpleDateFormat sdfTokyo = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdfTokyo.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
        System.out.println();
        System.out.println("nowDate="+nowDate);
        System.out.println("nowDate Taipei="+sdfTaipei.format(nowDate));
        System.out.println("nowDate Tokyo="+sdfTokyo.format(nowDate));
//        nowDate=Wed Jun 14 14:53:19 CST 2023
//        nowDate Taipei=2023-06-14 14:53:19
//        nowDate Tokyo=2023-06-14 15:53:19

java.time 的優點

immutable 且為 thread-safe,所有的 method 回傳的物件都是產生一個新的物件,物件本身的狀態是永久不變的,因此為 thread-safe。java.util.Date 並不是 thread-safe

因為 method 會回傳新的物件,因此可以做 method chaining

ZonedDateTime nextFriday = LocalDateTime.now()
                .plusHours(1)
                .with(TemporalAdjusters.next(DayOfWeek.FRIDAY))
                .atZone(ZoneId.of("Asia/Taipei"));
System.out.println("plus 1hr nextFriday="+nextFriday);

新舊寫法

以下是一些特定工作,新舊 API 的不同寫法

        /* get current timestamp */
        Date now = new Date();
        Instant instant = Instant.now();
        // 帶有時區的現在時間
        ZonedDateTime zonedDateTime = ZonedDateTime.now();

        /* 特定時間 */
        Date sDate = new GregorianCalendar(1990, Calendar.JANUARY, 15).getTime();
// New
        LocalDate sLocalDate = LocalDate.of(1990, Month.JANUARY, 15);

        /* 時間的加減 */
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.add(Calendar.HOUR_OF_DAY, -5);
        Date fiveHoursBeforeOld = calendar.getTime();

        // New
        LocalDateTime fiveHoursBeforeNew = LocalDateTime.now().minusHours(5);

        /* 修改特定欄位 */
        // Old
        GregorianCalendar calc = new GregorianCalendar();
        calc.set(Calendar.MONTH, Calendar.JUNE);
        Date inJuneOld = calc.getTime();

        // New
        LocalDateTime inJuneNew = LocalDateTime.now().withMonth(Month.JUNE.getValue());

        /* Date Time truncating */
        // Old
        Calendar nowCalc = Calendar.getInstance();
        nowCalc.set(Calendar.MINUTE, 0);
        nowCalc.set(Calendar.SECOND, 0);
        nowCalc.set(Calendar.MILLISECOND, 0);
        Date truncatedOld = nowCalc.getTime();

        // New
        LocalTime truncatedNew = LocalTime.now().truncatedTo(ChronoUnit.HOURS);

        /* TimeZone 轉換 */
        // Old
        GregorianCalendar cal2 = new GregorianCalendar();
        cal2.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
        Date centralEasternOld = calendar.getTime();

        // New
        ZonedDateTime centralEasternNew = LocalDateTime.now().atZone(ZoneId.of("Asia/Taipei"));

        /* Time difference */
        // Old
        GregorianCalendar calc3 = new GregorianCalendar();
        Date nowdate = new Date();
        calc3.add(Calendar.HOUR, 1);
        Date hourLater = calc3.getTime();
        long elapsed = hourLater.getTime() - nowdate.getTime();

        // New
        LocalDateTime nowLocalDateTime = LocalDateTime.now();
        LocalDateTime hourLaterLocalDateTime = LocalDateTime.now().plusHours(1);
        Duration span = Duration.between(nowLocalDateTime, hourLaterLocalDateTime);

        /* Date formatter, parsing */
        // Old
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date nowdate2 = new Date();
        String formattedDateOld = dateFormat.format(nowdate2);
        try {
            Date parsedDateOld = dateFormat.parse(formattedDateOld);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }

        // New
        LocalDate nowLocalDate = LocalDate.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String formattedDateNew = nowLocalDate.format(formatter);
        LocalDate parsedDateNew = LocalDate.parse(formattedDateNew, formatter);

新舊 classes 之間能夠互相轉換

        // GregorianCalendar 轉為 Instant
        Instant instantFromCalendar = GregorianCalendar.getInstance().toInstant();
        // GregorianCalendar 轉為 ZonedDateTime
        ZonedDateTime zonedDateTimeFromCalendar = new GregorianCalendar().toZonedDateTime();

        // Instant 轉為 Date
        Date dateFromInstant = Date.from(Instant.now());
        // ZonedDateTime 轉為 GregorianCalendar
        GregorianCalendar calendarFromZonedDateTime = GregorianCalendar.from(ZonedDateTime.now());
        // Date 轉為 Instant
        Instant instantFromDate = new Date().toInstant();
        // TimeZone 轉為 ZoneId
        ZoneId zoneIdFromTimeZone = TimeZone.getTimeZone("Asia/Taipei").toZoneId();

References

Set the Time Zone of a Date in Java | Baeldung

Convert Date to LocalDate or LocalDateTime and Back | Baeldung

Migrating to the New Java 8 Date Time API | Baeldung

How to set time zone of a java.util.Date? - Stack Overflow

沒有留言:

張貼留言