LocalDate, LocalTime và LocalDateTime là các lớp chính của gói java.time cung cấp khả năng xử lý ngày và giờ mà không phụ thuộc vào múi giờ. Theo tài liệu Oracle (Java 17, 2024), các kiểu này được thiết kế là bất biến (immutable) và an toàn luồng (thread-safe), khiến chúng an toàn cho các ứng dụng đa luồng. Chúng có sẵn trên Android thông qua desugaring từ API 26 và cho các phiên bản cũ hơn — thông qua thư viện ThreeTenABP.
Những điểm chính
LocalDate — một lớp biểu diễn ngày theo định dạng năm-tháng-ngày không có thông tin giờ và múi giờ. Nó được sử dụng để lưu trữ dữ liệu như sinh nhật, ngày sự kiện hoặc ngày hết hạn.
LocalDate lưu trữ năm trong phạm vi từ -999999999 đến +999999999, tháng từ 1 đến 12 và ngày trong tháng có tính đến năm nhuận. Lớp này hoàn toàn bất biến — mọi thao tác đều trả về một đối tượng mới.
LocalTime biểu diễn giờ trong ngày: giờ, phút, giây và nano giây. Độ chính xác tối đa lên đến nano giây. LocalTime không chứa thông tin ngày hay múi giờ, khiến nó thuận tiện để lưu trữ giờ mở cửa hoặc thời lượng quy trình.
LocalDateTime kết hợp LocalDate và LocalTime thành một đối tượng. Đây là kiểu được sử dụng phổ biến nhất khi cần lưu trữ cả ngày và giờ nhưng không yêu cầu ràng buộc múi giờ. Ví dụ: ngày và giờ của một buổi hòa nhạc theo định dạng địa phương.
Theo Tài liệu Oracle Java (2024), cả ba lớp được thiết kế dựa trên ý tưởng từ thư viện Joda-Time, nhưng với kiến trúc cải tiến và tích hợp đầy đủ vào thư viện chuẩn.
Gói java.time xuất hiện trong Java 8 để thay thế các lớp cũ Date, Calendar và SimpleDateFormat. Kiến trúc của nó được xây dựng trên nguyên tắc đối tượng bất biến và giao diện thông suốt.
Một đặc điểm chính — tất cả các lớp chính đều dựa trên giá trị (value-based). Điều này có nghĩa là các thể hiện của chúng được so sánh theo giá trị, không phải theo tham chiếu và không thể kế thừa. Để so sánh hai đối tượng, hãy sử dụng phương thức equals, không phải toán tử ==.
Gói được chia thành nhiều loại. Kiểu không có múi giờ — LocalDate, LocalTime, LocalDateTime — được sử dụng cho ngày và giờ địa phương. Kiểu có múi giờ — ZonedDateTime, OffsetDateTime, OffsetTime — thêm thông tin về độ lệch hoặc vùng. Kiểu tức thời — Instant — biểu diễn một điểm trên dòng thời gian theo UTC.
Sự phân chia này giải quyết một vấn đề cố hữu trong API cũ: nhà phát triển không bao giờ biết liệu đối tượng Date có chứa thông tin múi giờ hay không. Trong java.time, mỗi kiểu khai báo rõ ràng ngữ nghĩa của nó.
Lớp LocalDate cung cấp nhiều phương thức để tạo, đọc và sửa đổi ngày. Ngày hiện tại có thể lấy qua phương thức tĩnh now(). Một ngày cụ thể — qua phương thức of(int year, int month, int dayOfMonth).
Để đọc các thành phần ngày, sử dụng getter: getYear(), getMonthValue(), getDayOfMonth(), getDayOfWeek(), getDayOfYear(). Phương thức getMonth() trả về enum Month và getDayOfWeek() trả về enum DayOfWeek.
LocalDate hỗ trợ kiểm tra ngày. Các phương thức isBefore(), isAfter() và isEqual() cho phép so sánh ngày. Phương thức isLeapYear() kiểm tra năm có phải năm nhuận không. Phương thức lengthOfMonth() trả về số ngày trong tháng và lengthOfYear() trả về số ngày trong năm.
Để sửa đổi, sử dụng các phương thức withYear(), withMonth(), withDayOfMonth(), trả về đối tượng mới với thành phần đã thay đổi. Các phương thức plusDays(), minusMonths() và tương tự thực hiện số học ngày.
LocalTime biểu diễn giờ trong ngày với độ chính xác nano giây. Định dạng chuẩn là ISO-8601 (HH:mm:ss.nnnnnnnnn). Giá trị tối thiểu là 00:00, tối đa là 23:59:59.999999999.
Bạn có thể tạo đối tượng LocalTime bằng now() cho giờ hiện tại, of(int hour, int minute), of(int hour, int minute, int second) hoặc of(int hour, int minute, int second, int nanoOfSecond). Phương thức parse(CharSequence text) phân tích chuỗi theo định dạng ISO-8601.
Các getter bao gồm getHour(), getMinute(), getSecond(), getNano(). Phương thức toSecondOfDay() trả về số giây từ đầu ngày và toNanoOfDay() trả về nano giây. Điều này thuận tiện để tính thời lượng trong một ngày.
LocalTime hỗ trợ các thao tác so sánh và sửa đổi tương tự LocalDate: plusHours(), minusMinutes(), withHour(), withMinute(). Các phương thức isBefore() và isAfter() hoạt động có tính đến giờ là cyclic trong một ngày.
LocalDateTime kết hợp khả năng của LocalDate và LocalTime trong một lớp. Nó lưu trữ cả ngày và giờ, nhưng không có múi giờ. Đây là kiểu địa phương linh hoạt nhất, nhưng cần thận trọng khi sử dụng trong hệ thống phân tán.
Bạn có thể tạo LocalDateTime qua các phương thức tĩnh now(), of(LocalDate date, LocalTime time), of(int year, Month month, int dayOfMonth, int hour, int minute) và các phiên bản nạp chồng. Bạn cũng có thể kết hợp LocalDate và LocalTime qua phương thức atTime().
LocalDateTime cung cấp quyền truy cập vào tất cả các trường ngày và giờ qua getter tương ứng: toLocalDate() và toLocalTime() trả về các thành phần riêng lẻ. Phương thức truncatedTo(TemporalUnit unit) cho phép làm tròn giờ đến độ chính xác nhất định — ví dụ: đến phút.
Để chuyển đổi sang múi giờ, sử dụng phương thức atZone(ZoneId zone), trả về ZonedDateTime. Đây là cách duy nhất để thêm múi giờ vào LocalDateTime.
Cả ba lớp sử dụng mẫu tạo thống nhất qua các phương thức factory tĩnh. Các hàm tạo của lớp được khai báo là private — bạn không thể tạo đối tượng trực tiếp bằng new.
Các phương thức tạo chính:
Phương thức of có nhiều phiên bản nạp chồng. Đối với LocalDate, cần năm, tháng và ngày. Đối với LocalTime — giờ và phút (tùy chọn giây và nano giây). Đối với LocalDateTime — năm, tháng, ngày, giờ, phút. Tháng có thể truyền dưới dạng int (1-12) hoặc enum Month.
val today = LocalDate.now()
val specificDate = LocalDate.of(2026, Month.JULY, 21)
val parsedDate = LocalDate.parse("2026-07-21")
val currentTime = LocalTime.now()
val lunchTime = LocalTime.of(13, 30, 0)
val parsedTime = LocalTime.parse("13:30:00")
val now = LocalDateTime.now()
val meeting = LocalDateTime.of(2026, 7, 21, 15, 0)
Các lớp java.time được thiết kế để chuyển đổi thuận tiện giữa nhau. LocalDate có thể chuyển đổi thành LocalDateTime qua phương thức atTime(LocalTime) hoặc atStartOfDay(). LocalTime — qua atDate(LocalDate).
LocalDateTime có thể chuyển đổi ngược lại thành LocalDate qua toLocalDate() và thành LocalTime qua toLocalTime(). Để chuyển đổi thành ZonedDateTime, sử dụng phương thức atZone(ZoneId).
Chuyển đổi thành java.util.Date (để tương thích với mã cũ) cần một bước trung gian qua Instant và múi giờ. Theo Baeldung (2024), thao tác này được thực hiện qua Date.from(instant).
val date = LocalDate.of(2026, 7, 21)
val dateTime = date.atTime(LocalTime.of(10, 30))
val time = LocalTime.of(14, 0)
val dateTimeFromTime = time.atDate(date)
val extractedDate = dateTime.toLocalDate()
val extractedTime = dateTime.toLocalTime()
val zoned = dateTime.atZone(ZoneId.of("Europe/Moscow"))
Để định dạng và phân tích, lớp DateTimeFormatter được sử dụng. Nó cung cấp các định dạng được xác định trước qua hằng số (ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME) và khả năng tạo định dạng tùy chỉnh qua chuỗi mẫu.
Các mẫu định dạng sử dụng ký hiệu: yyyy — năm, MM — tháng (hai chữ số), dd — ngày, HH — giờ (0-23), mm — phút, ss — giây. Phương thức format() được gọi trên đối tượng ngày-giờ hoặc qua DateTimeFormatter.
DateTimeFormatter cũng hỗ trợ bản địa hóa qua các phương thức tĩnh ofLocalizedDate(FormatStyle), ofLocalizedTime(FormatStyle) và ofLocalizedDateTime(FormatStyle). Các kiểu có sẵn là SHORT, MEDIUM, LONG và FULL.
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formatted = LocalDateTime.now().format(formatter)
val parsed = LocalDate.parse(
"21.07.2026",
DateTimeFormatter.ofPattern("dd.MM.yyyy")
)
Cả ba lớp triển khai interface Comparable, cho phép so sánh tự nhiên. Phương thức compareTo() trả về số âm, không hoặc dương tùy theo thứ tự. Các phương thức isBefore(), isAfter() và isEqual() trả về boolean.
Đối với LocalDate, so sánh theo thứ tự thời gian — ngày sớm hơn được coi là nhỏ hơn. Đối với LocalTime — theo giờ trong ngày. Đối với LocalDateTime — trước theo ngày, sau theo giờ. Tất cả so sánh đều tính đúng năm nhuận và số ngày trong tháng.
Một điểm khác biệt quan trọng so với API cũ: equals() cho LocalDate, LocalTime và LocalDateTime so sánh giá trị, không phải tham chiếu. Điều này có nghĩa hai đối tượng có cùng trường sẽ bằng nhau, ngay cả khi chúng là các thể hiện khác nhau.
val d1 = LocalDate.of(2026, 7, 21)
val d2 = LocalDate.of(2026, 12, 25)
if (d1.isBefore(d2)) {
Log.d("Ngày", "d1 đứng trước d2")
}
val sortedDates = listOf(d2, d1).sorted()
Cả ba lớp hỗ trợ các phép toán số học qua phương thức plus và minus. Đối với LocalDate, có sẵn plusDays(), plusWeeks(), plusMonths(), plusYears() và các phương thức minus tương ứng. LocalTime hỗ trợ plusHours(), plusMinutes(), plusSeconds(), plusNanos().
LocalDateTime kế thừa tất cả các phép toán số học từ cả hai kiểu. Một tính năng đáng chú ý của LocalDate: khi thêm một tháng, kết quả xử lý chính xác độ dài khác nhau của các tháng. Ví dụ: 31 tháng 1 + 1 tháng = 28 (29 trong năm nhuận) tháng 2.
Đối với các thao tác phức tạp hơn, có lớp Period (cho ngày) và Duration (cho giờ). Các phương thức plus(TemporalAmount) và minus(TemporalAmount) chấp nhận các đối tượng này.
val today = LocalDate.now()
val nextWeek = today.plusDays(7)
val nextMonth = today.plusMonths(1)
val lastYear = today.minusYears(1)
val now = LocalTime.now()
val inTwoHours = now.plusHours(2)
val halfHourAgo = now.minusMinutes(30)
Hãy xem một ví dụ thực tế: ứng dụng theo dõi ca làm việc. Chúng ta cần tính thời lượng ca và xác định xem nó có rơi vào giờ ban đêm hay không. Chúng ta sử dụng LocalTime cho giờ bắt đầu và kết thúc, LocalDate cho ngày và LocalDateTime để tính các ca qua nửa đêm.
data class Shift(
val startTime: LocalTime,
val endTime: LocalTime,
val date: LocalDate
) {
fun isOvernight(): Boolean = endTime.isBefore(startTime)
fun durationInMinutes(): Long {
val start = LocalDateTime.of(date, startTime)
val end = LocalDateTime.of(
if (isOvernight()) date.plusDays(1) else date,
endTime
)
return Duration.between(start, end).toMinutes()
}
}
Ví dụ thứ hai — tính tuổi người dùng. Chúng ta sử dụng LocalDate cho ngày sinh và so sánh với ngày hiện tại, có tính đến ngày và tháng sinh.
fun calculateAge(birthDate: LocalDate): Int {
val today = LocalDate.now()
val period = Period.between(birthDate, today)
return period.years
}
Ví dụ thứ ba — làm việc với thông báo. LocalDateTime được sử dụng để lên lịch nhắc nhở. Chúng ta kiểm tra xem thời gian đã lên lịch đã đến chưa.
data class Reminder(
val id: Long,
val scheduledAt: LocalDateTime
) {
fun isDue(): Boolean =
LocalDateTime.now().isAfter(scheduledAt)
}
Hỗ trợ tích hợp cho java.time xuất hiện trên Android từ API 26 (Android 8.0 Oreo). Đối với thiết bị có phiên bản Android cũ hơn, cần sử dụng desugaring — cơ chế thêm hỗ trợ API Java mới vào các phiên bản trước.
Desugaring trong Android Gradle Plugin được cấu hình qua compileOptions trong build.gradle. Chỉ cần đặt isCoreLibraryDesugaringEnabled = true và thêm thư viện desugar_jdk_libs. Sau đó, java.time có sẵn cho tất cả cấp API từ 14 trở lên.
Đối với các dự án không thể sử dụng desugaring (ví dụ: dự án kế thừa trên AGP dưới 4.0), có thư viện ThreeTenABP — bản backport của java.time. Nó cung cấp các lớp tương tự (LocalDate, LocalTime, LocalDateTime), nhưng trong gói org.threeten.bp.
@Suppress("UnstableApiUsage")
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
"coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}
Lỗi thường gặp đầu tiên — sử dụng LocalDateTime trong hệ thống phân tán mà không tính đến múi giờ. Nếu máy chủ ở Europe/Moscow và máy khách ở Asia/Tokyo, LocalDateTime sẽ được hiểu khác nhau. Giải pháp: sử dụng Instant hoặc ZonedDateTime cho dữ liệu toàn cầu.
Lỗi thứ hai — phân tích chuỗi không đúng. Theo mặc định, LocalDate.parse() mong đợi định dạng ISO-8601 (yyyy-MM-dd). Nếu chuỗi ở định dạng khác, bạn cần truyền DateTimeFormatter một cách tường minh. Bạn cũng nên xử lý DateTimeParseException để ứng dụng không bị treo khi nhập liệu không hợp lệ.
Lỗi thứ ba — bỏ qua an toàn null. LocalDate, LocalTime và LocalDateTime là các đối tượng có thể là null. Trong Kotlin, nên sử dụng kiểu nullable với kiểm tra tường minh hoặc toán tử Elvis. Trong Java — kiểm tra null trước khi gọi phương thức.
Lỗi thứ tư — nhầm lẫn giữa LocalDateTime và ZonedDateTime. LocalDateTime không chứa thông tin múi giờ. Nếu bạn cần truyền một thời điểm tuyệt đối — sử dụng kiểu múi giờ. Nếu giờ địa phương đủ — sử dụng kiểu địa phương.
Câu hỏi thường gặp
Date lưu trữ số mili giây từ 1970-01-01 UTC, trong khi LocalDate lưu trữ năm, tháng và ngày không ràng buộc múi giờ. Date có thể thay đổi và không an toàn luồng, LocalDate bất biến và an toàn luồng. Date đã lỗi thời từ Java 8.
Có, LocalDateTime ánh xạ tốt sang kiểu SQL TIMESTAMP WITHOUT TIME ZONE. JPA và Room hỗ trợ nó qua TypeConverter. Đối với TIMESTAMP WITH TIME ZONE, sử dụng ZonedDateTime hoặc OffsetDateTime.
Sử dụng ChronoUnit.DAYS.between(startDate, endDate). Phương thức này trả về long — chênh lệch ngày. Để tính chi tiết hơn, sử dụng Period.between(), trả về Period với năm, tháng và ngày.
LocalTime hỗ trợ độ chính xác nano giây (9 chữ số thập phân). Nếu độ chính xác mili giây đủ, sử dụng truncateTo(ChronoUnit.MILLIS) trước khi lưu. Điều này ngăn vấn đề làm tròn trong quá trình tuần tự hóa.
Phương thức now() sử dụng đồng hồ hệ thống và múi giờ mặc định của thiết bị. Nếu thiết bị ở các múi giờ khác nhau, ngày có thể khác. Để có dấu thời gian thống nhất, sử dụng Instant.now(), luôn trả về giờ UTC.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm