الفصل 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 / Records | List.copyOf() | نسخة دفاعية، تضمن عدم التغيير |
| اختبارات الوحدة | List.of() | مختصر وواضح |
3. ArrayList vs LinkedList
ArrayList (مصفوفة ديناميكية) :
┌───┬───┬───┬───┬───┬───┬───┬ ┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │...│ ← وصول مباشر عبر الفهرس
└───┴───┴───┴───┴───┴───┴───┴ ┘
LinkedList (قائمة مزدوجة الربط) :
null ← [A] ⇄ [B] ⇄ [C] ⇄ [D] → null
| العملية | ArrayList | LinkedList |
|---|---|---|
get(index) | O(1) | O(n) |
add(element) (النهاية) | O(1) amorti | O(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" (أول مدخل، أول مخرج)
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, skip | Stream |
| النهائية (تنفذ) | 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."
جدول ملخص — أي مجموعة تستخدم؟
| الحاجة | المجموعة | لماذا |
|---|---|---|
| قائمة مرتبة، وصول عبر الفهرس | ArrayList | O(1) لـ get/set |
| بدون تكرارات | HashSet | O(1) لـ add/contains |
| بدون تكرارات + ترتيب | TreeSet | O(log n)، مرتبة |
| مفتاح → قيمة | HashMap | O(1) لـ put/get |
| مفتاح → قيمة، مرتبة حسب المفتاح | TreeMap | O(log n)، مرتبة |
| FIFO (قائمة انتظار) | ArrayDeque | أسرع من LinkedList |
| LIFO (مكدس) | ArrayDeque | يستبدل Stack (قديم) |
| خريطة آمنة للخيوط | ConcurrentHashMap | Lock striping |
| قائمة غير قابلة للتغيير | List.of() | آمنة للخيوط، مضغوطة |