في مقالتي السابقة ، " الإخفاء حول ImmutableList في Java " ، اقترحت حلاً لمشكلة عدم وجود قوائم ثابتة في Java ، والتي لم يتم حلها ، لا الآن ولا في أي وقت مضى ، في Java.
بعد ذلك تم حل هذا الحل فقط على مستوى "هناك فكرة من هذا القبيل" ، وتم تنفيذ التطبيق في التعليمات البرمجية ، وبالتالي تم النظر إلى كل شيء بشكل متشكك. في هذه المقالة ، أقترح حلاً معدلاً. يتم نقل منطق الاستخدام و API إلى مستوى مقبول. تنفيذ التعليمات البرمجية يصل إلى مستوى تجريبي.
بيان المشكلة
سوف نستخدم التعاريف الواردة في المقال الأصلي. على وجه الخصوص ، هذا يعني أن ImmutableList
هي قائمة ثابتة من الإشارات إلى بعض الكائنات. إذا تبين أن هذه الكائنات غير قابلة للتغيير ، فلن تكون القائمة أيضًا كائنًا ثابتًا ، على الرغم من الاسم. في الممارسة العملية ، من غير المحتمل أن يؤذي هذا أي شخص ، ولكن لتجنب التوقعات غير المبررة ، من الضروري ذكر ذلك.
من الواضح أيضًا أنه يمكن اختراق "ثبات" القائمة عن طريق الانعكاسات ، أو عن طريق إنشاء فصولك الخاصة في الحزمة نفسها ، يليها التسلق إلى الحقول المحمية في القائمة ، أو شيء مشابه.
بخلاف المقالة الأصلية ، لن نلتزم بمبدأ "الكل أو لا شيء": يبدو أن المؤلف يعتقد أنه إذا تعذر حل المشكلة على مستوى JDK ، فلا ينبغي فعل شيء. (في الواقع ، هناك سؤال آخر ، "لا يمكن حلها" أو "لم يكن لدى مؤلفي Java رغبة في حلها". يبدو لي أنه لا يزال من الممكن بإضافة واجهات وفئات وطرق إضافية لتقريب المجموعات الحالية من المظهر المرغوب ، على الرغم من أنه أقل جمالا مما لو كنت قد فكرت به على الفور ، ولكن الأمر الآن ليس كذلك.)
سننشئ مكتبة يمكنها التعايش بنجاح مع المجموعات الموجودة في Java.
الأفكار الرئيسية للمكتبة:
- هناك
MutableList
و MutableList
. عن طريق صب أنواع من المستحيل الحصول على واحد من الآخر. - في مشروعنا ، الذي نريد تحسينه باستخدام المكتبة ، استبدلنا جميع القوائم بأحد هاتين الواجهتين. إذا لم تتمكن من الاستغناء عن
List
، فعند أول فرصة سنقوم بتحويل List
من / إلى واحد من واجهتين. الأمر نفسه ينطبق على لحظات تلقي / نقل البيانات إلى مكتبات الطرف الثالث باستخدام List
. - يجب إجراء التحويلات المتبادلة بين
MutableList
و MutableList
، List
بأسرع ما يمكن (أي ، دون نسخ القوائم ، إن أمكن). بدون تحويلات "رخيصة" ذهابًا وإيابًا ، تبدأ الفكرة بأكملها في الظن.
تجدر الإشارة إلى أنه يتم النظر في القوائم فقط ، حيث يتم تنفيذها في الوقت الحالي فقط في المكتبة. ولكن لا شيء يمنع المكتبة من الإضافة إلى Set
and Map
s.
API
ImmutableList
يعد ImmutableList
هو خليفة ReadOnlyList
(والذي ، كما في المقالة السابقة ، عبارة عن واجهة List
منسوخة يتم إلقاء جميع أساليب التحور منها). الأساليب المضافة:
List<E> toList(); MutableList<E> mutable(); boolean contentEquals(Iterable<? extends E> iterable);
يوفر أسلوب القائمة toList
القدرة على تمرير قائمة غير ImmutableList
إلى أجزاء من التعليمات البرمجية في انتظار List
. يتم إرجاع مجمّع تُرجع فيه كل أساليب التعديل UnsupportedOperationException
، وتتم إعادة توجيه الطرق المتبقية إلى ImmutableList
الأصلي.
تحويل الأسلوب mutable
MutableList
إلى قائمة MutableList
. يتم إرجاع مجمّع يتم فيه إعادة توجيه كل الطرق إلى قائمة ImmutableList
الأصلية حتى التغيير الأول. قبل التغيير ، يتم ربط برنامج التضمين من قائمة ImmutableList
الأصلية ، مع نسخ محتوياته إلى قائمة ArrayList
الداخلية ، حيث يتم إعادة توجيه جميع العمليات.
الغرض من طريقة contentEquals
مقارنة محتويات القائمة مع محتويات Iterable
التعسفي الذي تم تمريره (بالطبع ، هذه العملية ذات مغزى فقط لتلك التطبيقات Iterable
التي لها ترتيب محدد للعناصر).
لاحظ أنه في تطبيقنا لـ ReadOnlyList
، listIterator
iterator
و listIterator
java.util.Iterator
/ java.util.Iterator
القياسية. تحتوي هذه التكرارات على طرق تعديل يجب إخمادها عن طريق طرح UnsupportedOperationException
. سيكون من الأفضل إعداد ReadOnlyIterator
بنا ، لكن في هذه الحالة لم نتمكن من الكتابة for (Object item : immutableList)
، والذي من شأنه أن يفسد على الفور كل متعة استخدام المكتبة.
MutableList
MutableList
هو سليل القائمة العادية. الأساليب المضافة:
ImmutableList<E> snapshot(); void releaseSnapshot(); boolean contentEquals(Iterable<? extends E> iterable);
تم تصميم طريقة snapshot
للحصول على "لقطة" للحالة الحالية من MutableList
باعتبارها MutableList
. يتم حفظ "لقطة" داخل MutableList
، وإذا لم تتغير الحالة في وقت استدعاء الأسلوب التالي ، ImmutableList
نفس مثيل ImmutableList
. يتم تجاهل "اللقطة" المخزنة في الداخل في المرة الأولى التي يتم فيها استدعاء أي طريقة تعديل ، أو عند releaseSnapshot
. يمكن استخدام طريقة releaseSnapshot
لحفظ الذاكرة إذا كنت متأكدًا من أن أحدًا لن يحتاج إلى "لقطة" ، ولكن لن يتم استدعاء أساليب التعديل قريبًا.
Mutabor
توفر الفئة Mutabor
مجموعة من الأساليب الثابتة التي تمثل "نقاط الدخول" إلى المكتبة.
نعم ، يُطلق على المشروع الآن "mutabor" (إنه يتوافق مع "mutable" ، ويعني في الترجمة "سوف أتحول" ، وهو ما يتفق جيدًا مع فكرة "تحويل" بعض أنواع المجموعات سريعًا إلى مجموعات أخرى).
public static <E> ImmutableList<E> copyToImmutableList(E[] original); public static <E> ImmutableList<E> copyToImmutableList(Collection<? extends E> original); public static <E> ImmutableList<E> convertToImmutableList(Collection<? extends E> original); public static <E> MutableList<E> copyToMutableList(Collection<? extends E> original); public static <E> MutableList<E> convertToMutableList(List<E> original);
copyTo*
تصميم أساليب copyTo*
لإنشاء مجموعات مناسبة عن طريق نسخ البيانات المقدمة. convertTo*
أساليب convertTo*
التحويل السريع للمجموعة المنقولة إلى النوع المطلوب ، وإذا لم يكن من الممكن التحويل بسرعة ، فإنها تؤدي عملية النسخ البطيء. إذا كان التحويل السريع ناجحًا ، فسيتم مسح المجموعة الأصلية ، ويُفترض أنه لن يتم استخدامها في المستقبل (على الرغم من إمكانية ذلك ، ولكن هذا بالكاد يكون منطقيًا).
الاستدعاءات إلى MutableList
ImmutableList
تطبيق MutableList
/ MutableList
مخفية. من المفترض أن يتعامل المستخدم فقط مع الواجهات ، ولا يقوم بإنشاء مثل هذه الكائنات ، ويستخدم الطرق الموضحة أعلاه لتحويل المجموعات.
تفاصيل التنفيذ
ImmutableListImpl
بتغليف مجموعة من الكائنات. يتوافق التطبيق تقريبًا مع تطبيق ArrayList
، والذي يتم من خلاله طرح جميع طرق التعديل والتحقق من التعديل المتزامن.
يعد تطبيق أساليب قائمة toList
و toList
تافهاً للغاية. إرجاع الأسلوب toList
مجمّع يعيد توجيه المكالمات إلى toList
معطى ؛ لا يحدث النسخ البطيء للبيانات.
إرجاع الأسلوب mutable
MutableListImpl
إنشاؤها استناداً إلى هذا ImmutableList
. لا يحدث نسخ البيانات حتى يتم استدعاء أي أسلوب تعديل على MutableList
المتلقاة.
MutableListImpl
يغلف ارتباطات إلى List
ImmutableList
. عند إنشاء كائن ، يتم دائمًا ملء أحد هذين الارتباطين فقط ، ويبقى الآخر null
.
protected ImmutableList<E> immutable; protected List<E> list;
الأساليب غير القابلة لإعادة التوجيه إعادة توجيه المكالمات إلى ImmutableList
إذا لم تكن null
، وإلى List
خلاف ذلك.
تعديل أساليب إعادة توجيه المكالمات إلى List
، بعد التهيئة:
protected void beforeChange() { if (list == null) { list = new ArrayList<>(immutable.toList()); } immutable = null; }
تبدو طريقة snapshot
كما يلي:
public ImmutableList<E> snapshot() { if (immutable != null) { return immutable; } immutable = InternalUtils.convertToImmutableList(list); if (immutable != null) {
تطبيق أساليب releaseSnapshot
و releaseSnapshot
تافه.
يتيح لك هذا النهج تقليل عدد نسخ البيانات أثناء الاستخدام "العادي" ، مع استبدال النسخ بتحويلات سريعة.
تحويل قائمة سريعة
التحويلات السريعة ممكنة لفئات ArrayList
أو Arrays$ArrayList
(نتيجة Arrays.asList()
). في الممارسة العملية ، في الغالبية العظمى من الحالات ، هذه الفئات هي بالضبط التي تأتي عبر.
داخل هذه الفئات تحتوي على مجموعة من العناصر. يكمن جوهر التحويل السريع في الحصول على مرجع إلى هذه المجموعة من خلال الانعكاسات (هذا حقل خاص) واستبداله بمرجع إلى صفيف فارغ. هذا يضمن أن المرجع الوحيد للصفيف يبقى مع كائننا ، ويظل الصفيف بدون تغيير.
في الإصدار السابق من المكتبة ، تم إجراء تحويلات سريعة لأنواع المجموعات عن طريق استدعاء المنشئ. في الوقت نفسه ، تدهور كائن المجموعة الأصلي (أصبح غير مناسب للاستخدام مرة أخرى) ، وهو ما لا تتوقعه من اللاوعي من المصمم. الآن ، يتم استخدام طريقة ثابتة خاصة للتحويل ، ولا تفسد المجموعة الأصلية ، ولكن يتم مسحها ببساطة. وبالتالي ، تم القضاء على السلوك غير العادي المخيف.
مشاكل مع يساوي / hashCode
تستخدم مجموعات Java أسلوبًا غريبًا للغاية لتطبيق أساليب equals
و hashCode
.
يتم إجراء المقارنة وفقًا للمحتوى الذي يبدو منطقيًا ، لكن فئة القائمة نفسها لا تؤخذ في الاعتبار. لذلك ، على سبيل المثال ، سيكون ArrayList
و LinkedList
مع نفس المحتوى equals
.
هنا هو تطبيق يساوي / hashCode من AbstractList (الذي منه موروث ArrayList) public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof List)) return false; ListIterator<E> e1 = listIterator(); ListIterator e2 = ((List) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { E o1 = e1.next(); Object o2 = e2.next(); if (!(o1==null ? o2==null : o1.equals(o2))) return false; } return !(e1.hasNext() || e2.hasNext()); } public int hashCode() { int hashCode = 1; for (E e : this) hashCode = 31*hashCode + (e==null ? 0 : e.hashCode()); return hashCode; }
وبالتالي ، الآن جميع تطبيقات List
مطلوبة أن يكون لها تنفيذ مماثل equals
(ونتيجة لذلك ، hashCode
). خلاف ذلك ، يمكنك الحصول على مواقف عندما تكون a.equals(b) && !b.equals(a)
، وهي ليست جيدة. وضع مماثل مع Set
و Map
.
عند تطبيقه على المكتبة ، فإن هذا يعني أن تطبيق equals
و hashCode
for MutableList
محدد مسبقًا ، وفي مثل هذا التطبيق ، لا يمكن أن يكون MutableList
و MutableList
مع نفس المحتويات equals
(لأن ImmutableList
ليست List
). لذلك ، تمت إضافة أساليب contentEquals
لمقارنة المحتوى.
يتم تنفيذ أساليب equals
و hashCode
لـ ImmutableList
مشابهة تمامًا للنسخة من AbstractList
، ولكن مع استبدال List
من قبل ReadOnlyList
.
في المجموع
يتم نشر مصادر المكتبة والاختبارات بالرجوع في شكل مشروع مخضرم.
في حالة رغبة شخص ما في استخدام المكتبة ، أنشأ مجموعة على اتصال للحصول على "تعليقات".
استخدام المكتبة واضح جدًا ، فيما يلي مثال قصير:
private boolean myBusinessProcess() { List<Entity> tempFromDb = queryEntitiesFromDatabase("SELECT * FROM my_table"); ImmutableList<Entity> fromDb = Mutabor.convertToImmutableList(tempFromDb); if (fromDb.isEmpty() || !someChecksPassed(fromDb)) { return false; }
حظا سعيدا للجميع! إرسال تقارير الأخطاء!