> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Java لمن يعرف البرمجة: JDK 25 والأدوات Toolchain
- URL: https://arabroot.io/articles/java-لمن-يعرف-البرمجة-الادوات-والصيغة/
- Published: 2026-10-31T07:23:46.000Z
- Updated: 2026-10-31T08:25:53.000Z
- Description: تثبيت JDK 25 وجولة في صيغة Java مقارنة بما تعرفه، ثم مشروع Gradle فيه منسق وفاحص واختبارات، مع أخطاء زرعناها لنرى أين يمسكها البناء.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

لنفرض أنك مطور تكتب Python أو JavaScript منذ سنوات، وانتقلت إلى فريق يدير خدمة الطلبات والمدفوعات لمتجر إلكتروني، والخدمة مكتوبة بـ Java منذ عشر سنوات، وأول مهمة وصلتك تعديل صغير في حساب ضريبة القيمة المضافة VAT على الفاتورة. والمهمة نفسها لا تحتاج أكثر من سطرين، ولكنك عندما تفتح المستودع تجد أمامك مجلدات عميقة مثل `src/main/java/store`، وملف بناء اسمه `build.gradle.kts`، وزميلاً يقول لك «شغل `gradle build` قبل أن ترفع التعديل»، وآخر يسألك «أي JDK عندك؟». وأنت لا تريد دورة في البرمجة من الصفر، فأنت تعرف المتغيرات والدوال والكائنات، وإنما تريد أن تعرف كيف تكتب ما تعرفه بطريقة Java، وما الأدوات التي يستخدمها الفريق، ولماذا هي بهذا الشكل، حتى تنجز المهمة اليوم وتفهم ما يحدث عندما يفشل البناء.

وهذا المقال هو الأول في مرحلة [لغة Java للمطورين](https://arabroot.io/start/#stage-lang-java)، وهي جزء من مسار لغات البرمجة الذي يأخذ كل لغة بنفس الترتيب: الأدوات والصيغة، ثم الأنواع والذاكرة والأخطاء، ثم التزامن، ثم خدمة حقيقية، ثم الأخطاء الشائعة. وسوف نبني فيه كل شيء بتجربة حقيقية على JDK 25 وGradle 9.8.2: برنامج من ملف واحد، ثم برنامج من عدة ملفات، ثم مشروع Gradle فيه منسق وفاحص واختبارات، ثم نزرع ثلاثة أخطاء ونرى أين أمسكها كل جزء من أدوات البناء. أما قصة اللغة نفسها من Sun إلى Oracle فقد شرحناها في [قصة Java](https://arabroot.io/articles/%D9%82%D8%B5%D8%A9-java-%D9%85%D9%86-sun-%D8%A7%D9%84%D9%89-oracle/)، ومسألة أي JDK تستخدم في الشركة ومتى تحتاج اشتراكاً في [هل Java مجانية؟](https://arabroot.io/articles/%D9%87%D9%84-java-%D9%85%D8%AC%D8%A7%D9%86%D9%8A%D8%A9-%D9%88%D8%A7%D9%8A-jdk-%D8%AA%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%81%D9%8A-%D8%B4%D8%B1%D9%83%D8%AA%D9%83/)، فلن نكرر ذلك هنا.

وسوف نناقش في هذا المقال ما يلي:

- لماذا ما زالت Java لغة الأنظمة الكبيرة، وما هي الآلة الافتراضية JVM وقطار الإصدارات.
- تثبيت JDK 25 وقراءة مخرج `java -version` سطراً بسطر، وما الأدوات التي تأتي معه.
- أول برنامج دون class بفضل الملفات المصدرية المختصرة Compact Source Files، والتشغيل المباشر من الملف، وJShell.
- جولة في الصيغة مقارنة بـ Python وJavaScript: الأنواع، والسجلات Records، وswitch، والمجموعات، والنصوص، وتجاوز المدى.
- أدوات المشروع: Gradle وهيكل المجلدات، والمنسق Formatter، والفحص الثابت Static Analysis، والاختبارات، والتشغيل.
- المشكلة التي ظهرت في التجربة: بناء أبطأ ست مرات بسبب مكان الملفات، ثم الأخطاء الشائعة وقائمة تحقق لأول يوم.

## لماذا Java اليوم؟ الآلة الافتراضية JVM والمنظومة Ecosystem

عندما تكتب Python فأنت تعطي الملف للمفسر CPython ويقرؤه وقت التشغيل، وعندما تكتب Go أو C فأنت تترجم الكود إلى ملف تنفيذي لمعالج ونظام تشغيل محددين. أما Java فقد اختارت طريقاً ثالثاً منذ 1995، وهو أن المترجم `javac` لا يخرج كود آلة، وإنما يخرج تعليمات لآلة افتراضية اسمها Java Virtual Machine واختصاراً JVM، وتسمى هذه التعليمات Bytecode وتحفظ في ملفات `.class`. وبعد ذلك تقوم الـ JVM على كل جهاز بتحميل هذه الملفات وتشغيلها، فتبدأ بتفسيرها، ثم تراقب الدوال التي تتكرر كثيراً وتترجمها أثناء التشغيل إلى كود آلة محسن بتقنية الترجمة أثناء التشغيل Just-In-Time واختصاراً JIT، وقد شرحنا هذه الآلية بالتفصيل وبالقياس في مقال [المترجم والمفسر وJIT](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AA%D8%B1%D8%AC%D9%85-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%B3%D8%B1-%D9%88-jit-%D9%88-webassembly-%D8%A8%D8%A7%D9%84%D8%AA%D9%81%D8%B5%D9%8A%D9%84/).

وهذا الاختيار هو سبب ما تراه في الشركات الكبيرة: ملف JAR بني على جهاز مطور يعمل بنفس الشكل على سيرفر Linux بمعالج ARM، والـ JVM نفسها تحسنت عبر ثلاثين سنة حتى صارت من أنضج بيئات التشغيل في جمع القمامة Garbage Collection والترجمة والمراقبة. ولهذا بنيت عليها لغات أخرى مثل Kotlin وScala وClojure، وصارت مكتبات Java في كل مجال تقريباً، من أطر الويب مثل Spring إلى معالجة البيانات مثل Kafka وElasticsearch. والجانب الآخر من هذه الصورة أن Java لها ثمن تدفعه في زمن البدء Startup Time وفي استهلاك الذاكرة مقارنة بلغة تترجم إلى ملف تنفيذي، وسوف ترى هذا الثمن بالأرقام في هذا المقال وفي مقال الخدمة الحقيقية من هذه المرحلة. وإذا كنت ما زلت تقارن بين اللغات لمشروع جديد فقد قارنا بين أربع لغات في [كيف تختار لغة البرمجة لمشروعك](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/).

وأهم ما تحتاج معرفته عن الإصدارات أن Java تصدر نسخة جديدة كل ستة أشهر، في مارس وسبتمبر، وأن بعض هذه النسخ تحمل دعماً طويل الأمد Long-Term Support واختصاراً LTS، والشركات في الغالب لا تنتقل إلا بين نسخ LTS. وآخرها حالياً Java 25 التي صدرت في سبتمبر 2025، وقبلها 21 ثم 17 ثم 11، ويمكنك رؤية جدول الدعم في [خارطة دعم Java SE](https://www.oracle.com/java/technologies/java-se-support-roadmap.html?ref=arabroot.io). ولذلك فإن السؤال الأول في أي فريق ليس «هل تعرف Java؟» وإنما «أي Java؟»، والسبب أن الكود المكتوب لـ Java 8 يختلف كثيراً في أسلوبه عن الكود المكتوب لـ Java 25، وكثير من النصائح التي ستجدها في الإنترنت كتبت قبل عشر سنوات ولم تعد الطريقة الأنسب اليوم.

## تثبيت JDK 25: أي توزيعة Distribution وأي إصدار Version؟

تحتاج لكتابة Java إلى عدة التطوير Java Development Kit واختصاراً JDK، وهي تحتوي على المترجم والأدوات والـ JVM معاً. وكل التوزيعات المعروفة مبنية من نفس الكود المصدري في مشروع OpenJDK، والفرق بينها في من يبنيها ويدعمها وبأي رخصة، وقد فصلنا ذلك في [دليل الرخص](https://arabroot.io/articles/%D9%87%D9%84-java-%D9%85%D8%AC%D8%A7%D9%86%D9%8A%D8%A9-%D9%88%D8%A7%D9%8A-jdk-%D8%AA%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%81%D9%8A-%D8%B4%D8%B1%D9%83%D8%AA%D9%83/). وفي هذا المقال استخدمنا Eclipse Temurin من مؤسسة Adoptium، وهو الإصدار الموجود داخل الـ Docker Image الرسمية لـ Gradle، فلم نحتج لتثبيت أي شيء على الجهاز نفسه غير Docker.

وعلى جهازك أمامك ثلاث طرق شائعة. الطريقة الأولى مدير الحزم في النظام، مثل `apt install openjdk-25-jdk` في Ubuntu أو `winget` في Windows أو `brew` في macOS، وهي مناسبة إذا كنت تعمل على مشروع واحد. والطريقة الثانية أداة SDKMAN في Linux وmacOS، وهي تسمح لك بتثبيت عدة إصدارات والتبديل بينها لكل مشروع، وهذا مفيد لأنك سوف تجد في نفس الشركة خدمة على Java 17 وأخرى على Java 21\. والطريقة الثالثة أن تبني داخل Container فيه JDK، وهي الطريقة التي سوف نستخدمها في كل تجارب هذه المرحلة حتى تكون النتائج قابلة للتكرار. والأمر التالي يشغل Container من Image فيها JDK 25 وGradle، ويطبع الإصدارات:

```bash
docker run --rm gradle:9.8.2-jdk25-noble sh -c 'java -version; javac -version; jshell --version; gradle --version'
```

والمخرج سوف يكون كما يلي:

```text
openjdk version "25.0.5" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing)
javac 25.0.5
jshell 25.0.5

------------------------------------------------------------
Gradle 9.8.2
------------------------------------------------------------

Build time:    2026-10-19 16:29:26 UTC
Revision:      8de7bcf76e785507ad92f0741ab9235a430c95e6

Kotlin:        2.4.10
Groovy:        4.0.33
Ant:           Apache Ant(TM) version 1.10.17 compiled on April 6 2026
Launcher JVM:  25.0.5 (Eclipse Adoptium 25.0.5+8-LTS)
```

في المخرج أعلاه لاحظ التالي:

- السطر الأول يعطيك رقم الإصدار `25.0.5` وتاريخه، والكلمة `LTS` تعني أنه من خط الدعم الطويل. والرقم الأول هو الإصدار الرئيسي Feature Release، والرقم الأخير هو التحديث الذي يحمل إصلاحات أمنية، وتصدر هذه التحديثات كل ثلاثة أشهر، ولذلك يجب أن تحدث الـ JDK في الإنتاج بانتظام مثل أي مكتبة أخرى.
- السطر الثاني يقول من بنى هذه النسخة: `Temurin-25.0.5+8`، والرقم بعد `+` هو رقم البناء Build Number. وهذا السطر هو أسرع طريقة لتعرف المورد Vendor إذا سألك أحد عن الرخصة.
- السطر الثالث عن الـ JVM نفسها: `64-Bit Server VM` هي HotSpot، و`mixed mode` تعني أنها تفسر وتترجم بـ JIT معاً، و`sharing` تعني أن مشاركة بيانات الـ Classes Class Data Sharing مفعلة، وهي ملف جاهز من الـ Classes الأساسية تحمله الـ JVM من القرص بدل أن تحللها في كل تشغيل، وهذا يقلل زمن البدء.
- الأسطر التالية تؤكد أن `javac` و`jshell` بنفس الإصدار، وأن Gradle 9.8.2 يعمل فوق نفس الـ JVM، ولاحظ أن Gradle يحمل معه Kotlin وGroovy لأن ملفات البناء تكتب بإحداهما، ولا علاقة لذلك بلغة مشروعك.

وإذا ثبت JDK على جهازك فتذكر متغير البيئة `JAVA_HOME`، فكثير من الأدوات مثل Gradle وMaven وبيئات التطوير تبحث عنه أولاً، وإذا كان يشير إلى نسخة قديمة فسوف ترى أخطاء غريبة رغم أن `java -version` في الطرفية يطبع النسخة الصحيحة.

### ماذا يوجد داخل JDK؟ أدوات سطر الأوامر Command-Line Tools

الصورة التالية تبين الطريق من الملف الذي تكتبه إلى البرنامج الذي يعمل، والأدوات التي تأتي مع الـ JDK وسوف تستخدمها في هذه المرحلة:

![مخطط يبين الكود المصدري ثم المترجم javac ثم Bytecode ثم الآلة الافتراضية JVM، وتحتها أدوات JDK مثل jshell وjar وjlink وjcmd](https://arabroot.io/content/images/2026/10/java-for-devs-01-jdk-toolchain.webp)

من الملف `.java` إلى الـ Bytecode ثم إلى الـ JVM، والأدوات التي تأتي مع JDK 25.

والأدوات التي تحتاجها في الأسبوع الأول قليلة: `javac` للترجمة، و`java` للتشغيل، و`jshell` للتجربة السريعة، و`jar` لحزم الـ Classes في ملف واحد. أما `jcmd` و`jstack` فسوف تحتاجهما عندما تفحص برنامجاً يعمل، فالأولى تطبع معلومات الذاكرة والخيوط وتطلب تفريغ الذاكرة Heap Dump، والثانية تطبع حالة كل خيط Thread، وسوف نستخدمهما في مقالي الذاكرة والتزامن من هذه المرحلة لنكشف تسرباً في الذاكرة وجموداً Deadlock. وأداة `jlink` تبني بيئة تشغيل مصغرة فيها فقط الوحدات Modules التي يحتاجها برنامجك، وقد جربناها في دليل الرخص. ولاحظ أن الـ JDK لم تعد تأتي معها بيئة تشغيل منفصلة اسمها JRE كما كانت قديماً، ولكن موردي التوزيعات يبنون Images باسم `jre` فيها الـ JVM دون المترجم والأدوات، وهي ما سوف نستخدمه في الإنتاج لأن حجمها أصغر.

## أول برنامج دون class: الملفات المصدرية المختصرة Compact Source Files

إذا كنت قد رأيت Java قبل سنوات فأنت تتذكر أن أصغر برنامج يحتاج class عامة ودالة `public static void main(String[] args)` وسطر `System.out.println`، وهذا كان أول ما يشتكي منه كل من يأتي من Python. وفي Java 25 صار هذا اختيارياً، فالمقترح [JEP 512: Compact Source Files and Instance Main Methods](https://openjdk.org/jeps/512?ref=arabroot.io) أصبح جزءاً نهائياً من اللغة، ويسمح لك بكتابة دالة `main` مباشرة في الملف، والمترجم ينشئ class ضمنية حولها. والملف التالي هو أول برنامج في التجربة، ويحسب مجموع ثلاثة أسعار بالهللات:

```java
void main() {
    var prices = List.of(990, 1450, 2300);
    int total = 0;
    for (int p : prices) {
        total += p;
    }
    IO.println("products: " + prices.size() + ", total halalas: " + total);
}
```

ونشغله دون أي خطوة ترجمة منفصلة:

```bash
java Hello.java
```

```text
products: 3, total halalas: 4740
```

في الكود أعلاه لاحظ التالي:

- لا توجد class ولا `public static` ولا `String[] args`، والدالة `main` هنا دالة عادية في كائن ينشئه المشغل.
- استخدمنا `List` دون أي `import`، لأن الملفات المختصرة تستورد تلقائياً كل الحزم التي تصدرها الوحدة `java.base`، وهي التي فيها المجموعات والنصوص والملفات.
- الكلاس `IO` جديدة في `java.lang`، وفيها `println` و`readln` للقراءة من الطرفية، وهي اختصار لـ `System.out.println` الذي ما زال يعمل كما هو.
- الأسعار بالهللات وليست بالريال، وسبب ذلك سوف تراه في مقال الأنواع من هذه المرحلة: الأعداد العشرية Floating Point لا تمثل 0.1 بدقة، والمبالغ المالية تحفظ أعداداً صحيحة بأصغر وحدة.

وقد يتساءل البعض: هل هذه Java «حقيقية» أم صيغة للتعليم فقط؟ والإجابة أنها Java كاملة، ونفس الملف يمكن أن يكبر إلى class عادية عندما تحتاج حزمة أو عدة classes، ولكنها مصممة للسكربتات الصغيرة والأدوات الداخلية والتجارب، أما مشروع الفريق فسوف يبقى على الشكل المعتاد من classes في حزم، كما سترى في مشروع Gradle بعد قليل.

### التشغيل من الملف مباشرة Source Launch: كم يكلف؟

الأمر `java Hello.java` لا يكتب أي ملف `.class` على القرص، وإنما يشغل المترجم داخل الذاكرة ثم ينفذ الناتج، وهذه الميزة موجودة منذ Java 11 بالمقترح [JEP 330](https://openjdk.org/jeps/330?ref=arabroot.io). وسهولة هذا الأمر قد تخفي عنك ثمنه، فقمنا بقياس الطريقتين خمس مرات لكل واحدة: التشغيل من الملف المصدري، ثم الترجمة مرة واحدة بـ `javac` والتشغيل من الـ Bytecode الجاهز. والمخرج كما يلي:

```text
java Hello.java        3.132 s
java Hello.java        3.125 s
java Hello.java        2.947 s
java Hello.java        3.299 s
java Hello.java        2.487 s
java -cp /tmp/out Hello 0.135 s
java -cp /tmp/out Hello 0.109 s
java -cp /tmp/out Hello 0.158 s
java -cp /tmp/out Hello 0.136 s
java -cp /tmp/out Hello 0.185 s
```

![ثلاث طرق لتشغيل كود Java مع مقارنة زمن التشغيل من الملف المصدري وزمن التشغيل من Bytecode جاهز](https://arabroot.io/content/images/2026/10/java-for-devs-02-run-paths.webp)

التشغيل من الملف المصدري يدفع ثمن الترجمة في كل مرة: 3 ثوان في المتوسط مقابل 0.14 ثانية.

والفرق كبير: حوالي ثلاث ثوان في المتوسط مقابل 0.14 ثانية، أي أن أغلب الوقت في الحالة الأولى يذهب إلى تشغيل المترجم `javac` نفسه، وهو برنامج Java كبير يبدأ بارداً في كل مرة قبل أن يترجم ملفاً من ثمانية أسطر. ولاحظ أن الأرقام أخذت داخل Container على جهاز كانت تعمل عليه برامج أخرى في نفس الوقت، فالرقم المطلق يختلف على جهازك، ولكن النسبة بين الطريقتين هي ما يهمك. وبالتالي فإن التشغيل من الملف المصدري مناسب لسكربت تشغله يدوياً، وغير مناسب إطلاقاً لأمر يعمل كل دقيقة في Cron Job، والحل الأنسب هو أن تترجم مرة واحدة وتشغل الـ Bytecode، وهذا ما تفعله أداة البناء لك.

### ماذا يحدث إذا ترجمت الملف لإصدار أقدم؟ الخيار --release

في الفريق سوف تجد غالباً أن الخدمة تبنى لإصدار محدد من Java حتى لو كان المطور يملك JDK أحدث، وهذا يتم بالخيار `--release` الذي يخبر المترجم أن يقبل فقط ما هو موجود في ذلك الإصدار. ولنرى ماذا يحدث إذا ترجمنا الملف المختصر لـ Java 21:

```bash
javac --release 21 -d /tmp/out21 Hello.java
```

```text
Hello.java:1: error: implicitly declared classes are not supported in -source 21
void main() {
^
  (use -source 25 or higher to enable implicitly declared classes)
1 error
```

والرسالة واضحة: الـ class الضمنية Implicitly Declared Class غير مدعومة قبل Java 25، والمترجم يقترح عليك الإصدار المطلوب. وهذه أول قاعدة تحتاجها في فريق فيه خدمات بإصدارات مختلفة: لا تفترض أن ميزة قرأت عنها موجودة في المشروع الذي أمامك، وابحث أولاً في ملف البناء عن `languageVersion` أو `release` أو `maven.compiler.release`، لأن هذا الرقم وليس الـ JDK المثبت عندك هو ما يحدد أي صيغة مسموحة.

### برنامج من عدة ملفات Multi-File Source Launch

منذ Java 22 صار التشغيل المباشر يقبل أكثر من ملف بالمقترح [JEP 458](https://openjdk.org/jeps/458?ref=arabroot.io)، فإذا كان الملف الذي تشغله يستخدم class في ملف آخر بنفس المجلد فإن المشغل يبحث عنه ويترجمه أيضاً. وفي التجربة وضعنا دالة الضريبة في ملف `Vat.java`:

```java
public class Vat {
    // 15% VAT in integer halalas, rounding half up
    static int withVat(int halalas) {
        return halalas + (halalas * 15 + 50) / 100;
    }
}
```

واستدعيناها من `Main.java`، ثم شغلنا `java Main.java`:

```text
total with VAT: 3416 halalas
```

والرقم 3416 هو نفس المثال الذي سوف نعود إليه في كل المرحلة: ثلاث قطع سعر الواحدة 9.90 ريال تعطي 29.70 ريال، والضريبة 15% منها 4.455 ريال، ومع التقريب لأقرب هللة نصفاً لأعلى Rounding Half Up تصبح 4.46، والمجموع 34.16 ريال. ولاحظ كيف حسبنا التقريب بالأعداد الصحيحة: نضرب في 15 ونضيف 50 ثم نقسم على 100، والقسمة في Java بين عددين صحيحين تحذف الجزء العشري، فإضافة النصف قبل القسمة هي التي تجعل 445.5 هللة تصبح 446\. وهذا الأسلوب يعمل للمبالغ الموجبة فقط، وسوف نكتب له اختباراً بعد قليل.

### التجربة السريعة في JShell REPL

إذا كنت معتاداً على تجربة سطر في مفسر Python أو في Node قبل أن تكتبه في الكود، فلديك نفس الشيء في Java باسم JShell، وهو حلقة قراءة وتنفيذ وطباعة Read-Eval-Print Loop واختصاراً REPL موجودة منذ Java 9\. وفي التجربة أدخلنا الأسطر التالية:

```java
int unit = 990;
unit * 3
(unit * 3 * 15 + 50) / 100
"9.90".equals("9.9")
var names = List.of("coffee", "dates")
names.add("cups")
```

والمخرج سوف يكون كما يلي:

```text
jshell> unit ==> 990

jshell> $2 ==> 2970

jshell> $3 ==> 446

jshell> $4 ==> false

jshell> names ==> [coffee, dates]

jshell> |  Exception java.lang.UnsupportedOperationException
|        at ImmutableCollections.uoe (ImmutableCollections.java:159)
|        at ImmutableCollections$AbstractImmutableCollection.add (ImmutableCollections.java:164)
|        at (#6:1)
```

في المخرج أعلاه لاحظ التالي:

- كل تعبير دون اسم يحفظ في متغير تلقائي مثل `$2` و`$3`، فيمكنك استخدامه في السطر التالي.
- السطر الرابع يقارن نصين بالدالة `equals` لا بالعلامة `==`، وسبب ذلك من أهم الفروق عن Python وJavaScript وسوف نعود إليه في مقال الأخطاء الشائعة: العلامة `==` بين كائنين تقارن هل هما نفس الكائن في الذاكرة، لا هل محتواهما متساو.
- السطر الأخير رمى استثناء، لأن `List.of` تنشئ قائمة غير قابلة للتعديل Immutable، ومحاولة الإضافة إليها ترمي `UnsupportedOperationException` وقت التشغيل، والمترجم لا يمنعك لأن النوع `List` فيه الدالة `add`. وهذه أول مفاجأة لمن يأتي من Python، فالقائمة هناك قابلة للتعديل دائماً، وإذا أردت قائمة تعدلها في Java فاستخدم `new ArrayList<>(...)`.

وتشغيل JShell داخل Container يحتاج الخيار `-i` في `docker run` حتى يصله الإدخال، وبدونه سوف يبقى ينتظر دون أي رسالة، وهذا ما حدث معنا في المحاولة الأولى. وإذا حفظت الأسطر في ملف ومررته إلى `jshell` فسوف ينفذها دون أن يطبع قيم التعبيرات، فاستخدم الإدخال من الطرفية عندما تريد رؤية النتائج.

## جولة في الصيغة Syntax مقارنة بما تعرفه

بعد أن عرفت كيف تشغل الكود، نأخذ الآن ملفاً واحداً فيه أغلب ما سوف تراه في كود الفريق، ونقرؤه مقارنة بما تعرفه من لغتك. والملف التالي هو `Tour.java` من التجربة، ويمثل جزءاً صغيراً من متجر: منتجات، ومخزون، وطريقة دفع، وإيصال:

```java
import java.util.*;
import java.util.stream.*;

public class Tour {

    // record: an immutable data carrier, like a Python dataclass(frozen=True) or a TS readonly type
    record Product(int id, String name, int priceHalalas) {}

    sealed interface Payment permits Cash, Card {}
    record Cash() implements Payment {}
    record Card(String last4) implements Payment {}

    static String describe(Payment p) {
        return switch (p) {              // switch expression with patterns, checked for exhaustiveness
            case Cash c -> "cash";
            case Card(String last4) -> "card ****" + last4;
        };
    }

    public static void main(String[] args) {
        var products = List.of(
            new Product(1, "coffee", 990),
            new Product(2, "dates", 4500),
            new Product(3, "cups", 1250));

        // static types, inferred with var
        var expensive = products.stream()
            .filter(p -> p.priceHalalas() > 1000)
            .map(Product::name)
            .toList();
        System.out.println("expensive: " + expensive);

        Map<String, Integer> stock = new HashMap<>();
        stock.put("coffee", 12);
        stock.merge("coffee", -2, Integer::sum);
        System.out.println("stock: " + stock);

        Optional<Product> found = products.stream().filter(p -> p.id() == 7).findFirst();
        System.out.println("id 7: " + found.map(Product::name).orElse("not found"));

        String receipt = """
            Order #%d
            total: %d halalas
            """.formatted(42, 7740);
        System.out.print(receipt);

        System.out.println(describe(new Card("4242")));
        System.out.println(products.get(0));           // records get toString/equals/hashCode
        System.out.println(products.get(0).equals(new Product(1, "coffee", 990)));

        int big = Integer.MAX_VALUE;
        System.out.println("int overflow: " + (big + 1));      // no exception, wraps around
        long sum = IntStream.rangeClosed(1, 1_000_000).asLongStream().sum();
        System.out.println("sum: " + sum);
    }
}
```

وعند تشغيل الأمر `java Tour.java` سوف تجد المخرج التالي:

```text
expensive: [dates, cups]
stock: {coffee=10}
id 7: not found
Order #42
total: 7740 halalas
card ****4242
Product[id=1, name=coffee, priceHalalas=990]
true
int overflow: -2147483648
sum: 500000500000
```

والجدول التالي يضع كل فكرة في هذا الملف بجانب ما يقابلها في Python وJavaScript، ثم نشرح الأجزاء التي تحتاج شرحاً:

![جدول يقارن صيغة Python وJavaScript وJava في المتغيرات والسجلات والقوائم والقواميس وswitch والنصوص وتجاوز المدى](https://arabroot.io/content/images/2026/10/java-for-devs-03-syntax-map.webp)

خريطة سريعة: الفكرة التي تعرفها، وكيف تكتب في Java 25.

### الأنواع الثابتة Static Typing والاستنتاج var

كل متغير في Java له نوع يعرفه المترجم قبل التشغيل، ولا يتغير بعد ذلك، وهذا هو الفرق الأكبر عن Python وJavaScript. والكلمة `var` الموجودة منذ Java 10 لا تجعل المتغير ديناميكياً، وإنما تطلب من المترجم أن يستنتج النوع من القيمة Type Inference، فالمتغير `expensive` نوعه `List<String>` حتى لو لم تكتبه، ولو حاولت أن تضع فيه رقماً بعد ذلك فسوف يرفض المترجم. واستخدم `var` للمتغيرات المحلية عندما يكون النوع واضحاً من السطر نفسه، واكتب النوع صراحة عندما يكون مفيداً للقارئ، كما فعلنا مع `Map<String, Integer> stock`، فهنا النوع يخبر من يقرأ الكود ماذا يوجد في القاموس.

ولاحظ أن Java تفرق بين الأنواع البدائية Primitive Types مثل `int` و`long` و`double` و`boolean`، وهي قيم مباشرة في الذاكرة، وبين الأنواع المرجعية Reference Types مثل `String` و`Integer` وكل الـ Classes، وهي مؤشرات إلى كائنات. والمجموعات Collections لا تقبل إلا الأنواع المرجعية، ولهذا كتبنا `Map<String, Integer>` وليس `int`، والمترجم يحول بين `int` و`Integer` تلقائياً فيما يسمى Autoboxing. وهذا التحويل له ثمن في الذاكرة وله فخ في المقارنة، وسوف نقيس الاثنين في مقال الأنواع والذاكرة.

### السجلات Records والأنماط Pattern Matching

السطر `record Product(int id, String name, int priceHalalas) {}` يغنيك عن عشرات الأسطر التي كان المطور يكتبها قديماً: حقول خاصة نهائية، ومنشئ Constructor، ودوال قراءة، و`equals` و`hashCode` و`toString`. والمخرج يبين ذلك، فطباعة المنتج أعطت `Product[id=1, name=coffee, priceHalalas=990]`، ومقارنته بمنتج آخر بنفس القيم أعطت `true`. ولاحظ أن دوال القراءة اسمها `priceHalalas()` وليس `getPriceHalalas()`، وأن السجل لا يقبل التعديل بعد إنشائه، فإذا أردت سعراً جديداً فأنت تنشئ سجلاً جديداً. وهذا يشبه `dataclass(frozen=True)` في Python، وهو الشكل المناسب لكل ما يمثل بيانات تنتقل بين أجزاء البرنامج، مثل طلب يصل من الـ API أو صف يرجع من قاعدة البيانات.

أما `sealed interface Payment permits Cash, Card` فتقول للمترجم إن طرق الدفع نوعان فقط ولا يوجد غيرهما، وبالتالي فإن `switch` في الدالة `describe` يستطيع أن يتحقق أنك غطيت كل الحالات دون أن تكتب `default`. وإذا أضاف زميلك نوعاً ثالثاً مثل `Wallet` بعد سنة، فإن المترجم سوف يرفض كل `switch` لم يغطه، وهذا أفضل بكثير من خطأ يظهر للعميل في الإنتاج. والسطر `case Card(String last4)` يفكك السجل ويأخذ الحقل منه مباشرة، وهذا ما يسمى أنماط السجلات Record Patterns، وهي قريبة من `match` في Python 3.10.

### المجموعات Collections والتدفقات Streams

المجموعات الأساسية التي سوف تراها في كل مكان ثلاث: `List` للقائمة المرتبة، و`Map` للقاموس، و`Set` لمجموعة دون تكرار. وكل واحدة منها واجهة Interface لها عدة تطبيقات، مثل `ArrayList` و`HashMap` و`HashSet`، وأسلوب Java أن تكتب نوع المتغير بالواجهة وتنشئ الكائن بالتطبيق، حتى تستطيع تغيير التطبيق لاحقاً دون تغيير بقية الكود. والدالة `merge` في المثال أنقصت المخزون من 12 إلى 10 في سطر واحد دون أن تقرأ القيمة ثم تكتبها، وهي من الدوال التي تستحق أن تعرفها مبكراً.

والتدفقات Streams هي الطريقة الحديثة لمعالجة المجموعات، وتقابل `filter` و`map` في JavaScript أو List Comprehension في Python. ولاحظ أن التدفق لا يعمل حتى تطلب نتيجة في النهاية مثل `toList()` أو `findFirst()`، وهذا ما يسمى التنفيذ الكسول Lazy Evaluation. وله فخ حقيقي وقعنا فيه في مقال التزامن من هذه المرحلة: الدالة `count()` قد لا تنفذ خطوة `map` قبلها أصلاً إذا كان العدد معروفاً من المصدر، فأعطى قياس كامل نتيجة 2 ملي ثانية لعمل يحتاج ثانيتين. والقيمة `Optional` في السطر الذي يبحث عن المنتج 7 هي طريقة Java لتقول «قد لا توجد نتيجة»، فبدلاً من أن ترجع الدالة `null` ويتذكر المستدعي أن يفحصه، ترجع صندوقاً قد يكون فارغاً، وتعطيك `orElse` قيمة بديلة. واستخدمها كقيمة راجعة من الدوال، ولا تستخدمها في حقول الكائنات أو المعاملات Parameters.

### النصوص Strings وتجاوز المدى Integer Overflow

الكتلة النصية Text Block بين ثلاث علامات تنصيص موجودة منذ Java 15، وتحفظ الأسطر كما هي وتحذف المسافات المشتركة في بداية الأسطر، و`formatted` تعمل مثل `printf`. ولا توجد في Java صيغة مثل f-strings في Python أو Template Literals في JavaScript، فإما `formatted` أو الجمع بالعلامة `+`.

أما السطر قبل الأخير في المخرج فهو أخطر ما في الملف: أضفنا 1 إلى أكبر قيمة يحملها `int` فأصبحت `-2147483648` دون أي استثناء. فالأعداد الصحيحة في Java لها حجم ثابت، `int` بـ 32 بت و`long` بـ 64 بت، وعندما تتجاوز الحد تلتف بصمت إلى الطرف الآخر، بينما في Python يكبر العدد كلما احتجت. وفي متجر يحسب المبالغ بالهللات فإن `int` يكفي حتى حوالي 21 مليون ريال في الفاتورة الواحدة، وهذا كاف لأغلب المتاجر، ولكن مجموع مبيعات سنة كاملة يحتاج `long`، ولهذا جمعنا المليون رقم في السطر الأخير بـ `asLongStream`. وإذا أردت أن يرمي الكود استثناء عند التجاوز بدل الالتفاف فاستخدم `Math.addExact` و`Math.multiplyExact`.

## أدوات المشروع Toolchain: Maven أم Gradle؟

إلى هنا كنا نشغل ملفات منفردة، ولكن مشروع الفريق يحتاج أكثر من ذلك: مكتبات خارجية تنزل بإصدارات محددة، وترجمة لعشرات الملفات، واختبارات، وفحص للتنسيق، وحزمة نهائية. وفي عالم Java أداتان تسيطران على هذا العمل: Maven التي تصف المشروع في ملف `pom.xml` وتتبع اصطلاحاً ثابتاً، وGradle التي تكتب البناء ككود في `build.gradle.kts` بلغة Kotlin أو `build.gradle` بلغة Groovy. وقد شرحنا الفرق بينهما بالتفصيل وبتجربة في [أدوات البناء](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D9%86%D8%A7%D8%A1-build-tools-%D9%85%D9%86-make-%D8%A7%D9%84%D9%89-maven-%D9%88-gradle-%D9%88-dotnet/)، وشرحنا كيف تحل الاعتماديات Dependencies وما قاعدة «الأقرب يفوز» في Maven في [مديرو الحزم](https://arabroot.io/articles/%D9%85%D8%AF%D9%8A%D8%B1%D9%88-%D8%A7%D9%84%D8%AD%D8%B2%D9%85-npm-%D9%88-pip-%D9%88-maven-%D9%88-cargo/).

والقاعدة العملية لك كمطور جديد في الفريق بسيطة: استخدم الأداة الموجودة في المشروع، ولا تقترح تغييرها في أسبوعك الأول. وإذا كنت تبدأ مشروعاً جديداً فكلاهما مناسب، وMaven أبسط في القراءة وأكثر انتشاراً في مشاريع الشركات القديمة، وGradle أسرع في البناء التدريجي وهي الأداة الافتراضية لمشاريع Android وKotlin. وفي هذه المرحلة اخترنا Gradle لأن مرحلة Kotlin تبني نفس الخدمة بنفس الأداة، فتكون المقارنة بين اللغتين على نفس الأرض، وكل ما سوف تراه من مفاهيم في هيكل المجلدات والاختبارات ينطبق على Maven أيضاً.

### هيكل المشروع Project Layout في Gradle

المشروع في التجربة اسمه `store-cli`، وهو أداة سطر أوامر صغيرة تحسب الفاتورة مع الضريبة. والصورة التالية تبين الملفات التي تكتبها، والملفات التي تخرجها الأداة:

![شجرة ملفات مشروع Gradle: ملفات البناء ومجلدات src/main/java وsrc/test/java، ومخرجات مجلد build](https://arabroot.io/content/images/2026/10/java-for-devs-04-gradle-layout.webp)

ما تضعه في Git على اليمين، وما تخرجه أداة البناء على اليسار.

وملف البناء كاملاً كما يلي:

```kotlin
plugins {
    application
    id("com.diffplug.spotless") version "8.10.4"
    id("net.ltgt.errorprone") version "5.2.0"
}

group = "store"
version = "0.1.0"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    errorprone("com.google.errorprone:error_prone_core:2.51.0")
    testImplementation(platform("org.junit:junit-bom:6.1.3"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

application {
    mainClass = "store.Main"
}

spotless {
    java {
        googleJavaFormat("2.0.0")
    }
}

tasks.test {
    useJUnitPlatform()
    testLogging {
        events("passed", "failed")
        exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.SHORT
    }
}
```

في الإعداد أعلاه لاحظ التالي:

- الإضافة `application` تضيف المهمة `run` وسكربتات التشغيل، والإضافتان الأخريان للمنسق والفحص الثابت، وكل إضافة لها إصدار مثبت، فلا يتغير البناء غداً لأن إصداراً جديداً صدر الليلة.
- الجزء `toolchain` يحدد إصدار Java الذي يبنى به المشروع بغض النظر عن الـ JDK الذي شغل Gradle، وهذا هو الرقم الذي ذكرناه في فقرة `--release`: هو ما يحدد الصيغة المسموحة.
- `mavenCentral()` هو السجل العام Registry الذي تنزل منه المكتبات، وهو نفس السجل الذي يستخدمه Maven.
- الأسطر في `dependencies` تقسم الاعتماديات حسب الاستخدام: `testImplementation` للاختبارات فقط فلا تدخل في الحزمة النهائية، و`errorprone` لا تدخل في الكود أصلاً وإنما في المترجم. والسطر `platform("org.junit:junit-bom:6.1.3")` قائمة مواد Bill of Materials تثبت إصدارات كل أجزاء JUnit معاً، فلا تكتب الإصدار في كل سطر.
- `mainClass` يجب أن يكون الاسم الكامل مع الحزمة، أي `store.Main` وليس `Main`.

وفي المشاريع الحقيقية سوف تجد أيضاً ملف `gradlew` ومجلد `gradle/wrapper`، وهو غلاف Gradle Wrapper ينزل إصدار Gradle المحدد في المشروع ويشغله، فيبني كل المطورين وخادم الـ CI بنفس الإصدار. واستخدم `./gradlew` بدلاً من `gradle` المثبت عندك متى وجدته. وفي التجربة استخدمنا `gradle` مباشرة لأن الـ Docker Image نفسها مثبتة على الإصدار 9.8.2.

### البناء الأول First Build: ماذا يحدث؟

الأمر `gradle build` يشغل سلسلة من المهام Tasks، وكل مهمة تعتمد على ما قبلها، والمخرج التالي من التجربة يبين ترتيبها:

```text
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :startScripts
> Task :distTar
> Task :distZip
> Task :assemble
> Task :spotlessJava
> Task :spotlessJavaCheck
> Task :spotlessCheck
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test

VatTest > rejectsNegative() PASSED

VatTest > roundsHalfUp() PASSED
```

والمهام بالترتيب: `compileJava` تترجم كود `src/main` ومعها Error Prone، و`processResources` تنسخ ملفات الإعداد ولم تجد شيئاً فطبعت `NO-SOURCE`، و`jar` تحزم الـ Classes، و`startScripts` و`distTar` و`distZip` تبني حزمة توزيع فيها سكربت تشغيل، ثم `spotlessCheck` تفحص التنسيق، ثم `compileTestJava` و`test` تترجمان الاختبارات وتشغلانها. وبعد ذلك تأتي `check` و`build` وهما مهمتان تجمعان ما قبلهما ولا تفعلان شيئاً بنفسيهما. وإذا شغلت الأمر مرة ثانية دون تغيير فسوف ترى `UP-TO-DATE` بجانب كل مهمة، لأن Gradle يحفظ بصمة المدخلات والمخرجات لكل مهمة ولا يعيدها إذا لم يتغير شيء، وهذا ما يسمى البناء التدريجي Incremental Build:

```text
BUILD SUCCESSFUL in 5s
9 actionable tasks: 9 up-to-date
```

ومن أين جاءت المكتبات؟ الأمر `gradle dependencies` يطبع الشجرة كاملة، وهذا جزء منها لاعتماديات الاختبار:

```text
testRuntimeClasspath - Runtime classpath of source set 'test'.
+--- org.junit:junit-bom:6.1.3
|    +--- org.junit.jupiter:junit-jupiter:6.1.3 (c)
|    +--- org.junit.platform:junit-platform-launcher:6.1.3 (c)
|    +--- org.junit.jupiter:junit-jupiter-engine:6.1.3 (c)
|    +--- org.junit.jupiter:junit-jupiter-api:6.1.3 (c)
|    +--- org.junit.jupiter:junit-jupiter-params:6.1.3 (c)
|    +--- org.junit.platform:junit-platform-engine:6.1.3 (c)
|    \--- org.junit.platform:junit-platform-commons:6.1.3 (c)
+--- org.junit.jupiter:junit-jupiter -> 6.1.3
|    +--- org.junit.jupiter:junit-jupiter-engine:6.1.3
|    |    +--- org.junit:junit-bom:6.1.3 (*)
|    |    +--- org.junit.platform:junit-platform-engine:6.1.3
|    |    |    +--- org.junit:junit-bom:6.1.3 (*)
|    |    |    +--- org.opentest4j:opentest4j:1.3.0
```

والحرف `(c)` يعني قيداً Constraint جاء من قائمة المواد، والسهم `-> 6.1.3` يعني أن السطر لم يحدد إصداراً فأخذه من القائمة، والنجمة `(*)` تعني أن الفرع طبع من قبل فلم يكرره. وكل هذه المكتبات نزلت مرة واحدة إلى `~/.gradle/caches`، وكان حجم هذا المجلد بعد التجربة 68 ميجابايت، وهو مشترك بين كل مشاريعك على الجهاز.

### المنسق Formatter: google-java-format عبر Spotless

في Python تستخدم Black أو Ruff، وفي JavaScript تستخدم Prettier، وفي Java لا يوجد منسق رسمي يأتي مع الـ JDK، ولذلك تتفق الفرق على أداة، وأكثرها انتشاراً [google-java-format](https://github.com/google/google-java-format?ref=arabroot.io) التي تطبق دليل أسلوب Google، وتشغل من داخل البناء بإضافة [Spotless](https://github.com/diffplug/spotless?ref=arabroot.io). والفائدة أن النقاش حول المسافات والأقواس ينتهي، فالأداة تقرر والبناء يفشل إذا خالفها أحد. وفي التجربة أضفنا مسافات زائدة إلى سطر في `Main.java` ثم شغلنا `gradle spotlessCheck`:

```text
FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':spotlessJavaCheck' (registered in build file 'build.gradle.kts').
> The following files had format violations:
      src/main/java/store/Main.java
          @@ -3,7 +3,7 @@
           public·class·Main·{
           ··public·static·void·main(String[]·args)·{
           ····int·unit·=·args.length·>·0·?·Integer.parseInt(args[0])·:·990;
          -········int·qty·=····args.length·>·1·?·Integer.parseInt(args[1])·:·3;
          +····int·qty·=·args.length·>·1·?·Integer.parseInt(args[1])·:·3;
           ····System.out.printf("%d·x·%d·halalas·=·%d·with·VAT%n",·qty,·unit,·Vat.total(unit,·qty));
```

والمخرج يعرض الفرق Diff بين ما كتبته وما تريده الأداة، والنقطة `·` تمثل مسافة حتى تراها، والسطر الذي يبدأ بـ `-` هو الحالي، والذي يبدأ بـ `+` هو المطلوب. ولاحظ أن google-java-format تستخدم مسافتين للإزاحة وليس أربعاً، وهذا يفاجئ من يأتي من Python. والإصلاح لا يكون يدوياً، وإنما بالأمر `gradle spotlessApply` الذي يعيد كتابة الملفات، وفي التجربة رجع السطر بعده إلى `int qty = args.length > 1 ? Integer.parseInt(args[1]) : 3;`. واجعل `spotlessCheck` جزءاً من الـ CI، و`spotlessApply` عادة قبل كل Commit.

### الفحص الثابت Static Analysis: Error Prone

المترجم `javac` يفحص الأنواع، ولكنه لا يعرف الأخطاء المنطقية المعروفة التي تمر رغم أن أنواعها صحيحة. وهنا يأتي دور أدوات الفحص الثابت، وفي Java ثلاث أدوات مشهورة: Checkstyle للأسلوب والتسمية، وPMD وSpotBugs لأنماط الأخطاء، و[Error Prone](https://errorprone.info/?ref=arabroot.io) من Google التي تعمل داخل المترجم نفسه فتظهر أخطاؤها كأخطاء ترجمة عادية. ولتجربتها حذفنا كلمة `throw` من سطر التحقق في `Vat.java`، فصار السطر ينشئ الاستثناء ولا يرميه، وهو خطأ حقيقي يحدث عندما يكتب المطور بسرعة، والكود بعده يترجم بشكل طبيعي مع `javac` وحده. وعند تشغيل `gradle compileJava` كان المخرج كما يلي:

```text
/tmp/p/src/main/java/store/Vat.java:10: error: [DeadException] Exception created but not thrown
      new IllegalArgumentException("negative amount: " + halalas);
      ^
    (see https://errorprone.info/bugpattern/DeadException)
  Did you mean 'throw new IllegalArgumentException("negative amount: " + halalas);'?
1 error
```

والرسالة فيها اسم النمط `DeadException` ورابط شرحه، ثم الاقتراح بالإصلاح نفسه. ولو مر هذا الخطأ لكانت الدالة تقبل المبالغ السالبة بصمت، وتحسب ضريبة سالبة على فاتورة مرتجع. وError Prone يحتاج مع الـ JDK الحديثة خيارات تفتح له بعض الحزم الداخلية في المترجم، والإضافة `net.ltgt.errorprone` تضيفها لك، ولهذا لا تحاول تشغيله يدوياً في أسبوعك الأول.

### الاختبارات Unit Tests مع JUnit 6

إطار الاختبار الأساسي في Java هو JUnit، وآخر إصدار رئيسي منه هو 6، والاختبار class في `src/test/java` بنفس حزمة الكود، وكل دالة عليها `@Test` اختبار مستقل. وهذا هو اختبار الضريبة في المشروع:

```java
package store;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class VatTest {
  @Test
  void roundsHalfUp() {
    // 9.90 SAR x 3 = 29.70, VAT 4.455 -> 4.46
    assertEquals(446, Vat.vatOf(2970));
    assertEquals(3416, Vat.total(990, 3));
  }

  @Test
  void rejectsNegative() {
    assertThrows(IllegalArgumentException.class, () -> Vat.vatOf(-1));
  }
}
```

ولنرى كيف يبدو الفشل، غيرنا سطر الحساب إلى `(halalas * 15) / 100`، أي القص Truncation بدل التقريب، وهو خطأ يكتبه كثيرون لأن الفرق هللة واحدة. وكان مخرج `gradle test` كما يلي:

```text
VatTest > rejectsNegative() PASSED

VatTest > roundsHalfUp() FAILED
    org.opentest4j.AssertionFailedError at VatTest.java:12

2 tests completed, 1 failed
BUILD FAILED in 28s
```

والرسالة الكاملة في تقرير الاختبار `build/test-results/test` كانت `expected: <446> but was: <445>`، أي أن الضريبة نقصت هللة. وقد يبدو هذا صغيراً، ولكنه في فاتورة ضريبية خطأ في المبلغ المصرح به، ويتكرر في كل فاتورة. ولاحظ أن الإعداد `exceptionFormat = SHORT` يطبع اسم الاستثناء والسطر فقط، فإذا أردت الرسالة في الطرفية مباشرة فاستخدم `FULL`، أو افتح التقرير `build/reports/tests/test/index.html` في المتصفح. وقد شرحنا أنواع الاختبارات ومتى تكتب كل نوع في [أنواع الاختبارات](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/).

والصورة التالية تجمع الأخطاء التي زرعناها، وأين أمسك البناء كل واحد منها:

![أربع محطات في البناء: الترجمة مع Error Prone، والتنسيق، والاختبار، والحزمة، والخطأ الذي أمسكته كل محطة](https://arabroot.io/content/images/2026/10/java-for-devs-05-build-gates.webp)

كل محطة في `gradle build` تمسك نوعاً مختلفاً من الأخطاء، ولا تغني واحدة عن الأخرى.

### التشغيل Run والحزمة JAR: no main manifest attribute

بعد أن نجح البناء، نشغل البرنامج. والطريقة الأولى من Gradle نفسه:

```bash
gradle -q run --args="990 3"
```

```text
3 x 990 halalas = 3416 with VAT
```

ولكن في الإنتاج لن يكون Gradle موجوداً، فالمتوقع أن تشغل الحزمة نفسها. وأول ما يجربه أي مطور هو `java -jar`، وهذا ما حدث:

```text
no main manifest attribute, in build/libs/store-cli-0.1.0.jar
```

والسبب أن ملف JAR هو ملف ZIP فيه الـ Classes وملف وصف اسمه `META-INF/MANIFEST.MF`، والأمر `java -jar` يقرأ من هذا الملف السطر `Main-Class` ليعرف من أين يبدأ، والإضافة `application` لا تكتب هذا السطر لأنها تتوقع أن تشغل البرنامج من سكربتاتها. ولهذا أمامك حلان: الأول أن تحدد الـ Class بنفسك مع مسار الـ Classes Classpath:

```bash
java -cp build/libs/store-cli-0.1.0.jar store.Main 990 3
```

والثاني أن تستخدم `gradle installDist` الذي يبني مجلداً فيه سكربت تشغيل لـ Linux وآخر لـ Windows، ومجلد `lib` فيه الحزمة وكل اعتمادياتها، والسكربت يبني الـ Classpath لك:

```text
build/install/store-cli/bin:
store-cli
store-cli.bat

build/install/store-cli/lib:
store-cli-0.1.0.jar
2 x 4500 halalas = 10350 with VAT
```

ولاحظ أن الحزمة هنا 1,741 بايت فقط لأن البرنامج لا يستخدم مكتبات خارجية، فإذا أضفت مكتبات فإن `java -jar` وحده لن يجدها حتى لو أضفت `Main-Class`، لأن الـ JAR العادي لا يحتوي اعتمادياته. ولهذا تبني أطر مثل Spring Boot ما يسمى Fat JAR فيه كل شيء، وهذا ما سوف تراه في مقال الخدمة الحقيقية من هذه المرحلة.

## المشكلة التي واجهناها: بناء بطيء على مجلد Windows Bind Mount

في التجربة كان المشروع على قرص Windows، وكنا نشغل Gradle داخل Container ونعطيه المجلد بـ Bind Mount، وهو ما يفعله كثير من المطورين على Windows مع Docker Desktop. والبناء الأول بعد تنزيل الاعتماديات لم ينته خلال عشر دقائق، فأوقفناه عند حد الوقت الذي وضعناه وهو في مهمة الاختبار. وعندما قسنا البناء الكامل `clean build` بعد أن صارت الاعتماديات في الـ Cache، ثم نسخنا نفس المشروع إلى مجلد داخل الـ Container وبنيناه هناك، كانت النتيجة كما طبعها Gradle:

```text
## 1. clean build on the Windows bind mount (/lab/store-cli)
BUILD SUCCESSFUL in 3m 32s
## 2. clean build inside the container file system (/tmp/p)
BUILD SUCCESSFUL in 33s
## 3. build again, nothing changed
BUILD SUCCESSFUL in 5s
9 actionable tasks: 9 up-to-date
```

![مقارنة زمن البناء على Bind Mount من قرص Windows وداخل نظام ملفات الـ Container، وزمن البناء الثاني دون تغيير](https://arabroot.io/content/images/2026/10/java-for-devs-06-bind-mount.webp)

نفس المشروع ونفس الأوامر: 3 دقائق و32 ثانية على مجلد Windows، و33 ثانية داخل الـ Container.

والفرق أكثر من ست مرات لنفس الكود ونفس الأوامر. والسبب أن Docker Desktop على Windows يشغل الـ Containers داخل آلة Linux في WSL 2، وكل قراءة أو كتابة على مجلد من قرص Windows تعبر طبقة تترجم بين نظامي الملفات، وGradle والمترجم يلمسان في كل بناء مئات الملفات الصغيرة: ملفات `.class`، وبصمات المهام في `.gradle`، وتقارير الاختبار. وفي الحالة الثالثة لم يتغير شيء، فقرأ Gradle البصمات فقط واكتفى بخمس ثوان.

والحل أن تضع المشروع حيث تعمل الأداة: إذا كنت تبني داخل Linux أو داخل Container فضع المستودع داخل نظام ملفات WSL نفسه أو في Volume، وإذا كنت تعمل على Windows مباشرة فثبت JDK وGradle على Windows ولا تمر عبر Docker في البناء اليومي. وبنفس المنطق لا تضع مجلد `~/.gradle` على Bind Mount، بل في Volume كما فعلنا في التجربة. وهذه المشكلة ليست خاصة بـ Java، ولكنها تظهر فيها بوضوح لأن البناء يكتب ملفاً لكل class.

## بيئة التطوير IDE: هل تحتاج IntelliJ IDEA؟

قد يتساءل البعض: كتبت Python سنوات في محرر بسيط، فهل أحتاج بيئة تطوير ثقيلة لـ Java؟ والإجابة أن Java من اللغات التي تستفيد من بيئة التطوير أكثر من غيرها، والسبب أن الأنواع الثابتة تعطي الأداة معلومات كاملة عن الكود، فتستطيع أن تكمل الكود وتعيد تسمية دالة في كل المشروع وتنتقل إلى تعريف أي class بدقة لا تتوفر في لغة ديناميكية. والخيارات الشائعة ثلاثة: IntelliJ IDEA وهي الأكثر استخداماً في الفرق ولها نسخة مجانية، وVS Code مع إضافة Java من Microsoft وRed Hat وهي مناسبة إذا كنت تعمل بعدة لغات، وEclipse التي ما زالت موجودة في بعض المؤسسات. وكلها تقرأ ملف البناء من Gradle أو Maven وتفهم المشروع منه، ولذلك اجعل ملف البناء هو المرجع، ولا تضف إعدادات للمشروع داخل بيئة التطوير وحدها، لأن خادم الـ CI لا يراها.

ونصيحة عملية للأسبوع الأول: تعلم في بيئة التطوير ثلاثة أشياء قبل غيرها، الانتقال إلى التعريف، والبحث عن كل من يستدعي دالة، وتشغيل اختبار واحد مع نقطة توقف Breakpoint، فهذه الثلاثة هي ما تحتاجه لتفهم كوداً كتبه غيرك. وإذا ظهر لك استثناء طويل فقد شرحنا كيف تقرأ الـ Stack Trace من الأسفل إلى الأعلى في [قراءة رسالة الخطأ والتصحيح](https://arabroot.io/articles/%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-%D8%B1%D8%B3%D8%A7%D9%84%D8%A9-%D8%A7%D9%84%D8%AE%D8%B7%D8%A7-stack-trace-%D9%88%D8%A7%D9%84%D8%AA%D8%B5%D8%AD%D9%8A%D8%AD-debugging/).

## أخطاء شائعة Common Mistakes في الأسبوع الأول

- **تثبيت أحدث JDK وتجاهل إصدار المشروع:** تكتب `record` أو `switch` بالأنماط، ثم يفشل البناء في الـ CI لأن المشروع على Java 17 أو 11\. والسبب أن ما يحدد الصيغة هو `toolchain` أو `release` في ملف البناء، فاقرأه أولاً.
- **اسم الملف لا يطابق الـ Class العامة:** الـ Class العامة `Vat` يجب أن تكون في ملف `Vat.java`، والحزمة `store` يجب أن تكون في مجلد `store`، وإلا رفض المترجم أو لم يجد أحدهما الآخر.
- **مقارنة النصوص بـ `==`:** تعمل أحياناً في الاختبار وتفشل في الإنتاج، والسبب أنها تقارن المرجع لا المحتوى، واستخدم `equals` دائماً.
- **تعديل قائمة من `List.of`:** يمر من المترجم ويرمي `UnsupportedOperationException` وقت التشغيل كما رأيت في JShell.
- **المبالغ المالية في `double`:** احفظها بالهللات في `int` أو `long`، أو في `BigDecimal` إذا احتجت كسوراً أدق، وسوف نقيس الفرق في مقال الأنواع.
- **تشغيل `java -jar` على JAR لا يحتوي `Main-Class` ولا اعتمادياته:** استخدم `installDist` أو إضافة تبني Fat JAR، أو إطاراً مثل Spring Boot يفعل ذلك.
- **إطفاء الفحص بدل إصلاحه:** عندما يفشل `spotlessCheck` أو Error Prone فالحل `spotlessApply` أو إصلاح الخطأ، وليس حذف الإضافة من ملف البناء، والسبب أن الفريق وضعها حتى لا يناقش نفس الأخطاء في كل مراجعة.
- **البناء على Bind Mount من Windows:** البناء أبطأ ست مرات كما رأيت، فضع المشروع داخل نظام ملفات Linux إذا كنت تبني في Container.
- **البحث عن حلول قديمة:** كثير من الإجابات في الإنترنت كتبت لـ Java 8، فإذا وجدت `Date` أو `Vector` أو getters مكتوبة يدوياً لكائن بيانات، فابحث عن الطريقة الحديثة أولاً: `java.time` و`ArrayList` و`record`.

## قائمة تحقق Checklist لأول يوم مع Java

| السؤال                                        | كيف تتحقق                                                                | لماذا                                       |
| --------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------- |
| أي إصدار Java يبني المشروع؟                   | languageVersion في build.gradle.kts أو maven.compiler.release في pom.xml | يحدد الصيغة المسموحة، لا الـ JDK عندك       |
| هل الـ JDK عندك من مورد مقبول في الشركة؟      | السطر الثاني من java -version                                            | الرخصة والتحديثات الأمنية                   |
| هل تستخدم الـ Wrapper؟                        | ./gradlew --version أو ./mvnw -v                                         | نفس إصدار أداة البناء عند الجميع وفي الـ CI |
| هل البناء الكامل ينجح عندك قبل أن تغير شيئاً؟ | ./gradlew build                                                          | تعرف أن أي فشل بعد ذلك من تعديلك            |
| ما المنسق والفاحص المستخدمان؟                 | الإضافات في ملف البناء، والأمر spotlessApply أو ما يقابله                | تتجنب تعليقات المراجعة على المسافات         |
| أين الاختبارات وكيف تشغل اختباراً واحداً؟     | ./gradlew test --tests 'store.VatTest'                                   | دورة سريعة أثناء التعديل                    |
| كيف تشغل الخدمة محلياً؟                       | المهمة run أو bootRun، أو ملف README                                     | تجرب التعديل كما سيعمل                      |
| أين المشروع على القرص؟                        | داخل نظام ملفات Linux إذا كنت تبني في Container                          | تجنب البناء الأبطأ ست مرات                  |

## تدريب عملي

- أضف إلى `Vat` دالة لمجموع فاتورة فيها عدة أسطر بأسعار وكميات مختلفة، واكتب لها اختباراً، ثم جرب فاتورة يتجاوز مجموعها حد `int` ولاحظ ما يحدث، وأصلحه بـ `long` أو `Math.multiplyExact`.
- أضف إلى `Tour.java` نوع دفع ثالثاً إلى `sealed interface Payment` دون أن تعدل الدالة `describe`، وشغل الملف، واقرأ رسالة المترجم.
- غير `googleJavaFormat("2.0.0")` إلى `palantirJavaFormat()` وشغل `spotlessCheck`، ولاحظ أي الأسطر تختلف بين الأسلوبين.
- حول مشروع `store-cli` إلى Maven بملف `pom.xml` يعطي نفس النتيجة، وقارن طول الملفين وزمن البناء.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- Java تترجم إلى Bytecode تشغله الـ JVM بالمفسر ثم بـ JIT، وهذا سبب قابليتها للنقل ونضجها، وسبب ثمنها في زمن البدء والذاكرة.
- Java 25 هي آخر إصدار LTS، وفيها تكتب برنامجاً صغيراً دون class بفضل JEP 512، وتشغله مباشرة بـ `java Hello.java`، ولكن هذا التشغيل كلف 3 ثوان مقابل 0.14 ثانية من Bytecode جاهز.
- ما يحدد الصيغة المسموحة في المشروع هو إصدار البناء في ملف البناء، وقد رأيت المترجم يرفض الملف المختصر مع `--release 21`.
- الأنواع ثابتة حتى مع `var`، والسجلات Records وsealed وswitch بالأنماط تغني عن كثير من الكود القديم، و`int` يلتف بصمت عند تجاوز الحد.
- مشروع Gradle النموذجي فيه ملف بناء وهيكل `src/main/java` و`src/test/java`، والبناء يمر بالترجمة مع Error Prone والتنسيق بـ Spotless والاختبارات بـ JUnit 6، وكل محطة أمسكت في التجربة خطأ لا تمسكه الأخرى.
- البناء على Bind Mount من Windows كان أبطأ ست مرات من البناء داخل الـ Container، فضع المشروع حيث تعمل أداة البناء.

وفي المقالات التالية من مرحلة [لغة Java للمطورين](https://arabroot.io/start/#stage-lang-java) سوف ننتقل إلى الأنواع والذاكرة والاستثناءات، ثم التزامن والخيوط الافتراضية، ثم نبني خدمة الطلبات كاملة بـ Spring Boot وPostgreSQL، وتجد هنا المقالات المرتبطة بما شرحناه:

[![](https://arabroot.io/content/images/2026/10/java-story-cover-cover.webp)دليلقصة Java من Sun إلى Oracle، وكيف بقيت مفتوحةكيف فتحت Sun كود Java ثم باعتها، وماذا حدث مع Oracle وGoogle في المحاكم، وكيف أصبحت Java تصدر كل ستة أشهر.](https://arabroot.io/articles/%D9%82%D8%B5%D8%A9-java-%D9%85%D9%86-sun-%D8%A7%D9%84%D9%89-oracle/)[![](https://arabroot.io/content/images/2026/10/java-licensing-cover-cover.webp)دليلهل Java مجانية؟ أي JDK تستخدم في شركتكمتى تحتاج اشتراكاً في Oracle JDK ومتى لا تحتاجه، وكيف تعرف نسخ Java التي تعمل عندك وتنقلها إلى نسخة مجانية بأمان.](https://arabroot.io/articles/%D9%87%D9%84-java-%D9%85%D8%AC%D8%A7%D9%86%D9%8A%D8%A9-%D9%88%D8%A7%D9%8A-jdk-%D8%AA%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%81%D9%8A-%D8%B4%D8%B1%D9%83%D8%AA%D9%83/)[![](https://arabroot.io/content/images/2026/10/build-tools-cover-cover.webp)دليلأدوات البناء Build Tools وماذا يحدث عندما تبني مشروعكما الذي يحدث بين ضغطة البناء والملف الناتج، ولماذا ينجح البناء على جهازك ويفشل على سيرفر CI، ومتى يعطيك نفس النتيجة مرتين.](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D9%86%D8%A7%D8%A1-build-tools-%D9%85%D9%86-make-%D8%A7%D9%84%D9%89-maven-%D9%88-gradle-%D9%88-dotnet/)[![](https://arabroot.io/content/images/2026/10/package-managers-cover-cover.webp)دليلمديرو الحزم Package Managers: npm و pip و Maven و Cargoنجرب npm وpip وMaven وCargo على مشروع واحد بأربع لغات، فنقيس كم حزمة تأتي مع كل سطر، ونرى كيف يجعل ملف القفل التثبيت متطابقاً، وكيف خدع حرف واحد في اسم مكتبة مطوري Rust.](https://arabroot.io/articles/%D9%85%D8%AF%D9%8A%D8%B1%D9%88-%D8%A7%D9%84%D8%AD%D8%B2%D9%85-npm-%D9%88-pip-%D9%88-maven-%D9%88-cargo/)[![](https://arabroot.io/content/images/2026/10/compilation-jit-cover-cover.webp)دليلالمترجم Compiler والمفسر Interpreter و JIT و WebAssemblyنشغل دالة واحدة بلغة C وPython وJavaScript وWebAssembly على نفس الجهاز، ونفتح ما كتبه كل مترجم لنعرف لماذا اختلف الوقت من ثلث ثانية إلى دقيقة.](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AA%D8%B1%D8%AC%D9%85-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%B3%D8%B1-%D9%88-jit-%D9%88-webassembly-%D8%A8%D8%A7%D9%84%D8%AA%D9%81%D8%B5%D9%8A%D9%84/)[![](https://arabroot.io/content/images/2026/10/choose-language-cover-cover.webp)دليلكيف تختار لغة البرمجة Programming Language لمشروعك؟نختار لغة خدمة حقيقية بأسئلة الفريق والتوظيف والمكتبات والرخصة، ثم نكتبها بأربع لغات ونقيس البناء والحجم والذاكرة والطلبات في الثانية بأنفسنا.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على JDK 25.0.5 (Temurin) وGradle 9.8.2 وJUnit 6.1.3 وSpotless 8.10.4 وError Prone 2.51.0 داخل Docker.