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

التعامل مع تواريخ الإصدارات القديمة

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

التعامل مع تواريخ الإصدارات القديمة

نادرًا ما تبدأ تطبيقات Java الحقيقية من صفحة بيضاء. ستصادر حتمًا واجهات برمجية ومكتبات وقواعد بيانات وطبقات تسلسل تتحدث لغة java.util.Date وjava.util.Calendar القديمة. صُمِّمت حزمة java.time مع توفير دوال جسر صريحة تتيح الهجرة التدريجية — لا حاجة لإعادة كتابة كل شيء دفعة واحدة.

يغطي هذا الدرس كل اتجاهات التحويل، والمزالق الخفية في دلالات المناطق الزمنية، والنمط العملي لتقرير متى تُحوِّل ومتى تترك النوع القديم كما هو.

خريطة سريعة للعالم القديم

قبل أن تبني الجسر بين العالمين تحتاج أن تعرف ما يقابل ماذا:

  • java.util.Date — طابع زمني بالملي ثانية منذ بداية حقبة Unix (1970-01-01T00:00:00Z). على الرغم من الاسم، ليس لديه مفهوم لتاريخ التقويم؛ فهو في الحقيقة غلاف حول قيمة long.
  • java.util.Calendar / GregorianCalendar — تمثيل قابل للتعديل يدرك المنطقة الزمنية ويجمع التاريخ والوقت وTimeZone معًا.
  • java.sql.Date وjava.sql.Time وjava.sql.Timestamp — أنواع JDBC ترث من java.util.Date وتحمل دقة إضافية لكنها تقوم على نفس أساس الملي ثانية منذ الحقبة.
تخزّن الأنواع القديمة الثلاثة ملي ثواني UTC داخليًا. أي تفسير "محلي" يجري وقت العرض بتطبيق TimeZone فوقها. يجعل java.time هذا العقد صريحًا بدلًا من إخفائه.

من java.util.Date إلى java.time

حصل Date على دالة جسر وحيدة: toInstant(). بما أن Date ليس سوى عداد ملي ثواني، فإن الهدف الطبيعي هو Instant، ومنه تستطيع الإسقاط على أي نوع مدرك للمنطقة الزمنية أو محلي تحتاجه.

import java.time.*; import java.util.Date; Date legacyDate = new Date(); // تاريخ قادم من واجهة برمجية قديمة // الخطوة 1: الهبوط على Instant (بلا خسارة — نفس الملي ثواني في java.time) Instant instant = legacyDate.toInstant(); // الخطوة 2: الإسقاط على منطقة زمنية للحصول على تاريخ/وقت مقروء ZonedDateTime zdt = instant.atZone(ZoneId.of("America/New_York")); // الخطوة 3: تجريد المنطقة إن اهتممت فقط بالحقول المحلية LocalDate date = zdt.toLocalDate(); LocalTime time = zdt.toLocalTime(); LocalDateTime ldt = zdt.toLocalDateTime(); System.out.println(instant); // 2024-03-15T14:30:00Z System.out.println(date); // 2024-03-15 System.out.println(time); // 10:30:00
لا تمرّر ZoneId.systemDefault() دون تفكير. المنطقة الافتراضية على الخادم تكون عادةً UTC أو منطقة نظام التشغيل وتتغير بين البيئات. كن صريحًا بشأن المنطقة المستهدفة وإلا أنتجت التحويلات بصمت تواريخ محلية مختلفة على أجهزة مختلفة.

من java.time إلى java.util.Date

الاتجاه المعاكس يستخدم المصنع الساكن Date.from(Instant). إذا كان مصدرك نوعًا مدركًا للمنطقة الزمنية حوّله إلى Instant أولًا.

// من Instant Instant instant = Instant.now(); Date legacyDate = Date.from(instant); // من ZonedDateTime ZonedDateTime zdt = ZonedDateTime.of(2024, 3, 15, 14, 30, 0, 0, ZoneId.of("UTC")); Date fromZdt = Date.from(zdt.toInstant()); // من LocalDateTime — يجب عليك اختيار المنطقة؛ لا يوجد افتراضي LocalDateTime ldt = LocalDateTime.of(2024, 3, 15, 14, 30); ZoneId zone = ZoneId.of("Europe/Berlin"); Date fromLdt = Date.from(ldt.atZone(zone).toInstant());
التحويل من LocalDate أو LocalDateTime يتطلب دائمًا منطقة زمنية. تفتقر هذه الأنواع عمدًا لمعلومات المنطقة الزمنية. إذا أهملت المنطقة ستُضطر لاختيارها — اجعل هذا الاختيار ظاهرًا في الكود لا مخفيًا داخل دالة مساعدة.

Calendar وGregorianCalendar

GregorianCalendar هي الفئة الملموسة التي تتعامل معها كل الوقت تقريبًا عند استخدام Calendar. لديها دالة تحويل مباشرة.

import java.util.GregorianCalendar; import java.util.Calendar; import java.time.*; // واجهة برمجية قديمة تُعيد Calendar Calendar cal = Calendar.getInstance(); // GregorianCalendar تحت الغطاء // أسقطها إلى GregorianCalendar واستخدم دالة الجسر GregorianCalendar gcal = (GregorianCalendar) cal; ZonedDateTime zdt = gcal.toZonedDateTime(); System.out.println(zdt); // 2024-03-15T14:30:00+01:00[Europe/Berlin] // العكس: إنشاء GregorianCalendar من ZonedDateTime GregorianCalendar back = GregorianCalendar.from(zdt);

يحفظ الجسر الـTimeZone الأصلي تمامًا، فـCalendar في Asia/Tokyo يصبح ZonedDateTime بـAsia/Tokyo — دون إزاحة زمنية صامتة.

ماذا لو لم يكن Calendar من نوع GregorianCalendar؟ تشحن JDK فئة فرعية وحيدة لـCalendar لتقويمات غير غريغورية: JapaneseImperialCalendar المستخدمة حين يكون locale الـJVM يابانيًا. في تلك الحالات استخرج الملي ثواني يدويًا: cal.getTimeInMillis()، ثم لفّها في Instant.ofEpochMilli(...).

أنواع JDBC: java.sql.Date و Time و Timestamp

أنواع JDBC هي أكثر مصادر التواريخ القديمة شيوعًا في تطبيقات الخلفية. لكل منها دالة جسر مباشرة toLocalXxx() — لاحظ أن هذه لا تمر بـInstant لأن تواريخ JDBC محلية صراحةً على مستوى البروتوكول.

import java.sql.*; import java.time.*; // --- java.sql.Date --- java.sql.Date sqlDate = java.sql.Date.valueOf("2024-03-15"); LocalDate localDate = sqlDate.toLocalDate(); // مباشر، لا منطقة زمنية مطلوبة java.sql.Date backToSql = java.sql.Date.valueOf(localDate); // --- java.sql.Time --- java.sql.Time sqlTime = java.sql.Time.valueOf("14:30:00"); LocalTime localTime = sqlTime.toLocalTime(); java.sql.Time backToTime = java.sql.Time.valueOf(localTime); // --- java.sql.Timestamp --- java.sql.Timestamp ts = java.sql.Timestamp.valueOf("2024-03-15 14:30:00.123456"); LocalDateTime ldt = ts.toLocalDateTime(); // محلي بلا منطقة، يحافظ على الدقة الكسرية Instant instant = ts.toInstant(); // المسار المدرك للمنطقة حين تحتاج UTC java.sql.Timestamp backToTs = java.sql.Timestamp.valueOf(ldt);
فضّل Timestamp.valueOf(LocalDateTime) على Timestamp.from(Instant) عند الكتابة في عمود قاعدة بيانات من نوع DATETIME (لا TIMESTAMP WITH TIME ZONE). مسار valueOf يحافظ على حقول التقويم الحرفية؛ مسار from يطبّق المنطقة الافتراضية للـJVM مما قد يُزيح القيم بصمت في MySQL وما شابهها.

نمط هجرة عملي

حين تُورَث قاعدة كود كبيرة لا تستطيع دائمًا إعادة كتابة كل طبقة دفعة واحدة. إليك استراتيجية تدريجية آمنة:

  1. حوّل عند الحدود. دع فئات النموذج الداخلية وكائنات نقل البيانات تستخدم java.time. حوّل من/إلى الأنواع القديمة فقط عند حدود الإدخال/الإخراج — حيث تستدعي مكتبة قديمة أو تقرأ من JDBC.
  2. اكتب دالة محوّل مخصصة. مركز منطق التحويل. عندما تُحدّث المكتبة القديمة لاحقًا تغيّر مكانًا واحدًا فقط.
  3. استخدم @SuppressWarnings("deprecated") بتحفّظ. ابقِ كود التحويل القديم معزولًا ليسهل حذفه حين تختفي التبعية القديمة.
// فئة محوّل — مكان واحد يمتلك جميع تحويلات الإصدارات القديمة public final class DateBridge { private DateBridge() {} /** تحويل Date قديم من مكتبة طرف ثالث إلى LocalDateTime * باستخدام المنطقة الزمنية المعيارية للتطبيق. */ public static LocalDateTime toLocalDateTime(Date legacy, ZoneId appZone) { return legacy.toInstant().atZone(appZone).toLocalDateTime(); } /** تحويل LocalDateTime مجددًا إلى Date للمكتبة ذاتها. */ public static Date fromLocalDateTime(LocalDateTime ldt, ZoneId appZone) { return Date.from(ldt.atZone(appZone).toInstant()); } }

الخلاصة

لكل نوع قديم جسر نظيف إلى java.time:

  • Date.toInstant() / Date.from(Instant) — البوابة العالمية.
  • GregorianCalendar.toZonedDateTime() / GregorianCalendar.from(ZonedDateTime) — يحفظ المنطقة الزمنية الأصلية.
  • java.sql.Date.toLocalDate() وTime.toLocalTime() وTimestamp.toLocalDateTime() — مسارات JDBC بلا منطقة زمنية.

القاعدة الذهبية: كن صريحًا دائمًا بشأن المناطق الزمنية أثناء التحويل. معظم الأخطاء التي اعترت الواجهة القديمة نشأت من افتراضات ضمنية للمنطقة الزمنية — يُلزمك java.time بالتصريح بهذه الافتراضات في الكود.