99 Ngày Java — Ngày 26: Date/Time API (java.time)
Ngày 25 khép lại mảng kỹ thuật viết method — hôm nay ta bước vào API thực dụng bậc nhất của JDK: Date/Time API trong gói java.time. Thời gian nghe đơn giản nhưng là nơi chôn nhiều bug nhất của backend: tháng lệch một, múi giờ đọc nhầm, lịch hẹn chạy sai giờ. Bài hôm nay đi qua lý do java.time thay thế hoàn toàn Date/Calendar, bộ ba LocalDate/LocalTime/LocalDateTime, cặp khái niệm quan trọng nhất — giờ máy Instant và giờ người ZonedDateTime — rồi Duration/Period và format/parse.

Vì sao java.time thay thế Date và Calendar
// Thế hệ cũ — java.util.Date & Calendar — ba tội lớn:
Date d = new Date(126, 7, 22); // năm 2026 = 126?! tháng 8 = 7?!
d.setTime(0); // MUTABLE — ai giữ tham chiếu đều sửa được
// và Date thực chất là... một mốc millisecond, không phải "ngày"
// java.time (Java 8+) sửa cả ba:
LocalDate ngay = LocalDate.of(2026, 8, 22); // tháng 8 là 8 — hết nhầm
ngay = ngay.plusDays(7); // IMMUTABLE — trả về BẢN MỚI
// mỗi kiểu một khái niệm rõ ràng: ngày, giờ, mốc thời gian, múi giờ
// Quy tắc chuyển đổi tư duy:
// java.util.Date → Instant / LocalDateTime
// Calendar → ZonedDateTime
// SimpleDateFormat → DateTimeFormatter (thread-safe!)Ba lỗi thiết kế của thế hệ cũ: năm tính từ 1900 và tháng đánh số từ 0 (nguồn bug kinh điển), đối tượng mutable nên bị sửa lén qua tham chiếu (đúng nỗi đau Ngày 24 với key của HashMap), và một lớp
Dategánh mọi khái niệm nên chẳng khái niệm nào rõ.java.timethiết kế theo triết lý ngược lại: immutable toàn bộ (giá trị như record Ngày 20 — chia sẻ thoải mái, thread-safe miễn phí), mỗi lớp một khái niệm, và tên method đọc thành văn:plusDays,isAfter,withDayOfMonth.Gặp API cũ trong code kế thừa? Cầu nối hai chiều có sẵn:
date.toInstant()vàDate.from(instant)— khoanh vùng API cũ ở ranh giới, phần lõi viết mới hoàn toàn bằngjava.time.
LocalDate, LocalTime, LocalDateTime — thời gian không múi giờ
LocalDate ngay = LocalDate.now(); // 2026-08-22
LocalTime gio = LocalTime.of(18, 10); // 18:10
LocalDateTime lich = LocalDateTime.of(ngay, gio); // 2026-08-22T18:10
// Immutable: mọi phép biến đổi TRẢ VỀ đối tượng mới (bài học Ngày 20)
LocalDate hanNop = ngay.plusWeeks(2).minusDays(1);
ngay.plusDays(30); // vô nghĩa nếu bỏ kết quả — "ngay" không đổi!
// Truy vấn và điều chỉnh ngữ nghĩa:
lich.getDayOfWeek(); // SATURDAY (enum — Ngày 23!)
ngay.withDayOfMonth(1); // về đầu tháng
ngay.with(TemporalAdjusters.lastDayOfMonth()); // cuối tháng — 28, 30 hay 31 tự lo
// So sánh đúng cách — đừng dùng equals cho "trước/sau":
hanNop.isAfter(ngay); // true
hanNop.isBefore(ngay); // falseChữ Local nghĩa là chưa gắn múi giờ: "ngày 22/08" và "18:10" thuần túy — đủ dùng cho ngày sinh, giờ mở cửa, hạn nộp bài; nhưng chưa đủ để chỉ một khoảnh khắc duy nhất trên trái đất.
Vì immutable, quên hứng kết quả là bug im lặng:
ngay.plusDays(30)đứng một mình không làm gì cả — IDE và compiler đều cảnh báo kết quả bị bỏ rơi, hãy để ý; đổi lại, chuỗiplusWeeks(2).minusDays(1)an toàn tuyệt đối.TemporalAdjustersgói những phép "lịch" dễ sai nhất — cuối tháng, thứ Hai đầu tiên của tháng sau — thành method có tên;getDayOfWeek()trả về enumDayOfWeeknên switch với nó được exhaustive check (Ngày 23).
Instant & ZonedDateTime — giờ máy và giờ người
// Hai loại thời gian — đừng trộn lẫn:
// Instant = giờ MÁY: một mốc tuyệt đối trên trục thời gian (UTC)
// ZonedDateTime = giờ NGƯỜI: mốc đó nhìn qua lăng kính một múi giờ
Instant moc = Instant.parse("2026-08-22T11:10:00Z"); // chuẩn ISO-8601, hậu tố Z = UTC
ZoneId vn = ZoneId.of("Asia/Ho_Chi_Minh");
ZonedDateTime gioVN = moc.atZone(vn); // 2026-08-22T18:10+07:00
// đúng bài đăng này: hẹn 18:10 giờ Việt Nam = lưu 11:10 UTC trong DB
// Chiều ngược lại — từ giờ tường sang mốc tuyệt đối:
LocalDateTime tuong = LocalDateTime.of(2026, 8, 22, 18, 10);
Instant luuDb = tuong.atZone(vn).toInstant();
// Quy tắc vàng của backend:
// LƯU TRỮ và TÍNH TOÁN bằng Instant (UTC)
// chỉ đổi sang ZonedDateTime ở TẦNG HIỂN THỊ
// vì "18:10" không có múi giờ là mơ hồ — còn Instant thì không bao giờInstantlà mốc tuyệt đối — không phụ thuộc ai đang nhìn từ đâu;ZonedDateTimelà mốc đó qua lăng kính một múi giờ. Cùng một Instant, Hà Nội thấy 18:10, London thấy 12:10 — mốc không đổi, cách đọc đổi.Ví dụ sống ngay trong bài: series này hẹn giờ đăng 18:10 giờ Việt Nam, nhưng DB lưu
2026-08-22T11:10:00Z— một chuỗi ISO-8601 kết thúc bằngZ(UTC). Máy chủ ở đâu cũng so sánh đúng, còn 18:10 hiển thị chỉ là chuyện của tầng giao diện.Dùng tên vùng
Asia/Ho_Chi_Minhthay vì độ lệch+07:00cứng: nhiều nước có giờ mùa hè (DST) khiến độ lệch thay đổi trong năm — ZoneId tra bảng lịch sử múi giờ và tự xử lý, offset cứng thì không.
Duration, Period & DateTimeFormatter
// Hai kiểu "khoảng thời gian" — máy đo và người đếm:
Duration d = Duration.between(instantBatDau, instantKetThuc);
d.toMinutes(); // 90 — chính xác từng giây, hợp với Instant
Period p = Period.between(ngayDangKy, LocalDate.now());
p.getYears(); p.getMonths(); // "2 năm 3 tháng" — lịch của người, hợp LocalDate
// Duration.ofMinutes(30), Period.ofDays(7) — cộng thẳng vào thời gian:
hanThanhToan = donHang.plus(Duration.ofMinutes(15));
// Format & parse — DateTimeFormatter:
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
lich.format(fmt); // "22/08/2026 18:10"
LocalDate.parse("22/08/2026", DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// parse sai định dạng → DateTimeParseException — fail-fast, đừng nuốt
// SimpleDateFormat cũ KHÔNG thread-safe — formatter mới dùng chung thoải máiDurationđếm bằng giây — khoảng cách vật lý giữa hai Instant, hợp với timeout, đo thời gian chạy;Periodđếm bằng năm-tháng-ngày của lịch — "2 năm 3 tháng" của con người, nơi tháng lúc 28 lúc 31 ngày là chuyện Period tự lo.DateTimeFormattersửa lỗi chí mạng củaSimpleDateFormat: thread-safe, nên khai báo một lần làm hằng sốstatic final(Ngày 18) và dùng chung toàn ứng dụng — đúng mẫu mà mọi codebase Spring vẫn làm.Parse dữ liệu người dùng luôn có thể nổ
DateTimeParseException: bắt và trả thông báo rõ ràng ở tầng validation — đừngcatchrồi lặng lẽ thay bằngnow(), một dạng nuốt lỗi khiến dữ liệu sai không dấu vết.
Bài tập nhỏ
Viết method nhận ngày sinh (
LocalDate) trả về tuổi chính xác bằngPeriod— test với người sinh 29/02 năm nhuận.Cho chuỗi
"2026-08-22T11:10:00Z": parse thànhInstant, đổi sang giờ Việt Nam và giờ Tokyo — giải thích vì sao hai kết quả hiển thị khác nhau nhưngequalstrên Instant vẫn là một.Tính "23:59:59 ngày cuối tháng này theo giờ Việt Nam" rồi đổi ra
Instantđể lưu DB — dùngTemporalAdjusters.lastDayOfMonth().Tạo
DateTimeFormatterpatterndd/MM/yyyy HH:mm, parse một chuỗi sai định dạng và quan sátDateTimeParseException— rồi viết bản validation trả thông báo tiếng Việt thân thiện.
Kết luận
Bốn ý mang về: java.time thay thế trọn bộ API cũ bằng thiết kế immutable, mỗi lớp một khái niệm; họ Local là thời gian chưa gắn múi giờ — đủ cho lịch nội bộ, chưa đủ cho một khoảnh khắc toàn cầu; quy tắc vàng lưu và tính bằng Instant (UTC), hiển thị bằng ZonedDateTime với tên vùng thay vì offset cứng; và Duration/Period + DateTimeFormatter thread-safe khép kín bộ công cụ. Ngày 27 ta sang mảng số: Math, Random & BigDecimal — vì sao 0.1 + 0.2 không bằng 0.3, và kiểu nào mới xứng đáng giữ tiền. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


