لن تكون هناك مجموعات ثابتة في Java - لا الآن ولا على الإطلاق

مرحبا بالجميع!

اليوم ، يتم توجيه انتباهك إلى ترجمة مقال مكتوب بعناية حول إحدى المشكلات الأساسية لجافا - قابلية التغيير ، وكيف تؤثر على بنية هياكل البيانات وكيفية التعامل معها. هذه المادة مأخوذة من مدونة Nicolai Parlog ، التي حاولنا في الواقع أن نضع أسلوبها الأدبي الرائع في الترجمة. يتميز نيكولاس نفسه بشكل ملحوظ مقتطف من مدونة الشركة JUG.ru على Habré ؛ دعنا نستشهد بهذا المقطع بالكامل:


Nikolay Parlog هو مثل هذا المتأنق الذي يقوم بإجراء مراجعات على ميزات Java. لكنه ليس من Oracle في نفس الوقت ، لذا فإن المراجعات صريحة ومفهومة بشكل مدهش. في بعض الأحيان من بعدهم يتم طرد شخص ما ، ولكن نادرا ما. سيتحدث نيكولاي عن مستقبل جافا ، ماذا سيكون في الإصدار الجديد. يجيد التحدث عن الاتجاهات وعمومًا عن العالم الكبير. إنه رفيق جيد القراءة ومثقف للغاية. حتى التقارير البسيطة ممتعة للاستماع إليها ، في كل وقت تتعلم فيه شيئًا جديدًا. علاوة على ذلك ، يعرف نيكولاس ما يقوله. وهذا يعني أنه يمكنك الوصول إلى أي تقرير والاستمتاع به فقط ، حتى لو لم يكن هذا هو الموضوع الخاص بك على الإطلاق. هو التدريس. كتب "نظام Java Java System" لنظام Manning Publishing House ، ويحتفظ بمدونات حول تطوير البرمجيات في codefx.org ، ويشارك منذ فترة طويلة في العديد من المشاريع مفتوحة المصدر. يمكن تعيينه مباشرة في المؤتمر ، فهو مستقل. صحيح ، لحسابهم الخاص مكلفة للغاية. هنا هو التقرير .

نقرأ ونصوت. من يحب المنشور بشكل خاص - نوصي أيضًا بالاطلاع على تعليقات القارئ على المنشور الأصلي.

التقلبات سيئة ، أليس كذلك؟ وفقا لذلك ، ثبات هو جيد . هياكل البيانات الرئيسية التي تكون فيها قابلية التثبيط مثمرة بشكل خاص هي المجموعات: في جافا قائمة ( List ) ومجموعة ( Set ) وقاموس ( Map ). ومع ذلك ، على الرغم من أن JDK يأتي مع مجموعات غير قابلة للتغيير (أو غير قابلة للتعديل؟) ، فإن نظام الكتابة لا يعرف شيئًا عن هذا. لا يوجد أي قائمة غير قابلة ImmutableList في JDK ، ويبدو أن هذا النوع من جوافة عديم الفائدة تمامًا بالنسبة لي. لكن لماذا؟ لماذا لا تضيف فقط Immutable... إلى هذا المزيج ولا تقل أنه يجب أن يكون؟

ما هي مجموعة غير قابلة للتغيير؟


في مصطلحات JDK ، تغيرت معاني الكلمتين " غير قابل للتغيير " و " غير قابل للتعديل " خلال السنوات القليلة الماضية. في البداية ، كان يُطلق على "غير قابل للتعديل" مثيل لم يسمح بالتغيير (قابلية التغيير): استجابةً لتغيير الأساليب ، ألقى UnsupportedOperationException . ومع ذلك ، يمكن تغييره بطريقة أخرى - ربما لأنه كان مجرد غلاف حول مجموعة قابلة للتغيير. تنعكس طرق العرض هذه في الأساليب Collections::unmodifiableList و unmodifiableSet و unmodifiableMap ، وكذلك في JavaDoc .

في البداية ، يشير المصطلح " غير قابل للتغيير " إلى المجموعات التي يتم إرجاعها بواسطة أساليب المصنع لمجموعات Java 9 . لا يمكن تغيير المجموعات نفسها بأي شكل من الأشكال (نعم ، هناك انعكاس ، لكنها لا تهم) ، لذلك يبدو أنها تبرر اسمها. للأسف ، غالبا ما ينشأ الالتباس بسبب هذا. افترض أن هناك طريقة تعرض جميع العناصر من مجموعة غير قابلة للتغيير على الشاشة - هل ستحصل دائمًا على نفس النتيجة؟ نعم؟ أم لا؟

إذا لم تجب على الفور ، فهذا يعني أنك حصلت على لمحة دقيقة عن الارتباك الممكن هنا. " مجموعة غير قابلة للتغيير من الوكلاء السريين " - يبدو أنها تبدو لعنة جميلة مشابهة لـ " مجموعة غير قابلة للتغيير من الوكلاء السريين غير القابلين للتغيير " ، ولكن قد لا يكون هذان الكيانان متطابقين. لا يمكن تحرير مجموعة غير قابلة للتغيير باستخدام عمليات الإدراج / الحذف / التنظيف ، وما إلى ذلك ، ولكن إذا كانت العوامل السرية قابلة للتغيير (على الرغم من أن سمات الشخصيات في أفلام التجسس سيئة للغاية بحيث لا تصدقها حقًا) ، فإن هذا لا يعني ذلك أن المجموعة الكاملة من العملاء السريين غير قابلة للتغيير. لذلك ، يوجد الآن تحول نحو تسمية مثل هذه المجموعات غير القابلة للتعديل ، وليس الثابتة ، والمكرسة في الإصدار الجديد من JavaDoc .

قد تحتوي المجموعات الثابتة التي تمت مناقشتها في هذه المقالة على عناصر قابلة للتغيير.

أنا شخصياً لا أحب هذا التنقيح للمصطلحات. في رأيي ، يجب أن يعني مصطلح "مجموعة غير قابلة للتغيير" فقط أن المجموعة نفسها غير قابلة للتغيير ، ولكن يجب ألا تميز العناصر الموجودة فيها. في هذه الحالة ، هناك نقطة واحدة أكثر إيجابية: مصطلح "ثبات" في نظام جافا البيئي لا يتحول إلى هراء تام.

بطريقة أو بأخرى ، في هذه المقالة سوف نتحدث عن مجموعات غير قابلة للتغيير ، حيث ...

  • يتم تحديد الحالات الموجودة في المجموعة في مرحلة التصميم
  • هذه النسخ - كمية متساوية ، لا تقلل ولا تضيف
  • لم يتم الإدلاء ببيانات فيما يتعلق بتبادل هذه العناصر.

دعونا نتحدث عن حقيقة أننا سنمارس الآن إضافة مجموعات ثابتة. أن تكون دقيقا - قائمة ثابتة. كل ما سيقال عن القوائم ينطبق بالمثل على مجموعات الأنواع الأخرى.

الشروع في إضافة مجموعات غير قابلة للتغيير!

لنقم بإنشاء واجهة ImmutableList وجعلها ، بالنسبة إلى List ، اه ... ماذا؟ النوع الفائق أو النوع الفرعي؟ دعونا نتناول الخيار الأول.



بشكل جميل ، لا يوجد ImmutableList طرق ImmutableList ، لذا فإن استخدامها دائمًا آمن ، أليس كذلك؟ اذا؟ لا يا سيدي.

 List<Agent> agents = new ArrayList<>(); // ,  `List`  `ImmutableList` ImmutableList<Agent> section4 = agents; //    section4.forEach(System.out::println); //    `section4` agents.add(new Agent("Motoko"); //  "Motoko" – ,      ?! section4.forEach(System.out::println); 

يوضح هذا المثال أنه من الممكن نقل هذه القائمة غير القابلة للتغيير بالكامل إلى واجهة برمجة التطبيقات ، والتي يمكن أن يتم تشغيلها بناءً على القابلية للتغيير ، وبالتالي ، تلغي جميع الضمانات التي قد يشير إليها اسم من هذا النوع. إليك وصفة يمكن أن تؤدي إلى كارثة.

حسنًا ، ثم يمد ImmutableList List . ربما؟



الآن ، إذا كانت واجهة برمجة التطبيقات تتوقع قائمة ثابتة ، فستتلقى هذه القائمة ، ولكن هناك عيبان:

  • يجب أن تظل القوائم غير القابلة للتغيير تقدم طرقًا لتعديلها (كما هي محددة في النموذج الفائق) ، وسوف يلقي التطبيق الوحيد الممكن استثناءً
  • مثيلات List ImmutableList أيضًا مثيلات List ، وعندما يتم تعيينها لمثل هذا المتغير ، وتم تمريره على أنه وسيطة ، أو إرجاع من هذا النوع ، يكون من المنطقي افتراض افتراض أن قابلية التحويل مسموح بها.

وبالتالي ، اتضح أنه لا يمكنك استخدام ImmutableList محليًا إلا لأنه يمرر حدود واجهة برمجة التطبيقات (API) List ، مما يتطلب منك مستوى احترازيًا فائقًا من الاحتياطات أو ينفجر في وقت التشغيل. ليس سيئًا مثل توسيع List ImmutableList قابلة ImmutableList ، ولكن هذا الحل لا يزال بعيدًا عن المثالية.

هذا هو بالضبط ما كنت أفكر فيه عندما قلت أن نوع ImmutableList من جوافة لا فائدة منه عملياً. هذه نموذج رمز رائع ، يمكن الاعتماد عليه للغاية عند العمل مع قوائم ثابتة غير محلية (لهذا السبب أنا أستخدمها بنشاط) ، ولكن من السهل للغاية اللجوء إليها ، حيث تجاوزت القلعة المجمعة المضمونة والمضمونة ، والتي كانت جدرانها تتألف من أنواع غير قابلة للتغيير - وفقط في هذا أنواع ثابتة يمكن أن تكشف تماما إمكاناتها. هذا أفضل من لا شيء ، لكنه غير فعال كحل JDK.

إذا تعذر على القائمة ImmutableList تمديد List ، وما زال الحل البديل لا يعمل ، فكيف من المفترض أن تجعل الأمر كله يعمل؟

ثبات هو ميزة


كانت المشكلة التي واجهناها في المحاولتين الأوليين لإضافة أنواع غير قابلة للتغيير هي إدراكنا الخاطئ بأن القابلية للتغير هي ببساطة عدم وجود شيء: نحن نأخذ List ، نزيل الكود المتغير منه ، نحصل على قائمة ImmutableList قابلة ImmutableList . ولكن ، في الواقع ، كل هذا لا يعمل بهذه الطريقة.

إذا قمنا ببساطة بإزالة الطرق المعدلة من List ، فسنحصل على List للقراءة فقط. أو ، بالالتزام بالمصطلحات الواردة أعلاه ، يمكن تسميتها UnmodifiableList - يمكن أن تتغير ، فقط لن تقوم بتغييرها.

الآن يمكننا إضافة شيئين آخرين إلى هذه الصورة:

  • يمكننا أن نجعلها قابلة للتغيير عن طريق إضافة الأساليب المناسبة
  • يمكننا أن نجعلها غير قابلة للتغيير عن طريق إضافة الضمانات المناسبة.

عدم الثبات هو عدم وجود قابلية للتغير ، ولكن ضمان عدم وجود تغييرات
في هذه الحالة ، من المهم أن نفهم أننا في كلتا الحالتين نتحدث عن ميزات كاملة - عدم ثبات عدم وجود تغييرات ، ولكن ضمان أنه لن تكون هناك تغييرات. قد لا تكون ميزة موجودة بالضرورة شيء يتم استخدامه بشكل جيد ، بل يمكنها أيضًا ضمان عدم حدوث شيء سيء في التعليمات البرمجية - في هذه الحالة ، على سبيل المثال ، حول أمان سلاسل الرسائل.

من الواضح أن قابلية التغيير وعدم الثبات تتعارض مع بعضهما البعض ، وبالتالي لا يمكننا في نفس الوقت استخدام التسلسلين الهرميين الوارثين أعلاه. تكتسب الأنواع إمكانيات من أنواع أخرى ، لذلك بغض النظر عن كيفية قصها ، إذا كان أحد الأنواع يرث من الآخر ، فسيحتوي على كلتا الميزتين.

حسنًا ، لا يمكن لـ " List و " List ImmutableList قابلة ImmutableList تمديد بعضها البعض. لكن تم إحضارنا إلى هنا من خلال العمل مع UnmodifiableList ، واتضح حقًا أن كلا النوعين لهما نفس واجهة برمجة التطبيقات ، للقراءة فقط ، مما يعني أنه يجب عليهما توسيعه.



على الرغم من أنني لن أسمي الأشياء هذه الأسماء بالضبط ، إلا أن التسلسل الهرمي من هذا النوع معقول. في سكالا ، على سبيل المثال ، يتم ذلك عمليا . الفرق هو أن النمط المشترك المشترك ، والذي أطلقنا عليه اسم UnmodifiableList ، يحدد الأساليب القابلة للتغيير التي تُرجع مجموعة معدلة وتترك النص الأصلي بدون تغيير. وبالتالي ، تبين أن القائمة الثابتة غير ثابتة وتعطي البديل القابل للتغيير مجموعتين من أساليب التعديل - الموروثة لتلقي نسخ معدلة وله للتغييرات في المكان.

ماذا عن جافا؟ هل من الممكن تحديث مثل هذا التسلسل الهرمي عن طريق إضافة أنواع جديدة من الأشقاء والإخوة إليها؟

هل من الممكن تحسين المجموعات غير القابلة للتعديل وغير القابلة للتغيير؟
بالطبع ، لا توجد مشكلة في إضافة ImmutableList UnmodifiableList و ImmutableList وإنشاء التسلسل الهرمي للميراث الموضح أعلاه. المشكلة هي أنه في المدى القصير والمتوسط ​​سيكون عديم الفائدة عمليا. اسمحوا لي أن أشرح.

ImmutableList أن تكون UnmodifiableList و ImmutableList و List بأنواعها - في هذه الحالة ، ستكون واجهات برمجة التطبيقات قادرة على التعبير بوضوح عما يحتاجون إليه وما يقدمونه.

 public void payAgents(UnmodifiableList<Agent> agents) { //      , //         } public void sendOnMission(ImmutableList<Agent> agents) { //   ( ), //  ,     } public void downtime(List<Agent> agents) { //       , //        ,      } public UnmodifiableList<Agent> teamRoster() { //   ,     , //      ,     -  } public ImmutableList<Agent> teamOnMission() { //    ,      } public List<Agent> team() { //    ,    , //        } 

ومع ذلك ، ما لم تبدأ المشروع من نقطة الصفر ، فمن المحتمل أن يكون لديك مثل هذه الوظيفة ، وسوف يبدو مثل هذا:

 //   ,  `Iterable<Agent>` //  ,   ,        public void payAgents(List<Agent> agents) { } public void sendOnMission(List<Agent> agents) { } public void downtime(List<Agent> agents) { } //      , //    ,  `List`     public List<Agent> teamRoster() { } // ,     `Stream<Agent>` public List<Agent> teamOnMission() { } public List<Agent> team() { } 

هذا ليس جيدًا ، لأنه لكي تكون المجموعات الجديدة التي قدمناها للتو مفيدة ، نحن بحاجة إلى العمل معهم! (UF). ما ذكرنا أعلاه هو رمز التطبيق ، وبالتالي ، فإن إعادة ImmutableList UnmodifiableList ImmutableList UnmodifiableList و ImmutableList ، ويمكنك تنفيذه ، كما هو موضح في القائمة أعلاه. يمكن أن يكون هذا جزءًا كبيرًا من العمل ، إلى جانب الارتباك عندما تحتاج إلى تنظيم تفاعل الشفرة القديمة والمحدثة ، ولكن يبدو على الأقل أنه ممكن.

ماذا عن الأطر ، المكتبات ، و JDK نفسها؟ كل شيء هنا يبدو قاتما. ستؤدي محاولة تغيير المعلمة أو نوع الإرجاع من List إلى List ImmutableList إلى عدم التوافق مع الكود المصدري ، أي: لن يتم تجميع شفرة المصدر الحالية مع الإصدار الجديد ، لأن هذه الأنواع لا ترتبط ببعضها البعض. وبالمثل ، فإن تغيير نوع الإرجاع من List إلى نوع جديد UnmodifiableList سيؤدي إلى أخطاء في الترجمة.

مع إدخال أنواع جديدة ، سيكون من الضروري إجراء تغييرات وإعادة ترجمة في جميع أنحاء النظام البيئي.

ومع ذلك ، حتى إذا قمنا بتوسيع نوع المعلمة من List إلى List UnmodifiableList ، فسوف نواجه مشكلة ، لأن هذا التغيير يتسبب في عدم التوافق على مستوى الرمز البريدي . عندما يستدعي الكود المصدري إحدى الطرق ، يقوم المترجم بتحويل هذه المكالمة إلى رمز ثنائي يشير إلى الطريقة المستهدفة من خلال:

  • اسم الفئة التي تم إعلان هدفها
  • اسم الطريقة
  • أنواع المعلمات الأسلوب
  • طريقة عودة النوع

أي تغيير في المعلمة أو نوع الإرجاع للأسلوب سوف يتسبب في أن يشير الرمز الفرعي إلى التوقيع الخاطئ عند الإشارة إلى الطريقة ؛ نتيجة لذلك ، NoSuchMethodError خطأ NoSuchMethodError أثناء التنفيذ. إذا كان التغيير الذي تجريه متوافقًا مع الكود المصدري - على سبيل المثال ، إذا كنت تتحدث عن تضييق نوع الإرجاع أو توسيع نوع المعلمة - فيجب أن تكون إعادة التجميع كافية. ومع ذلك ، مع التغييرات البعيدة المدى ، على سبيل المثال ، مع إدخال مجموعات جديدة ، كل شيء ليس بهذه البساطة: من أجل دمج هذه التغييرات ، تحتاج إلى إعادة ترجمة نظام جافا البيئي بالكامل. هذا هو العمل المفقود.

الطريقة الوحيدة التي يمكن تصورها للاستفادة من هذه المجموعات الجديدة دون كسر التوافق هي تكرار كل طريقة موجودة باسم جديد ، وتغيير واجهة برمجة التطبيقات ، ثم تحديد الإصدار القديم على أنه غير مرغوب فيه. هل يمكنك أن تتخيل مدى أهمية هذه المهمة الضخمة واللا نهائية تقريبًا؟!

انعكاس


بالطبع ، أنواع المجموعات غير القابلة للتغيير هي شيء رائع أود الحصول عليه ، لكن من غير المرجح أن نرى شيئًا كهذا في JDK. لا ImmutableList المختصة ImmutableList و ImmutableList توسيع بعضها البعض (في الواقع ، كلاهما UnmodifiableList نوع القائمة نفسه UnmodifiableList ، وهو للقراءة فقط) ، مما يجعل من الصعب دمج هذه الأنواع في واجهات برمجة التطبيقات الموجودة.

خارج سياق أي علاقات محددة بين الأنواع ، يكون تغيير تواقيع الطريقة الحالية محفوفًا بالمشاكل دائمًا ، حيث إن هذه التغييرات لا تتوافق مع الرمز الفرعي. عند تقديمها ، يلزم إعادة التجميع على الأقل ، وهذا تغيير مدمر سيؤثر على نظام جافا البيئي بأكمله.

لذلك ، أعتقد أنه لن يحدث شيء مثل هذا - أبدًا ولن يحدث أبدًا.

Source: https://habr.com/ru/post/ar470149/


All Articles