> ## 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 الحديث Modern Java
- URL: https://arabroot.io/articles/اخطاء-شائعة-واسلوب-java-الحديث/
- Published: 2026-10-31T09:45:06.000Z
- Updated: 2026-10-31T10:44:31.000Z
- Description: أربعة عشر خطأً يقع فيها من يأتي إلى Java من لغة أخرى، لكل خطأ تشغيل حقيقي يبين النتيجة الخاطئة ثم الطريقة الصحيحة، ثم خطة قراءة من المصادر الرسمية.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

بعد شهر في فريق خدمة الطلبات صرت تكتب Java كل يوم، والبناء ينجح والاختبارات تمر، ولكن مراجعات الكود Code Review ما زالت تعود إليك بتعليقات من نوع «هذا يعمل، ولكنه ليس أسلوب Java». وفي نفس الشهر وصلت إلى الفريق ثلاثة أخطاء من الإنتاج كلها من كود كتبه مطورون جاؤوا من لغات أخرى: تقرير المبيعات الشهري ظهر فيه مجموع سالب في شهر التخفيضات، ومهمة تصدير الفواتير صارت تأخذ دقائق بعد أن كبر عدد الطلبات، وتواريخ بعض الإيصالات جاءت في سنة 2031\. ولم يرم أي من هذه الأخطاء استثناء، ولم يعترض المترجم على أي سطر فيها، وكلها مرت من الاختبارات.

وهذا المقال هو الأخير في مرحلة [لغة Java للمطورين](https://arabroot.io/start/#stage-lang-java)، وهو مرجع ترجع إليه أكثر من مرة: أربعة عشر خطأً شائعاً يقع فيها من يأتي إلى Java من Python أو JavaScript أو PHP أو C#، ولكل خطأ تشغيل حقيقي على JDK 25 يبين النتيجة الخاطئة، ثم الطريقة الصحيحة بأسلوب Java الحديث وتشغيلها. وبعد الأخطاء خطة قراءة من المصادر الرسمية لمن يريد أن يكمل. وبعض الأخطاء ذكرناها بإيجاز في مقالات سابقة من المرحلة، مثل مقارنة `Integer` بـ `==` في [مقال الأنواع والذاكرة](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%88%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%AB%D9%86%D8%A7%D8%A1%D8%A7%D8%AA-%D9%81%D9%8A-java/)، فسوف نشير إليها هنا دون أن نعيد شرحها.

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

- كيف بنينا التجربة: ملف واحد فيه كل خطأ بتشغيل خاطئ ثم تشغيل صحيح.
- أخطاء المساواة والهاش: `==` على النصوص، و`equals` دون `hashCode`، والمفتاح الذي يتغير.
- أخطاء المجموعات: الحذف أثناء الحلقة، و`Arrays.asList` و`List.of`، وUnboxing لقيمة غائبة.
- أخطاء الأعداد: `BigDecimal` من `double`، والمساواة بين 2.0 و2.00، والالتفاف في المجاميع، والقسمة الصحيحة.
- أخطاء النصوص والوقت: `+=` في الحلقات، و`SimpleDateFormat` المشترك، وswitch القديمة.
- أخطاء التزامن والموارد: `parallel().forEach` إلى قائمة عادية، والملفات التي لا تغلق، و`Optional.get()`.
- قائمة مراجعة لأسلوب Java الحديث، وخطة قراءة من المصادر الرسمية.

## كيف بنينا التجربة Lab؟

كل الأخطاء في ملف واحد اسمه `Idioms.java`، وكل خطأ حالة `case` في `switch` تشغل الطريقة الخاطئة ثم الطريقة الصحيحة وتطبع النتيجتين، وخطأ الملفات المفتوحة في ملف منفصل `Leaks.java` لأنه يحتاج حداً لعدد الملفات. وشغلنا كل الحالات داخل Container من نفس الـ Image التي استخدمناها في كل المرحلة، وفيها JDK 25.0.5، بحد معالجين:

```bash
mkdir -p /tmp/i && javac -d /tmp/i Idioms.java Leaks.java
for c in strings hash mutablekey cme aslist bigdecimal divide concat dateformat optional parallel average unboxing switch; do
  echo "### $c"; java -cp /tmp/i Idioms $c
done
bash -c 'ulimit -n 256; java -cp /tmp/i Leaks leak; java -cp /tmp/i Leaks close'
```

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

![ست مجموعات من الأخطاء الشائعة في Java: المساواة والهاش، والمجموعات، والأعداد، والنصوص والوقت، والتزامن، والموارد](https://arabroot.io/content/images/2026/10/java-idioms-01-mistakes-map.webp)

أربعة عشر خطأً في ست مجموعات، وكلها مرت من المترجم دون اعتراض.

ولاحظ شيئاً مشتركاً بين أغلب هذه الأخطاء قبل أن نبدأ: كثير منها لا يرمي أي استثناء، وإنما يعطي قيمة خاطئة تمر بهدوء إلى قاعدة البيانات أو التقرير، وهذا أخطر من الاستثناء لأنه لا يظهر في السجلات Logs. ولهذا فإن معرفة هذه الأنماط مسبقاً، ووجود أداة فحص مثل Error Prone التي أضفناها في [المقال الأول من المرحلة](https://arabroot.io/articles/java-%D9%84%D9%85%D9%86-%D9%8A%D8%B9%D8%B1%D9%81-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B5%D9%8A%D8%BA%D8%A9/)، أرخص بكثير من اكتشافها في الإنتاج.

وقبل أن نبدأ، ملاحظة عن الأرقام: ثلاث حالات من الأربع عشرة تعتمد على التوقيت بين الخيوط أو على سرعة الجهاز، وهي `dateformat` و`parallel` و`concat`، فإذا شغلتها على جهازك فسوف تحصل على أعداد مختلفة من النتائج الخاطئة وأزمنة مختلفة، وقد تحصل أحياناً على صفر أخطاء في تشغيل واحد من `parallel`. وهذا جزء من الدرس وليس عيباً في التجربة، فالخطأ الذي يظهر في بعض التشغيلات فقط هو بالضبط النوع الذي يمر من الاختبار ويظهر في الإنتاج، ولهذا كررنا حالة `parallel` ثلاث مرات في نفس التشغيل. أما الحالات الإحدى عشرة الباقية فنتيجتها ثابتة في كل تشغيل، لأنها تعتمد على قواعد اللغة والمكتبة وحدها، وتستطيع أن تشغل أي حالة منفردة بتمرير اسمها، مثل `java Idioms.java hash`، دون أن تحتاج Gradle أو أي مشروع.

## المساواة Equality والهاش Hashing

### الخطأ 1: مقارنة النصوص Strings بالعلامة ==

في Python وJavaScript تقارن النصوص بـ `==` وتقارن المحتوى، وفي Java تقارن `==` بين كائنين هل هما نفس الكائن في الذاكرة. والفخ أن هذا يعمل أحياناً، لأن النصوص الثابتة المكتوبة في الكود يحفظها المترجم في مكان واحد String Pool، فيصبح `"coffee" == "coffee"` صحيحاً. ولكن النص الذي يأتي من طلب HTTP أو من ملف أو من قاعدة بيانات كائن جديد، وهذا ما مثلناه بقراءة النص من `Scanner`:

```java
String fromInput = new Scanner("coffee\n").nextLine();     // like a value read from a request
System.out.println("== on equal text:     " + (fromInput == "coffee"));
System.out.println("equals on equal text: " + fromInput.equals("coffee"));
```

```text
== on equal text:     false
equals on equal text: true
```

والنص هو نفسه حرفاً بحرف، ومع ذلك أعطت `==` القيمة `false`. والطريقة الصحيحة `equals` دائماً، وإذا كان أحد الطرفين قد يكون `null` فاكتب الثابت أولاً `"coffee".equals(input)` أو استخدم `Objects.equals(a, b)`. ونفس القاعدة تنطبق على `Integer` و`Long` كما رأيت في مقال الأنواع، حيث أعطت المقارنة `true` للقيمة 127 و`false` للقيمة 128.

### الخطأ 2: equals دون hashCode

عندما تعرف `equals` في class لتقول إن منتجين متساويان إذا كان لهما نفس الرمز SKU، يجب أن تعرف `hashCode` بنفس المنطق، وهذا عقد مكتوب في [توثيق Object](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.html?ref=arabroot.io): الكائنان المتساويان يجب أن يكون لهما نفس الهاش. ومن يأتي من Python يعرف هذا من `__eq__` و`__hash__`، ولكن Python تجعل الكائن غير قابل للهاش إذا عرفت الأولى دون الثانية، أما Java فتترك `hashCode` الافتراضي المبني على هوية الكائن، ولا تعترض:

```java
static class Sku {                       // equals without hashCode
    final String code;
    Sku(String code) { this.code = code; }
    @Override public boolean equals(Object o) { return o instanceof Sku s && s.code.equals(code); }
}
record SkuRecord(String code) {}         // equals and hashCode generated together
```

```text
HashSet<Sku> size after adding an equal Sku: 2, contains: false
HashSet<SkuRecord> size: 1, contains: true
```

![مخطط أدراج HashSet: كائنان متساويان في درجين مختلفين بسبب غياب hashCode، وكائن record في درج واحد، ومفتاح تغير بعد وضعه](https://arabroot.io/content/images/2026/10/java-idioms-02-hash-equals.webp)

الـ HashSet يختار الدرج من `hashCode` أولاً، ولا يستدعي `equals` إلا داخل نفس الدرج.

والنتيجة أن المجموعة فيها نفس المنتج مرتين، والبحث عنه يعطي `false`. والسبب أن `HashSet` يحسب الهاش أولاً ليختار الدرج Bucket، ثم يقارن بـ `equals` فقط مع ما في نفس الدرج، والهاش الافتراضي مختلف لكل كائن فلا تصل المقارنة أصلاً. والحل الأنسب في Java الحديثة أن تستخدم `record` لكل كائن قيمة Value Object، فهو يولد `equals` و`hashCode` و`toString` من نفس الحقول معاً، وإذا احتجت class عادية فاجعل بيئة التطوير تولد الدالتين معاً، أو استخدم `Objects.hash`.

### الخطأ 3: مفتاح Map يتغير بعد وضعه

الخطأ الثالث أخفى من الثاني، لأن الدالتين موجودتان وصحيحتان. وضعنا سلة مشتريات كمفتاح في `HashMap` وهي فارغة، ثم أضفنا لها منتجاً:

```java
Map<Cart, String> owners = new HashMap<>();
Cart cart = new Cart();
owners.put(cart, "customer-7");
cart.items.add("dates");                       // the key changes after it was stored
System.out.println("get after mutation: " + owners.get(cart) + ", size: " + owners.size() + ", containsValue: " + owners.containsValue("customer-7"));
```

```text
get after mutation: null, size: 1, containsValue: true
```

والقاموس فيه عنصر واحد، والقيمة موجودة، ولكن البحث بنفس الكائن يعطي `null`. والسبب أن الهاش يحسب من محتوى السلة، فعندما تغير المحتوى تغير الهاش، والعنصر بقي في الدرج الذي حسب له عند الإدخال، فالبحث يذهب إلى درج آخر فارغ. والقاعدة: مفاتيح `Map` وعناصر `Set` يجب أن تكون ثابتة Immutable، ولهذا فإن `String` و`Integer` والسجلات Records مفاتيح ممتازة، أما الكائن الذي يتغير فاستخدم معرفه الثابت كمفتاح بدلاً منه، مثل رقم السلة. ولاحظ أن `record` وحده لا يحمي إذا كان أحد حقوله قائمة قابلة للتعديل، فاجعل الحقل `List.copyOf(...)` في منشئ السجل.

## المجموعات Collections

### الخطأ 4: الحذف داخل حلقة for-each ConcurrentModificationException

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

```java
List<Integer> stock = new ArrayList<>(List.of(5, 0, 3, 0, 8));
try {
    for (Integer s : stock) if (s == 0) stock.remove(s);
} catch (ConcurrentModificationException e) {
    System.out.println("for-each + remove: " + e);
}
stock = new ArrayList<>(List.of(5, 0, 3, 0, 8));
stock.removeIf(s -> s == 0);
System.out.println("removeIf: " + stock);
```

```text
for-each + remove: java.util.ConcurrentModificationException
removeIf: [5, 3, 8]
```

والاسم `ConcurrentModificationException` مضلل، فلا يوجد أي خيط آخر هنا، وإنما الحلقة تستخدم مكرراً Iterator يحفظ عدد التعديلات على القائمة، فإذا تغير العدد من خارجه رمى الاستثناء في الدورة التالية، كما يشرح [توثيق الاستثناء](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/ConcurrentModificationException.html?ref=arabroot.io). والطريقة الصحيحة `removeIf` التي تحذف في مرور واحد، أو `stream().filter(...).toList()` إذا أردت قائمة جديدة. ولاحظ تفصيلاً آخر في الكود الخاطئ: `stock.remove(s)` مع `Integer` تستدعي الحذف بالقيمة، أما مع `int` فتستدعي الحذف بالموقع، وهذا فخ آخر من نفس العائلة.

### الخطأ 5: القوائم الثابتة Immutable Lists من Arrays.asList أو List.of ليست ArrayList

```java
List<String> fixed = Arrays.asList("coffee", "dates");
try { fixed.add("cups"); } catch (UnsupportedOperationException e) { System.out.println("Arrays.asList add: " + e); }
try { List.of("coffee", null); } catch (NullPointerException e) { System.out.println("List.of with null: " + e); }
List<String> growable = new ArrayList<>(List.of("coffee", "dates"));
growable.add("cups");
```

```text
Arrays.asList add: java.lang.UnsupportedOperationException
List.of with null: java.lang.NullPointerException
new ArrayList<>(...): [coffee, dates, cups]
```

كل هذه الطرق ترجع النوع `List`، فالمترجم يسمح لك باستدعاء `add` عليها، ولكن كل واحدة لها سلوك مختلف وقت التشغيل: `Arrays.asList` غلاف حول مصفوفة بحجم ثابت، تقبل التعديل بـ `set` ولا تقبل الإضافة أو الحذف، و`List.of` غير قابلة للتعديل إطلاقاً ولا تقبل `null` حتى عند الإنشاء، و`new ArrayList<>(...)` هي القائمة القابلة للتعديل بالكامل. والقاعدة: استخدم `List.of` للقوائم الثابتة، وهي الافتراضية في الكود الحديث لأنها تمنع التعديل غير المقصود، وانسخها إلى `ArrayList` صراحة عندما تحتاج التعديل، ولا ترجع من دالة عامة قائمة يتوقع المستدعي أن يعدلها وهي ثابتة دون أن توثق ذلك.

### الخطأ 6: Unboxing لقيمة null من Map

```java
Map<String, Integer> stock = new HashMap<>(Map.of("coffee", 12));
int dates = stock.get("dates");                 // null unboxed to int
```

```text
unboxing null: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
getOrDefault: 0
```

السطر لا يحتوي أي استدعاء ظاهر، ومع ذلك رمى `NullPointerException`. فالمفتاح غير موجود، و`get` رجعت `null`، والمترجم أضاف بصمت استدعاء `intValue()` ليحول `Integer` إلى `int`، والرسالة المفيدة من JDK 14 تكشف هذا الاستدعاء الخفي. ومن يأتي من Python ينتظر `KeyError` واضحاً، ومن يأتي من JavaScript ينتظر `undefined` يمر بهدوء، وJava تعطيك الاثنين في أسوأ شكل: لا خطأ عند الغياب، ثم خطأ غامض بعده. والطريقة الصحيحة أن تقرر ماذا يعني الغياب: `getOrDefault("dates", 0)` إذا كان صفراً منطقياً، أو `Optional.ofNullable(...)` إذا كان حالة يجب أن يعالجها المستدعي، أو `merge` و`computeIfAbsent` عندما تعدل القاموس.

## الأعداد Numbers

### الخطأ 7: BigDecimal من double، والمساواة بين 2.0 و2.00

من يعرف أن `double` لا يصلح للمال ينتقل إلى `BigDecimal`، ثم يقع في فخين داخلها:

```text
new BigDecimal(9.90):      9.9000000000000003552713678800500929355621337890625
new BigDecimal("9.90"):    9.90
2.0 equals 2.00:           false
2.0 compareTo 2.00 == 0:   true
HashSet of 2.0 and 2.00:   2 elements
```

السطر الأول يبين أن المنشئ الذي يأخذ `double` يحفظ القيمة الثنائية الحقيقية للعدد 9.90 بكل أرقامها، فالخطأ دخل قبل أن تصل إلى `BigDecimal`، والطريقة الصحيحة أن تنشئها من نص كما في السطر الثاني، أو من `BigDecimal.valueOf(double)` التي تمر عبر التمثيل النصي المختصر. والسطر الثالث أغرب: الرقمان 2.0 و2.00 غير متساويين بـ `equals`، لأن [BigDecimal](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/math/BigDecimal.html?ref=arabroot.io) تعتبر عدد الخانات العشرية Scale جزءاً من القيمة، ولذلك أصبح في `HashSet` عنصران لنفس السعر. والمقارنة الرقمية تكون بـ `compareTo(...) == 0`، وإذا احتجت أن تستخدمها مفتاحاً فوحد الخانات بـ `setScale(2, RoundingMode.HALF_UP)` قبل ذلك. ولهذا السبب نحسب في هذه المرحلة بالهللات في `long`، فهو أبسط وأسرع ولا فخاخ فيه طالما أن أصغر وحدة معروفة.

وفخ ثالث في `BigDecimal` يظهر عند أول قسمة. فإذا أردت أن تقسم 100 ريال على ثلاثة أقساط دون أن تحدد التقريب، فإن `divide` ترمي استثناء بدل أن تعطيك نتيجة تقريبية، لأنها لا تستطيع أن تمثل الناتج بعدد محدود من الخانات:

```text
divide without rounding: Non-terminating decimal expansion; no exact representable decimal result.
divide(3, 2, HALF_UP): 33.33
```

وهذا سلوك جيد في الحقيقة، لأنه يجبرك أن تقرر قاعدة التقريب بدل أن تقررها اللغة عنك، فاكتب دائماً عدد الخانات ونوع التقريب `RoundingMode` في كل قسمة، وفكر أين تذهب الهللة الباقية، فثلاثة أقساط من 33.33 مجموعها 99.99، والمتجر يحتاج أن يضيف الهللة إلى أحد الأقساط.

### الخطأ 8: مجموع int يلتف، والقسمة الصحيحة Integer Division

هذا هو خطأ تقرير المبيعات الذي بدأنا به. طلبيتا جملة في شهر التخفيضات، كل واحدة بأكثر من مليار هللة:

```java
int[] basketTotals = {1_500_000_000, 1_200_000_000};        // halalas, two big wholesale orders
int sum = 0;
for (int t : basketTotals) sum += t;
```

```text
int sum: -1594967296, int average: -797483648
long sum: 2700000000, average: 1350000000
7 / 2 = 3, 7 / 2.0 = 3.5
```

والمجموع تجاوز الحد الأعلى لـ `int` وهو حوالي 2.1 مليار، فالتف إلى قيمة سالبة دون استثناء، والمتوسط سالب أيضاً. والحل `long` لكل مجموع، أو `Math.addExact` إذا أردت استثناء عند التجاوز. أما السطر الأخير فيذكرك أن القسمة بين عددين صحيحين في Java تحذف الجزء العشري، مثل C# وGo وبخلاف Python 3 وJavaScript، فمتوسط 7 على 2 هو 3\. وفي حسابات المال لا تحول إلى `double` لتحل هذا، وإنما قرر قاعدة التقريب واكتبها صراحة، كما فعلنا في دالة الضريبة بإضافة النصف قبل القسمة.

![جدول بأربعة أخطاء في الأعداد: BigDecimal من double، وequals بين 2.0 و2.00، ومجموع int، والقسمة الصحيحة، مع ما طبعته Java والطريقة الصحيحة](https://arabroot.io/content/images/2026/10/java-idioms-05-numbers.webp)

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

## النصوص Strings والوقت Time

### الخطأ 9: تجميع النص String Concatenation بـ += داخل حلقة طويلة

هذا خطأ مهمة تصدير الفواتير. بناء نص طويل بإضافة جزء صغير في كل دورة يبدو طبيعياً في Python وJavaScript، ومحركاتهما تحسن هذا النمط في كثير من الحالات، أما في Java فالنص غير قابل للتعديل، وكل `+=` ينشئ نصاً جديداً وينسخ إليه كل ما قبله:

```text
n=20,000  += in loop:   344 ms   StringBuilder:   8 ms   same: true
n=40,000  += in loop:   624 ms   StringBuilder:  10 ms   same: true
n=80,000  += in loop:  2368 ms   StringBuilder:   0 ms   same: true
```

![أعمدة تبين زمن بناء نص بالجمع داخل حلقة مقابل StringBuilder لثلاثة أطوال](https://arabroot.io/content/images/2026/10/java-idioms-03-concat.webp)

مضاعفة الطول من 40,000 إلى 80,000 ضاعفت الزمن أربع مرات تقريباً مع `+=`، وبقي `StringBuilder` عند بضع ملي ثوان.

ولاحظ شكل النمو: من 40,000 إلى 80,000 حرف تضاعف الطول، وتضاعف الزمن حوالي أربع مرات، وهذا ما تتوقعه من نسخ يكبر مع مربع الطول. وفي مهمة التصدير التي كانت تعمل بسرعة مع ألف فاتورة، صار الزمن دقائق مع عشرين ألفاً دون أن يتغير الكود. والحل `StringBuilder` الذي يكبر مخزنه الداخلي ويضيف إليه دون نسخ كل مرة، أو `String.join` و`Collectors.joining` إذا كانت لديك قائمة جاهزة، أو الكتابة مباشرة إلى `Writer` إذا كان الناتج ملفاً. ولا تحتاج `StringBuilder` في سطر واحد مثل `"a" + b + "c"`، فالمترجم يحوله إلى كود فعال من نفسه، والمشكلة فقط في الحلقة.

### الخطأ 10: SimpleDateFormat مشترك بين الخيوط

وهذا خطأ الإيصالات التي ظهرت بتاريخ 2031\. في كود Java القديم ستجد غالباً حقلاً `static` من نوع `SimpleDateFormat` يستخدمه الجميع، لأن إنشاءه يبدو مكلفاً. والمشكلة أن هذا الكلاس يحفظ حالة داخلية أثناء التحليل والتنسيق، فإذا استخدمته عدة خيوط في نفس الوقت اختلطت حالاتها، وهذا مكتوب في [توثيقه](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/text/SimpleDateFormat.html?ref=arabroot.io) تحت Synchronization. وفي التجربة حللنا ثم نسقنا أربعة تواريخ معروفة 4,000 مرة بثمانية خيوط وكائن واحد مشترك:

```text
SimpleDateFormat shared by 8 threads: 2271 wrong results of 4000, e.g. [2026-03-15 -> 2031-10-04, 2026-03-15 -> 2026-07-15, 2026-07-04 -> 2026-07-15]
java.time LocalDate + DateTimeFormatter: 0 wrong results of 4000
now in Riyadh time zone: 2026-10-31T12:00+03:00[Asia/Riyadh]
```

![نتائج SimpleDateFormat المشترك بين ثمانية خيوط مع تواريخ خاطئة، ونتائج parallel forEach إلى ArrayList مع عناصر ضائعة](https://arabroot.io/content/images/2026/10/java-idioms-04-shared-state.webp)

كائن غير آمن للخيوط في يد عدة خيوط: أكثر من نصف التواريخ خاطئة، وعناصر تضيع من القائمة، دون أي استثناء.

أكثر من نصف النتائج خاطئة، ومنها 15 مارس 2026 الذي أصبح 4 أكتوبر 2031، وكلها دون استثناء واحد. والحل ليس قفلاً حول الكائن، وإنما مكتبة `java.time` الموجودة منذ Java 8: `LocalDate` للتاريخ دون وقت، و`Instant` للحظة زمنية، و`ZonedDateTime` مع `ZoneId` للوقت في منطقة زمنية مثل `Asia/Riyadh` التي تمثل توقيت السعودية، و`DateTimeFormatter` للتنسيق. وكلها ثابتة Immutable وآمنة للخيوط، فأعطت صفر أخطاء في نفس التجربة، ويمكنك أن تحفظ الـ Formatter في حقل `static final` دون قلق. وإذا رأيت `java.util.Date` أو `Calendar` أو `SimpleDateFormat` في كود جديد فهذه علامة على أن المطور نقل الكود من مصدر قديم.

وقد يتساءل البعض: لماذا يكون كلاس في المكتبة القياسية غير آمن بهذا الشكل؟ والإجابة أن `SimpleDateFormat` صمم في Java 1.1 عام 1997، وهو يحفظ داخله كائن `Calendar` يكتب فيه أجزاء التاريخ أثناء التحليل ثم يقرؤها، فإذا كتب خيطان في نفس الكائن في نفس اللحظة قرأ كل منهما خليطاً من أجزاء التاريخين، وهذا يفسر التواريخ الغريبة مثل 2031\. وعندما صممت `java.time` في Java 8 بني كل شيء فيها ثابتاً من البداية، فكل عملية ترجع كائناً جديداً ولا يوجد شيء يتغير بين الخيوط. ولاحظ أيضاً أن الخطأ لم يظهر في اختبار يشغل التحويل مرة واحدة، ولن يظهر في بيئة التطوير التي يصلها طلب واحد في كل مرة، وإنما يظهر تحت الضغط في الإنتاج، وبنسبة تتغير مع عدد الطلبات المتزامنة.

### الخطأ 11: switch القديمة دون break

```java
switch (status) {                                   // old switch: falls through without break
    case "paid": label += "paid ";
    case "shipped": label += "shipped ";
    default: label += "unknown";
}
String modern = switch (status) {
    case "paid" -> "paid";
    case "shipped" -> "shipped";
    default -> "unknown";
};
```

```text
paid: old switch -> "paid shipped unknown", arrow switch -> "paid"
shipped: old switch -> "shipped unknown", arrow switch -> "shipped"
```

الصيغة القديمة الموروثة من C تكمل التنفيذ إلى الحالة التالية Fall Through إذا لم تكتب `break`، فالطلب المدفوع حصل على الحالات الثلاث. ومن يأتي من Python وGo لا يتوقع هذا السلوك، لأن `match` في Python وswitch في Go لا تكمل إلى ما بعدها. والصيغة الحديثة بالسهم `->` الموجودة منذ Java 14 لا تكمل أبداً، وهي تعبير يرجع قيمة، والمترجم يجبرك أن تغطي كل الحالات عندما تكون على `enum` أو `sealed`. واستخدمها في كل كود جديد، ولا تخلط الصيغتين في نفس `switch`.

## التزامن Concurrency والموارد Resources

### الخطأ 12: parallel().forEach إلى ArrayList

من قرأ أن `parallel()` تسرع التدفقات يضيفها إلى كود يجمع النتائج في قائمة خارجية:

```java
List<Integer> collected = new ArrayList<>();
IntStream.range(0, 100_000).parallel().forEach(collected::add);   // shared, not thread-safe
```

```text
run 1 forEach into ArrayList: size 89667 (expected 100000)
run 2 forEach into ArrayList: size 93634 (expected 100000)
run 3 forEach into ArrayList: size 99806 (expected 100000)
toList(): size 100000
```

والنتيجة ناقصة في كل مرة وبعدد مختلف، لأن `ArrayList` ليست آمنة للخيوط، وعدة خيوط من المجمع المشترك تكتب في نفس المصفوفة الداخلية في نفس الوقت، وهي نفس حالة السباق Race Condition التي شرحناها في [مقال التزامن](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%81%D9%8A-java-%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D8%A7%D9%84%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A9-virtual-threads/). وقد يرمي نفس الكود أحياناً `ArrayIndexOutOfBoundsException`، ولهذا وضعنا له `catch` في التجربة، ولكنه في الثلاث مرات ضيع العناصر بصمت. والطريقة الصحيحة أن تترك التدفق يجمع النتيجة بنفسه بـ `toList()` أو `collect`، فهو يجمع كل جزء في خيطه ثم يدمج الأجزاء بأمان. والقاعدة الأعم: الدوال داخل التدفق يجب ألا تعدل شيئاً خارجه، وإذا احتجت أثراً جانبياً فاستخدم حلقة عادية.

### الخطأ 13: ملف لا يغلق Too many open files

في Python يغلق الملف غالباً عندما يختفي المتغير لأن عد المراجع Reference Counting يحذف الكائن فوراً، وفي Java لا يحدث ذلك، فالجامع قد لا يعمل لدقائق، والملف يبقى مفتوحاً حتى ذلك الحين. وفي `Leaks.java` قرأنا نفس ملف الأسعار 2,000 مرة دون إغلاقه، مع حد 256 ملفاً مفتوحاً للعملية بالأمر `ulimit -n 256`:

```java
var r = new BufferedReader(new FileReader(f.toFile()));   // never closed
r.readLine();
```

```text
leak: failed after 252 reads: java.io.FileNotFoundException: /tmp/prices.csv (Too many open files)
close: read the file 2000 times
```

وبعد 252 قراءة رفض النظام فتح ملف جديد، والعدد أقل من 256 لأن الـ JVM نفسها تفتح بضعة ملفات. ولاحظ أن الرسالة `FileNotFoundException` مضللة، فالملف موجود، والسبب الحقيقي بين القوسين. ومع `try (var r = ...)` نجحت القراءات الألفين، لأن كل ملف أغلق عند نهاية الكتلة. وفي الخدمات يظهر نفس الخطأ مع اتصالات قاعدة البيانات واتصالات HTTP، فتنفد بعد ساعات من التشغيل، ولهذا تعامل مع كل ما ينفذ `AutoCloseable` بـ try-with-resources، كما شرحنا في مقال الأنواع.

والحد الذي وضعناه في التجربة 256 صغير عمداً حتى يظهر الخطأ بسرعة، أما في الإنتاج فالحد عادة بالآلاف، ولهذا لا يظهر الخطأ في اليوم الأول، وإنما بعد ساعات أو أيام عندما تتراكم الملفات أو الاتصالات المفتوحة، وغالباً في وقت الذروة. وقد يتساءل البعض: ألا يغلق جامع القمامة الملف عندما يحذف الكائن؟ والإجابة أن بعض الكلاسات في الـ JDK تنظف مواردها عند جمعها عبر `Cleaner`، ولكن لا يوجد أي ضمان متى يعمل الجامع، وفي الكومة الكبيرة قد لا يعمل لدقائق إذا لم يكن هناك ضغط على الذاكرة، والملفات تنفد قبل الذاكرة. ولهذا فإن الاعتماد على الجامع لإغلاق الموارد من أسوأ الطرق على الإطلاق، والطريقة الوحيدة المضمونة هي الإغلاق الصريح. ولتعرف عدد الملفات المفتوحة في خدمة تعمل على Linux انظر في المجلد `/proc/<pid>/fd`، فإذا كان العدد يكبر مع الوقت دون أن ينزل فلديك تسرب في الموارد.

### الخطأ 14: Optional.get() دون فحص

```text
Optional.get(): java.util.NoSuchElementException: No value present
orElseThrow with a message: order 9 not found
```

من يتعلم `Optional` لأول مرة يستخدمها كما كان يستخدم `null`: ينشئها ثم يستدعي `get()` مباشرة، فيحصل على `NoSuchElementException: No value present` بدل `NullPointerException`، دون أي معلومة عن الطلب المفقود. والطرق الصحيحة: `orElse` لقيمة بديلة، و`map` و`ifPresent` لتعمل على القيمة إن وجدت، و`orElseThrow` مع استثناء يحمل رسالة واضحة كما في السطر الثاني، وهو الذي تحوله طبقة الـ API إلى رد 404، كما فعلنا في خدمة الطلبات.

## ماذا تمسك الأدوات؟ Error Prone على نفس الملف

بعد أن جمعنا الأخطاء في ملف واحد صار السؤال العملي: كم منها كان سيمسكه الفحص الثابت Static Analysis قبل أن يصل إلى المراجعة؟ فأخذنا `Idioms.java` و`Leaks.java` كما هما، ووضعناهما في مشروع Gradle صغير فيه Error Prone 2.51.0 بنفس الإعداد الذي استخدمناه في المقال الأول من المرحلة، وشغلنا الترجمة فقط:

```text
Idioms.java:1: warning: [DefaultPackage] Java classes shouldn't use default package
Idioms.java:34: warning: [ReferenceEquality] Comparison using reference equality instead of value equality
Idioms.java:18: error: [EqualsHashCode] Classes that override equals should also override hashCode.
Idioms.java:55: warning: [ListRemoveAmbiguous] Ambiguous call to List.remove; clarify if index-based or value-based removal was intended by adding a comment
Idioms.java:55: warning: [ModifyCollectionInEnhancedForLoop] Modifying a collection while iterating over it in a loop may cause a ConcurrentModificationException to be thrown or lead to undefined behavior.
Idioms.java:72: warning: [BigDecimalLiteralDouble] new BigDecimal(double) loses precision in this case. The exact value here is `new BigDecimal("9.9000000000000003552713678800500929355621337890625")`.
Idioms.java:74: warning: [BigDecimalEquals] BigDecimal#equals has surprising behavior: it also compares scale.
Idioms.java:104: warning: [FutureReturnValueIgnored] Return value of methods returning Future must be checked. Ignoring returned Futures suppresses exceptions thrown from the code that completes the Future.
Idioms.java:120: warning: [FutureReturnValueIgnored] Return value of methods returning Future must be checked. Ignoring returned Futures suppresses exceptions thrown from the code that completes the Future.
Idioms.java:130: error: [ReturnValueIgnored] Return value of 'orElseThrow' must be used
Idioms.java:171: warning: [FallThrough] Execution may fall through from the previous case; add a `// fall through` comment before this line if it was deliberate
Idioms.java:172: warning: [FallThrough] Execution may fall through from the previous case; add a `// fall through` comment before this line if it was deliberate
Leaks.java:1: warning: [DefaultPackage] Java classes shouldn't use default package
Leaks.java:14: warning: [DefaultCharset] Implicit use of the platform default charset, which can result in differing behaviour between JVM executions or incorrect behavior if the encoding of the data source doesn't match expectations.
Leaks.java:16: warning: [DefaultCharset] Implicit use of the platform default charset, which can result in differing behaviour between JVM executions or incorrect behavior if the encoding of the data source doesn't match expectations.
```

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

- أمسك Error Prone خمسة من الأخطاء الأربعة عشر: `==` على النصوص باسم `ReferenceEquality`، و`equals` دون `hashCode` كخطأ يوقف البناء، والحذف أثناء الحلقة باسمين، و`BigDecimal` من `double` مع القيمة الحقيقية كاملة، و`BigDecimal.equals` مع 2.0 و2.00، وswitch القديمة باسم `FallThrough`.
- ولم يمسك التسعة الباقية: المفتاح الذي يتغير، و`Arrays.asList` مع `add`، وUnboxing القيمة الغائبة، والتفاف المجموع، و`+=` في الحلقة، و`SimpleDateFormat` المشترك، و`parallel().forEach` إلى قائمة، والملف الذي لا يغلق، و`Optional.get()`. وأغلبها يحتاج معرفة بما يحدث وقت التشغيل لا تعرفه أداة تقرأ الكود دون أن تشغله، وبعضها له فحوص في Error Prone غير مفعلة افتراضياً.
- وأمسك أشياء لم نقصدها: `FutureReturnValueIgnored` لأننا أهملنا ما ترجعه `submit` في حالة التواريخ، فلو رمت مهمة استثناء لضاع، و`ReturnValueIgnored` في السطر الذي استدعينا فيه `orElseThrow` لنرى الاستثناء فقط دون أن نستخدم القيمة، و`DefaultCharset` لأن `FileReader` دون ترميز يعتمد على ترميز النظام، وهذا خطأ حقيقي في كود يقرأ ملفات فيها نص عربي.

والنتيجة العملية أن أداة الفحص تغطي قرابة نصف القائمة بتكلفة سطرين في ملف البناء، وهذا يستحق، ولكنها لا تغني عن معرفة الأنماط، ولا عن الاختبارات التي تشغل الكود فعلاً، ولا عن مراجعة يقرأ فيها زميل الكود بعين من يعرف هذه الفخاخ. ولاحظ أن الرسائل نفسها تعلمك، فرسالة `BigDecimalLiteralDouble` تكتب لك القيمة الحقيقية التي حفظتها، ورسالة `FallThrough` تخبرك كيف توثق السقوط إذا كان مقصوداً، ورسالة `ListRemoveAmbiguous` تذكرك بالفرق بين الحذف بالقيمة والحذف بالموقع الذي ذكرناه في الخطأ 4.

## من أين تأتي هذه العادات؟ لغتك الأولى First Language

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

- **من Python:** تتوقع أن `==` تقارن المحتوى، وأن الأعداد الصحيحة لا تلتف أبداً، وأن القسمة بـ `/` تعطي عدداً عشرياً، وأن الملف يغلق عندما يختفي المتغير، وأن `dict.get` يرجع قيمة بديلة. وفي Java كل هذا مختلف: `equals`، و`int` بحجم ثابت، والقسمة الصحيحة، وtry-with-resources، و`getOrDefault`.
- **من JavaScript وTypeScript:** تعودت أن النص قيمة بدائية تقارن بـ `===`، وأن المصفوفة تقبل أي إضافة، وأن الكود يعمل في خيط واحد فلا يوجد سباق إلا بين `await` وآخر. وفي Java تعمل الخيوط بالتوازي فعلاً، ولهذا يصبح `SimpleDateFormat` المشترك و`parallel().forEach` إلى قائمة أخطاء حقيقية، وقد شرحنا نموذج JavaScript في [حلقة الأحداث](https://arabroot.io/articles/%D8%AD%D9%84%D9%82%D8%A9-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-event-loop-%D9%81%D9%8A-javascript/).
- **من PHP:** المصفوفة في PHP قائمة وقاموس معاً وتنسخ عند الإسناد، وكل طلب يبدأ بذاكرة نظيفة وينتهي، فلا يوجد تسرب يعيش أياماً ولا حالة مشتركة بين الطلبات. وفي خدمة Java تعيش الكائنات بين الطلبات، فالحقل `static` مشترك بين كل الطلبات وكل الخيوط، وهذا مصدر خطأ التواريخ وخطأ التسرب الذي رأيته في مقال الذاكرة.
- **من C#:** اللغة الأقرب إلى Java، ومع ذلك فإن `==` على النصوص في C# تقارن المحتوى لأن اللغة تعيد تعريفها، و`decimal` نوع مدمج لا يحتاج `BigDecimal`، وswitch في C# تمنع السقوط إلى الحالة التالية. فمطور C# يقع تحديداً في الأخطاء 1 و7 و11.
- **من Go:** تعودت على القيم الصفرية بدل `null`، وعلى الأخطاء كقيم راجعة، وعلى أن `map` ترجع قيمة صفرية للمفتاح الغائب. ولهذا يفاجئك Unboxing القيمة الغائبة في الخطأ 6، و`Optional.get()` في الخطأ 14.

والنصيحة العملية: عندما تبدأ لغة جديدة، اكتب لنفسك قائمة قصيرة بافتراضات لغتك الأولى، وتحقق من كل واحد منها في اللغة الجديدة بتجربة صغيرة في JShell قبل أن تبني عليه كوداً حقيقياً، فهذه التجارب الصغيرة هي بالضبط ما فعلناه في هذا المقال، وهي أرخص بكثير من اكتشاف الافتراض الخاطئ في تقرير مبيعات شهري.

## كيف تكتب اختبارات Tests تمسك هذه الأخطاء؟

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

- **اختبر بقيم خارج الحالات السهلة:** مقارنة `Integer` بـ `==` مرت في الاختبار بكمية 100 وفشلت عند 200، فضع في الاختبارات قيماً أكبر من 127، ومبالغ قريبة من حدود `int` كما في الخطأ 8، ونصوصاً عربية ورموزاً خارج المستوى الأساسي.
- **اختبر القيم الغائبة صراحة:** لكل دالة تقرأ من `Map` أو قاعدة بيانات اختبار للمفتاح غير الموجود، فهذا يكشف الخطأ 6 و14 قبل الإنتاج.
- **اختبر التساوي من الجهتين:** إذا كتبت `equals` فاختبر أيضاً أن كائنين متساويين يعاملان ككائن واحد في `HashSet` وفي مفاتيح `HashMap`، فهذا الاختبار وحده كان سيمسك الخطأ 2.
- **اختبر التزامن بانطلاق متزامن:** الخطأ 10 والخطأ 12 لا يظهران بخيط واحد، واختبار يطلق عدة خيوط معاً بعد `CountDownLatch` ويكرر العملية آلاف المرات يكشفهما، وهذا ما فعلناه في اختبار المخزون بعشرين طلباً متزامناً في خدمة الطلبات.
- **اختبر الحجم لا الصحة فقط:** الخطأ 9 لا يغير النتيجة، وإنما الزمن، فاختبار أداء بسيط بحجم بيانات قريب من الإنتاج، يشغل في الـ CI ويفشل إذا تجاوز حداً واسعاً، يكشف النمو غير الخطي قبل أن يكشفه العميل.
- **اختبر الموارد بحدود صغيرة:** كما فعلنا مع `ulimit -n 256`، شغل اختبارات التكامل بحدود صغيرة للملفات وللاتصالات في مجمع قاعدة البيانات، فالتسرب يظهر في دقائق بدل أيام.

وقد شرحنا أنواع الاختبارات ومتى تكتب كل نوع في [أنواع الاختبارات](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/)، وطريقة تقييم جودة الاختبارات نفسها بالطفرات في [اختبار الطفرات](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%B7%D9%81%D8%B1%D8%A7%D8%AA-mutation-testing/)، وهي طريقة مفيدة هنا تحديداً، لأنها تغير `equals` إلى `==` أو تحذف `break` وتسألك: هل فشل أي اختبار؟

## أسلوب Java الحديث Modern Java في جدول واحد

أغلب الأخطاء السابقة لها جذر مشترك: كود مكتوب بعادات لغة أخرى، أو بأسلوب Java قبل عشر سنوات. والجدول التالي يجمع ما تراه في الكود القديم، وما يقابله في Java 21 و25، وهو ما تبحث عنه في المراجعة:

| بدلاً من                                        | اكتب                                      | منذ         |
| ----------------------------------------------- | ----------------------------------------- | ----------- |
| class بيانات بـ getters وequals وhashCode يدوية | record                                    | Java 16     |
| instanceof ثم تحويل يدوي                        | if (o instanceof Card c) أو أنماط السجلات | Java 16 و21 |
| switch بـ break                                 | switch بالسهم كتعبير، مع sealed           | Java 14 و17 |
| Date وCalendar وSimpleDateFormat                | java.time                                 | Java 8      |
| Arrays.asList وCollections.unmodifiableList     | List.of وMap.of وList.copyOf              | Java 9 و10  |
| stream().collect(Collectors.toList())           | stream().toList()                         | Java 16     |
| نص بعدة أسطر بـ + و\\n                          | Text Block """                            | Java 15     |
| إغلاق في finally                                | try-with-resources                        | Java 7      |
| مجمع خيوط لطلبات تنتظر الشبكة                   | الخيوط الافتراضية Virtual Threads         | Java 21     |
| null كقيمة راجعة لـ «لا يوجد»                   | Optional مع orElseThrow                   | Java 8 و10  |
| تحديد النوع في كل متغير محلي واضح               | var                                       | Java 10     |

ولا يعني هذا أن تعيد كتابة كل الكود القديم في الفريق، فهذا مكلف ولا يضيف قيمة للمستخدم. وإنما أن تكتب الكود الجديد بهذا الأسلوب، وأن تحسن الكود القديم الذي تلمسه في كل تعديل، وأن تضيف أدوات مثل Error Prone التي تمسك أنماطاً مثل `equals` دون `hashCode` و`==` على النصوص قبل أن تصل إلى المراجعة.

### وماذا عن Lombok؟ السجلات Records أم توليد الكود

في كثير من مشاريع Java ستجد مكتبة Lombok، وهي تولد الـ getters والـ setters و`equals` و`hashCode` من تعليقات مثل `@Data` و`@Value`، وكانت لسنوات الحل الأشهر لطول كود كائنات البيانات. ومع السجلات Records صار جزء كبير من استخدامها غير ضروري للكود الجديد، فالسجل جزء من اللغة نفسها، والمترجم يفهمه، ولا يحتاج إضافة في بيئة التطوير ولا في البناء، ويعمل مع أنماط السجلات في `switch` و`instanceof`. ولكن السجل ثابت دائماً، فإذا كان المشروع يعتمد على كائنات تتغير، مثل الكيانات Entities في JPA، فستبقى Lombok أو الكود اليدوي هناك. والقاعدة في مشروع قائم: لا تخلط الأسلوبين في نفس النوع من الكائنات دون سبب، واتبع ما يقرره الفريق، واستخدم `record` لكل كائن بيانات جديد لا يتغير، مثل طلبات الـ API وردودها كما فعلنا في خدمة الطلبات.

## قائمة مراجعة Code Review Checklist لكود Java

- هل كل مقارنة بين كائنات بـ `equals` أو `Objects.equals`، وكل `==` بين أنواع بدائية فقط؟
- هل كل class فيها `equals` فيها `hashCode`، أو هي `record`؟
- هل مفاتيح `Map` وعناصر `Set` ثابتة؟
- هل الحذف من المجموعات بـ `removeIf` أو مكرر صريح، وليس داخل for-each؟
- هل يوجد `get` من `Map` يتحول إلى نوع بدائي دون فحص الغياب؟
- هل المبالغ في `long` بأصغر وحدة أو `BigDecimal` من نص، والمجاميع في `long`؟
- هل يوجد `+=` على نص داخل حلقة قد تطول؟
- هل يوجد `Date` أو `SimpleDateFormat`، وخاصة في حقل `static`؟
- هل الدوال داخل التدفقات خالية من الآثار الجانبية، و`parallel()` مستخدمة لسبب مقاس؟
- هل كل ملف واتصال ومورد في try-with-resources؟
- هل يوجد `Optional.get()` أو `catch` فارغة؟
- هل switch الجديدة بالسهم، وهل تغطي كل حالات `enum` و`sealed`؟

## خطة القراءة Reading Plan من المصادر الرسمية

إذا وصلت إلى هنا فأنت تعرف ما يكفي لتعمل في فريق Java، والخطوة التالية أن تبني فهماً منظماً من المصادر الرسمية بدل الإجابات المتفرقة. والخطة التالية لأربعة أسابيع بساعة يومياً تقريباً، وكلها مصادر مجانية من Oracle وOpenJDK والمشاريع نفسها:

![خطة قراءة من أربعة أسابيع: اللغة، ثم المكتبة، ثم التشغيل، ثم المشروع، من المصادر الرسمية](https://arabroot.io/content/images/2026/10/java-idioms-06-reading-plan.webp)

أربعة أسابيع من المصادر الرسمية: اللغة، ثم المكتبة، ثم التشغيل، ثم أدوات المشروع.

1. **الأسبوع الأول، اللغة:** ابدأ بموقع [dev.java/learn](https://dev.java/learn/?ref=arabroot.io) الذي تكتبه Oracle وفريق Java، وفيه مسار [Language](https://dev.java/learn/language/?ref=arabroot.io) الذي يشرح الصيغة الحديثة كاملة، ثم صفحة [Records](https://dev.java/learn/records/?ref=arabroot.io). ولا تقرأ كل شيء بالترتيب، فأنت تعرف البرمجة، وإنما ركز على ما لا يوجد في لغتك: الأنواع البدائية والمرجعية، وGenerics، والواجهات Interfaces، وsealed.
2. **الأسبوع الثاني، المكتبة القياسية:** مسار [Collections and Streams](https://dev.java/learn/api/collections-and-streams/?ref=arabroot.io) ثم [Date Time](https://dev.java/learn/api/date-time-regex/?ref=arabroot.io)، ومعهما [توثيق Java SE 25 API](https://docs.oracle.com/en/java/javase/25/docs/api/index.html?ref=arabroot.io)، وتعود إليه لكل كلاس تستخدمه، واقرأ فيه التحذيرات المكتوبة تحت Implementation Note وSynchronization، فهي التي تشرح أغلب الأخطاء في هذا المقال.
3. **الأسبوع الثالث، التشغيل:** صفحة [Virtual Threads](https://dev.java/learn/api/virtual-threads/?ref=arabroot.io)، ثم [دليل ضبط جامع القمامة](https://docs.oracle.com/en/java/javase/25/gctuning/introduction-garbage-collection-tuning.html?ref=arabroot.io)، ثم [المراقبة والتشخيص](https://dev.java/learn/jvm/monitoring-troubleshooting/?ref=arabroot.io) لأدوات مثل jcmd وJFR التي استخدمناها في هذه المرحلة.
4. **الأسبوع الرابع، المشروع:** دليل Gradle [Building Java Projects](https://docs.gradle.org/9.8.2/userguide/building%5Fjava%5Fprojects.html?ref=arabroot.io)، ثم [مرجع Spring Boot](https://docs.spring.io/spring-boot/reference/?ref=arabroot.io) إذا كان فريقك يستخدمه، ثم صفحة [JDK 25](https://openjdk.org/projects/jdk/25/?ref=arabroot.io) في OpenJDK التي تجمع كل JEP في الإصدار الذي تعمل عليه.

وعندما تختلف إجابتان في الإنترنت عن سلوك اللغة، فالمرجع الأخير هو [مواصفة لغة Java](https://docs.oracle.com/javase/specs/jls/se25/html/index.html?ref=arabroot.io) للإصدار الذي تعمل عليه، وهي ليست للقراءة من البداية إلى النهاية، ولكن فصل الاستثناءات والتحويلات فيها أوضح مما يتوقع كثيرون. وأخيراً، جرب الأمثلة الصغيرة في [Java Playground](https://dev.java/playground/?ref=arabroot.io) في المتصفح أو في JShell قبل أن تكتبها في المشروع.

## تدريب عملي

- شغل Error Prone على `Idioms.java` كما في المقال الأول من المرحلة، وعد كم خطأ من الأربعة عشر أمسكه وحده، وأي الأسماء أعطاها لها.
- أصلح `Cart` بحيث لا يمكن أن يتغير بعد إنشائه، باستخدام `record` و`List.copyOf`، ثم أعد تشغيل الحالة `mutablekey`.
- غير حالة `concat` لتكتب إلى ملف بـ `BufferedWriter` بدل بناء نص في الذاكرة، وقس الزمن والذاكرة لمليون سطر.
- ابحث في مشروع فريقك عن `SimpleDateFormat` و`new BigDecimal(` متبوعة برقم، و`Optional.get()`، وقيم كم منها يحتاج إصلاحاً.

## الخلاصة

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

- أغلب الأخطاء الأربعة عشر لم ترم أي استثناء ولم يعترض عليها المترجم، وإنما أعطت قيماً خاطئة: مجموع سالب، وتاريخ في 2031، وعناصر ضائعة من قائمة.
- المساواة في Java بـ `equals` للكائنات، و`equals` يحتاج `hashCode` معه، والسجلات Records تحل الاثنين، ومفاتيح القواميس يجب ألا تتغير.
- `List.of` و`Arrays.asList` و`ArrayList` ثلاثة سلوكيات مختلفة لنفس النوع، والحذف أثناء الحلقة يكون بـ `removeIf`.
- المال في `long` بالهللات أو `BigDecimal` من نص، والمجاميع في `long`، و2.0 لا تساوي 2.00 بـ `equals`.
- `+=` في الحلقة كلف 2.4 ثانية لنص من 80,000 حرف، و`SimpleDateFormat` المشترك أعطى 2,271 تاريخاً خاطئاً من 4,000، و`java.time` صفراً.
- الدوال داخل التدفقات بلا آثار جانبية، وكل مورد في try-with-resources، و`Optional` مع `orElseThrow` برسالة واضحة.
- Error Prone أمسك خمسة من الأخطاء الأربعة عشر قبل التشغيل، ومعها ثلاث مشكلات لم نقصدها مثل الترميز الافتراضي، والباقي يمسكه اختبار يختار قيمه بوعي بهذه الفخاخ، مثل الأرقام فوق 127 والمفاتيح الغائبة والخيوط المتزامنة.
- تعلم الباقي من dev.java وتوثيق Java SE 25 ومواصفة اللغة، بخطة قصيرة ومركزة على ما يختلف عن لغتك.

وبهذا تكتمل مرحلة [لغة Java للمطورين](https://arabroot.io/start/#stage-lang-java)، وتجد هنا مقالاتها والمقالات المرتبطة بها، وفي مرحلة Kotlin من نفس المسار نبني نفس الخدمة على نفس الـ JVM بلغة أقصر وأمان من القيم الفارغة:

[![](https://arabroot.io/content/images/2026/10/java-for-devs-cover-cover.webp)دليلJava لمن يعرف البرمجة: JDK 25 والأدوات Toolchainتثبيت JDK 25 وجولة في صيغة Java مقارنة بما تعرفه، ثم مشروع Gradle فيه منسق وفاحص واختبارات، مع أخطاء زرعناها لنرى أين يمسكها البناء.](https://arabroot.io/articles/java-%D9%84%D9%85%D9%86-%D9%8A%D8%B9%D8%B1%D9%81-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B5%D9%8A%D8%BA%D8%A9/)[![](https://arabroot.io/content/images/2026/10/java-types-memory-cover-cover.webp)دليلالأنواع والذاكرة Memory والاستثناءات في Javaلماذا أعطت مقارنة 128 بـ 128 القيمة false، وكم يكلف مليون رقم في الذاكرة، وماذا تختار الـ JVM داخل Container صغير، وكيف تقرأ استثناءات Java وتصلح تسرب الذاكرة.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%88%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%AB%D9%86%D8%A7%D8%A1%D8%A7%D8%AA-%D9%81%D9%8A-java/)[![](https://arabroot.io/content/images/2026/10/java-concurrency-cover-cover.webp)دليلالتزامن في Java: الخيوط الافتراضية Virtual Threadsنفس المئة طلب ونفس الألفي بصمة SHA-256 بكل أدوات التزامن في Java 25 مع الأرقام، ثم سباق على المخزون وجمود بين خيطين نصنعهما ونصلحهما.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%81%D9%8A-java-%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D8%A7%D9%84%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A9-virtual-threads/)[![](https://arabroot.io/content/images/2026/10/error-handling-cover-cover.webp)دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/)[![](https://arabroot.io/content/images/2026/10/debugging-basics-cover-cover.webp)دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.](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/)

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

- أكتوبر 2026: كتابة المقال، وتشغيل الحالات الأربع عشرة على JDK 25.0.5 (Temurin) داخل Docker.