قرأت المقال "لن تكون هناك مجموعات ثابتة في Java - لا الآن ولا أبدًا" واعتقدت أن مشكلة عدم وجود قوائم ثابتة في Java ، والتي تجعل المؤلف حزينًا ، يمكن حلها على نطاق محدود. أقدم أفكاري وأجزاء من التعليمات البرمجية حول هذا الموضوع.
(هذا مقال إجابة ، اقرأ المقال الأصلي أولاً.)
UnmodifiableList مقابل ImmutableList
السؤال الأول الذي يطرح نفسه هو: لماذا أحتاج إلى قائمة UnmodifiableList
قابلة UnmodifiableList
، إذا كان هناك قائمة غير قابلة ImmutableList
؟ نتيجة للمناقشة ، يتم عرض فكرتين بخصوص معنى UnmodifiableList
في تعليقات المقالة الأصلية:
- تتلقى الطريقة قائمة
UnmodifiableList
، ولا يمكنها تغييرها بنفسها ، ولكنها تعلم أنه يمكن تغيير المحتويات بواسطة سلسلة رسائل أخرى (وتعرف كيفية معالجتها بشكل صحيح) - لا تؤثر مؤشرات الترابط الأخرى على أن
UnmodifiableList
و ImmutableList
لطريقة ما ، لكن UnmodifiableList
يستخدم كطريقة "خفيفة الوزن" أكثر.
الخيار الأول يبدو نادرًا جدًا في الممارسة. وبالتالي ، إذا ImmutableList
تنفيذ تطبيق "سهل" لـ ImmutableList
، فإن UnmodifiableList
يصبح غير ضروري للغاية. لذلك ، في المستقبل سوف ننسى ذلك وسوف ننفذ فقط قائمة ImmutableList
للتطبيق.
بيان المشكلة
سنقوم بتطبيق خيار ImmutableList
:
- يجب أن تكون واجهة برمجة التطبيقات متطابقة مع واجهة برمجة تطبيقات
List
المعتادة في جزء "القراءة". يجب أن يكون جزء "الكتابة" غائبًا. - لا يجب أن تكون
List
ImmutableList
الموروث مرتبطة بقائمة الميراث. لماذا هكذا - يفهم المقال الأصلي. - فمن المنطقي أن تفعل التنفيذ عن طريق القياس مع
ArrayList
. هذا هو الخيار الأسهل. - يجب أن يتجنب التطبيق نسخ المصفوفات كلما أمكن ذلك.
تطبيق ImmutableList
أولاً ، نتعامل مع API. نقوم بفحص واجهات Collection
والقوائم ونسخ جزء "القراءة" منها إلى واجهاتنا الجديدة.
public interface ReadOnlyCollection<E> extends Iterable<E> { int size(); boolean isEmpty(); boolean contains(Object o); Object[] toArray(); <T> T[] toArray(T[] a); boolean containsAll(Collection<?> c); } public interface ReadOnlyList<E> extends ReadOnlyCollection<E> { E get(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); ReadOnlyList<E> subList(int fromIndex, int toIndex); }
بعد ذلك ، قم بإنشاء فئة ImmutableList
. يشبه التوقيع ArrayList
(لكن ينفذ واجهة ReadOnlyList
بدلاً من List
).
public class ImmutableList<E> implements ReadOnlyList<E>, RandomAccess, Cloneable, Serializable
نقوم بنسخ تنفيذ الفصل من ArrayList
و refactor بحزم ، ونطرد كل ما يتعلق بجزء "الكتابة" ، ونتحقق من التعديل المتزامن ، إلخ.
سوف يكون البنائين على النحو التالي:
public ImmutableList() public ImmutableList(E[] original) public ImmutableList(Collection<? extends E> original)
الأول يخلق قائمة فارغة. الثاني بإنشاء قائمة عن طريق نسخ الصفيف. لا يمكننا الاستغناء عن النسخ إذا كنا نريد تحقيق التغيير. والثالث هو أكثر إثارة للاهتمام. يقوم مُنشئ ArrayList
مماثل أيضًا بنسخ البيانات من المجموعة. سنفعل نفس الشيء ، ما لم يكن orginal
هو مثيل ArrayList
أو Arrays$ArrayList
(هذا ما يتم إرجاعه بواسطة طريقة Arrays.asList()
). يمكننا أن نفترض بأمان أن هذه الحالات سوف تغطي 90 ٪ من مكالمات المنشئ.
في هذه الحالات ، سنقوم "بسرقة" الصفيف original
خلال الانعكاسات (هناك أمل في أن يكون هذا أسرع من نسخ صفائف الجيجابايت). جوهر "السرقة":
- نصل إلى الحقل الخاص
original
، الذي يخزن الصفيف ( ArrayList.elementData
) - انسخ الرابط إلى المجموعة إلى أنفسنا
- وضعت في حقل مصدر فارغ
protected static final Field data_ArrayList; static { try { data_ArrayList = ArrayList.class.getDeclaredField("elementData"); data_ArrayList.setAccessible(true); } catch (NoSuchFieldException | SecurityException e) { throw new IllegalStateException(e); } } public ImmutableList(Collection<? extends E> original) { Object[] arr = null; if (original instanceof ArrayList) { try { arr = (Object[]) data_ArrayList.get(original); data_ArrayList.set(original, null); } catch (@SuppressWarnings("unused") IllegalArgumentException | IllegalAccessException e) { arr = null; } } if (arr == null) {
كعقد ، نفترض أنه عندما يتم استدعاء المنشئ ، يتم ImmutableList
القائمة القابلة للتغيير إلى قائمة غير قابلة للتغيير. لا يمكن استخدام القائمة الأصلية بعد ذلك. عند محاولة استخدام ، يصل NullPointerException
. يضمن هذا عدم تغيير المصفوفة "المسروقة" وستكون قائمتنا ثابتة بالفعل (باستثناء الحالة عندما يصل شخص ما إلى المصفوفة من خلال الانعكاسات).
فصول أخرى
لنفترض أننا قررنا استخدام قائمة ImmutableList
قابلة ImmutableList
في مشروع حقيقي.
يتفاعل المشروع مع المكتبات: يستقبل منهم ويرسل لهم قوائم مختلفة. في الغالبية العظمى من الحالات ، ستكون هذه القوائم قائمة ArrayList
. يسمح ImmutableList
التطبيق الموصوف لـ ImmutableList
بتحويل ArrayList
الناتج بسرعة إلى ImmutableList
. مطلوب أيضًا تنفيذ التحويل للقوائم المرسلة إلى المكتبات: ImmutableList
to List
. من أجل التحويل السريع ، فأنت بحاجة إلى برنامج ImmutableList
للتطبيق يقوم بتطبيق List
، وإلقاء استثناءات عند محاولة الكتابة إلى القائمة (على غرار Collections.unmodifiableList
).
أيضا ، المشروع نفسه يعالج القوائم بطريقة أو بأخرى. من المنطقي إنشاء فئة MutableList
تمثل قائمة قابلة للتغيير ، مع تنفيذ يستند إلى ArrayList
. في هذه الحالة ، يمكنك إعادة ArrayList
المشروع عن طريق استبدال ArrayList
بكل فئة تعلن بوضوح عن نية: إما MutableList
أو MutableList
.
تحتاج إلى تحويل سريع من MutableList
إلى MutableList
والعكس بالعكس. في الوقت نفسه ، على عكس تحويل ArrayList
إلى ImmutableList
، لم يعد بإمكاننا إفساد القائمة الأصلية.
عادة ما يكون التحويل بطيئًا ، مع نسخ الصفيف. لكن بالنسبة للحالات التي لا MutableList
المتلقاة دائمًا ، يمكنك إنشاء مجمّع: MutableList
، الذي يحفظ مرجعًا إلى ImmutableList
ويستخدمه في أساليب "القراءة" ، وإذا تم استدعاء طريقة "الكتابة" ، عندئذٍ ينسى فقط عن ImmutableList
، بعد نسخ محتوياته الصفيف لنفسه ، ثم يعمل بالفعل مع CopyOnWriteArrayList
(شيء مشابه عن بعد موجود في CopyOnWriteArrayList
).
يعني تحويل "العودة" تلقي لقطة من محتويات قائمة MutableList
في وقت استدعاء الأسلوب. مرة أخرى ، في معظم الحالات لا يمكنك الاستغناء عن نسخ صفيف ، ولكن يمكنك إنشاء برنامج تحسين لتحسين حالات عدة تحويلات لم تتغير محتويات قائمة MutableList
. هناك خيار آخر لتحويل "رجوع": يتم جمع بعض البيانات في MutableList
، وعندما يتم الانتهاء من جمع البيانات ، يجب تحويل MutableList
إلى الأبد إلى ImmutableList
. يتم تنفيذه أيضًا دون مشاكل مع غلاف آخر.
في المجموع
يتم نشر نتائج التجربة في شكل الشفرة هنا
يتم ImmutableList
نفسه ، الموضح في قسم "الفئات الأخرى" (ليس بعد؟).
يمكننا أن نفترض أن فرضية المقال الأصلي ، "المجموعات الثابتة في Java لن تكون" خاطئة.
إذا كانت هناك رغبة ، فمن الممكن تمامًا استخدام نهج مماثل. نعم ، مع عكازات صغيرة. نعم ، ليس داخل النظام بأكمله ، ولكن فقط في مشاريعهم (على الرغم من أن العديد منهم قد توغلوا ، فسيتم سحبه تدريجياً إلى المكتبات).
شيء واحد: إذا كانت هناك رغبة ... (تاهيتي ، تاهيتي ... لم نكن في أي تاهيتي! إنهم يطعموننا جيدًا هنا.)