أستطيع إخبارك بإصدار Java لكل قاعدة كود عملت عليها، لأنني كتبت معظمها. Java 6 لـJ2ME في الجامعة عام 2009. Java 7 و8 في BluLogix وAFAQ. Java 12 في مرحلة ما في الوسط. Java 17 في Bytro وما بعده. Kotlin كلغة يومية منذ 2020.

هذه ليست مراجعة لمواصفات اللغة. هذا ما تغيّر فعلاً بالنسبة لي كمهندس عملي عند كل نقطة تحول - وكيف كانت تبدو السنوات الموحشة بينها حين وصلت.

Java 6: حقبة J2ME، أي العذاب

أول Java لي كانت J2ME - Java Micro Edition - كتابة تطبيقات موبايل للهواتف الأساسية في الجامعة حوالي 2009. إن لم تكن قد كتبت J2ME يوماً، دعني أرسم لك الصورة: لا generics في المجموعة الفرعية التي حصلت عليها، لا Collections API كامل، تجريد شاشة يختلف بحسب الجهاز، وتنسيق تعبئة (JAR كـMIDlet) جعلك تعرف البايتات والأحجام بشكل حميمي لن يعرفه مهندسو اليوم أبداً.

القيد كان هو النقطة. تطبيق J2ME يتجاوز 64 كيلوبايت كان يُرفض من مشغّلين بعينهم. كتبت كود تخطيط لا أستطيع إظهاره لأحد دون شرح. كنت أُصحّح على الجهاز لأن المحاكيات كانت خاطئة بما يكفي لتضليلك. شحنت تطبيقات تعمل على هواتف Nokia بذاكرة heap من 2 ميجابايت.

هنا تعلمت أن تجريدات الـJVM ليست مجانية، وأنك تستطيع جعلها رخيصة إذا فهمت بالضبط ما تدفعه. هذا الدرس كان مفيداً كل يوم منذ ذلك الحين.

Java 7: try-with-resources أنهى ثلاث فئات كاملة من الأخطاء

صدر Java 7 عام 2011. حين كنت أستخدمه مهنياً في BluLogix (2013)، الترقية التي أحببتها فوراً كانت try-with-resources.

قبل ذلك، إغلاق الموارد في Java كان ضريبة محددة: كتلة finally تحتاج بحد ذاتها تحقق من null، لأن المورد ربما لم يُفتح، والإغلاق ربما يُلقي استثناءً، وإذا رمى أثناء وجودك في استثناء أصلاً، ستبتلع الاستثناء الأصلي، والآن أنت تُصحّح شبحاً. الأسلوب كان معروفاً ومزعجاً على نطاق واسع:

InputStream is = null;
try {
    is = new FileInputStream(path);
    // do stuff
} catch (IOException e) {
    // handle
} finally {
    if (is != null) {
        try { is.close(); } catch (IOException ignored) {}
    }
}

مع Java 7:

try (InputStream is = new FileInputStream(path)) {
    // do stuff
}

هذا ليس مجرد وسيلة راحة بسيطة. هذا المُصرِّف يُطبّق دورة حياة الموارد التي كانت تتطلب انضباطاً ويقظة. أقدّر أنني أصلحت ما لا يقل عن اثني عشر خطأ تسريب موارد في قواعد كود قديمة بترقيتها إلى try-with-resources. ولم أكتب خطأً كهذا منذ ذلك الحين.

Diamond operator (<>) صدر مع Java 7 أيضاً - أصبح بإمكانك كتابة new ArrayList<>() بدلاً من new ArrayList<String>(). يبدو سخيفاً الآن. آنذاك، أزال احتكاكاً محدداً كنت أواجهه 30 مرة في اليوم.

Java 8: النسخة التي أعادت صياغة تفكيري حقاً

Java 8 (2014) هي أكبر نقطة تحوّل في مسيرتي مع Java. ليس بسبب lambdas بشكل مجرد. بل بسبب ما مكّنته lambdas في الواقع: Streams API، وOptional، ومراجع الأساليب - ومجموع الثلاثة معاً يغيّر طريقة تفكيري في تحويل البيانات.

قبل Java 8، معالجة قائمة في Java كانت حلقة for-each، ومتراكم محلي، وتحقق من null في كل وصول، وبناء مجموعات صريح. الكود كان صحيحاً لكن مطوّلاً بطريقة تجعل القصد صعب القراءة بنظرة سريعة.

بعد Java 8:

orders.stream()
    .filter(o -> o.getStatus() == ACTIVE)
    .map(Order::getTotal)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

المنطق في الـpipeline. تقرأه من الأعلى للأسفل ويظهر شكل تحويل البيانات. التراكم ضمني. filter وmap وreduce عمليات مُسمّاة بدلالات واضحة.

ما لاحظته فعلاً: بدأت مراجعات الكود تحمل محادثات مختلفة. بدلاً من “هذه الحلقة تبدو خاطئة، لنتتبعها”، كنا نتحدث عن “لماذا تُجسّد هنا، هل يمكنك البقاء كسولاً؟” و”هذا filter + map يمكن أن يكون flatMap.” الأوليات غيّرت ما كنا نتجادل بشأنه.

Optional كانت موضع خلاف. لا تزال كذلك. أحبها لأنواع الإرجاع حيث الغياب ذو معنى دلالي وتريد إجبار المُستدعي على مواجهة ذلك الغياب. لا أحب Optional كنوع حقل، ومعامل دالة، أو عنصر مجموعة. هذا، أعتقد، هو الموقف الصحيح. استغرق الأمر سنتين للوصول إليه بشكل كامل.

حقبة Java 9 Modules: الفوضى الأسطورية

Java 9 شحن نظام Java Platform Module System (JPMS). نظرياً: طريقة للإعلان عن حدود الوحدات الصريحة، والتحكم في الحزم التي تكشفها، وبناء تطبيق Java كبير النطاق أكثر قابلية للصيانة. في الواقع: ست سنوات من العذاب لأي شخص لديه اعتماديات.

المشكلة كانت في النظام البيئي. JPMS تطلّب من كل مكتبة إما الشحن كوحدة مُسمّاة صحيحة أو المعاملة كـ”وحدة تلقائية” بأسماء وحدات مشتقة بشكل اكتشافي قد تتغير بين الـbuilds. لم يكن معظم النظام البيئي لـJava جاهزاً. النتيجة: إضافة علامة --module-path لمشروعك ثم قضاء يوم في حل تعارضات split-package، والوحدات غير المُسمّاة، وتعاويذ --add-opens للـreflection الذي تعتمد عليه المكتبات.

كتبت --add-opens java.base/java.lang=ALL-UNNAMED أكثر مما أستطيع عدّه. في كل مرة أفعلها، جزء صغير من روحي يغادر.

Java 10 و11 و12 حسّنت الأمور على الهوامش. Local variable type inference (var) وصل في Java 10 واعتمدته فوراً - ليس لأنه يوفر أحرفاً، بل لأنه يمنعك من كتابة النوع مرتين حين يقول المُنشئ بالفعل ما هو:

var connections = new HashMap<String, ConnectionPool>();

وضع الوحدات تحسّن ببطء. Spring وHibernate والكبار الآخرون أصلحوا أوضاعهم. بحلول وصولي لـJava 17، كانت درامات JPMS في الغالب ضجيجاً خلفياً لا حادثة نشطة.

Java 17: أخيراً Java الحديثة

Java 17 إصدار LTS (سبتمبر 2021) وهو أول إصدار أنظر فيه إلى Java ككل وأفكر: هذه لغة حديثة.

Records:

record Point(double x, double y) {}

وكفى. نوع قيمة غير قابل للتغيير، equals وhashCode وtoString كلها مُولَّدة. لا Lombok. لا كلاس من 40 سطراً. مجرد إعلان عما تكون عليه البيانات. أستخدم records في كل مكان الآن - DTOs وكائنات الأوامر وحمولات الأحداث وكائنات قيم الدومين. تكلفة القوالب الجاهزة لأنواع قيم Java انتقلت من “مزعجة” إلى “صفر”.

Sealed classes + pattern matching:

sealed interface Shape permits Circle, Rectangle {}

double area = switch (shape) {
    case Circle c -> Math.PI * c.radius() * c.radius();
    case Rectangle r -> r.width() * r.height();
};

المُصرِّف يعرف أن Shape يمكن أن يكون فقط Circle أو Rectangle. إذا أضفت نوعاً ثالثاً في permits ونسيت معالجته في الـswitch، تحصل على خطأ تصريف. هذا أنواع البيانات الجبرية تصل إلى Java بعد 25 عاماً من وجودها في ML. أفضل متأخراً من لا شيء - وإصدار Java منها عملي وطبيعي ويتكامل بنظافة مع نظام النوع الحالي.

Text blocks - حرفيات نصية متعددة الأسطر بلا جنون escape - وصلت في Java 15 وأستخدمها باستمرار لـSQL ومقتطفات JSON في الاختبارات وأي نص يمتد لأكثر من سطرين.

صعود Kotlin بالتوازي

أريد أن أكون صريحاً هنا: بحلول وصول Java 17 مع records والـsealed types، كنت قد كتبت Kotlin لسنتين في Bytro وهو أعاد معايرة توقعاتي. Data classes. سلامة null مبنية في نظام النوع. Extension functions. Coroutines للـasync. Smart casts تُلغي نصف سلاسل instanceof لديك.

Kotlin هو الذات الظلية لـJava - ما كانت Java لتكون عليه لو صُمِّمت من الصفر حوالي 2012 لا 1995. تعمل على الـJVM، تتعامل مع Java بشكل شبه مثالي، وتُصلح معظم الأشياء التي استغرقت Java حتى إصدار 17 لإصلاحها.

أكتب Kotlin حين أريد كتابة Kotlin. أكتب Java 17 حين المشروع بـJava ولا أريد إدخال اعتمادية لغة. كلاهما لغة يومية. الـJVM تحتهما هو نفس الـJVM، مما يعني أن معرفة ضبط GC، ومهارات تشخيص الـheap، وقراءة thread dumps، ومراقبة JMX - كل ذلك ينتقل. الـJVM هو الاستثمار الدائم؛ Java وKotlin هما طبقتا اللغة فوقه.

ما تُعلّمك إياه 15 سنة على الـJVM فعلاً

الجواب الصادق: تتوقف عن الاندهاش من التخصيصات وتبدأ التفكير الاستراتيجي فيها. تُطوّر آراء حول توقف garbage collector يمكنك الدفاع عنها بأرقام. تفهم لماذا يهم String interning وبالضبط متى يهم ومتى لا يهم.

تحسينات اللغة حقيقية. Java 17 أفضل بشكل كبير من Java 6. لكن المهارات الأساسية - فهم نموذج التكلفة، وقراءة تشخيصات JVM، والتفكير في أمان الخيوط، والتصميم للقابلية للاختبار - تلك لا تتغير بنفس قدر تغيير بناء الجمل.

J2ME عام 2009 علّمتني عدّ البايتات. Streams عام 2014 علّمتني التفكير في pipelines. Records عام 2021 أخبرتني أن اللغة أخيراً تلحق بكيفية تفكيري في أنواع القيم منذ سنوات.

ما أزلت هنا. الـJVM ما زال هنا. Java في الإصدار 21 الآن (LTS). لديّ آراء حول virtual threads (Project Loom حقيقي وجيد). وسيكون لديّ آراء حول ما يصدر في Java 25.

هذا، فيما يبدو لي، هو ما تبدو عليه علاقة 15 عاماً مع منصة ما.