> ## 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.

# Kotlin لمن يعرف البرمجة: الأدوات والصيغة Syntax في يوم
- URL: https://arabroot.io/articles/kotlin-لمن-يعرف-البرمجة-الادوات-والصيغة/
- Published: 2026-10-31T07:13:11.000Z
- Updated: 2026-10-31T08:10:05.000Z
- Description: ثبتنا Kotlin 2.4.21 على JDK 25 وبنينا مشروعاً صغيراً بـ Gradle من الصفر، لترى الصيغة مقارنة بما تعرفه من Java وPython، والأدوات التي سوف تستخدمها كل يوم والمشكلات التي تظهر في أول يوم.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

لنفرض أنك تعمل في شركة ناشئة تبيع القهوة والتمور عبر تطبيق جوال، وتطبيق Android مكتوب بـ Kotlin منذ سنتين على يد مطورين اثنين، بينما الخدمة الخلفية Backend مكتوبة بـ Python وتكتبها أنت. وفي اجتماع الفريق يقترح أحدهم أن تنتقل خدمة الطلبات إلى Kotlin على السيرفر، والسبب أن الفريق كله سوف يقرأ لغة واحدة، ونماذج البيانات مثل المنتج والطلب سوف تكتب مرة واحدة، والـ JVM التي يعمل عليها الكود من أكثر البيئات نضجاً في تشغيل الخدمات. والطلب منك بسيط في ظاهره: جهز بيئة Kotlin على جهازك، واكتب أول مشروع يعمل ويختبر، وتعرف على الأدوات التي سوف تستخدمها كل يوم، قبل أن نبدأ في كتابة الخدمة نفسها.

وأنت لست مبتدئاً، فأنت تعرف المتغيرات والدوال والكلاسات، وتعرف كيف تكتب اختباراً وكيف تقرأ رسالة خطأ، ولا تحتاج من يشرح لك ما هي الحلقة. ما تحتاجه هو خريطة سريعة: ماذا أثبت بالضبط؟ ما الفرق بين المترجم `kotlinc` وأداة البناء Gradle؟ كيف تبدو الصيغة Syntax مقارنة بما أكتبه اليوم في Python أو Java؟ وما الذي يحدث فعلاً عندما أضغط زر التشغيل؟ وهذا المقال هو الأول في مرحلة [لغة Kotlin للمطورين](https://arabroot.io/start/#stage-lang-kotlin)، وهي جزء من مسار «لغات البرمجة عملياً» الذي يأخذ كل لغة بنفس الشكل حتى تستطيع المقارنة بينها: الأدوات أولاً، ثم الأنواع والذاكرة، ثم التزامن، ثم خدمة حقيقية، ثم الأخطاء الشائعة. وكل الأوامر في هذا المقال نفذناها فعلاً على Kotlin 2.4.21 وJDK 25، والمخرج المعروض هو المخرج الحقيقي.

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

- من أين جاءت Kotlin، ولماذا صارت لغة Android الأولى، ولماذا يكتبها البعض على السيرفر.
- ما الذي تثبته بالضبط: الـ JDK والمترجم وأداة البناء، والإصدارات التي استخدمناها.
- المترجم `kotlinc` وحده: أول برنامج، وأول خطأ حقيقي ظهر لنا وهو `NoClassDefFoundError`، وملفات السكربت `.main.kts`.
- جولة سريعة في الصيغة مقارنة بـ Java وPython: القيم والمتغيرات، والقيم الفارغة null، وكلاسات البيانات Data Classes، والتعبير `when`، ودوال الامتداد Extension Functions.
- مشروع Gradle من الصفر: الأمر `gradle init`، وبنية المجلدات Project Layout، وملف `build.gradle.kts` سطراً بسطر.
- البناء والتشغيل والاختبار، وكم استغرق كل منها في المرة الأولى والثانية.
- أدوات التنسيق والفحص: ktlint وktfmt وdetekt، والمشكلة التي ظهرت مع JDK 25.
- الأخطاء الشائعة في أول يوم، وقائمة تحقق Checklist.

## لماذا ظهرت Kotlin؟ قصة اللغة Language History باختصار

بدأت شركة JetBrains، وهي الشركة التي تصنع IntelliJ IDEA، العمل على Kotlin في 2010، وكان الدافع عملياً جداً: كود الشركة نفسه مكتوب بـ Java، والفريق يريد لغة تعمل على نفس الـ JVM وتستخدم نفس المكتبات، ولكن بكود أقصر وبحماية من الأخطاء التي تتكرر في Java كل يوم، وعلى رأسها `NullPointerException`. ولم يكن الهدف لغة أكاديمية جديدة، وإنما لغة «عملية» Pragmatic، وهي الكلمة التي اختارتها الشركة في [إعلان الإصدار 1.0](https://blog.jetbrains.com/kotlin/2016/02/kotlin-1-0-released-pragmatic-language-for-jvm-and-android/?ref=arabroot.io) في فبراير 2016، حيث وعدت بالتوافق مع الإصدارات القادمة، وبأن أي كود Java موجود يمكن أن يعيش بجانب كود Kotlin في نفس المشروع ويستدعي كل منهما الآخر.

والتحول الكبير جاء من Google، ففي مؤتمر Google I/O عام 2017 أعلنت دعم Kotlin رسمياً في Android، ثم في 2019 أعلنت أن تطوير Android صار «Kotlin أولاً» Kotlin-first، وهذا موثق في صفحة [Android's Kotlin-first approach](https://developer.android.com/kotlin/first?ref=arabroot.io)، ومعناه أن الأدوات والمكتبات والوثائق الجديدة تكتب لـ Kotlin قبل Java. ولهذا السبب ستجد أن أغلب من يكتب Kotlin اليوم جاء من Android، وهذه بالضبط حالة الفريق في قصتنا. وعلى السيرفر انتشرت Kotlin بشكل أبطأ، ولكنها اليوم مدعومة رسمياً في Spring Boot، ولها إطار خاص من JetBrains اسمه Ktor سوف نستخدمه في مقال الخدمة الحقيقية من هذه المرحلة.

وفي مايو 2024 صدر الإصدار 2.0 ومعه المترجم الجديد K2، وهو إعادة كتابة كاملة للواجهة الأمامية Frontend من المترجم، وتشرح صفحة [What's new in Kotlin 2.0](https://kotlinlang.org/docs/whatsnew20.html?ref=arabroot.io) ما تغير. والمهم لك كمطور جديد أن كل ما تقرؤه اليوم يفترض K2، وأن بعض المقالات القديمة على الإنترنت تتحدث عن سلوك تغير. والإصدارات تسير اليوم بإيقاع ثابت تشرحه صفحة [Kotlin release process](https://kotlinlang.org/docs/releases.html?ref=arabroot.io): إصدار لغة 2.x.0 كل ستة أشهر تقريباً، وإصدار أدوات 2.x.20 بينهما، وإصدارات إصلاح صغيرة مثل 2.x.21\. والإصدار الذي استخدمناه هو [Kotlin 2.4.21](https://github.com/JetBrains/kotlin/releases/tag/v2.4.21?ref=arabroot.io) الصادر في 8 أكتوبر 2026، وهو آخر إصدار مستقر وقت كتابة المقال، أما 2.5.0 فما زال تجريبياً Beta.

وقد يتساءل البعض: إذا كانت Kotlin تعمل على الـ JVM مثل Java، فلماذا لا أكتب Java الحديثة مباشرة، وهي تطورت كثيراً في السنوات الأخيرة وصار فيها Records والتعبير `switch` والخيوط الافتراضية Virtual Threads؟ والإجابة أن هذا خيار صحيح تماماً لكثير من الفرق، ومرحلة Java في هذا المسار تبني نفس الخدمة بـ Java الحديثة حتى تقارن بنفسك. ولكن هناك ثلاثة أشياء تجعل Kotlin مختلفة فعلاً، وليست مجرد Java بصيغة أقصر: نظام الأنواع يفرق بين القيمة التي قد تكون null والتي لا تكون، والروتينات المساعدة Coroutines جزء من طريقة كتابة الكود غير المتزامن، والكود الواحد يمكن أن يعمل على الـ JVM وعلى Android وعلى المتصفح والجوال عبر Kotlin Multiplatform. وفي حالة فريقنا فإن السبب الأول هو الأهم: الفريق يكتب Kotlin أصلاً، ولا يريد لغتين.

## ماذا تثبت بالضبط؟ الـ JDK والمترجم Compiler وأداة البناء Build Tool

أول ما يربك القادم من Python أو Node.js أن Kotlin ليست برنامجاً واحداً تثبته فيعمل، وإنما ثلاث طبقات، ولكل طبقة إصدار مستقل، وكثير من مشكلات اليوم الأول سببها خلط بين هذه الإصدارات. والصورة التالية تبين الطريق الذي يمر به ملف `.kt` حتى يعمل:

![الطريق من ملف Kotlin إلى برنامج يعمل على JVM: المترجم kotlinc أو Gradle، ثم ملفات class ومكتبة kotlin-stdlib، ثم JVM من JDK 25](https://arabroot.io/content/images/2026/10/kotlin-for-devs-01-toolchain.webp)

ثلاث طبقات بثلاثة إصدارات: الـ JDK يشغل، والمترجم يحول الكود إلى Bytecode، وGradle ينظم البناء ويجلب المكتبات

والطبقات الثلاث كما يلي:

- **الـ JDK (Java Development Kit)**: وهو الذي يحتوي الآلة الافتراضية JVM التي تشغل الكود، وأدوات مثل `java` و`javap`. وKotlin على الـ JVM تحتاجه دائماً، لأن المخرج النهائي هو ملفات `.class` مثل Java تماماً. واستخدمنا JDK 25، وهو إصدار الدعم الطويل LTS الحالي، من توزيعة Temurin المجانية. وإذا كنت محتاراً بين توزيعات الـ 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/).
- **المترجم Kotlin Compiler**: ويأتي بطريقتين، إما كأداة سطر أوامر مستقلة اسمها `kotlinc` تنزلها كملف zip من صفحة الإصدار، وإما كإضافة Plugin داخل Gradle تنزل المترجم تلقائياً بالإصدار المكتوب في ملف البناء. وفي العمل اليومي سوف تستخدم الطريقة الثانية تقريباً دائماً، والأولى مفيدة لتفهم ما يحدث وللتجارب السريعة.
- **أداة البناء Gradle**: وهي التي تقرأ ملف `build.gradle.kts`، وتنزل المكتبات من Maven Central، وتشغل المترجم والاختبارات وتجمع الناتج. وMaven تعمل مع Kotlin أيضاً، ولكن Gradle هي الافتراضية في Android وفي أغلب مشاريع Kotlin على السيرفر، ولذلك اخترناها. وشرحنا الفكرة العامة لأدوات البناء ودورة حياتها في مقال [أدوات البناء Build Tools](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/)، فلن نكررها هنا.

وعلى جهازك الشخصي فإن أسهل طريق هو IntelliJ IDEA، فهو ينزل الـ JDK والمترجم ويفتح مشاريع Gradle مباشرة، أو أداة SDKMAN على Linux وmacOS التي تثبت `java` و`kotlin` و`gradle` بإصدارات محددة. ولكننا في هذا المقال وكل مقالات المرحلة استخدمنا Docker Image رسمية فيها Gradle 9.8.2 مع JDK 25، وذلك لسببين: أن أي قارئ يستطيع أن يعيد نفس التجربة بنفس الإصدارات بالضبط، وأن جهاز التطوير يبقى نظيفاً. والأمر التالي يبين ما في هذه الـ Image:

```bash
docker run --rm gradle:9.8.2-jdk25-noble sh -c 'java -version 2>&1; gradle --version | sed -n 1,12p'
```

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

```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)

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

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

Kotlin:        2.4.10
Groovy:        4.0.33
```

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

- السطر الأول يقول إن الـ JDK هو 25.0.5 وأنه LTS، والسطر الثاني يحدد المورد Temurin ورقم البناء 8\. وهذان هما ما تكتبه في ملف الإصدارات لأي مشروع.
- السطر `Kotlin: 2.4.10` هو أهم سطر في المخرج، وهو مصدر خلط شائع: هذا ليس إصدار Kotlin الذي سوف يترجم كودك، وإنما الإصدار المدمج داخل Gradle نفسه لتشغيل ملفات `.gradle.kts`. أما كودك فيترجمه الإصدار المكتوب في الإضافة `kotlin("jvm") version "2.4.21"` داخل ملف البناء، وسوف ترى الفرق بعد قليل.
- الـ Image مبنية على Ubuntu 24.04، والمستخدم داخلها هو root، ولذلك نحدد مجلد ذاكرة Gradle بأنفسنا في Volume منفصل حتى لا تنزل المكتبات في كل تشغيل.

## المترجم kotlinc وحده: أول برنامج Hello World

قبل Gradle سوف نبدأ بالمترجم وحده، والسبب أن Gradle يخفي عنك تفاصيل مهمة سوف تحتاجها يوم يظهر خطأ غريب في الإنتاج. نزلنا الملف `kotlin-compiler-2.4.21.zip` من صفحة الإصدار على GitHub، وحجمه 89,733,618 بايت، وتحققنا من الهاش SHA-256 مقارنة بالملف `.sha256` المنشور بجانبه، ثم فككناه فصار مجلداً اسمه `kotlinc` بحجم 94 ميجابايت، وفيه الأدوات `kotlinc` و`kotlin` ومجلد `lib` بالمكتبات. والملف الأول الذي كتبناه هو التالي:

```kotlin
// أول ملف Kotlin: دالة main في أعلى الملف، ولا يوجد كلاس إلزامي كما في Java
fun main(args: Array<String>) {
    val name = args.firstOrNull() ?: "Kotlin"   // val: قيمة لا تتغير، والنوع String استنتجه المترجم
    var count = 0                               // var: متغير يتغير
    for (i in 1..3) count += i
    println("مرحباً $name، المجموع = $count، الطول = ${name.length}")
}
```

في الكود أعلاه لاحظ التالي، خاصة إذا كنت قادماً من Java:

- الدالة `main` تقف وحدها في الملف، دون `public class` ودون `static`. وفي Kotlin تسمى هذه دالة في المستوى الأعلى Top-Level Function، وهي طبيعية تماماً وليست حيلة.
- `val` تعني قيمة تعطيها مرة واحدة ولا تتغير، مثل `final` في Java، و`var` متغير عادي. والقاعدة في كود Kotlin الجيد أن تبدأ دائماً بـ `val`، ولا تستخدم `var` إلا عندما تحتاج التغيير فعلاً.
- لم نكتب نوع `name` ولا `count`، لأن المترجم يستنتجه من القيمة، وهذا الاستنتاج Type Inference لا يجعل اللغة ديناميكية مثل Python، فالنوع ثابت وقت الترجمة، ولو حاولت أن تضع رقماً في `name` بعد ذلك فسوف يرفض المترجم.
- العلامة `?:` اسمها عامل إلفيس Elvis Operator، ومعناها «إذا كان ما على اليسار null فخذ ما على اليمين». والدالة `firstOrNull()` ترجع null إذا كانت القائمة فارغة بدل أن ترمي استثناء.
- النص `"$name"` قالب نصي String Template، مثل f-string في Python، و`${name.length}` لأي تعبير أطول من اسم متغير.

الآن سوف نقوم بترجمة الملف إلى ملف JAR يحتوي مكتبة Kotlin معه، ثم نشغله بـ `java` العادية، وكل ذلك داخل Container من الـ Image السابقة، والسكربت الكامل موجود في `lab/hello/run-kotlinc.sh`:

```bash
kotlinc -version
kotlinc Hello.kt -include-runtime -d hello.jar
ls -l hello.jar
java -jar hello.jar
java -jar hello.jar سلمان
```

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

```text
info: kotlinc-jvm 2.4.21 (JRE 25.0.5+8-LTS)
compile seconds: 27.51
-rw-r--r-- 1 root root 5679656 Oct 31 07:18 hello.jar
مرحباً Kotlin، المجموع = 6، الطول = 6
مرحباً سلمان، المجموع = 6، الطول = 5
```

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

- المترجم يعرف نفسه بـ `kotlinc-jvm 2.4.21`، ويقول إنه يعمل على JRE 25\. فالمترجم نفسه برنامج JVM، ولذلك يحتاج الـ JDK حتى قبل أن يترجم سطراً واحداً.
- ترجمة ملف من سبعة أسطر استغرقت 27.51 ثانية، وهذا رقم يصدم القادم من Go أو حتى من Python. والسبب أن كل استدعاء لـ `kotlinc` يشغل JVM جديدة من الصفر، ويحمل المترجم كاملاً، ثم يرمي كل شيء بعد ملف واحد، وأضف إلى ذلك أن الجهاز كان مشغولاً بتجارب أخرى في نفس الوقت. ولهذا السبب بالضبط يستخدم Gradle عملية خلفية تبقى حية اسمها Gradle Daemon، ومعها خادم ترجمة Kotlin Daemon، حتى لا تدفع تكلفة التشغيل في كل مرة.
- حجم `hello.jar` حوالي 5.4 ميجابايت لبرنامج يطبع سطراً واحداً، لأن الخيار `-include-runtime` وضع داخله مكتبة Kotlin الأساسية وما تحتاجه. وهذا الحجم لا يتكرر مع كل ملف تضيفه، فهو تكلفة ثابتة تدفعها مرة واحدة.
- النص العربي ظهر سليماً في الحالتين، لأن Kotlin تستخدم UTF-8 لملفات المصدر افتراضياً، والـ JVM الحديثة تستخدم UTF-8 للمخرج منذ JDK 18.

### الخطأ الأول NoClassDefFoundError: أين ذهبت مكتبة kotlin-stdlib؟

الخطأ الحقيقي الأول الذي ظهر لنا كان في التجربة التالية، حيث ترجمنا نفس الملف إلى مجلد عادي دون الخيار `-include-runtime`، وهذا ما يفعله أي شخص قادم من Java يريد ملفات `.class` فقط:

```bash
kotlinc Hello.kt -d classes
find classes -type f | sort
javap -p -cp classes HelloKt
java -cp classes HelloKt
```

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

```text
classes/HelloKt.class
classes/META-INF/main.kotlin_module
Compiled from "Hello.kt"
public final class HelloKt {
  public static final void main(java.lang.String[]);
}
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
	at HelloKt.main(Hello.kt)
Caused by: java.lang.ClassNotFoundException: kotlin.jvm.internal.Intrinsics
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
```

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

- الأداة `javap` من الـ JDK قرأت الملف المترجم، وكشفت شيئاً مهماً: الدالة التي كتبناها في المستوى الأعلى صارت دالة `static` داخل كلاس اسمه `HelloKt`، أي اسم الملف مع اللاحقة `Kt`. وهذا ما يجعل Java تستطيع أن تستدعي أي دالة Kotlin، وهو أيضاً ما سوف تكتبه في `mainClass` داخل Gradle بعد قليل.
- الكلاس `kotlin.jvm.internal.Intrinsics` غير موجود. وهذا الكلاس ليس من كودنا، وإنما من مكتبة Kotlin الأساسية `kotlin-stdlib`، والمترجم أضاف استدعاء له في بداية `main` ليتحقق أن المصفوفة `args` ليست null، لأن نوعها في Kotlin `Array<String>` وليس `Array<String>?`. فحتى برنامج من سبعة أسطر يعتمد على المكتبة الأساسية، والـ JVM وحدها لا تعرفها.

والإصلاح أن تضع المكتبة في الـ Classpath، والملف `kotlin-stdlib.jar` موجود في مجلد `lib` من المترجم وحجمه 1,853,315 بايت:

```bash
java -cp classes:/opt/kotlinc/lib/kotlin-stdlib.jar HelloKt
```

```text
مرحباً Kotlin، المجموع = 6، الطول = 6
```

وهذا الدرس سوف يتكرر معك بأشكال أخرى، فكل برنامج Kotlin على الـ JVM يحتاج المكتبة الأساسية وقت التشغيل، وأداة البناء هي التي تضمن وجودها: Gradle يضعها في مجلد `lib` عند التوزيع، أو داخل ملف JAR واحد إذا استخدمت إضافة تجمع الاعتماديات. وإذا ظهر لك `NoClassDefFoundError` لكلاس يبدأ بـ `kotlin.` في Docker Image أو على سيرفر، فالسبب غالباً أنك نسخت ملف JAR الخاص بالمشروع وحده دون اعتمادياته.

### سكربتات Kotlin Scripts: ملف main.kts يعمل دون مشروع

وللمهام الصغيرة، مثل سكربت يقرأ ملف CSV أو يجرب فكرة، تدعم Kotlin ملفات السكربت التي تنتهي بـ `.main.kts`، وهي تعمل مباشرة دون دالة `main` ودون مشروع، وتستطيع أن تطلب مكتبات من Maven في رأس الملف كما تشرح صفحة [Kotlin scripting](https://kotlinlang.org/docs/custom-script-deps-tutorial.html?ref=arabroot.io). والسكربت الذي جربناه هو التالي:

```kotlin
// سكربت Kotlin: يعمل مباشرة دون مشروع ودون main
data class Product(val nameAr: String, val priceHalalas: Int)

val products = listOf(Product("قهوة عربية", 4500), Product("تمر سكري", 3200), Product("هيل", 990))
products.filter { it.priceHalalas > 1000 }
    .sortedBy { it.priceHalalas }
    .forEach { println("${it.nameAr}: ${it.priceHalalas / 100.0} ريال") }
println(products.first())
```

```bash
kotlin prices.main.kts
```

```text
تمر سكري: 32.0 ريال
قهوة عربية: 45.0 ريال
Product(nameAr=قهوة عربية, priceHalalas=4500)
script seconds: 8.21
```

والسكربت أخرج المنتجين اللذين سعرهما أكثر من 10 ريالات مرتبين بالسعر، ثم طبع أول منتج بصيغة `Product(nameAr=..., priceHalalas=...)` دون أن نكتب دالة `toString`، وهذه أول مرة ترى فيها ما يعطيك إياه `data class`، وسوف نعود إليه بعد قليل. واستغرق التشغيل 8.21 ثانية لأن السكربت يترجم أولاً ثم يعمل، وهذا مقبول لأداة تشغلها يدوياً، وغير مناسب لأي شيء يعمل كثيراً. ولاحظ أيضاً أننا قسمنا الهللات على `100.0` وليس `100`، لأن قسمة عددين صحيحين في Kotlin تعطي عدداً صحيحاً مثل Java، وليس رقماً عشرياً مثل `/` في Python 3، وهذه من أول المفاجآت للقادم من Python.

## جولة في الصيغة Syntax: ما الذي يختلف عن Java أو Python؟

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

![جدول يقارن صيغة Kotlin بصيغة Java وPython في التعريف والقيم الفارغة والكلاسات والشروط والدوال](https://arabroot.io/content/images/2026/10/kotlin-for-devs-02-syntax-map.webp)

نفس الأفكار التي تعرفها، بكلمات مختلفة: أغلب الجديد في Kotlin هو نوع null وكلاسات البيانات والتعبير when

| الفكرة               | Java                  | Python                     | Kotlin                                      |
| -------------------- | --------------------- | -------------------------- | ------------------------------------------- |
| قيمة ثابتة           | final var x = 5;      | x = 5 (لا يوجد ثابت حقيقي) | val x = 5                                   |
| متغير                | var x = 5;            | x = 5                      | var x = 5                                   |
| قيمة قد تكون فارغة   | أي مرجع قد يكون null  | أي متغير قد يكون None      | String? فقط، وString لا يقبل null           |
| كلاس بيانات          | record Product(...)   | @dataclass                 | data class Product(...)                     |
| تفرع على الحالات     | switch مع case ... -> | match                      | when، وهو تعبير يرجع قيمة                   |
| دالة قصيرة           | طريقة داخل كلاس       | def f(x): return x \* 2    | fun f(x: Int) = x \* 2                      |
| دوال ثابتة           | static                | دالة في الوحدة Module      | دالة في المستوى الأعلى، أو companion object |
| مقارنة المحتوى       | a.equals(b)           | a == b                     | a == b (تستدعي equals)                      |
| مقارنة المرجع        | a == b                | a is b                     | a === b                                     |
| الاستثناءات المفحوصة | موجودة throws         | غير موجودة                 | غير موجودة                                  |

ولاحظ الصف قبل الأخير بالذات إذا كنت قادماً من Java، فالعلامة `==` في Kotlin تقارن المحتوى وليس المرجع، أي أنها تستدعي `equals`، والمقارنة بالمرجع لها علامة ثالثة `===`. وهذا يعني أن الخطأ الكلاسيكي في Java، حيث يقارن أحدهم نصين بـ `==` فتنجح المقارنة في الاختبار وتفشل في الإنتاج، لا يحدث في Kotlin أصلاً.

### القيم الفارغة Null Safety: النوع String? غير النوع String

أهم فكرة في Kotlin كلها هي أن null جزء من النوع. فالمتغير من النوع `String` لا يمكن أن يحمل null أبداً، والمترجم يرفض أي كود يحاول ذلك، أما إذا أردت قيمة قد تكون فارغة فتكتب `String?`، وعندها لا يسمح لك المترجم أن تستدعي عليها أي دالة مباشرة حتى تتعامل مع حالة null. والمثال التالي من ملف `Main.kt` في مشروعنا:

```kotlin
val byId = products.associateBy { it.id } // Map<Int, Product>
val wanted: Product? = byId[7] // النوع Product? يعني: قد تكون null
println("المنتج 7: ${wanted?.nameAr ?: "غير موجود"}")
```

والقراءة من `Map` بمفتاح غير موجود ترجع null، ولذلك نوع `byId[7]` هو `Product?` وليس `Product`. ولو كتبت `wanted.nameAr` مباشرة لرفض المترجم البناء برسالة تقول إن الاستدعاء غير آمن على نوع قد يكون null. والعلامة `?.` اسمها الاستدعاء الآمن Safe Call، وتعني «إذا كانت القيمة null فالنتيجة null، وإلا استدع الخاصية»، ومعها `?:` تعطي القيمة البديلة، فطبع البرنامج «المنتج 7: غير موجود» بدل أن يرمي استثناء. وللمقارنة، نفس السطر في Java يرمي `NullPointerException` وقت التشغيل، وفي Python يرمي `AttributeError: 'NoneType' object has no attribute`، وكلاهما يظهر للمستخدم وليس لك. وتشرح صفحة [Null safety](https://kotlinlang.org/docs/null-safety.html?ref=arabroot.io) باقي الأدوات، ومنها العلامة `!!` التي تقول للمترجم «أنا متأكد أنها ليست null»، وهي ترمي `NullPointerException` إذا كنت مخطئاً، ولذلك لا ينصح باستخدامها إلا نادراً، وسوف نجربها مع حالات أخرى في المقال الثاني من المرحلة.

### كلاسات البيانات Data Classes والقيم الافتراضية Default Arguments

والملف `Product.kt` في المشروع يعرف المنتج بهذا الشكل:

```kotlin
package shop

// data class: equals و hashCode و toString و copy يكتبها المترجم
data class Product(
    val id: Int,
    val nameAr: String,
    val priceHalalas: Int,
    val stock: Int = 0, // قيمة افتراضية بدل كتابة أكثر من Constructor
)

// دالة امتداد Extension Function: نضيف دالة إلى Int دون أن نرث منه
fun Int.asRiyals(): String = "%d.%02d ريال".format(this / 100, this % 100)

fun Product.isAvailable(qty: Int): Boolean = qty in 1..stock
```

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

- السطر الذي يبدأ بـ `data class` يعرف الكلاس والـ Constructor والخصائص Properties في مكان واحد. والكلمة `data` تجعل المترجم يكتب الدوال `equals` و`hashCode` و`toString` و`copy` بناء على هذه الخصائص، وهي نفس فكرة `record` في Java الحديثة و`@dataclass` في Python، وتشرحها صفحة [Data classes](https://kotlinlang.org/docs/data-classes.html?ref=arabroot.io).
- الخاصية `stock` لها قيمة افتراضية Default Argument، وفي Java كنت سوف تكتب Constructor ثانياً أو نمط Builder لهذا الغرض. وعند الاستدعاء تستطيع أن تسمي المعامل Named Argument كما في Python: `Product(1, "قهوة عربية", 4500, stock = 12)`.
- الفاصلة بعد آخر معامل Trailing Comma مسموحة، وهي مفيدة في مراجعة الكود لأن إضافة سطر جديد لا تغير السطر الذي قبله.
- كل المبالغ بالهللات من النوع `Int` وليس `Double`، وهذه قاعدة سوف نلتزم بها في كل المرحلة، والسبب شرحناه في مقال [كيف تختار لغة البرمجة لمشروعك](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/) حين ظهر فرق هللة واحدة بين اللغات بسبب التقريب.

والدالة `copy` مهمة لأن الخصائص `val` لا تتغير، فإذا أردت منتجاً بسعر مختلف فأنت تنشئ نسخة جديدة. والسطران التاليان من `Main.kt`:

```kotlin
val cheaper = products[0].copy(priceHalalas = 4000)
println(cheaper)
println(cheaper == products[0].copy(priceHalalas = 4000))
```

أخرجا السطرين `Product(id=1, nameAr=قهوة عربية, priceHalalas=4000, stock=12)` و`true`، فالنسخة احتفظت بكل الخصائص الأخرى، والمقارنة بـ `==` قارنت المحتوى بين كائنين مختلفين في الذاكرة فأعطت `true`.

### دوال الامتداد Extension Functions: دالة جديدة على كلاس لا تملكه

والسطر `fun Int.asRiyals()` يضيف دالة إلى النوع `Int` نفسه، فتستطيع أن تكتب `2970.asRiyals()` كأنها دالة أصلية فيه. وهذه ليست وراثة، ولا تغير كلاس `Integer` في الـ JVM، وإنما المترجم يحولها إلى دالة `static` عادية تأخذ الرقم كأول معامل، تماماً مثل الدوال المساعدة `StringUtils.format(x)` في Java، ولكن بصيغة أسهل في القراءة. وتشرح صفحة [Extensions](https://kotlinlang.org/docs/extensions.html?ref=arabroot.io) قاعدة مهمة هنا: دوال الامتداد تختار وقت الترجمة حسب النوع المعلن، ولا تعمل مثل الدوال الافتراضية Virtual Methods، ولذلك لا تستخدمها لتغيير سلوك كلاس في الوراثة. والمكتبة الأساسية في Kotlin مبنية كلها تقريباً بهذه الطريقة، فالدوال `filter` و`map` و`firstOrNull` التي استخدمناها على القوائم هي دوال امتداد على واجهات `Iterable` من Java.

### التعبير when والأنواع المغلقة Sealed Types

والملف `Pricing.kt` فيه أهم فكرتين بعد null، وهما الأنواع المغلقة Sealed والتعبير `when`:

```kotlin
package shop

const val VAT_PERCENT = 15

// الضريبة بالهللات مع التقريب للأعلى عند النصف Half Up، بالأعداد الصحيحة فقط
fun vatHalalas(subtotal: Int): Int = (subtotal * VAT_PERCENT + 50) / 100

sealed interface Line {
    data class Item(val product: Product, val qty: Int) : Line
    data class Discount(val halalas: Int) : Line
}

fun subtotal(lines: List<Line>): Int =
    lines.sumOf { line ->
        when (line) { // when على sealed: المترجم يعرف كل الحالات، ولا يحتاج else
            is Line.Item -> line.product.priceHalalas * line.qty // smart cast: line صار Item هنا
            is Line.Discount -> -line.halalas
        }
    }
```

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

- الواجهة `sealed interface Line` تقول للمترجم إن كل الأنواع التي تطبقها معروفة ومكتوبة في نفس الوحدة، وهي هنا نوعان فقط: سطر منتج وسطر خصم. وتشرح صفحة [Sealed classes and interfaces](https://kotlinlang.org/docs/sealed-classes.html?ref=arabroot.io) هذه الفكرة، وهي تقابل `sealed interface` في Java 17 وما بعدها.
- التعبير `when` هنا يرجع قيمة، ولم نكتب فيه `else`، لأن المترجم يعرف أن `Line` لها حالتان فقط وقد غطيناهما. ولو أضاف أحد في المستقبل نوعاً ثالثاً مثل `Line.Shipping`، فسوف يفشل البناء في هذا السطر بالذات حتى يتعامل معه، وهذا أفضل بكثير من حالة `default` تبتلع النوع الجديد بصمت.
- بعد `is Line.Item` استخدمنا `line.product` مباشرة دون تحويل Cast، لأن المترجم يعرف أن `line` صار من النوع `Item` داخل هذا الفرع، وهذا اسمه التحويل الذكي Smart Cast.
- دالة الضريبة تحسب بالأعداد الصحيحة: نضرب في 15 ونضيف 50 ثم نقسم على 100، فيقرب النصف للأعلى Half Up دون أي `Double`. وهذه نفس الدالة التي سوف نبني عليها الخدمة الحقيقية في المقال الرابع.
- `const val` ثابت وقت الترجمة Compile-Time Constant، ويقابل `static final` في Java، ولا يقبل إلا الأرقام والنصوص.

ولاحظ أيضاً أن الدالة `vatHalalas` كتبت بعلامة `=` بدل الأقواس و`return`، وهذا اسمه جسم التعبير Expression Body، ويستخدم عندما تكون الدالة تعبيراً واحداً. والصورة العامة أن Kotlin تحب التعبيرات Expressions أكثر من الجمل Statements: `if` تعبير يرجع قيمة، و`when` تعبير، و`try` تعبير، ولذلك لا يوجد في Kotlin العامل الثلاثي `a ? b : c` لأن `if (a) b else c` تقوم بنفس العمل.

### القوائم Collections والدوال المجهولة Lambdas

والقادم من Python سوف يجد نفسه في بيته مع القوائم، فالكود التالي من `Main.kt`:

```kotlin
products.filterNot { it.isAvailable(1) }.forEach { println("نفد: ${it.nameAr}") }
```

يقابل في Python `[p for p in products if not available(p)]`، وفي Java `products.stream().filter(...).forEach(...)`. والأقواس المعقوفة `{ ... }` بعد اسم الدالة هي دالة مجهولة Lambda، والكلمة `it` هي اسم المعامل الوحيد إذا لم تسمه. وعندما يكون آخر معامل في الدالة Lambda، تستطيع أن تكتبها خارج الأقواس العادية، وهذه القاعدة اسمها Trailing Lambda، وهي التي تجعل كود Kotlin يبدو كأنه لغة خاصة، كما سوف ترى في ملفات Gradle نفسها، وفي مسارات Ktor في مقال الخدمة.

وهناك فرق واحد مهم عن Python يجب أن تتذكره: `listOf(...)` ترجع قائمة للقراءة فقط Read-Only من النوع `List`، ولا تستطيع أن تضيف إليها، وإذا أردت قائمة تتغير فتستخدم `mutableListOf(...)` ونوعها `MutableList`. والقائمة للقراءة فقط ليست ثابتة تماماً Immutable في الذاكرة، فهي غالباً `ArrayList` من Java خلف واجهة لا تعرض دوال التعديل، وهذا فرق دقيق سوف نجربه في مقال الأخطاء الشائعة من هذه المرحلة.

وعند تشغيل البرنامج كاملاً بـ `gradle run` كان المخرج كما يلي:

```text
المنتج 7: غير موجود
المجموع 69.70 ريال، الضريبة 10.46 ريال، الإجمالي 80.16 ريال
نفد: تمر سكري
Product(id=1, nameAr=قهوة عربية, priceHalalas=4000, stock=12)
true
```

والسطر الثاني يستحق أن نتحقق منه بأنفسنا: ثلاث علب هيل بـ 9.90 ريال تساوي 29.70، وقهوة بـ 45 ريالاً، وخصم 5 ريالات، فالمجموع 69.70 ريال أي 6,970 هللة. والضريبة 15% تساوي 1,045.5 هللة، والنصف يقرب للأعلى فتصبح 1,046 هللة، أي 10.46 ريال، والإجمالي 80.16 ريال. والسطر الثالث يقول إن التمر نفد، لأن مخزونه صفر، والدالة `isAvailable(1)` تسأل هل الرقم 1 داخل المدى `1..0`، وهو مدى فارغ.

## مشروع Gradle من الصفر: الأمر gradle init وبنية المشروع Project Layout

الآن سوف نترك المترجم وحده وننتقل إلى الطريقة التي سوف تعمل بها كل يوم. والبداية الأسرع هي أن تطلب من Gradle أن ينشئ لك مشروعاً كاملاً:

```bash
gradle init --type kotlin-application --dsl kotlin --project-name demo --package demo \
  --no-split-project --java-version 25 --test-framework kotlintest --use-defaults
```

واستغرق الأمر 42.8 ثانية في المرة الأولى، وأنشأ الملفات التالية:

```text
./app/build.gradle.kts
./app/src/main/kotlin/demo/App.kt
./app/src/test/kotlin/demo/AppTest.kt
./.gitattributes
./.gitignore
./gradle/libs.versions.toml
./gradle.properties
./gradlew
./gradlew.bat
./gradle/wrapper/gradle-wrapper.jar
./gradle/wrapper/gradle-wrapper.properties
./settings.gradle.kts
```

والصورة التالية تشرح دور كل ملف، وبعدها التفاصيل:

![شجرة مشروع Gradle بلغة Kotlin مع شرح دور كل ملف: settings.gradle.kts وbuild.gradle.kts وlibs.versions.toml وgradlew ومجلدات src](https://arabroot.io/content/images/2026/10/kotlin-for-devs-03-project-layout.webp)

بنية مشروع Kotlin على Gradle: نفس اصطلاح Maven للمجلدات، وملفات البناء نفسها مكتوبة بـ Kotlin

- `settings.gradle.kts`: اسم المشروع والوحدات Modules التي فيه. وهنا وحدة واحدة اسمها `app`، وفي المشاريع الكبيرة تجد وحدات مثل `domain` و`api`.
- `app/build.gradle.kts`: ملف بناء الوحدة: الإضافات Plugins والاعتماديات Dependencies وإعدادات المترجم. واللاحقة `.kts` تعني أنه مكتوب بـ Kotlin نفسها Kotlin DSL، وليس بـ Groovy كما في المشاريع القديمة التي تجد فيها `build.gradle` فقط.
- `gradle/libs.versions.toml`: كتالوج الإصدارات Version Catalog، وهو ملف واحد تكتب فيه كل أرقام الإصدارات للمكتبات والإضافات، ثم تشير إليها الملفات الأخرى بأسماء مثل `libs.guava`. وتشرح وثائق Gradle فكرته في صفحة [Version Catalogs](https://docs.gradle.org/current/userguide/version%5Fcatalogs.html?ref=arabroot.io)، وفائدته تظهر عندما يكون عندك عشر وحدات تستخدم نفس المكتبة.
- `gradlew` و`gradle/wrapper`: غلاف Gradle Wrapper، وهو سكربت صغير ينزل إصدار Gradle المحدد في `gradle-wrapper.properties` ويشغله. والقاعدة في أي مشروع حقيقي أن تشغل `./gradlew` وليس `gradle` المثبت على جهازك، حتى يبني كل المطورين وخط CI بنفس الإصدار. ونحن في المختبر استخدمنا `gradle` مباشرة لأن الـ Image تحدد الإصدار أصلاً.
- `src/main/kotlin` و`src/test/kotlin`: الكود والاختبارات، وهو نفس اصطلاح Maven مع `kotlin` بدل `java`. وتستطيع أن تضع ملفات Java في `src/main/java` بجانبها في نفس الوحدة، والمترجمان يريان بعضهما.

وعند قراءة الملف المولد ظهر شيء يستحق الانتباه في `libs.versions.toml`:

```text
[versions]
guava = "33.5.0-jre"
junit-jupiter-engine = "6.0.1"

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.4.10" }
```

فالأمر `gradle init` كتب إضافة Kotlin بالإصدار 2.4.10، وهو الإصدار المدمج في Gradle 9.8.2 كما رأينا في مخرج `gradle --version`، وليس آخر إصدار مستقر وهو 2.4.21\. وهذا ليس خطأ، ولكنه يعني أن المشروع الجديد يبدأ متأخراً بإصدارين للإصلاحات، ولذلك أول ما تفعله بعد `gradle init` هو أن تراجع أرقام الإصدارات في الكتالوج وترفعها إلى آخر إصدار مستقر. ولاحظ أيضاً أن الملف المولد أضاف مكتبة Guava كمثال، وهي غير ضرورية في أغلب المشاريع، وأن JUnit هنا هو الإصدار 6.0.1، بينما آخر إصدار من JUnit 6 وقت كتابة المقال هو 6.1.3.

### ملف البناء build.gradle.kts سطراً بسطر

وبدلاً من المشروع المولد بنينا مشروعنا الخاص في المجلد `lab/shop` بوحدة واحدة وملف بناء صغير نفهم كل سطر فيه:

```kotlin
plugins {
    kotlin("jvm") version "2.4.21"
    application
    id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
    id("io.gitlab.arturbosch.detekt") version "1.23.8"
}

repositories { mavenCentral() }

dependencies {
    testImplementation(kotlin("test"))
}

kotlin { jvmToolchain(25) }

application { mainClass.set("shop.MainKt") }

ktlint { version.set("1.8.0") }

tasks.test { useJUnitPlatform() }
```

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

- `kotlin("jvm") version "2.4.21"` هو السطر الذي يحدد إصدار Kotlin الذي يترجم كودك. فإذا سألك أحد «أي إصدار Kotlin تستخدمون؟» فالإجابة هنا وليس في مخرج `gradle --version`.
- `application` إضافة من Gradle نفسه، تعطيك المهمة `run` لتشغيل البرنامج، والمهمة `installDist` لتجهيز مجلد توزيع فيه سكربت تشغيل وكل الاعتماديات.
- `kotlin { jvmToolchain(25) }` يقول إن هذا المشروع يترجم ويختبر بـ JDK 25، بغض النظر عن الـ JDK الذي يشغل Gradle نفسه. وهذه فكرة سلسلة الأدوات Toolchains التي تشرحها [وثائق Gradle](https://docs.gradle.org/current/userguide/toolchains.html?ref=arabroot.io)، وتشرح [وثائق Kotlin](https://kotlinlang.org/docs/gradle-configure-project.html?ref=arabroot.io) علاقتها بالخيار `jvmTarget`. وتأكدنا أن Kotlin 2.4.21 يقبل JDK 25 هدفاً دون أي تحذير، فالـ Bytecode الناتج من الإصدار 69 الخاص بـ Java 25.
- `mainClass.set("shop.MainKt")`: اسم الكلاس الذي يحتوي `main`. ولأن `main` في الملف `Main.kt` داخل الحزمة `shop`، فاسم الكلاس هو `shop.MainKt`، وهذا بالضبط ما كشفته الأداة `javap` في التجربة الأولى.
- `testImplementation(kotlin("test"))` يضيف مكتبة `kotlin.test`، وهي طبقة رقيقة فوق JUnit، ولذلك نحتاج `useJUnitPlatform()` في مهمة الاختبار.
- الإضافتان الأخيرتان لأدوات الفحص، وسوف نجربهما بعد قليل، وإحداهما هي التي فشلت معنا.

## البناء والتشغيل والاختبار Build, Run, Test: كم يستغرق كل منها؟

والاختبار الذي كتبناه في `src/test/kotlin/shop/PricingTest.kt` يتحقق من الحالة التي سوف تتكرر في كل لغات المسار، وهي ثلاث علب قهوة بـ 9.90 ريال:

```kotlin
package shop

import kotlin.test.Test
import kotlin.test.assertEquals

class PricingTest {
    @Test
    fun `vat of 29_70 riyals rounds half up to 4_46`() {
        assertEquals(446, vatHalalas(2970)) // 4.455 ريال تصبح 4.46
    }

    @Test
    fun `discount line lowers the subtotal`() {
        val p = Product(1, "هيل", 990, stock = 5)
        assertEquals(2470, subtotal(listOf(Line.Item(p, 3), Line.Discount(500))))
    }
}
```

ولاحظ أسماء الدوال بين علامتي `` ` ``، فـ Kotlin تسمح بأسماء فيها مسافات بهذه الطريقة، والعرف أن تستخدمها في الاختبارات فقط حتى يقرأ تقرير الاختبار كجملة. ثم شغلنا الأوامر الأساسية بالترتيب، وكل أمر داخل Container جديد يشترك مع غيره في Volume واحد لذاكرة Gradle، والسكربت الكامل في `lab/run-gradle.sh`:

```bash
gradle run --console=plain -q
gradle test --console=plain
gradle run --console=plain -q
gradle installDist --console=plain -q
build/install/shop/bin/shop
```

ومخرج الاختبار كان كما يلي، بعد حذف أسطر التحذيرات العامة من Gradle:

```text
> Task :compileKotlin UP-TO-DATE
> Task :compileJava NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar
> Task :compileTestKotlin
> Task :testClasses UP-TO-DATE
> Task :test

BUILD SUCCESSFUL in 19s
4 actionable tasks: 3 executed, 1 up-to-date
<testcase name="vat of 29_70 riyals rounds half up to 4_46()" classname="shop.PricingTest" time="0.115"/>
<testcase name="discount line lowers the subtotal()" classname="shop.PricingTest" time="0.037"/>
```

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

- المهمة `compileKotlin` كانت `UP-TO-DATE`، لأن `gradle run` قبلها ترجم الكود، وGradle يعرف أن المصدر لم يتغير فلا يعيد الترجمة. وهذا البناء التدريجي Incremental Build هو السبب في أن الأمر الثاني أسرع من الأول بكثير.
- `compileJava NO-SOURCE` تعني أن Gradle بحث عن ملفات Java في `src/main/java` فلم يجد، وهذا طبيعي، وهو يذكرك أن المشروع جاهز لكود مختلط.
- تقرير JUnit بصيغة XML يحتوي الاختبارين بأسمائهما الكاملة، واستغرق الأول 115 جزءاً من الألف من الثانية لأنه يحمل الكلاسات لأول مرة، والثاني 37.

والجدول التالي يجمع الأزمنة التي قسناها، وهي أزمنة الأمر كاملاً من الـ Shell، أي تشمل تشغيل Gradle وفحص الملفات وليس الترجمة وحدها:

![أعمدة تقارن زمن gradle run في المرة الأولى بذاكرة فارغة 552 ثانية، ثم 97 ثانية مع ذاكرة ممتلئة وDaemon جديد، ثم 8 إلى 12 ثانية في المرة الثانية، وتشغيل البرنامج من installDist بين 0.6 و1.6 ثانية](https://arabroot.io/content/images/2026/10/kotlin-for-devs-04-build-times.webp)

أغلب وقت المرة الأولى تنزيل، وأغلب وقت المرة الثانية تشغيل Gradle نفسه، والبرنامج المبني يعمل في أقل من ثانيتين

| الأمر                       | الحالة                          | الزمن                      |
| --------------------------- | ------------------------------- | -------------------------- |
| gradle run                  | أول مرة، ذاكرة Gradle فارغة     | 552.1 ثانية                |
| gradle test                 | بعدها مباشرة                    | 56.1 ثانية                 |
| gradle run                  | المكتبات موجودة، Container جديد | 97.0 ثانية                 |
| gradle test                 | بعدها                           | 19.8 ثانية                 |
| gradle run                  | المرة الثانية دون تغيير         | 8.1 و12.1 ثانية في تشغيلين |
| gradle installDist          | بعد البناء                      | 10.6 و15.5 ثانية           |
| build/install/shop/bin/shop | البرنامج المبني وحده            | 1.6 ثم 0.6 ثم 0.9 ثانية    |

والأرقام تحتاج قراءة صحيحة حتى لا تحكم على اللغة من الرقم الأول. فالتشغيل الأول استغرق أكثر من تسع دقائق، وأغلب هذا الوقت تنزيل: إضافة Kotlin للـ Gradle، والمترجم المدمج Kotlin Compiler Embeddable، وإضافتا ktlint وdetekt ومكتباتهما، ومكتبة JUnit، وكل ذلك من الإنترنت إلى ذاكرة Gradle في الـ Volume. وهذا يحدث مرة واحدة على كل جهاز وكل خط CI جديد، ولذلك من أهم إعدادات أي خط CI لمشروع Kotlin أن يحفظ مجلد ذاكرة Gradle بين التشغيلات. والتشغيل الذي استغرق 97 ثانية كانت فيه المكتبات موجودة، ولكن كل Container يبدأ دون Gradle Daemon، فيشغل Gradle من الصفر ويترجم ملفات البناء ويشغل Kotlin Daemon، وهذا وضع خط CI عادي. أما على جهازك فالـ Daemon يبقى حياً بين الأوامر، والمرة الثانية تأخذ ثوان معدودة كما رأيت في الصف الخامس. وكل هذه الأرقام من جهاز كان يشغل تجارب أخرى في نفس الوقت، فخذها كترتيب حجم وليس كقياس دقيق.

وأهم ملاحظة في الجدول هي الصف الأخير: البرنامج المبني يعمل في أقل من ثانيتين، فكل البطء الذي رأيته في الأدوات وليس في الكود الناتج. والمجلد الذي أنشأه `installDist` فيه ما يلي:

```text
build/install/shop/bin/shop
build/install/shop/bin/shop.bat
build/install/shop/lib/annotations-13.0.jar
build/install/shop/lib/kotlin-stdlib-2.4.21.jar
build/install/shop/lib/shop.jar
20K	build/install/shop/lib/annotations-13.0.jar
1.8M	build/install/shop/lib/kotlin-stdlib-2.4.21.jar
12K	build/install/shop/lib/shop.jar
```

فكودنا كله 12 كيلوبايت، والمكتبة الأساسية 1.8 ميجابايت، وهي نفس المكتبة التي غابت في تجربة `NoClassDefFoundError`، ومعها مكتبة صغيرة اسمها `annotations-13.0.jar` تحمل علامات `@Nullable` و`@NotNull` التي تحتاجها المكتبة الأساسية. والسكربت `bin/shop` يبني الـ Classpath من مجلد `lib` ويشغل `java`، وهذا المجلد بالضبط هو ما سوف ننسخه إلى Docker Image في مقال الخدمة الحقيقية.

## أدوات التنسيق Formatter والفحص Linter: ktlint، ktfmt، detekt

في Python تعودت على `black` و`ruff`، وفي JavaScript على Prettier وESLint، وفي Kotlin الأدوات الشائعة ثلاث، والصورة التالية تبين دور كل واحدة:

![ثلاث أدوات: ktlint للتنسيق والقواعد الشكلية، وktfmt للتنسيق فقط، وdetekt للفحص الأعمق، مع المشكلة التي ظهرت عند تشغيل detekt 1.23.8 على JDK 25](https://arabroot.io/content/images/2026/10/kotlin-for-devs-05-lint-tools.webp)

التنسيق والفحص أداتان مختلفتان، والمشكلة التي ظهرت لنا كانت في أداة الفحص مع JDK 25

- **ktlint**: أداة من Pinterest تفحص الكود حسب [أعراف كتابة Kotlin الرسمية Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html?ref=arabroot.io) وتصلح أغلب المخالفات تلقائياً، وتعمل من سطر الأوامر أو عبر إضافة [ktlint-gradle](https://github.com/JLLeitschuh/ktlint-gradle?ref=arabroot.io) التي استخدمناها. ووثائقها في [مستودع ktlint](https://github.com/pinterest/ktlint?ref=arabroot.io).
- **ktfmt**: أداة من Meta تنسق الكود فقط ولا تسألك عن رأيك، مثل `black` في Python، ومصدرها في [مستودع ktfmt](https://github.com/facebook/ktfmt?ref=arabroot.io). وتختار بينها وبين ktlint، ولا تستخدم الاثنتين معاً على نفس الكود لأن كلاً منهما سوف يعيد تنسيق ما كتبته الأخرى.
- **detekt**: أداة فحص ثابت Static Analysis أعمق، تبحث عن الدوال الطويلة والأرقام السحرية Magic Numbers والاستثناءات التي تبتلع، وتشبه SonarQube أو قواعد `ruff` الأعمق. ووثائقها في [detekt.dev](https://detekt.dev/?ref=arabroot.io).

ولنرى ktlint يعمل أضفنا إلى نسخة من المشروع دالة كتبناها بتنسيق سيء عمداً: مسافات زائدة بعد `fun`، ونوع ملتصق بالنقطتين، وقوس في غير مكانه:

```kotlin
fun   badlyFormatted(x:Int):Int{
      return x*2 }
```

```bash
gradle ktlintCheck --console=plain
```

والمخرج سوف يكون كما يلي، بعد حذف أسطر التحذير عن `sun.misc.Unsafe` التي سوف نشرحها بعد قليل:

```text
> Task :ktlintMainSourceSetCheck FAILED
/w/shop/src/main/kotlin/shop/Main.kt:4:20 A multiline expression should start on a new line
/w/shop/src/main/kotlin/shop/Pricing.kt:9:21 Newline expected after opening parenthesis
/w/shop/src/main/kotlin/shop/Pricing.kt:9:43 Parameter should start on a newline
/w/shop/src/main/kotlin/shop/Pricing.kt:9:55 Newline expected before closing parenthesis
/w/shop/src/main/kotlin/shop/Pricing.kt:10:5 Expected a blank line for this declaration
/w/shop/src/main/kotlin/shop/Pricing.kt:10:25 Newline expected after opening parenthesis
/w/shop/src/main/kotlin/shop/Pricing.kt:10:41 Newline expected before closing parenthesis
/w/shop/src/main/kotlin/shop/Pricing.kt:16:66 Add a blank line between all when-conditions in case at least one multiline when-condition is found in the statement
/w/shop/src/main/kotlin/shop/Product.kt:16:4 Single space expected after the fun keyword
/w/shop/src/main/kotlin/shop/Product.kt:16:5 Unnecessary long whitespace
```

والمفاجأة في هذا المخرج أن ktlint لم يعترض على الدالة السيئة وحدها، وإنما على كودنا «النظيف» أيضاً، ففي `Pricing.kt` كتبنا تعريف `sealed interface` بأسطر مضغوطة، والأداة تريد كل معامل في سطر، وسطراً فارغاً بين الحالات في `when` إذا كانت إحداها متعددة الأسطر، وفي `Main.kt` تريد أن يبدأ التعبير `listOf(` في سطر جديد. وكل سطر في المخرج فيه اسم الملف ورقم السطر ورقم العمود والقاعدة، فتستطيع أن تذهب إلى المكان مباشرة. وهذه المخالفات ليست أخطاء في البرنامج، وإنما في الشكل، ولكن قيمة الأداة أن الفريق كله يكتب بشكل واحد، فتنظر في مراجعة الكود Code Review إلى المنطق وليس إلى المسافات. ثم شغلنا الإصلاح التلقائي:

```bash
gradle ktlintFormat --console=plain -q
tail -4 src/main/kotlin/shop/Product.kt
```

```text
fun Product.isAvailable(qty: Int): Boolean = qty in 1..stock

fun badlyFormatted(x: Int): Int = x * 2
```

ولاحظ أن الأداة لم تصلح المسافات فقط، وإنما حولت الدالة من جسم بأقواس و`return` إلى جسم تعبير Expression Body بعلامة `=`، وهذا هو الشكل المفضل في أعراف Kotlin لدالة من تعبير واحد. والقاعدة العملية أن تشغل `ktlintFormat` قبل كل Commit، و`ktlintCheck` في خط CI حتى يفشل البناء إذا نسي أحد.

أما أسطر التحذير التي حذفناها من المخرج فكانت تتكرر بهذا الشكل:

```text
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.jetbrains.kotlin.com.intellij.util.containers.Unsafe (file:/home/gradle/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compiler-embeddable/2.2.21/...)
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
```

والتحذير يأتي من الـ JDK 25 نفسه، فهو يحذر من أن دوال الذاكرة في `sun.misc.Unsafe` سوف تحذف في إصدار قادم، والكود الذي استدعاها جزء من المترجم المدمج داخل ktlint، ولاحظ الإصدار في المسار: 2.2.21، وليس 2.4.21 الذي نستخدمه. فأدوات الفحص تحمل معها نسخة خاصة من مترجم Kotlin لتحليل الكود، وهذه النسخة قد تكون أقدم من لغتك. وهذا التحذير لا يوقف شيئاً اليوم، ولكنه يشرح المشكلة التالية.

### المشكلة التي ظهرت: detekt مع JDK 25

وعند تشغيل `gradle detekt` بالإصدار المستقر الأخير من الأداة، وهو 1.23.8، فشل البناء بهذه الرسالة:

```text
> Task :detekt FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':detekt' (registered by plugin 'io.gitlab.arturbosch.detekt').
> Invalid value (25) passed to --jvm-target, must be one of [1.6, 1.8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
```

والرسالة واضحة: الإضافة قرأت `jvmToolchain(25)` من مشروعنا، ومررت 25 كهدف لمحلل detekt، والمحلل لا يعرف إلا الأهداف حتى 22، لأنه مبني على مترجم Kotlin أقدم من JDK 25\. والحل الذي تقترحه وثائق الأداة لمثل هذه الحالة هو أن تحدد هدفاً مختلفاً لمهمة detekt وحدها، دون أن تغير هدف المشروع، فأضفنا السطر التالي إلى ملف البناء:

```kotlin
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach { jvmTarget = "22" }
```

وشغلنا المهمة مرة ثانية، ففشلت برسالة أقصر وأغرب:

```text
> Task :detekt FAILED
* What went wrong:
Execution failed for task ':detekt' (registered by plugin 'io.gitlab.arturbosch.detekt').
> 25.0.5
```

والرسالة `25.0.5` وحدها هي رقم إصدار الـ JDK الذي يشغل Gradle، وهذا يعني أن detekt 1.23.8 لم يستطع أن يفهم إصدار الـ JVM التي يعمل عليها أصلاً، فالمشكلة ليست في الهدف وإنما في البيئة. وهذا درس مهم لأي قادم إلى منظومة الـ JVM: اللغة والـ JDK والأدوات تتحرك كل منها بإيقاع مختلف، وعندما تستخدم أحدث JDK فقد تسبق بعض الأدوات بأشهر.

والإصدار القادم من الأداة، وهو detekt 2.0، ما زال تجريبياً Alpha وقت كتابة المقال، وقد غير اسم الإضافة إلى `dev.detekt`، فجربناه بتغيير سطر واحد في ملف البناء:

```kotlin
id("dev.detekt") version "2.0.0-alpha.6"
```

```text
> Task :detekt
Successfully generated markdown at file:///w/shop/build/reports/detekt/detekt.md
Successfully generated html at file:///w/shop/build/reports/detekt/detekt.html
Successfully generated sarif at file:///w/shop/build/reports/detekt/detekt.sarif
Successfully generated checkstyle at file:///w/shop/build/reports/detekt/detekt.xml
e: /w/shop/src/main/kotlin/shop/Pricing.kt:6:64 This expression contains a magic number. Consider defining it to a well named constant. [MagicNumber]
e: /w/shop/src/main/kotlin/shop/Pricing.kt:6:70 This expression contains a magic number. Consider defining it to a well named constant. [MagicNumber]
e: /w/shop/src/main/kotlin/shop/Product.kt:12:59 This expression contains a magic number. Consider defining it to a well named constant. [MagicNumber]
e: /w/shop/src/main/kotlin/shop/Product.kt:12:71 This expression contains a magic number. Consider defining it to a well named constant. [MagicNumber]
> Task :detekt FAILED
> Analysis failed with 4 issues.
BUILD FAILED in 4m 34s
```

هذه المرة عملت الأداة على JDK 25، وكتبت تقاريرها بأربع صيغ، منها SARIF التي تفهمها منصات مثل GitHub لعرض الملاحظات على سطور الكود مباشرة، ثم وجدت أربع ملاحظات من نوع الرقم السحري Magic Number: الرقمان 50 و100 في دالة الضريبة، والرقمان 100 في دالة `asRiyals`. وهذه ملاحظات في محلها، فالرقم 100 هنا معناه «عدد الهللات في الريال»، والأفضل أن يكون ثابتاً اسمه `HALALAS_PER_RIYAL` حتى يقرأ الكود كما يفكر الإنسان. واستغرقت المهمة أكثر من أربع دقائق لأنها نزلت الأداة ومترجمها لأول مرة. والقرار العملي لفريقنا: ktlint في كل بناء من اليوم الأول، وdetekt 2 في خط CI منفصل لا يوقف الدمج حتى يصدر الإصدار المستقر، أو البقاء على JDK 21 إذا كان الفريق يحتاج detekt 1.23.8 المستقر، وهذه مقايضة يجب أن تقررها بوعي وليس بالصدفة.

## ماذا يحدث إذا استدعيت كود Java من Kotlin؟ التوافق Java Interop

والسؤال الذي يطرحه كل فريق قادم من Java: هل نستطيع أن نستخدم مكتباتنا وكودنا القديم؟ والإجابة نعم، وهذا من أقوى ما في اللغة، وتشرح صفحة [Calling Java from Kotlin](https://kotlinlang.org/docs/java-interop.html?ref=arabroot.io) التفاصيل. فأي كلاس Java تستدعيه من Kotlin كأنه كلاس Kotlin، والـ Getters والـ Setters تظهر كخصائص، فتكتب `user.name` بدل `user.getName()`، والاتجاه المعاكس يعمل أيضاً كما رأيت مع `HelloKt`. ولكن هناك نقطة واحدة يجب أن تعرفها من اليوم الأول: كود Java لا يقول هل القيمة قد تكون null أم لا، فإذا رجعت دالة Java بـ `String`، فإن Kotlin لا تعرف هل هو `String` أم `String?`، وتعامله كنوع منصة Platform Type تكتبه الرسائل بالشكل `String!`، وتترك لك القرار. وهذا هو المكان الوحيد الذي يمكن أن يظهر فيه `NullPointerException` في كود Kotlin لم تستخدم فيه `!!`، وسوف نجربه بكود Java حقيقي يرجع null في المقال الثاني من المرحلة، لأنه يستحق تجربة كاملة.

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

- **الخلط بين إصدار Kotlin في Gradle وإصدار Kotlin في المشروع**: السطر `Kotlin: 2.4.10` في `gradle --version` لا علاقة له بكودك، والإصدار الصحيح في `kotlin("jvm") version` أو في الكتالوج.
- **نسخ ملف JAR وحده إلى السيرفر**: ثم يظهر `NoClassDefFoundError: kotlin/...`، والسبب أن المكتبة الأساسية `kotlin-stdlib` خارج الملف. استخدم `installDist` أو إضافة تجمع الاعتماديات في ملف واحد.
- **تشغيل `gradle` المثبت بدل `./gradlew`**: فيبني كل مطور بإصدار مختلف من Gradle، وتظهر أخطاء لا يراها غيره.
- **الحكم على اللغة من زمن أول بناء**: أغلب الدقائق الأولى تنزيل وتشغيل Daemon، والمقياس الصحيح هو المرة الثانية وزمن تشغيل البرنامج المبني.
- **قسمة الأعداد الصحيحة**: `4500 / 100` تساوي 45 و`990 / 100` تساوي 9، وليس 9.9 كما في Python، فإذا أردت رقماً عشرياً فأحد الطرفين يجب أن يكون `Double`، وفي المبالغ المالية لا تفعل ذلك أصلاً واحسب بالهللات.
- **كتابة `var` لكل شيء**: عادة من Python وJavaScript، وفي Kotlin تبدأ بـ `val` وتغير إلى `var` عندما يطلب المترجم ذلك.
- **استخدام `!!` لإسكات المترجم**: كل `!!` هو `NullPointerException` مؤجل، والطريقة الصحيحة هي `?.` و`?:` أو فحص صريح.
- **استخدام أحدث JDK دون التحقق من الأدوات**: كما حدث مع detekt 1.23.8، فقبل أن ترفع الـ JDK في مشروع قائم شغل كل مهام البناء والفحص وليس الترجمة وحدها.
- **تشغيل ktlint وktfmt معاً**: اختر واحدة للتنسيق، وإلا فسوف يعيد كل منهما تنسيق ما كتبته الأخرى في كل Commit.

## قائمة تحقق Checklist لمشروع Kotlin جديد

- هل الـ JDK إصدار LTS من توزيعة مجانية معروفة، ورقمه مكتوب في ملف الإصدارات؟
- هل إصدار Kotlin محدد في الكتالوج أو ملف البناء، وهو آخر إصدار مستقر وليس الإصدار الذي كتبه `gradle init`؟
- هل `jvmToolchain` محدد، حتى لا يعتمد البناء على الـ JDK المثبت عند كل مطور؟
- هل غلاف `gradlew` في المستودع، والجميع يستخدمه؟
- هل خط CI يحفظ ذاكرة Gradle بين التشغيلات؟
- هل أداة تنسيق واحدة فقط (ktlint أو ktfmt) تعمل في CI وتفشل البناء عند المخالفة؟
- هل أداة الفحص الأعمق تعمل على نفس الـ JDK الذي يستخدمه المشروع، أم تحتاج إعداداً خاصاً؟
- هل المبالغ المالية بالأعداد الصحيحة، ولا يوجد `Double` في أي حساب سعر؟
- هل مجلد التوزيع أو ملف JAR النهائي يحتوي `kotlin-stdlib`؟

## تدريب عملي

- أضف نوعاً ثالثاً إلى `sealed interface Line` اسمه `Shipping`، وشغل `gradle build`، واقرأ رسالة المترجم عند التعبير `when`، ثم أضف الحالة الجديدة.
- غير `val wanted: Product? = byId[7]` إلى `val wanted: Product = byId[7]`، ولاحظ أين يعترض المترجم، ثم جرب `byId.getValue(7)` وشغل البرنامج، واقرأ الاستثناء الذي يظهر.
- اكتب سكربت `.main.kts` يقرأ ملف CSV صغيراً فيه أسماء منتجات وأسعار بالهللات، ويطبع الإجمالي مع الضريبة باستخدام الدالة `vatHalalas`.
- شغل `javap -p` على الكلاس `shop.Product` بعد البناء، وعد الدوال التي كتبها المترجم بسبب الكلمة `data`.

## الخلاصة

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

- Kotlin على الـ JVM ثلاث طبقات بثلاثة إصدارات: JDK 25 يشغل، والمترجم 2.4.21 يحول الكود إلى ملفات `.class`، وGradle 9.8.2 ينظم البناء، وإصدار Kotlin المدمج في Gradle (2.4.10) ليس إصدار مشروعك.
- الدوال في المستوى الأعلى تصبح دوال `static` في كلاس اسمه اسم الملف مع `Kt`، وكل برنامج يحتاج `kotlin-stdlib` وقت التشغيل، وغيابها يعطي `NoClassDefFoundError`.
- أهم ما يختلف في الصيغة: null جزء من النوع (`String?`)، و`==` تقارن المحتوى، و`data class` تكتب لك `equals` و`copy`، و`when` على نوع `sealed` يجبرك على تغطية كل الحالات.
- أول بناء استغرق 552 ثانية أغلبها تنزيل، والمرة الثانية بين 8 و12 ثانية، والبرنامج المبني يعمل في أقل من ثانيتين.
- ktlint اعترض على عشرة أسطر وأصلحها تلقائياً، وdetekt 1.23.8 لم يعمل على JDK 25، وهذا مثال على أن الأدوات قد تتأخر عن أحدث JDK.

وفي المقال التالي من مرحلة [لغة Kotlin للمطورين](https://arabroot.io/start/#stage-lang-kotlin) سوف ننتقل إلى نظام الأنواع وأمان القيم الفارغة والذاكرة والأخطاء، ونقيس ما تكلفه الأنواع التي تقبل null والكلاسات القيمية Value Classes. وإذا كنت تريد الصورة العامة قبل التفاصيل فهذه المقالات المنشورة تكمل هذا المقال:

[![](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/)[![](https://arabroot.io/content/images/2026/10/polyglot-programming-cover-cover.webp)دليلالبرمجة بعدة لغات Polyglot: متى تفيد ومتى تعقد مشروعك؟نستدعي نفس الدالة من Python بخمس طرق، عبر FFI إلى C وGo وعبر HTTP وgRPC، ونقيس كلفة كل حد بين لغتين وتعقيد البناء وما يحدث عندما يتعطل الطرف الآخر.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A8%D8%B9%D8%AF%D8%A9-%D9%84%D8%BA%D8%A7%D8%AA-polyglot-%D9%85%D8%AA%D9%89-%D8%AA%D9%81%D9%8A%D8%AF-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%B9%D9%82%D8%AF-%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)[![](https://arabroot.io/content/images/2026/10/paradigms-types-cover-cover.webp)دليلأنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟نحل مسألة واحدة بعدة أساليب برمجة، ونرى هل تكتشف اللغة أخطاءك قبل التشغيل أم بعده، وماذا يعني ذلك عند اختيار لغة لمشروعك.](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](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/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/)

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

- أكتوبر 2026: كتابة المقال، وتنفيذ التجارب على Kotlin 2.4.21 وGradle 9.8.2 وOpenJDK 25.0.5 (Temurin) داخل Docker، مع ktlint 1.8.0 وdetekt 1.23.8 و2.0.0-alpha.6.