انتقل إلى المحتوى الرئيسي

الفصل 6: الفخاخ الأساسية في جافا

الهدف

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


1. == مقابل .equals() — السؤال الأكثر طرحًا​

القاعدة الذهبية​

المشغلماذا يقارن؟يستخدم لـ
==عناوين الذاكرة (المراجع)الأنواع البدائية (int, boolean, char...)
.equals()المحتوى (القيم)الكائنات (String, Integer, List...)

أمثلة مع String​

String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b); // true ← نفس المرجع (String Pool)
System.out.println(a == c); // false ← مراجع مختلفة (new ينشئ كائنًا جديدًا)
System.out.println(a.equals(c)); // true ← نفس المحتوى

لماذا a == b هي true ؟​

تحافظ جافا على String Pool (ذاكرة مؤقتة) في الذاكرة. عندما تكتب "hello" بشكل ثابت في الكود، تعيد جافا استخدام نفس الكائن:

الذاكرة:
┌──────────────────────────────────┐
│ String Pool │
│ ┌─────────┐ │
│ │ "hello" │ ← a و b يشيران هنا
│ └─────────┘ │
└──────────────────────────────────┘

┌──────────────────────────────────┐
│ Heap (الكومة) │
│ ┌─────────┐ │
│ │ "hello" │ ← c يشير هنا (new = دائمًا كائن جديد)
│ └─────────┘ │
└──────────────────────────────────┘

أمثلة مع Integer (الفخ المفضل!)​

Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true ← ذاكرة مؤقتة لـ Integer [-128, 127]

Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false ← خارج الذاكرة المؤقتة!
System.out.println(a.equals(b)); // true ← نفس القيمة
فخ المقابلة

تخزن جافا Integer من -128 إلى 127. بعد ذلك، == يعيد false حتى لو كانت القيم متطابقة. لهذا السبب يجب دائمًا استخدام .equals() لمقارنة الكائنات.

الممارسة الجيدة: "CONSTANT".equals(variable)​

String status = getStatus(); // قد يعيد null

// خطير — NullPointerException إذا كان status null
if (status.equals("ACTIVE")) { ... }

// آمن — لا يطلق NPE أبدًا
if ("ACTIVE".equals(status)) { ... }
عبارة المقابلة

"== يقارن المراجع (عناوين الذاكرة)، .equals() يقارن المحتوى. أستخدم دائمًا .equals() للكائنات. بالنسبة للسلاسل، أضع الثابت على اليسار لتجنب NullPointerException."


2. hashCode() — ما هو ولماذا هو مهم؟​

تعريف بسيط​

hashCode() يعيد عددًا صحيحًا يمثل كائنًا. إنه مثل بصمة الإصبع: سريع في المقارنة، لكنه ليس فريدًا بنسبة 100%.

عقدة hashCode/equals​

قاعدة مطلقة

إذا كان كائنان .equals()، فيجب أن يكون لهما نفس hashCode().

إذا قمت بإعادة تعريف .equals()، يجب عليك أيضًا إعادة تعريف hashCode().

// سيء — يعيد تعريف equals بدون hashCode
public class User {
private String email;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}
// لا يوجد hashCode() → خطأ مع HashMap, HashSet!
}

// جيد — يعيد تعريف الاثنين
public class User {
private String email;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}

@Override
public int hashCode() {
return Objects.hash(email);
}
}

لماذا هو حاسم؟ HashMap و HashSet يعتمدان عليه!​

User user1 = new User("alice@mail.com");
User user2 = new User("alice@mail.com");

// بدون hashCode صحيح:
Set<User> set = new HashSet<>();
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 2 ← سيء! يجب أن يكون 1

// مع hashCode صحيح:
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 1 ← صحيح

كيف تستخدم HashMap hashCode​

hashCode("alice@mail.com") → 12345

HashMap الداخلية:
┌──────┬──────────────────┐
│ slot │ المحتوى │
├──────┼──────────────────┤
│ ... │ │
│ 12345│ → User("alice") │ ← hashCode يحدد الفتحة
│ ... │ │
└──────┴──────────────────┘

1. hashCode() → يجد الفتحة الصحيحة (سريع، O(1))
2. equals() → يتحقق من أنه نفس الكائن

مع Lombok (الأبسط)​

@Data  // يولد equals(), hashCode(), toString(), getters, setters
public class User {
private String email;
private String name;
}

// أو للتحكم في الحقول المستخدمة:
@EqualsAndHashCode(of = {"email"})
public class User {
private String email;
private String name;
}

مع Java Records (الأحدث)​

public record User(String email, String name) {}
// equals(), hashCode(), toString() يتم إنشاؤها تلقائيًا!
عبارة المقابلة

"hashCode يشبه بصمة الإصبع للكائنات. يستخدمه HashMap للعثور على الفتحة الصحيحة في O(1). العقدة تقول: إذا كان كائنان متساويان، فيجب أن يكون لهما نفس hashCode. لهذا السبب أعيد تعريف الاثنين معًا دائمًا — أو أستخدم Lombok @Data أو Java Records."


3. toString() — للتصحيح​

بدون toString​

User user = new User("Alice", "alice@mail.com");
System.out.println(user);
// → com.example.User@1a2b3c4d ← غير مفيد!

مع toString​

@Override
public String toString() {
return "User{name='%s', email='%s'}".formatted(name, email);
}

System.out.println(user);
// → User{name='Alice', email='alice@mail.com'} ← مفيد!

مع Lombok أو Record​

@Data       // يولد toString() تلقائيًا
public class User { ... }

// أو
public record User(String name, String email) {}
// toString() → User[name=Alice, email=alice@mail.com]

4. الأنواع البدائية مقابل فئات التغليف​

بدائيتغليفالحجمالقيمة الافتراضية
intInteger4 بايت0
longLong8 بايت0L
doubleDouble8 بايت0.0
booleanBoolean1 بتfalse
charCharacter2 بايت'\u0000'

متى تستخدم ماذا؟​

الحالةاستخدملماذا
المتغيرات المحلية، الحساباتint, doubleأسرع، لا يوجد تغليف
سمات الكيانات JPALong, Integerيمكن أن يكون null (مهم للـ id غير المحفوظ بعد)
المجموعات (List, Map)Integer, Longالمجموعات لا تقبل البدائيات
المعلمات الاختياريةIntegerيمكن أن يكون null للإشارة إلى "غير مقدم"

التغليف التلقائي / فك التغليف​

// التغليف التلقائي: int → Integer (تلقائي)
Integer a = 42;

// فك التغليف: Integer → int (تلقائي)
int b = a;

// فخ: فك التغليف من null → NullPointerException!
Integer c = null;
int d = c; // 💥 NullPointerException
فخ المقابلة
Integer a = null;
if (a == 0) { ... } // 💥 NullPointerException (فك التغليف من null)

دائمًا تحقق من null قبل مقارنة تغليف مع بدائي.

عبارة المقابلة

"أستخدم البدائيات للمتغيرات المحلية والحسابات، والتغليف للـ entity IDs والمجموعات. الفخ الرئيسي هو فك التغليف من null، الذي يطلق NullPointerException."


5. final — ثلاثة استخدامات مختلفة​

السياقالمعنىمثال
متغيرالمرجع لا يمكن أن يتغيرfinal List<String> list = new ArrayList<>()
طريقةلا يمكن تجاوزها (override)public final void process()
فئةلا يمكن توريثهاpublic final class String

فخ: final لا يجعل الكائن غير قابل للتغيير​

final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ حسنًا — نغير المحتوى
list = new ArrayList<>(); // ❌ خطأ — نحاول تغيير المرجع
الذاكرة:
┌─────────────────┐
list (final) ──→ │ ArrayList │
(المرجع │ ["hello"] │ ← يمكن أن يتغير المحتوى
لا يتحرك) └─────────────────┘
عبارة المقابلة

"final يعني أن المرجع لا يمكن إعادة تعيينه، لكن الكائن نفسه يمكن أن يتغير. للحصول على عدم قابلية التغيير الحقيقية، أستخدم List.of() أو Collections.unmodifiableList()."


6. static — ينتمي إلى الفئة، وليس إلى المثيل​

public class Counter {
static int totalCount = 0; // مشترك بين جميع المثيلات
int instanceCount = 0; // خاص بكل مثيل

public Counter() {
totalCount++;
instanceCount++;
}
}

Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();

System.out.println(Counter.totalCount); // 3 (مشترك)
System.out.println(c3.instanceCount); // 1 (خاص بـ c3)

static في الممارسة​

الاستخداممثالممارسة جيدة؟
الثوابتstatic final int MAX = 100نعم
طرق الأدواتMath.max(), Collections.sort()نعم
المتغيرات المشتركةstatic int countانتباه (سلامة الخيوط)
الفئات الداخليةstatic class Builderنعم (نمط Builder)
في Spring Boot

تجنب المتغيرات static القابلة للتغيير في beans Spring. الـ beans هي بالفعل singleton افتراضيًا — استخدم الحقول العادية للمثيل.


7. String — غير قابل للتغيير حسب التصميم​

السلاسل غير قابلة للتغيير​

String s = "hello";
s.toUpperCase();
System.out.println(s); // "hello" ← لم يتغير!

s = s.toUpperCase();
System.out.println(s); // "HELLO" ← سلسلة جديدة تم تعيينها

كل تعديل ينشئ كائنًا جديدًا من String.

التسلسل في الحلقة: StringBuilder​

// سيء — ينشئ N كائنات String وسيطة → O(n²)
String result = "";
for (int i = 0; i < 10000; i++) {
result += i + ",";
}

// جيد — كائن واحد فقط، تعديلات في المكان → O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();

String Pool (ذاكرة مؤقتة)​

String a = "hello";          // يذهب إلى String Pool
String b = "hello"; // يعيد استخدام نفس الكائن من Pool
String c = new String("hello"); // ينشئ كائنًا جديدًا في Heap

System.out.println(a == b); // true (نفس المرجع في Pool)
System.out.println(a == c); // false (c في Heap، ليس في Pool)

مقارنة السلاسل​

الطريقةالاستخدام
.equals()مقارنة حساسة لحالة الأحرف
.equalsIgnoreCase()مقارنة غير حساسة لحالة الأحرف
.compareTo()ترتيب أبجدي (يعيد int)
.contains()التحقق مما إذا كان يحتوي على سلسلة فرعية
.startsWith() / .endsWith()التحقق من البداية/النهاية
.isBlank()فارغ أو مسافات فقط (Java 11+)
.isEmpty()فارغ (طول 0)
عبارة المقابلة

"السلاسل غير قابلة للتغيير في جافا. كل تعديل ينشئ كائنًا جديدًا. للتسلسل في الحلقات، أستخدم StringBuilder. للمقارنة، أستخدم دائمًا .equals()، أبدًا ==."


8. Optional — تجنب NullPointerException​

المشكلة​

// NullPointerException إذا كانت findById تعيد null
User user = userRepository.findById(1L);
String email = user.getEmail(); // 💥 NPE إذا كان user null

الحل: Optional​

Optional<User> optional = userRepository.findById(1L);

// النهج 1: isPresent
if (optional.isPresent()) {
User user = optional.get();
}

// النهج 2: orElse (قيمة افتراضية)
User user = optional.orElse(new User("anonymous"));

// النهج 3: orElseThrow (استثناء)
User user = optional.orElseThrow(() ->
new ResourceNotFoundException("User not found with id: 1"));

// النهج 4: map (تحويل)
String email = optional.map(User::getEmail).orElse("N/A");

الأنماط المضادة التي يجب تجنبها​

// ❌ سيء — يعادل فحص null
if (optional.isPresent()) {
return optional.get();
} else {
return null;
}

// ✅ جيد
return optional.orElse(null);

// ❌ سيء — Optional كمعامل
public void process(Optional<String> name) { ... }

// ✅ جيد — Optional فقط كنوع إرجاع
public Optional<User> findByEmail(String email) { ... }
عبارة المقابلة

"يمثل Optional قيمة قد تكون موجودة أو لا. أستخدمه كنوع إرجاع، أبدًا كمعامل. أفضل orElseThrow() للقيم المطلوبة و map() للتحويلات."


9. التمرير بالقيمة مقابل التمرير بالمرجع​

جافا تمرر كل شيء بالقيمة​

// البدائيات: يتم نسخ القيمة
void increment(int x) {
x++; // يعدل النسخة المحلية
}
int a = 5;
increment(a);
System.out.println(a); // 5 ← لم يتغير

// الكائنات: يتم نسخ المرجع (ليس الكائن)
void addItem(List<String> list) {
list.add("new"); // يعدل الكائن المشار إليه بواسطة نسخة المرجع
}
List<String> myList = new ArrayList<>();
addItem(myList);
System.out.println(myList); // [new] ← تم التعديل!

// لكن إعادة تعيين المرجع لا يغير شيئًا
void replace(List<String> list) {
list = new ArrayList<>(); // يغير النسخة المحلية، ليس الأصل
}
replace(myList);
System.out.println(myList); // [new] ← لا تزال القائمة القديمة

مخطط الذاكرة​

استدعاء: addItem(myList)

main() addItem()
┌──────┐ ┌──────┐
│myList │──┐ ┌────│ list │ ← نسخة من المرجع
└──────┘ │ │ └──────┘
↓ ↓
┌──────────────┐
│ ArrayList │ ← كلاهما يشير إلى نفس الكائن
│ ["new"] │
└──────────────┘
عبارة المقابلة

"جافا تمرر دائمًا بالقيمة. بالنسبة للبدائيات، يتم نسخ القيمة. بالنسبة للكائنات، يتم نسخ المرجع — لذا يمكنك تعديل الكائن من خلال النسخة، لكن لا يمكنك جعل المتغير الأصلي يشير إلى كائن مختلف."


10. الواجهات مقابل الفئات المجردة​

الجانبالواجهةالفئة المجردة
الكلمة المفتاحيةimplementsextends
الوراثة المتعددةنعم (عدة واجهات)لا (فئة واحدة)
المنشئلانعم
الحقول المثيلةلا (فقط static final)نعم
الطرق الملموسةنعم (منذ Java 8، default)نعم
متى تستخدمعقد / قدرة ("يمكن أن يفعل")قاعدة مشتركة ("هو")
// الواجهة = عقد (ما يمكن للكائن فعله)
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}

class Duck implements Flyable, Swimmable {
public void fly() { ... }
public void swim() { ... }
}

// الفئة المجردة = قاعدة مشتركة (ما هو الكائن)
abstract class Animal {
protected String name;

Animal(String name) { this.name = name; }

abstract void makeSound();

void breathe() { System.out.println(name + " breathes"); }
}

class Dog extends Animal {
Dog(String name) { super(name); }
void makeSound() { System.out.println("Woof!"); }
}
عبارة المقابلة

"أستخدم الواجهات لتعريف العقود (ما يمكن للكائن فعله) والفئات المجردة لمشاركة السلوك المشترك (ما هو الكائن). منذ Java 8، يمكن للواجهات أن تحتوي على طرق افتراضية، مما ضباب الخط — لكنني لا أزال أفضل الواجهات للمرونة والوراثة المتعددة."


ملخص سريع للمقابلة​

السؤالالإجابة في جملة واحدة
== مقابل .equals()== يقارن المراجع، .equals() يقارن المحتوى
hashCode()بصمة رقمية لكائن، تستخدمها HashMap/HashSet
عقدة equals/hashCodeإذا كان a.equals(b) فإن a.hashCode() == b.hashCode()
finalمرجع غير قابل لإعادة التعيين (ليس عدم قابلية تغيير الكائن)
staticينتمي إلى الفئة، ليس إلى المثيل
String غير قابل للتغييركل تعديل ينشئ كائنًا جديدًا
Optionalحاوية يمكن أن تكون فارغة، تتجنب فحوصات null
التمرير بالقيمةجافا تنسخ القيمة (أو المرجع للكائنات)
واجهة مقابل مجردةواجهة = عقد، مجردة = قاعدة مشتركة

⬅️ الفصل 5: التصحيح | الفصل 7: مجموعات جافا →