لماذا java.time؟
لماذا java.time؟
دعم Java التواريخ والأوقات منذ إصدارها الأول عام 1995، وقد أثبت ذلك التصميم الأصلي ضعفه مع مرور الوقت. في هذه المرحلة من المسار تعرفت بالفعل على lambda وStream والمحدّدات العامة (generics)، وعرفت كيف يبدو كود Java النظيف المُحكَم. الفصلان java.util.Date وjava.util.Calendar هما مثال كلاسيكي على النقيض تمامًا. فهم سبب إخفاقهما، وما الذي تُصلحه java.time تحديدًا، سيجعلك تستخدم الواجهة البرمجية الجديدة باقتناع لا بتقليد أعمى.
مشكلات java.util.Date
أُضيفت java.util.Date في JDK 1.0 وتحمل عبء عقود من العيوب. أبرز إخفاقاتها:
- القابلية للتغيير (Mutability). يمكن تعديل كائن
Dateبعد إنشائه؛ يكفي أن يحتفظ أي مستدعٍ بمرجع للكائن ثم يستدعيsetTime()ليُغيّر القيمة في صمت، مما يجبرك على نسخ دفاعية عند تمرير التواريخ بين الدوال والخيوط. - الاسم المُضلِّل. رغم أن اسمها "Date" (تاريخ)، فإنها تُمثّل في الحقيقة لحظةً زمنية محددة (إزاحة بالميلي-ثانية من بداية حقبة Unix). لا يوجد نوع منفصل لتاريخ محلي أو وقت محلي أو طابع زمني مع منطقة زمنية.
- فهرسة الأشهر من الصفر. يناير = 0، وديسمبر = 11. أفرز ذلك أخطاء انزياح بمقدار واحد في كل تطبيق استخدم الفصل تقريبًا.
- توابع مُهمَلة. معظم توابع
Date—getYear()وgetMonth()وgetDay()— أُهملت منذ Java 1.1 لأنها لم تتعامل مع المناطق الزمنية بشكل صحيح، فأُحيلت إلىCalendarلكن ظلت الواجهة المعطوبة قائمة.
Calendar لم تُصلح المشكلة
أُضيف java.util.Calendar في Java 1.1 ليحل محل Date في العمليات الحسابية على التقويم. لكنه فشل في عدة نقاط:
- لا يزال قابلًا للتغيير. تحمل كائنات Calendar مشكلات السلامة الخيطية ذاتها.
- واجهة برمجية مطوّلة وعرضة للخطأ. كل عملية تمر عبر ثوابت صحيحة (
Calendar.MONTH،Calendar.DAY_OF_WEEK)، وهي ليست آمنة من حيث النوع ويسهل إساءة استخدامها. - خلط المسؤوليات. يجمع كائن Calendar واحد لحظةً زمنيةً ومنطقةً زمنيةً ولغةً محلية معًا، مما يجعل التفكير فيما يُمثّله في أي وقت أمرًا عسيرًا.
- الأشهر لا تزال تبدأ من الصفر. الثابت
Calendar.JANUARYقيمته0. - تنسيق ضعيف. كان يتعين اللجوء إلى
SimpleDateFormat، وهو ليس آمنًا للاستخدام من خيوط متعددة.
SimpleDateFormat واحدًا كحقل ثابت (static) تفاديًا لإعادة الإنشاء المتكررة، ثم يستدعيه من خيوط متعددة. لأن SimpleDateFormat يحتفظ بحالة التحليل داخليًا، يُنتج الوصول المتزامن تواريخ خاطئة أو استثناءات. كان الحل المعتاد هو ThreadLocal — تحايل على عيب في التصميم.
المسكّن المؤقت: مكتبة Joda-Time
لجأ المجتمع في نهاية المطاف إلى مكتبة Joda-Time مفتوحة المصدر، التي أنشأها ستيفن كولبورن. أثبتت Joda-Time أن معالجة التواريخ والأوقات يمكن أن تُنفَّذ بإتقان في Java: أنواع قيم غير قابلة للتغيير، وفصل واضح للمفاهيم (تاريخ محلي مقابل لحظة زمنية مقابل مدة)، وواجهة برمجية سلسة. أصبحت معيارًا فعليًا في تطبيقات Java الجادة منذ نحو عام 2005 تقريبًا.
كان نجاح Joda-Time هائلًا لدرجة أن تصميمها ألهم مباشرةً JSR-310 — طلب مواصفات Java الذي أصبح java.time في Java 8. وكان ستيفن كولبورن نفسه قائد المبادرتين.
java.time هي في جوهرها إعادة تصوّر لـ Joda-Time ضمن JDK، تبدو الواجهتان البرمجيتان متشابهتين جدًا. إذا قرأت يومًا كودًا قديمًا يستخدم Joda-Time، فإن المفاهيم تتماثل تقريبًا بشكل كامل. الفرق الرئيسي أن java.time في المكتبة القياسية وتستفيد من تكامل أوثق مع بقية Java.
ما الذي تُصلحه java.time
صُمِّمت حزمة java.time (والحزم الفرعية java.time.format وjava.time.temporal وjava.time.zone) من الصفر لمعالجة كل إخفاقات Date وCalendar:
- عدم قابلية التغيير. كل نوع أساسي —
LocalDateوLocalTimeوLocalDateTimeوZonedDateTimeوInstant— غير قابل للتغيير وآمن للخيوط. توابع التعديل (مثلplusDays()وwithYear()) تُعيد كائنًا جديدًا دون أن تمس الأصل. - فصل واضح للمفاهيم. لكل مفهوم نوعه الخاص: تاريخ تقويمي بدون وقت (
LocalDate)، وقت بدون تاريخ (LocalTime)، طابع زمني موجّه للآلة (Instant)، مدة بين لحظتين (Duration)، فترة تقويمية (Period)، وتاريخ-وقت بمنطقة زمنية (ZonedDateTime). تختار الأضيق نوعًا الذي يناسب احتياجك. - ثوابت أشهر مقروءة. تُمثَّل الأشهر بالتعداد
Month: منMonth.JANUARYإلىMonth.DECEMBER. لا مزيد من ارتباك الفهرسة من الصفر. - تنسيق آمن للخيوط.
DateTimeFormatterغير قابل للتغيير وآمن للخيوط. عرّف نسخة واحدة كثابت واشاركها بحرية. - واجهة برمجية سلسة ومقروءة. العمليات تُقرأ كالإنجليزية:
date.plusWeeks(2).withDayOfMonth(1). أسماء التوابع متسقة عبر جميع الأنواع. - ISO-8601 افتراضيًا. التحليل والتنسيق يعتمدان مواصفة ISO القياسية افتراضيًا، فـ
LocalDate.parse("2024-01-15")تعمل مباشرة.
فلسفة التصميم: الفصول القائمة على القيمة
أنواع java.time هي فصول قائمة على القيمة (value-based). هذا يعني أنك تقارنها بـ .equals() (أو توابع المقارنة المخصصة مثل isBefore() وisAfter())، لا بـ == أبدًا. المقارنة بالهوية على هذه الفصول بلا معنى — كائنان من LocalDate يُمثّلان التاريخ ذاته متساويان حتى لو كانا نسختين مستقلتين. هذا العقد نفسه الذي تتبعه String والأنواع المُغلَّفة مثل Integer.
compareTo() وتوصل النية بوضوح. استخدم equals() لفحوصات المساواة المعيارية (كالمجموعات) وتوابع المقارنة المُسمَّاة في كل مكان آخر.
الخلاصة
كانت java.util.Date وjava.util.Calendar قابلتين للتغيير، مُربكتَي التسمية، تبدأن أشهرهما من الصفر، وتحملان مخاطر السلامة الخيطية. أثبتت Joda-Time إمكانية تصميم أفضل، فاعتمدت java.time (Java 8، JSR-310) ذلك التصميم كمكتبة قياسية. النتيجة واجهة برمجية مبنية على عدم قابلية التغيير، وتصنيف واضح لمفاهيم التاريخ والوقت، وثوابت أشهر آمنة النوع، وتنسيق آمن للخيوط، وتسلسل توابع سلس. كل درس تالٍ في هذا الدليل يبني على هذه الأسس.