LocalDate و LocalTime و LocalDateTime هي الفئات الرئيسية لحزمة java.time التي توفر التعامل مع التاريخ والوقت بدون ربط بمنطقة زمنية. وفقاً لوثائق Oracle (Java 17, 2024)، صممت هذه الأنواع لتكون غير قابلة للتغيير (immutable) وآمنة للخيوط (thread-safe)، مما يجعلها آمنة للتطبيقات متعددة الخيوط. أصبحت متاحة على Android من خلال desugaring بدءاً من API 26، وللإصدارات الأقدم — من خلال مكتبة ThreeTenABP.
النقاط الرئيسية
LocalDate — فئة تمثل تاريخاً بتنسيق سنة-شهر-يوم بدون معلومات عن الوقت أو المنطقة الزمنية. تُستخدم لتخزين بيانات مثل أعياد الميلاد وتواريخ الأحداث أو تواريخ انتهاء الصلاحية.
يخزن LocalDate سنة في نطاق من -999999999 إلى +999999999، وشهراً من 1 إلى 12، ويوماً من الشهر مع مراعاة السنوات الكبيسة. الفئة غير قابلة للتغيير تماماً — أي عملية تُرجع كائناً جديداً.
LocalTime يمثل الوقت من اليوم: الساعات والدقائق والثواني والنانوثواني. الدقة القصوى تصل إلى نانوثانية. لا يحتوي LocalTime على معلومات عن التاريخ أو المنطقة الزمنية، مما يجعله مناسباً لتخزين أوقات افتتاح المتاجر أو مدة العمليات.
LocalDateTime يجمع LocalDate و LocalTime في كائن واحد. هو النوع الأكثر استخداماً عندما تحتاج إلى تخزين كل من التاريخ والوقت، ولكن لا يلزم الربط بمنطقة زمنية. على سبيل المثال، تاريخ ووقت حفلة موسيقية بالتنسيق المحلي.
وفقاً لوثائق Oracle Java (2024)، صُممت الفئات الثلاث بناءً على أفكار من مكتبة Joda-Time، ولكن بهندسة محسنة وتكامل كامل في المكتبة القياسية.
حزمة java.time ظهرت في Java 8 كبديل للفئات القديمة Date و Calendar و SimpleDateFormat. هندستها مبنية على مبادئ الكائنات غير القابلة للتغيير والواجهة الانسيابية.
الميزة الرئيسية — جميع الفئات الأساسية هي value-based. هذا يعني أن مثيلاتها تُقارن حسب القيمة وليس حسب المرجع، ولا يمكن توريثها. لمقارنة كائنين، يُستخدم طريقة equals وليس العامل ==.
تنقسم الحزمة إلى عدة فئات. الأنواع بدون منطقة زمنية — LocalDate و LocalTime و LocalDateTime — تُستخدم للتواريخ والأوقات المحلية. الأنواع مع منطقة زمنية — ZonedDateTime و OffsetDateTime و OffsetTime — تضيف معلومات عن الإزاحة أو المنطقة. الأنواع اللحظية — Instant — تمثل نقطة على الخط الزمني بتوقيت UTC.
هذا التقسيم يحل مشكلة كانت متأصلة في API القديم: المطور لم يكن يعرف أبداً ما إذا كان كائن Date يحتوي على معلومات المنطقة الزمنية أم لا. في java.time، كل نوع يُصرح صراحةً بدلالاته.
فئة LocalDate توفر العديد من الطرق لإنشاء وقراءة وتعديل التواريخ. يمكن الحصول على التاريخ الحالي من خلال الطريقة الثابتة now(). تاريخ محدد — من خلال الطريقة of(int year, int month, int dayOfMonth).
لقراءة مكونات التاريخ تُستخدم getters: getYear() و getMonthValue() و getDayOfMonth() و getDayOfWeek() و getDayOfYear(). الطريقة getMonth() تُرجع التعداد Month، و getDayOfWeek() تُرجع التعداد DayOfWeek.
LocalDate يدعم التحقق من التواريخ. الطرق isBefore() و isAfter() و isEqual() تسمح بمقارنة التواريخ. الطريقة isLeapYear() تتحقق مما إذا كانت السنة كبيسة. الطريقة lengthOfMonth() تُرجع عدد الأيام في الشهر، و lengthOfYear() تُرجع عدد الأيام في السنة.
للتعديل، تُستخدم الطرق withYear() و withMonth() و withDayOfMonth()، التي تُرجع كائناً جديداً بالمكون المعدل. الطرق plusDays() و minusMonths() وما شابهها تنفذ حسابات التاريخ.
LocalTime يمثل الوقت من اليوم بدقة نانوثانية. التنسيق القياسي هو ISO-8601 (HH:mm:ss.nnnnnnnnn). القيمة الدنيا هي 00:00، والقصوى هي 23:59:59.999999999.
يمكن إنشاء كائن LocalTime باستخدام now() للوقت الحالي، أو of(int hour, int minute)، أو of(int hour, int minute, int second)، أو of(int hour, int minute, int second, int nanoOfSecond). الطريقة parse(CharSequence text) تحلل سلسلة نصية بتنسيق ISO-8601.
تتضمن getters: getHour() و getMinute() و getSecond() و getNano(). الطريقة toSecondOfDay() تُرجع عدد الثواني من بداية اليوم، و toNanoOfDay() تُرجع النانوثواني. هذا مفيد لحساب المدة داخل يوم واحد.
LocalTime يدعم نفس عمليات المقارنة والتعديل التي يدعمها LocalDate: plusHours() و minusMinutes() و withHour() و withMinute(). الطرق isBefore() و isAfter() تعمل مع مراعاة أن الوقت دوري ضمن اليوم.
LocalDateTime يجمع إمكانيات LocalDate و LocalTime في فئة واحدة. يخزن كل من التاريخ والوقت، ولكن بدون منطقة زمنية. هو النوع المحلي الأكثر مرونة، لكنه يتطلب الحذر عند استخدامه في الأنظمة الموزعة.
يمكن إنشاء LocalDateTime من خلال الطرق الثابتة now() و of(LocalDate date, LocalTime time) و of(int year, Month month, int dayOfMonth, int hour, int minute) وإصداراتها المحملة بشكل زائد. يمكن أيضاً دمج LocalDate و LocalTime من خلال الطريقة atTime().
LocalDateTime يوفر الوصول إلى جميع حقول التاريخ والوقت من خلال getters المقابلة: toLocalDate() و toLocalTime() يُرجعان المكونات الفردية. الطريقة truncatedTo(TemporalUnit unit) تسمح بتقريب الوقت إلى دقة معينة — على سبيل المثال، إلى الدقائق.
للتحويل إلى منطقة زمنية، تُستخدم الطريقة atZone(ZoneId zone)، التي تُرجع ZonedDateTime. هذه هي الطريقة الوحيدة لإضافة منطقة زمنية إلى LocalDateTime.
الفئات الثلاث تستخدم نمط إنشاء موحد من خلال طرق المصنع الثابتة. مُنشئات الفئات مُعلنة كـ خاصة (private) — لا يمكن إنشاء كائن مباشرة باستخدام new.
الطرق الرئيسية للإنشاء:
لدى method of العديد من الإصدارات المحملة بشكل زائد. بالنسبة لـ LocalDate، تحتاج إلى سنة وشهر ويوم. بالنسبة لـ LocalTime — ساعات ودقائق (اختيارياً ثوانٍ و nanos). بالنسبة لـ LocalDateTime — سنة وشهر ويوم وساعات ودقائق. يمكن تمرير الشهر كـ int (1-12) أو كالتعداد 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)
فئات java.time مصممة للتحويل المريح بين بعضها البعض. LocalDate يمكن تحويله إلى LocalDateTime من خلال طريقة atTime(LocalTime) أو atStartOfDay(). LocalTime — من خلال atDate(LocalDate).
LocalDateTime يمكن تحويله مرة أخرى إلى LocalDate من خلال toLocalDate() وإلى LocalTime من خلال toLocalTime(). للتحويل إلى ZonedDateTime، تُستخدم طريقة atZone(ZoneId).
التحويل إلى java.util.Date (للتوافق مع الكود القديم) يتطلب خطوة وسيطة عبر Instant ومنطقة زمنية. وفقاً لـ Baeldung (2024)، تتم هذه العملية عبر 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"))
للتنسيق والتحليل، تُستخدم فئة DateTimeFormatter. توفر تنسيقات محددة مسبقاً من خلال الثوابت (ISO_LOCAL_DATE و ISO_LOCAL_TIME و ISO_LOCAL_DATE_TIME) وإمكانية إنشاء تنسيقات مخصصة من خلال سلاسل النمط.
رموز التنسيق تستخدم: yyyy — سنة، MM — شهر (برقمين)، dd — يوم، HH — ساعة (0-23)، mm — دقيقة، ss — ثانية. الطريقة format() تُستدعى على كائن التاريخ والوقت أو من خلال DateTimeFormatter.
DateTimeFormatter يدعم أيضاً التوطين من خلال الطرق الثابتة ofLocalizedDate(FormatStyle) و ofLocalizedTime(FormatStyle) و ofLocalizedDateTime(FormatStyle). الأنماط المتاحة هي SHORT و MEDIUM و LONG و 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")
)
جميع الفئات الثلاث تنفذ واجهة Comparable، مما يسمح بمقارنتها بشكل طبيعي. الطريقة compareTo() تُرجع رقماً سالباً أو صفراً أو موجباً حسب الترتيب. الطرق isBefore() و isAfter() و isEqual() تُرجع قيمة منطقية.
بالنسبة لـ LocalDate، المقارنة تكون زمنية — التاريخ الأقدم يُعتبر أصغر. بالنسبة لـ LocalTime — حسب الوقت من اليوم. بالنسبة لـ LocalDateTime — أولاً حسب التاريخ، ثم حسب الوقت. جميع المقارنات تراعي بشكل صحيح السنوات الكبيسة وعدد الأيام في الأشهر.
فرق مهم عن API القديم: equals() لـ LocalDate و LocalTime و LocalDateTime تُقارن القيم وليس المراجع. هذا يعني أن كائنين بنفس الحقول سيكونان متساويين، حتى لو كانا مثيلين مختلفين.
val d1 = LocalDate.of(2026, 7, 21)
val d2 = LocalDate.of(2026, 12, 25)
if (d1.isBefore(d2)) {
Log.d("التاريخ", "d1 قبل d2")
}
val sortedDates = listOf(d2, d1).sorted()
جميع الفئات الثلاث تدعم العمليات الحسابية من خلال الطرق plus و minus. بالنسبة لـ LocalDate، تتوفر plusDays() و plusWeeks() و plusMonths() و plusYears() والطرق minus المقابلة. LocalTime يدعم plusHours() و plusMinutes() و plusSeconds() و plusNanos().
LocalDateTime يرث جميع العمليات الحسابية من كلا النوعين. ميزة ملحوظة في LocalDate: عند إضافة شهر، تتعامل النتائج بشكل صحيح مع أطوال الأشهر المختلفة. على سبيل المثال، 31 يناير + شهر واحد = 28 (29 في سنة كبيسة) فبراير.
للعمليات الأكثر تعقيداً، توجد فئة Period (للتواريخ) و Duration (للوقت). الطرق plus(TemporalAmount) و minus(TemporalAmount) تقبل هذه الكائنات.
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)
لننظر إلى مثال عملي: تطبيق لتسجيل نوبات العمل. نحتاج إلى حساب مدة النوبة وتحديد ما إذا كانت تقع في الوقت الليلي. نستخدم LocalTime لوقت البداية والنهاية، و LocalDate للتاريخ، و LocalDateTime لحساب النوبات التي تعبر منتصف الليل.
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()
}
}
المثال الثاني — حساب عمر المستخدم. نستخدم LocalDate لتاريخ الميلاد ونقارنه بالتاريخ الحالي مع مراعاة يوم وشهر الميلاد.
fun calculateAge(birthDate: LocalDate): Int {
val today = LocalDate.now()
val period = Period.between(birthDate, today)
return period.years
}
المثال الثالث — العمل مع الإشعارات. LocalDateTime يُستخدم لجدولة التذكيرات. نتحقق مما إذا كان الوقت المجدول قد حان.
data class Reminder(
val id: Long,
val scheduledAt: LocalDateTime
) {
fun isDue(): Boolean =
LocalDateTime.now().isAfter(scheduledAt)
}
الدعم المدمج لـ java.time ظهر على Android بدءاً من API 26 (Android 8.0 Oreo). للأجهزة ذات إصدارات Android الأقدم، تحتاج إلى استخدام desugaring — آلية تضيف دعم API Java الجديدة في الإصدارات المبكرة.
يتم تكوين desugaring في Android Gradle Plugin من خلال compileOptions في build.gradle. يكفي تعيين isCoreLibraryDesugaringEnabled = true وإضافة مكتبة desugar_jdk_libs. بعد ذلك، يصبح java.time متاحاً لجميع مستويات API بدءاً من 14.
للمشاريع التي لا يمكنها استخدام desugaring (مثل المشاريع القديمة على AGP أقل من 4.0)، توجد مكتبة ThreeTenABP — backport لجافا.تايم. توفر نفس الفئات (LocalDate و LocalTime و LocalDateTime)، ولكن في حزمة org.threeten.bp.
@Suppress("UnstableApiUsage")
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
"coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}
الخطأ الأول الشائع — استخدام LocalDateTime في الأنظمة الموزعة دون مراعاة المناطق الزمنية. إذا كان الخادم في Europe/Moscow والعميل في Asia/Tokyo، سيتم تفسير LocalDateTime بشكل مختلف. الحل: استخدم Instant أو ZonedDateTime للبيانات العالمية.
الخطأ الثاني — تحليل غير صحيح للسلاسل النصية. افتراضياً، LocalDate.parse() يتوقع تنسيق ISO-8601 (yyyy-MM-dd). إذا كانت السلسلة بتنسيق مختلف، تحتاج إلى تمرير DateTimeFormatter صراحةً. يجب أيضاً معالجة DateTimeParseException حتى لا يتعطل التطبيق عند إدخال غير صحيح.
الخطأ الثالث — تجاهل السلامة من null. LocalDate و LocalTime و LocalDateTime هي كائنات يمكن أن تكون null. في Kotlin، يُوصى باستخدام الأنواع القابلة للnull مع فحوصات صريحة أو عامل Elvis. في Java — تحقق من null قبل استدعاء الطرق.
الخطأ الرابع — الخلط بين LocalDateTime و ZonedDateTime. LocalDateTime لا يحتوي على أي معلومات عن المنطقة الزمنية. إذا كنت بحاجة إلى تمرير لحظة زمنية مطلقة — استخدم الأنواع الزمنية. إذا كان الوقت المحلي كافياً — استخدم الأنواع المحلية.
الأسئلة الشائعة
Date يخزن عدد الملي ثانية من 1970-01-01 UTC، بينما LocalDate يخزن السنة والشهر واليوم بدون ربط بمنطقة زمنية. Date قابل للتغيير وغير آمن للخيوط، LocalDate غير قابل للتغيير وآمن للخيوط. Date مهمل منذ Java 8.
نعم، LocalDateTime يُخطط جيداً لنوع SQL TIMESTAMP WITHOUT TIME ZONE. JPA و Room يدعمانه من خلال TypeConverter. لـ TIMESTAMP WITH TIME ZONE، استخدم ZonedDateTime أو OffsetDateTime.
استخدم ChronoUnit.DAYS.between(startDate, endDate). هذه الطريقة تُرجع long — الفرق بالأيام. لحساب أكثر تفصيلاً، استخدم Period.between() الذي يُرجع Period بالسنوات والأشهر والأيام.
LocalTime يدعم دقة النانوثانية (9 أرقام عشرية). إذا كانت دقة الملي ثانية كافية، استخدم truncateTo(ChronoUnit.MILLIS) قبل الحفظ. هذا يمنع مشاكل التقريب أثناء التسلسل.
الطريقة now() تستخدم ساعة النظام والمنطقة الزمنية الافتراضية للجهاز. إذا كانت الأجهزة في مناطق زمنية مختلفة، قد يختلف التاريخ. للحصول على طابع زمني موحد، استخدم Instant.now() الذي يُرجع دائماً الوقت بتوقيت UTC.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا