1. Bối cảnh lịch sử: trước Java 8 người ta làm việc với ngày giờ như thế nào
Ngày xưa (trước Java 8), để làm việc với ngày và thời gian, người ta dùng các lớp java.util.Date, java.util.Calendar và để định dạng — java.text.SimpleDateFormat. Đó là trường hợp khiến lập trình viên khắp thế giới thở dài và, nghiến răng, viết những thứ kiểu như:
import java.util.Date;
Date now = new Date();
System.out.println(now); // Sẽ in ra thứ gì đó kỳ lạ kiểu như "Wed Jun 05 14:15:22 MSK 2025"
Nghe có vẻ đơn giản, nhưng thực tế thì không hề “màu hồng”. Dưới đây chỉ là một vài “niềm vui” của API cũ:
- Date — là đối tượng có thể thay đổi (mutable). Có thể vô tình bị sửa đổi, và điều đó thường dẫn đến lỗi.
- Các tháng trong Date và Calendar bắt đầu từ 0 (tháng 1 là 0, tháng 12 là 11), còn ngày thì bắt đầu từ 1.
- SimpleDateFormat không an toàn luồng: nếu hai luồng cùng định dạng ngày đồng thời, bạn có thể nhận kết quả khó lường.
- Rất nhiều phương thức được đánh dấu @Deprecated (lỗi thời), và IDE liên tục “dọa” bạn bằng các cảnh báo màu vàng.
- Làm việc với múi giờ là một nỗi đau thực sự: rất dễ nhầm lẫn giữa giờ địa phương và UTC, còn chuyển đổi giờ mùa hè/đông thì tốt nhất đừng nhắc tới.
Ví dụ nỗi đau
import java.util.Date;
Date date = new Date(2025, 5, 1); // năm 2025, tháng 5 (tháng sáu?), ngày 1
System.out.println(date); // Không như bạn mong đợi!
2. Sự ra đời java.time: cách tiếp cận mới
Đến năm 2014, đã rõ ràng: API cũ không chỉ bất tiện — mà còn nguy hiểm. Vì vậy Java đã có gói mới — java.time, hiện thực đặc tả JSR‑310. API này lấy cảm hứng từ thư viện Joda-Time nổi tiếng và nhanh chóng trở thành tiêu chuẩn de facto.
Các package và lớp chính
- java.time — gói chính, nơi chứa các lớp mới về ngày và thời gian.
- java.time.format — để định dạng và phân tích (parse) ngày giờ.
- java.time.temporal — cho các thao tác thời gian nâng cao hơn.
- java.time.zone — để làm việc với múi giờ.
Đây là các “nhân vật chính” của API mới:
| Lớp | Dùng để làm gì? | Ví dụ sử dụng |
|---|---|---|
|
Chỉ ngày (năm, tháng, ngày) | Ngày sinh, không có giờ |
|
Chỉ thời gian (giờ, phút, giây) | Giờ hẹn, không có ngày |
|
Ngày và thời gian, không có múi giờ | Sự kiện cục bộ |
|
Ngày và thời gian có múi giờ | Cuộc hẹn tại Minsk theo giờ địa phương |
|
Mốc thời gian tuyệt đối (UTC) | Dấu mốc sự kiện trong log |
|
Khoảng thời gian (giờ, phút, giây) | Thời lượng cuộc gọi |
|
Khoảng (năm, tháng, ngày) | Thâm niên làm việc, tuổi |
Ví dụ: tạo ngày theo cách mới
import java.time.LocalDate;
LocalDate today = LocalDate.now();
System.out.println(today); // Ví dụ: "2025-06-05"
3. Ưu điểm của API mới
Tính bất biến (immutable)
Tất cả các lớp trong java.time đều bất biến. Nghĩa là: nếu bạn tạo một đối tượng LocalDate, bạn không thể thay đổi nó. Bất kỳ thao tác nào (ví dụ, cộng 1 ngày) đều trả về một đối tượng mới.
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
System.out.println(today); // 2025-06-05
System.out.println(tomorrow); // 2025-06-06
Làm việc rõ ràng với múi giờ
Trong API cũ rất dễ quên đối tượng đang ở múi giờ nào. Trong java.time mọi thứ đều rõ ràng: nếu cần múi giờ — dùng ZonedDateTime, nếu không cần — dùng LocalDateTime.
import java.time.ZonedDateTime;
import java.time.ZoneId;
ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
System.out.println(MinskTime); // 2025-06-05T14:23:45+03:00[Europe/Minsk]
Các phương thức tiện lợi cho tính toán và so sánh
LocalDate today = LocalDate.now();
LocalDate nextMonth = today.plusMonths(1);
boolean isAfter = LocalDate.now().plusDays(1).isAfter(today); // true
Định dạng và phân tích (parse)
Định dạng và parse — qua DateTimeFormatter (chi tiết — ở bài sau):
import java.time.format.DateTimeFormatter;
LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
String formatted = today.format(formatter); // "05.08.2025"
4. Tương thích: làm việc với mã cũ như thế nào
Trong thực tế, rất thường xuyên bạn phải làm việc với thư viện hoặc hệ thống cũ dùng Date và Calendar. May mắn thay, API mới thân thiện với di sản: có thể chuyển đổi các kiểu cũ sang kiểu mới và ngược lại.
Chuyển đổi Date ↔ Instant
import java.util.Date;
import java.time.Instant;
// Date → Instant
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
// Instant → Date
Date dateBack = Date.from(instant);
Chuyển đổi Calendar ↔ ZonedDateTime
import java.util.Calendar;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.util.Date;
// Calendar → ZonedDateTime
Calendar calendar = Calendar.getInstance();
ZonedDateTime zdt = ZonedDateTime.ofInstant(
calendar.toInstant(),
calendar.getTimeZone().toZoneId()
);
// ZonedDateTime → Calendar
Calendar calBack = Calendar.getInstance();
calBack.setTime(Date.from(zdt.toInstant()));
Bảng: đối chiếu các lớp cũ và mới
| Lớp cũ | Lớp mới | Ghi chú |
|---|---|---|
|
|
Thời gian tuyệt đối |
|
|
Ngày và giờ có múi giờ |
|
|
Định dạng/parse ngày |
5. Thực hành: những bước đầu với java.time
Giả sử bạn có một người dùng với ngày sinh. Hãy lưu và in ngày này:
import java.time.LocalDate;
public class UserProfile {
private String name;
private LocalDate birthDate;
public UserProfile(String name, LocalDate birthDate) {
this.name = name;
this.birthDate = birthDate;
}
public void printProfile() {
System.out.println("Tên: " + name);
System.out.println("Ngày sinh: " + birthDate);
}
}
public class Main {
public static void main(String[] args) {
UserProfile user = new UserProfile("Alisa", LocalDate.of(1998, 12, 25));
user.printProfile();
}
}
Kết quả:
Tên: Alisa
Ngày sinh: 1998-12-25
6. So sánh: API cũ vs API mới
Ví dụ: cộng một tuần vào ngày sinh
Cách cũ (Date/Calendar):
import java.util.Calendar;
Calendar cal = Calendar.getInstance();
cal.set(1998, Calendar.DECEMBER, 25);
cal.add(Calendar.WEEK_OF_YEAR, 1);
System.out.println(cal.getTime()); // Rườm rà và khó hiểu
Cách mới (java.time):
import java.time.LocalDate;
LocalDate birthDate = LocalDate.of(1998, 12, 25);
LocalDate nextWeek = birthDate.plusWeeks(1);
System.out.println(nextWeek); // 1999-01-01
Với API mới, mã ngắn hơn, đơn giản hơn và an toàn hơn.
7. Các lỗi thường gặp khi làm việc với java.time
Lỗi số 1: quên rằng các đối tượng là bất biến.
Nếu gọi date.plusDays(1); mà không lưu kết quả, ngày ban đầu sẽ giữ nguyên.
Lỗi số 2: nhầm lẫn giữa LocalDate và LocalDateTime.
LocalDate chỉ lưu ngày (năm, tháng, ngày), còn LocalDateTime lưu cả thời gian. Đừng nhầm lẫn nếu bạn cần xử lý cả giờ và phút.
Lỗi số 3: sử dụng các lớp cũ trong các dự án mới.
Nếu có thể — luôn dùng java.time. Các lớp cũ chỉ cần cho mục đích tương thích.
Lỗi số 4: xử lý múi giờ không đúng.
Nếu cần lưu sự kiện quan trọng cho nhiều vùng, hãy dùng ZonedDateTime hoặc ít nhất là Instant. LocalDateTime không chứa thông tin về múi giờ!
Lỗi số 5: cố so sánh trực tiếp LocalDate và LocalDateTime.
Đây là hai kiểu dữ liệu khác nhau, không thể so sánh trực tiếp. Hãy chuyển về cùng một kiểu trước.
GO TO FULL VERSION