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

الفصل 7: مجموعات جافا — دليل المقابلة

الهدف

يغطي هذا الفصل كل ما تحتاج لمعرفته حول مجموعات جافا لمقابلة: متى تستخدم ماذا، الفروقات بين List.of() و new ArrayList<>()، الفخاخ الشائعة، والأسئلة الكلاسيكية.


1. التسلسل الهرمي للمجموعات​

                    Iterable
│
Collection
╱ │ ╲
List Set Queue
╱ │ │ ╲ │
ArrayList │ HashSet │ LinkedList
LinkedList │ TreeSet │ PriorityQueue
│ LinkedHashSet
Vector
(legacy)

Map (ليست مجموعة!)
╱ │ ╲
HashMap TreeMap LinkedHashMap
│
ConcurrentHashMap

2. List.of() vs new ArrayList — قابل للتغيير مقابل غير قابل للتغيير​

الفرق الأساسي​

الإنشاءالنوعقابل للتعديل؟يقبل null؟
List.of("a", "b")قائمة غير قابلة للتغييرلالا
new ArrayList<>(List.of("a", "b"))قائمة قابلة للتعديلنعمنعم
Collections.unmodifiableList(list)عرض غير قابل للتغييرلا (لكن الأصلية نعم)نعم
List.copyOf(list)نسخة غير قابلة للتغييرلالا

أمثلة عملية​

// غير قابل للتغيير — لا يمكن تعديله
List<String> immutable = List.of("a", "b", "c");
immutable.add("d"); // 💥 UnsupportedOperationException
immutable.remove(0); // 💥 UnsupportedOperationException
immutable.set(0, "x"); // 💥 UnsupportedOperationException

// قابل للتعديل — يمكن تعديله
List<String> mutable = new ArrayList<>();
mutable.add("a"); // ✅ OK
mutable.add("b"); // ✅ OK
mutable.remove(0); // ✅ OK
mutable.set(0, "x"); // ✅ OK

// إنشاء قائمة قابلة للتعديل من قائمة غير قابلة للتغيير
List<String> mutable2 = new ArrayList<>(List.of("a", "b", "c"));
mutable2.add("d"); // ✅ OK

Map.of() و Set.of() — نفس المبدأ​

// خريطة غير قابلة للتغيير
Map<String, Integer> map = Map.of("a", 1, "b", 2);
map.put("c", 3); // 💥 UnsupportedOperationException

// خريطة قابلة للتعديل
Map<String, Integer> map2 = new HashMap<>(Map.of("a", 1, "b", 2));
map2.put("c", 3); // ✅ OK

// مجموعة غير قابلة للتغيير
Set<String> set = Set.of("a", "b", "c");
set.add("d"); // 💥 UnsupportedOperationException

// مجموعة قابلة للتعديل
Set<String> set2 = new HashSet<>(Set.of("a", "b", "c"));
set2.add("d"); // ✅ OK

Collections.emptyList() vs List.of()​

// كلاهما يعيد قائمة فارغة غير قابلة للتغيير
List<String> empty1 = Collections.emptyList(); // Java 5+
List<String> empty2 = List.of(); // Java 9+ (مفضل)

// كلاهما متكافئ، List.of() أكثر حداثة

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

الحالةاستخدملماذا
الثوابت، التكويناتList.of()غير قابل للتغيير، آمن للخيوط
بناء قائمة تدريجياًnew ArrayList<>()الحاجة لإضافة عناصر
إرجاع من طريقة (حالة فارغة)List.of() أو Collections.emptyList()لا تعيد أبداً null
DTO / RecordsList.copyOf()نسخة دفاعية، تضمن عدم التغيير
اختبارات الوحدةList.of()مختصر وواضح
عبارة المقابلة
"List.of() creates an immutable list — it's compact, null-safe, and thread-safe. new ArrayList<>() creates a mutable list that I can modify. I use List.of() for constants and return values, ArrayList for building collections."

3. ArrayList vs LinkedList​

ArrayList (مصفوفة ديناميكية) :
┌───┬───┬───┬───┬───┬───┬───┬ ┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │...│ ← وصول مباشر عبر الفهرس
└───┴───┴───┴───┴───┴───┴───┴ ┘

LinkedList (قائمة مزدوجة الربط) :
null ← [A] ⇄ [B] ⇄ [C] ⇄ [D] → null
العمليةArrayListLinkedList
get(index)O(1)O(n)
add(element) (النهاية)O(1) amortiO(1)
add(index, element) (الوسط)O(n)O(1) إذا كان لدينا العقدة
remove(index)O(n)O(1) إذا كان لدينا العقدة
الذاكرةمضغوطةأثقل (مؤشرات)
ذاكرة التخزين المؤقتة CPUجيدة (عناصر متجاورة)سيئة (عناصر متفرقة)
في الممارسة

استخدم ArrayList في 99% من الحالات. LinkedList نادراً ما يكون أسرع في الممارسة بسبب محلية ذاكرة التخزين المؤقتة CPU. حتى في حالات الإدخال المتكرر، غالباً ما يكون ArrayList أسرع.

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

"I almost always use ArrayList. LinkedList has better theoretical complexity for insertions, but ArrayList wins in practice due to CPU cache locality. LinkedList is useful as a Queue (FIFO) via the Deque interface."


4. HashMap — كيف تعمل؟​

الهيكل الداخلي​

HashMap (Java 8+) :
┌──────────────────────────────┐
│ Buckets (مصفوفة) │
├──────┬───────────────────────┤
│ [0] │ null │
│ [1] │ → Entry(key1, val1) │
│ [2] │ null │
│ [3] │ → Entry(key2, val2) → Entry(key3, val3) ← تصادم!
│ [4] │ null │
│ ... │ │
│ [n] │ → Entry(keyN, valN) │
└──────┴───────────────────────┘

إذا تجاوزت قائمة مرتبطة في دلو 8 عناصر → تتحول إلى شجرة حمراء-سوداء (O(log n))

كيف تعمل؟​

Map<String, Integer> map = new HashMap<>();
map.put("Alice", 25);

// الخطوة 1 : hashCode("Alice") → 63650026
// الخطوة 2 : 63650026 % حجم_المصفوفة → مؤشر الدلو
// الخطوة 3 : يخزن Entry("Alice", 25) في هذا الدلو

التصادم = نفس الدلو​

// إذا كان لمفتاحين نفس مؤشر الدلو:
map.put("Alice", 25); // الدلو 3
map.put("Bob", 30); // الدلو 3 أيضاً (تصادم)

// جافا تنشئ قائمة مرتبطة في الدلو 3 :
// bucket[3] → Entry("Alice", 25) → Entry("Bob", 30)
// لاسترجاع "Bob"، جافا تمر عبر القائمة وتستخدم equals()

التعقيدات​

العمليةالحالة المتوسطةأسوأ حالة (الكثير من التصادمات)
put()O(1)O(log n) (شجرة منذ Java 8)
get()O(1)O(log n)
remove()O(1)O(log n)
containsKey()O(1)O(log n)
containsValue()O(n)O(n)

5. HashMap vs TreeMap vs LinkedHashMap​

الخريطةالترتيبالتعقيدمتى تستخدمها
HashMapلا يوجد ترتيب مضمونO(1)الافتراضي، الأسرع
TreeMapمرتبة حسب المفتاح (طبيعي أو Comparator)O(log n)الحاجة إلى مفاتيح مرتبة
LinkedHashMapترتيب الإدخالO(1)الحاجة إلى ترتيب الإدخال
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("banana", 2);
hashMap.put("apple", 1);
hashMap.put("cherry", 3);
// التكرار: ترتيب عشوائي

Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("banana", 2);
treeMap.put("apple", 1);
treeMap.put("cherry", 3);
// التكرار: apple → banana → cherry (أبجدي)

Map<String, Integer> linkedMap = new LinkedHashMap<>();
linkedMap.put("banana", 2);
linkedMap.put("apple", 1);
linkedMap.put("cherry", 3);
// التكرار: banana → apple → cherry (ترتيب الإدخال)

6. HashSet vs TreeSet vs LinkedHashSet​

المجموعةالترتيبالتكراراتالتعقيد
HashSetلا يوجدلاO(1)
TreeSetمرتبةلاO(log n)
LinkedHashSetترتيب الإدخاللاO(1)
// HashSet = الأكثر استخداماً
Set<String> unique = new HashSet<>(List.of("b", "a", "c", "a"));
// → [a, b, c] (بدون تكرار، ترتيب عشوائي)

// TreeSet = مرتبة تلقائياً
Set<String> sorted = new TreeSet<>(List.of("banana", "apple", "cherry"));
// → [apple, banana, cherry]
تذكير

HashSet يستخدم hashCode() و equals() لاكتشاف التكرارات. إذا لم تقم كائناتك المخصصة بتنفيذ هذه الطرق، فلن تعمل المجموعة بشكل صحيح.


7. Queue و Deque​

// Queue (FIFO : First In, First Out) — قائمة انتظار
Queue<String> queue = new LinkedList<>();
queue.offer("الأول"); // إضافة إلى النهاية
queue.offer("الثاني");
queue.offer("الثالث");
queue.poll(); // إزالة من البداية → "الأول"

// Deque (Double-Ended Queue) — مكدس أو قائمة
Deque<String> deque = new ArrayDeque<>();

// مثل Stack (LIFO) :
deque.push("a");
deque.push("b");
deque.pop(); // → "b" (آخر مدخل، أول مخرج)

// مثل Queue (FIFO) :
deque.offerLast("a");
deque.offerLast("b");
deque.pollFirst(); // → "a" (أول مدخل، أول مخرج)
لا تستخدم Stack

java.util.Stack هي فئة قديمة (Java 1.0) متزامنة بشكل غير ضروري. استخدم ArrayDeque بدلاً منها.

// ❌ قديم
Stack<String> stack = new Stack<>();

// ✅ حديث
Deque<String> stack = new ArrayDeque<>();

8. التكرار على المجموعات​

الطرق الخمس للتكرار​

List<String> names = List.of("Alice", "Bob", "Charlie");

// 1. For-each (الأكثر شيوعاً)
for (String name : names) {
System.out.println(name);
}

// 2. Stream API (وظيفي)
names.stream()
.filter(n -> n.startsWith("A"))
.forEach(System.out::println);

// 3. forEach مع lambda
names.forEach(name -> System.out.println(name));

// 4. Iterator (عندما تحتاج إلى الحذف أثناء التكرار)
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.equals("Bob")) it.remove();
}

// 5. For الكلاسيكي مع الفهرس (فقط ArrayList)
for (int i = 0; i < names.size(); i++) {
System.out.println(names.get(i));
}

الفخ: ConcurrentModificationException​

// ❌ خطأ — تعديل قائمة أثناء for-each
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) list.remove(s); // 💥 ConcurrentModificationException
}

// ✅ الحل 1: Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove(); // OK
}

// ✅ الحل 2: removeIf (Java 8+)
list.removeIf(s -> s.equals("b")); // OK، أكثر اختصاراً

// ✅ الحل 3: Stream + filter (ينشئ قائمة جديدة)
List<String> filtered = list.stream()
.filter(s -> !s.equals("b"))
.toList();
عبارة المقابلة

"You can't modify a collection while iterating with for-each. I use Iterator.remove(), removeIf(), or streams to safely remove elements during iteration."


9. مجموعات آمنة للخيوط​

المجموعةآمنة للخيوط؟البديل الآمن للخيوط
ArrayListلاCopyOnWriteArrayList أو Collections.synchronizedList()
HashMapلاConcurrentHashMap
HashSetلاConcurrentHashMap.newKeySet()
LinkedListلاConcurrentLinkedQueue
// ❌ HashMap ليست آمنة للخيوط
Map<String, Integer> map = new HashMap<>();

// ✅ ConcurrentHashMap آمنة للخيوط بدون تزامن شامل
Map<String, Integer> map = new ConcurrentHashMap<>();

// ❌ Hashtable آمنة للخيوط لكنها قديمة وبطيئة (متزامنة في كل مكان)
Map<String, Integer> map = new Hashtable<>(); // لا تستخدم
عبارة المقابلة

"For concurrent access, I use ConcurrentHashMap instead of HashMap — it uses lock striping for better throughput than the legacy synchronized Hashtable."


10. Stream API — ملخص سريع​

List<User> users = getUsers();

// التصفية
List<User> active = users.stream()
.filter(User::isActive)
.toList();

// التحويل
List<String> emails = users.stream()
.map(User::getEmail)
.toList();

// الترتيب
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getName))
.toList();

// التجميع
Map<String, List<User>> byRole = users.stream()
.collect(Collectors.groupingBy(User::getRole));

// الاختزال (مجموع، أقصى، أدنى)
double totalSalary = users.stream()
.mapToDouble(User::getSalary)
.sum();

// التحقق
boolean anyAdmin = users.stream().anyMatch(u -> "ADMIN".equals(u.getRole()));
boolean allActive = users.stream().allMatch(User::isActive);
boolean noneBlocked = users.stream().noneMatch(User::isBlocked);

العمليات​

النوعأمثلةيعيد
الوسيطة (كسولة)filter, map, sorted, distinct, limit, skipStream
النهائية (تنفذ)toList, collect, forEach, count, sum, reduce, anyMatchنتيجة
عبارة المقابلة

"Streams are lazy — intermediate operations like filter and map don't execute until a terminal operation like toList() is called. I use streams for declarative, readable data processing."


جدول ملخص — أي مجموعة تستخدم؟​

الحاجةالمجموعةلماذا
قائمة مرتبة، وصول عبر الفهرسArrayListO(1) لـ get/set
بدون تكراراتHashSetO(1) لـ add/contains
بدون تكرارات + ترتيبTreeSetO(log n)، مرتبة
مفتاح → قيمةHashMapO(1) لـ put/get
مفتاح → قيمة، مرتبة حسب المفتاحTreeMapO(log n)، مرتبة
FIFO (قائمة انتظار)ArrayDequeأسرع من LinkedList
LIFO (مكدس)ArrayDequeيستبدل Stack (قديم)
خريطة آمنة للخيوطConcurrentHashMapLock striping
قائمة غير قابلة للتغييرList.of()آمنة للخيوط، مضغوطة

⬅️ الفصل 6: فخاخ جافا | ⬅️ العودة إلى الوحدة 18