واجهة التاريخ والوقت

لماذا java.time؟

15 دقيقة الدرس 1 من 13

لماذا 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. أفرز ذلك أخطاء انزياح بمقدار واحد في كل تطبيق استخدم الفصل تقريبًا.
  • توابع مُهمَلة. معظم توابع DategetYear() وgetMonth() وgetDay() — أُهملت منذ Java 1.1 لأنها لم تتعامل مع المناطق الزمنية بشكل صحيح، فأُحيلت إلى Calendar لكن ظلت الواجهة المعطوبة قائمة.
// الطريقة القديمة — مليئة بالمزالق import java.util.Date; Date d = new Date(2024 - 1900, 0, 15); // السنة كإزاحة من 1900، الشهر 0 = يناير System.out.println(d); // يطبع سلسلة طويلة تعتمد على الإعدادات المحلية // مزلق القابلية للتغيير Date start = new Date(); Date copy = start; // لا تزال تشير إلى الكائن ذاته — لا نسخ! copy.setTime(0); // الآن تحوّل start إلى حقبة Unix — مفاجأة! System.out.println(start.getTime()); // 0

Calendar لم تُصلح المشكلة

أُضيف java.util.Calendar في Java 1.1 ليحل محل Date في العمليات الحسابية على التقويم. لكنه فشل في عدة نقاط:

  • لا يزال قابلًا للتغيير. تحمل كائنات Calendar مشكلات السلامة الخيطية ذاتها.
  • واجهة برمجية مطوّلة وعرضة للخطأ. كل عملية تمر عبر ثوابت صحيحة (Calendar.MONTH، Calendar.DAY_OF_WEEK)، وهي ليست آمنة من حيث النوع ويسهل إساءة استخدامها.
  • خلط المسؤوليات. يجمع كائن Calendar واحد لحظةً زمنيةً ومنطقةً زمنيةً ولغةً محلية معًا، مما يجعل التفكير فيما يُمثّله في أي وقت أمرًا عسيرًا.
  • الأشهر لا تزال تبدأ من الصفر. الثابت Calendar.JANUARY قيمته 0.
  • تنسيق ضعيف. كان يتعين اللجوء إلى SimpleDateFormat، وهو ليس آمنًا للاستخدام من خيوط متعددة.
import java.util.Calendar; Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, 2024); cal.set(Calendar.MONTH, Calendar.JANUARY); // القيمة 0 — يسهل نسيان ذلك وكتابة 1 cal.set(Calendar.DAY_OF_MONTH, 15); // إضافة 3 أشهر؟ cal.add(Calendar.MONTH, 3); // يعمل، لكنه مطوّل // SimpleDateFormat ليس آمنًا للخيوط — مشاركته بين خيوط متعددة تُفسد النتائج // java.text.SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat والخيوط المتعددة. خطأ إنتاجي شائع: يُنشئ المطوّر كائن SimpleDateFormat واحدًا كحقل ثابت (static) تفاديًا لإعادة الإنشاء المتكررة، ثم يستدعيه من خيوط متعددة. لأن SimpleDateFormat يحتفظ بحالة التحليل داخليًا، يُنتج الوصول المتزامن تواريخ خاطئة أو استثناءات. كان الحل المعتاد هو ThreadLocal — تحايل على عيب في التصميم.

المسكّن المؤقت: مكتبة Joda-Time

لجأ المجتمع في نهاية المطاف إلى مكتبة Joda-Time مفتوحة المصدر، التي أنشأها ستيفن كولبورن. أثبتت Joda-Time أن معالجة التواريخ والأوقات يمكن أن تُنفَّذ بإتقان في Java: أنواع قيم غير قابلة للتغيير، وفصل واضح للمفاهيم (تاريخ محلي مقابل لحظة زمنية مقابل مدة)، وواجهة برمجية سلسة. أصبحت معيارًا فعليًا في تطبيقات Java الجادة منذ نحو عام 2005 تقريبًا.

كان نجاح Joda-Time هائلًا لدرجة أن تصميمها ألهم مباشرةً JSR-310 — طلب مواصفات Java الذي أصبح java.time في Java 8. وكان ستيفن كولبورن نفسه قائد المبادرتين.

نظرة ثاقبة حول JSR-310. لأن 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") تعمل مباشرة.
import java.time.LocalDate; import java.time.Month; // غير قابل للتغيير — لا طفرات عرضية LocalDate today = LocalDate.now(); LocalDate nextMonth = today.plusMonths(1); // كائن جديد؛ today لم يتغير // تعداد Month — مقروء وآمن من حيث النوع LocalDate conference = LocalDate.of(2024, Month.SEPTEMBER, 10); // سلسلة تعبيرية LocalDate firstOfNextMonth = today.plusMonths(1).withDayOfMonth(1); System.out.println(today); // 2024-01-15 System.out.println(firstOfNextMonth); // 2024-02-01

فلسفة التصميم: الفصول القائمة على القيمة

أنواع java.time هي فصول قائمة على القيمة (value-based). هذا يعني أنك تقارنها بـ .equals() (أو توابع المقارنة المخصصة مثل isBefore() وisAfter())، لا بـ == أبدًا. المقارنة بالهوية على هذه الفصول بلا معنى — كائنان من LocalDate يُمثّلان التاريخ ذاته متساويان حتى لو كانا نسختين مستقلتين. هذا العقد نفسه الذي تتبعه String والأنواع المُغلَّفة مثل Integer.

import java.time.LocalDate; LocalDate a = LocalDate.of(2024, 1, 15); LocalDate b = LocalDate.of(2024, 1, 15); System.out.println(a == b); // false — نسختان مختلفتان (لا تعتمد على هذا) System.out.println(a.equals(b)); // true — التاريخ التقويمي ذاته System.out.println(a.isEqual(b));// true — مساواة مع مراعاة نظام التقويم (مُفضَّلة للتواريخ)
فضّل isEqual() وisBefore() وisAfter(). هذه التوابع أكثر تعبيرًا من compareTo() وتوصل النية بوضوح. استخدم equals() لفحوصات المساواة المعيارية (كالمجموعات) وتوابع المقارنة المُسمَّاة في كل مكان آخر.

الخلاصة

كانت java.util.Date وjava.util.Calendar قابلتين للتغيير، مُربكتَي التسمية، تبدأن أشهرهما من الصفر، وتحملان مخاطر السلامة الخيطية. أثبتت Joda-Time إمكانية تصميم أفضل، فاعتمدت java.time (Java 8، JSR-310) ذلك التصميم كمكتبة قياسية. النتيجة واجهة برمجية مبنية على عدم قابلية التغيير، وتصنيف واضح لمفاهيم التاريخ والوقت، وثوابت أشهر آمنة النوع، وتنسيق آمن للخيوط، وتسلسل توابع سلس. كل درس تالٍ في هذا الدليل يبني على هذه الأسس.