الفصل 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. الأنواع البدائية مقابل فئات التغليف
| بدائي | تغليف | الحجم | القيمة الافتراضية |
|---|---|---|---|
int | Integer | 4 بايت | 0 |
long | Long | 8 بايت | 0L |
double | Double | 8 بايت | 0.0 |
boolean | Boolean | 1 بت | false |
char | Character | 2 بايت | '\u0000' |
متى تستخدم ماذا؟
| الحالة | استخدم | لماذا |
|---|---|---|
| المتغيرات المحلية، الحسابات | int, double | أسرع، لا يوجد تغليف |
| سمات الكيانات JPA | Long, 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) |
تجنب المتغيرات 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. الواجهات مقابل الفئات المجردة
| الجانب | الواجهة | الفئة المجردة |
|---|---|---|
| الكلمة المفتاحية | implements | extends |
| الوراثة المتعددة | نعم (عدة واجهات) | لا (فئة واحدة) |
| المنشئ | لا | نعم |
| الحقول المثيلة | لا (فقط 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 |
| التمرير بالقيمة | جافا تنسخ القيمة (أو المرجع للكائنات) |
| واجهة مقابل مجردة | واجهة = عقد، مجردة = قاعدة مشتركة |