المجموعات
84. أخبرنا عن التكرارات وكيفية استخدامها
تعد المجموعات موضوعًا مفضلاً في أي مقابلة لمطوري Java. عند الإجابة على أسئلة حول التسلسل الهرمي للمجموعة، غالبًا ما يقول المرشحون إنه يبدأ بواجهة المجموعة . ولكن هذا ليس كذلك. هناك واجهة أخرى بمستوى أعلى: Iterable . تتكون هذه الواجهة من طريقة iterator() ، والتي تتيح لك الوصول إلى كائن Iterator للمجموعة الحالية. وما هو كائن Iterator هذا بالضبط ؟ يوفر كائن Iterator القدرة على التنقل عبر المجموعة والتكرار على عناصرها، ولا يحتاج المستخدم إلى معرفة تفاصيل التنفيذ المحددة للمجموعة. بمعنى آخر، هو نوع من المؤشر إلى عناصر المجموعة، كما لو كان يلقي نظرة خاطفة على أحدها. يحتوي المكرر على طرق مثل:-
hasNext() — يُرجع صحيحًا إذا كان التكرار يحتوي على عنصر آخر (تتيح لك هذه الطريقة معرفة متى وصلت إلى نهاية المجموعة)؛
-
next() — يُرجع العنصر التالي في التكرار. إذا لم يكن هناك واحد، فسيتم طرح NoSuchElementException . وهذا يعني أنه قبل استخدام هذه الطريقة، من الأفضل استخدام طريقة hasNext() للتأكد من وجود العنصر التالي؛
-
إزالة () - إزالة العنصر الأخير الذي تم استلامه من المجموعة باستخدام الطريقة التالية () . إذا لم يتم استدعاء next() مطلقًا، فإن استدعاء إزالة() سيؤدي إلى طرح IllegalStateException ؛
-
forEachRemaining(<Consumer>) — ينفذ الإجراء الذي تم تمريره على كل عنصر من عناصر المجموعة (ظهرت هذه الطريقة في Java 8).
List<String> list = new ArrayList<>();
list.add("Hello ");
list.add("World, ");
list.add("It's ");
list.add("Amigo!");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
iterator.next();
iterator.remove();
}
System.out.println(list.size());
ستعرض وحدة التحكم ما يلي:
iterator.forEachRemaining(x -> System.out.print(x));
ولكن بمجرد القيام بذلك، يصبح المكرِّر غير مناسب لمزيد من الاستخدام: لقد اجتاز القائمة بأكملها، ولا يملك المكرِّر العادي طرقًا للتكرار للخلف. وهذا يجعلنا ندخل في مناقشة لطيفة حول LinkedList ، على وجه التحديد، أسلوب listIterator() الخاص به ، والذي يُرجع نوعًا مُحسّنًا من المكرر: ListIterator . بالإضافة إلى أساليب المكرر العادي (القياسي)، فإن هذا النوع يحتوي على ما يلي:
-
add(<Element>) — يضيف عنصرًا جديدًا إلى القائمة؛
-
hasPrevious() — يُرجع صحيحًا إذا كان هناك عنصر يقع قبل العنصر التالي (إذا كان هناك عنصر سابق)؛
-
nextIndex() — يُرجع فهرس العنصر التالي؛
-
السابق () - إرجاع العنصر السابق (العنصر الذي يسبق العنصر التالي)؛
-
PreviousIndex يُرجع فهرس العنصر السابق.
-
set(<Element>) - يستبدل العنصر الأخير الذي تم إرجاعه بواسطة next() أو Previous() .
85. ما هو التسلسل الهرمي للمجموعات الموجود في Java Collection Framework؟
هناك نوعان من التسلسلات الهرمية للمجموعات في Java. التسلسل الهرمي الأول هو التسلسل الهرمي للمجموعات، والذي يحتوي على البنية التالية: وينقسم إلى المجموعات الفرعية التالية:-
المجموعة عبارة عن واجهة تصف مجموعة، وهي بنية بيانات تحتوي على عناصر فريدة غير مرتبة (غير متكررة). تحتوي هذه الواجهة على بعض التطبيقات القياسية: TreeSet و HashSet و LinkedHashSet .
-
القائمة عبارة عن واجهة تصف بنية البيانات التي تخزن تسلسلاً مرتبًا للكائنات. يمكن إدراج الكائنات الموجودة في القائمة وإزالتها بواسطة فهرسها في القائمة (مثل المصفوفة، ولكن مع تغيير الحجم الديناميكي). تحتوي هذه الواجهة أيضًا على بعض التطبيقات القياسية: ArrayList و Vector ( مهمل وغير مستخدم فعليًا ) و LinkedList .
-
قائمة الانتظار هي واجهة تصف بنية البيانات التي تقوم بتخزين العناصر في قائمة انتظار First In First Out (FIFO) . تحتوي هذه الواجهة على التطبيقات القياسية التالية: LinkedList (هذا صحيح، فهي تنفذ أيضًا قائمة الانتظار ) و PriotityQueue .
86. ما هو الهيكل الداخلي لقائمة ArrayList؟
ArrayList يشبه المصفوفة، لكنه يمكن أن يتوسع ديناميكيًا . ماذا يعني ذالك؟ تحت الغطاء، يستخدم ArrayList مصفوفة عادية، أي أنه يخزن عناصره في مصفوفة داخلية حجمها الافتراضي هو 10 خلايا. بمجرد امتلاء المصفوفة الداخلية، يتم إنشاء مصفوفة جديدة. يتم تحديد حجم المصفوفة الجديدة بهذه الصيغة:<size of the current array> * 3 / 2 + 1
لذلك، إذا كان حجم المصفوفة لدينا هو 10، فسيكون حجم المصفوفة الجديدة: 10 * 3 / 2 + 1 = 16. ثم يتم نسخ جميع القيم من المصفوفة الأصلية (القديمة) إليها باستخدام المصفوفة المضمنة طريقة System.arraycopy() ، ويتم حذف المصفوفة الأصلية. باختصار، هذه هي الطريقة التي تطبق بها ArrayList تغيير الحجم الديناميكي. دعونا نفكر في طرق ArrayList الأكثر شيوعًا : 1. add(<Element>) - يضيف عنصرًا إلى نهاية المصفوفة (في آخر خلية فارغة)، بعد التحقق أولاً مما إذا كانت هناك خلية متاحة في المصفوفة. إذا لم يكن الأمر كذلك، فسيتم إنشاء مصفوفة جديدة، ويتم نسخ العناصر فيها. التعقيد الزمني لهذه العملية هو O(1). توجد طريقة إضافة (<Index>, <Elelement>) مشابهة . فهو يضيف عنصرًا ليس إلى نهاية القائمة (المصفوفة)، بل إلى الخلية المحددة التي يشير إليها الفهرس الذي يأتي كوسيطة. في هذه الحالة، سيختلف التعقيد الزمني حسب المكان الذي تضيف فيه:
- إذا كانت الإضافة قريبة من بداية القائمة، فسيكون التعقيد الزمني قريبًا من O(N)، لأن جميع العناصر الموجودة على يمين العنصر الجديد يجب نقلها خلية واحدة إلى اليمين؛
- إذا تم إدراج العنصر في المنتصف، فسيكون O(N/2)، لأننا نحتاج فقط إلى نقل نصف عناصر القائمة خلية واحدة إلى اليمين.
87. ما هو الهيكل الداخلي للقائمة المرتبطة؟
تحتوي قائمة ArrayList على عناصر في المصفوفة الداخلية، لكن القائمة المرتبطة تقوم بتخزينها في قائمة مرتبطة بشكل مزدوج. وهذا يعني أن كل عنصر يحتوي على رابط للعنصر السابق والعنصر التالي . العنصر الأول لا يرتبط بالعنصر السابق (بعد كل شيء، هو الأول). ويعتبر أيضًا رأس القائمة، وكائن LinkedList له إشارة مباشرة إليه. وبالمثل، فإن العنصر الأخير لا يحتوي على العنصر التالي، لأنه هو ذيل القائمة. يشير كائن LinkedList أيضًا إليه مباشرةً . وهذا يعني أن التعقيد الزمني للوصول إلى رأس القائمة أو ذيلها هو O(1). في ArrayList ، إذا كبرت القائمة، فإن المصفوفة الداخلية تنمو. كل شيء هنا أسهل: إضافة مرجع أمر بسيط مثل تغيير بعض الروابط. دعونا نلقي نظرة على بعض طرق LinkedList الأكثر استخدامًا: 1. add(<Element>) — يضيف عنصرًا إلى نهاية القائمة، أي بعد العنصر الأخير (5)، سيتم إضافة رابط للعنصر الجديد كما يلي . سيشير المرجع السابق في العنصر الجديد إلى العنصر (5) الذي يسبقه الآن في القائمة. التعقيد الزمني لهذه العملية هو O(1)، نظرًا لأننا نحتاج فقط إلى الارتباط بالعنصر الأخير، وكما تتذكر، فإن كائن LinkedList له إشارة مباشرة إلى الذيل، والوصول إليه له حد أدنى من التعقيد الزمني الثابت. 2. add(<Index>, <Element>) - يضيف عنصرًا حسب الفهرس. عند إضافة عنصر، على سبيل المثال، في منتصف القائمة، تكرر هذه الطريقة أولاً العناصر من الرأس والذيل (في كلا الاتجاهين) حتى يتم العثور على المكان المطلوب. إذا أضفنا عنصرا بين العنصر الثالث والرابع (في الصورة أعلاه)، فإن الرابط التالي للعنصر الثالث سيشير إلى العنصر الجديد. وسيشير العنصر السابق للعنصر المضاف حديثًا إلى العنصر الثالث. بدوره، سيشير الرابط السابق للعنصر الرابع القديم الآن إلى العنصر الجديد، وسيشير الرابط التالي للعنصر الجديد إلى العنصر الرابع الجديد: يعتمد التعقيد الزمني لهذه الطريقة على فهرس العنصر الجديد:- إذا كان قريبًا من الرأس أو الذيل، فستقترب العملية من O(1)، لأنه لن يكون من الضروري فعليًا التكرار على العناصر؛
- إذا كان قريبًا من المنتصف، فسيكون لدينا O(N/2)، نظرًا لأن الطريقة ستبحث من الرأس والذيل في نفس الوقت حتى يتم العثور على العنصر المطلوب.
88. ما هو الهيكل الداخلي لـ HashMap؟
قد يكون هذا أحد أسئلة المقابلة الأكثر شيوعًا التي تطرح على مرشحي مطوري Java. تعمل HashMap مع أزواج القيمة الرئيسية . كيف يتم تخزينها داخل HashMap نفسها؟ يحتوي HashMap على مجموعة داخلية من العقد:Node<K,V>[] table
افتراضيًا، حجم المصفوفة هو 16، ويتضاعف في كل مرة يتم ملؤها بالعناصر (أي عند الوصول إلى LOAD_FACTOR ؛ فهو يحدد العتبة لمدى امتلاء المصفوفة - بشكل افتراضي، هو 0.75 ) . تقوم كل عقدة بتخزين تجزئة للمفتاح، والمفتاح، والقيمة، ومرجع إلى العنصر التالي: في هذه الحالة، يعني "المرجع إلى العنصر التالي" أننا نتعامل مع قائمة مرتبطة بشكل فردي، حيث يحتوي كل عنصر على رابط للعنصر التالي. بمعنى آخر، تقوم HashMap بتخزين بياناتها في مجموعة من القوائم المرتبطة بشكل فردي. ولكن اسمحوا لي أن أقول على الفور أنه عندما تحتوي خلية من مصفوفة الجدول على قائمة مرتبطة بشكل فردي تتكون من أكثر من عنصر واحد، فهذا ليس جيدًا. وتسمى هذه الظاهرة الاصطدام . ولكن أول الأشياء أولا. دعونا نرى كيف يتم حفظ زوج جديد باستخدام طريقة الوضع . أولاً، تحصل الطريقة على hashCode() للمفتاح . وهذا يعني أنه لكي تعمل HashMap بشكل صحيح، يجب أن تكون المفاتيح التي تستخدمها هي فئات تتجاوز هذه الطريقة. يتم بعد ذلك استخدام رمز التجزئة هذا في طريقة التجزئة الداخلية لتحديد فهرس لبعض الخلايا داخل صفيف الجدول. يتيح لنا الفهرس الناتج الوصول إلى خلية معينة من مصفوفة الجدول. لدينا حالتان هنا:
- الخلية فارغة — في هذه الحالة، يتم تخزين قيمة العقدة الجديدة فيها.
- الخلية ليست فارغة — في هذه الحالة، تتم مقارنة قيم المفاتيح. إذا كانت متساوية، فستحل قيمة العقدة الجديدة محل القيمة القديمة؛ إذا لم تكن متساوية، فسيتم الوصول إلى القائمة التالية ، ومقارنة مفتاحها... وهكذا، حتى تقوم القيمة الجديدة إما بالكتابة فوق بعض القيمة القديمة أو نصل إلى نهاية القائمة المرتبطة بشكل فردي ثم نقوم بتخزين القيمة الجديدة هناك باسم العنصر الأخير.
GO TO FULL VERSION