JavaRush /مدونة جافا /Random-AR /لكل حلقة ولكل حلقة: قصة حول كيفية التكرار، تم التكرار، ول...
Viacheslav
مستوى

لكل حلقة ولكل حلقة: قصة حول كيفية التكرار، تم التكرار، ولكن لم يتم التكرار

نشرت في المجموعة

مقدمة

الحلقات هي إحدى الهياكل الأساسية للغات البرمجة. على سبيل المثال، يوجد على موقع أوراكل قسم " الدرس: أساسيات اللغة "، حيث تحتوي الحلقات على درس منفصل " البيان ". لنقم بتحديث الأساسيات: تتكون الحلقة من ثلاثة تعبيرات (عبارات): التهيئة (التهيئة)، والحالة (الإنهاء)، والزيادة (الزيادة):
لكل حلقة ولكل حلقة: قصة حول كيفية التكرار والتكرار ولكن لم أكرر - 1
ومن المثير للاهتمام أنها كلها اختيارية، مما يعني أنه يمكننا، إذا أردنا، أن نكتب:
for (;;){
}
صحيح، في هذه الحالة سوف نحصل على حلقة لا نهاية لها، لأنه لا نحدد شرطًا للخروج من الحلقة (الإنهاء). يتم تنفيذ تعبير التهيئة مرة واحدة فقط، قبل تنفيذ الحلقة بأكملها. من الجدير دائمًا أن نتذكر أن الدورة لها نطاقها الخاص. وهذا يعني أن التهيئة والإنهاء والزيادة وجسم الحلقة ترى نفس المتغيرات . من السهل دائمًا تحديد النطاق باستخدام الأقواس المتعرجة. كل ما داخل الأقواس لا يظهر خارج الأقواس، لكن كل ما خارج الأقواس يظهر داخل الأقواس. التهيئة هي مجرد تعبير. على سبيل المثال، بدلاً من تهيئة متغير، يمكنك بشكل عام استدعاء أسلوب لن يُرجع أي شيء. أو قم بتخطيها فقط، مع ترك مسافة فارغة قبل الفاصلة المنقوطة الأولى. يحدد التعبير التالي شرط الإنهاء . طالما أن هذا صحيح ، يتم تنفيذ الحلقة. وإذا كان خطأ ، فلن يبدأ تكرار جديد. إذا نظرت إلى الصورة أدناه، فسنحصل على خطأ أثناء التجميع وسيشكو IDE: التعبير الخاص بنا في الحلقة غير قابل للوصول. وبما أنه لن يكون لدينا تكرار واحد في الحلقة، فسوف نخرج على الفور، لأن خطأ شنيع:
لكل حلقة ولكل حلقة: قصة كيف قمت بالتكرار والتكرار ولكن لم أكرر - 2
من المفيد مراقبة التعبير الموجود في بيان الإنهاء : فهو يحدد بشكل مباشر ما إذا كان تطبيقك سيحتوي على حلقات لا نهاية لها. الزيادة هي أبسط تعبير. يتم تنفيذه بعد كل تكرار ناجح للحلقة. ويمكن أيضًا تخطي هذا التعبير. على سبيل المثال:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
كما ترون من المثال، في كل تكرار للحلقة سنقوم بزيادة 2، ولكن فقط طالما أن القيمة outerVarأقل من 10. بالإضافة إلى ذلك، نظرًا لأن التعبير في عبارة الزيادة هو في الواقع مجرد تعبير، فإنه يمكن أن تحتوي على أي شيء. ولذلك لا يمنع أحد من استعمال النقصان بدلا من الزيادة، أي: تقليل القيمة. يجب عليك دائمًا مراقبة كتابة الزيادة. +=يؤدي الزيادة أولاً ثم التعيين، ولكن إذا كتبنا العكس في المثال أعلاه، فسنحصل على حلقة لا نهائية، لأن المتغير outerVarلن يتلقى القيمة المتغيرة أبدًا: في هذه الحالة سيتم =+حسابها بعد التعيين. بالمناسبة، الأمر نفسه بالنسبة لزيادات العرض ++. على سبيل المثال، كان لدينا حلقة:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
الدورة عملت ولم تكن هناك مشاكل. ولكن بعد ذلك جاء رجل إعادة الهيكلة. لم يفهم الزيادة وقام بما يلي:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
إذا ظهرت علامة الزيادة أمام القيمة، فهذا يعني أنها ستزيد أولاً ثم تعود إلى المكان الذي أشارت إليه. في هذا المثال، سنبدأ فورًا في استخراج العنصر الموجود في الفهرس 1 من المصفوفة، متخطيين العنصر الأول. ثم في الفهرس 3 سوف نتعطل بسبب الخطأ " java.lang.ArrayIndexOutOfBoundsException ". كما كنت قد خمنت، فقد نجح هذا من قبل ببساطة لأنه يتم استدعاء الزيادة بعد اكتمال التكرار. عند نقل هذا التعبير إلى التكرار، تعطل كل شيء. كما اتضح، حتى في حلقة بسيطة يمكنك إحداث فوضى) إذا كان لديك مصفوفة، فربما تكون هناك طريقة أسهل لعرض جميع العناصر؟
لكل حلقة ولكل حلقة: حكاية كيف قمت بالتكرار والتكرار ولكن لم أكرر - 3

لكل حلقة

بدءًا من Java 1.5، قدم لنا مطورو Java تصميمًا for each loopموصوفًا على موقع Oracle في الدليل يسمى " The For-Each Loop " أو للإصدار 1.5.0 . بشكل عام، سوف يبدو مثل هذا:
لكل حلقة ولكل حلقة: حكاية كيف قمت بالتكرار والتكرار ولكن لم أكرر - 4
يمكنك قراءة وصف هذا البناء في مواصفات لغة Java (JLS) للتأكد من أنه ليس سحريًا. وقد تم وصف هذا البناء في الفصل " 14.14.2. المعزز للبيان ". كما ترون، يمكن استخدام for every حلقة مع المصفوفات وتلك التي تنفذ الواجهة java.lang.Iterable . وهذا يعني، إذا كنت تريد حقًا، يمكنك تنفيذ واجهة java.lang.Iterable ويمكن استخدام كل حلقة مع فصلك. ستقول على الفور، "حسنًا، إنه كائن قابل للتكرار، لكن المصفوفة ليست كائنًا. نوعاً ما." وستكون مخطئا، لأن... في Java، المصفوفات هي كائنات تم إنشاؤها ديناميكيًا. تخبرنا مواصفات اللغة بما يلي: " المصفوفات في لغة برمجة Java هي كائنات ". بشكل عام، المصفوفات هي جزء من سحر JVM، لأن... إن كيفية تنظيم المصفوفة داخليًا غير معروفة وتقع في مكان ما داخل Java Virtual Machine. يمكن لأي شخص مهتم قراءة الإجابات على stackoverflow: " كيف تعمل فئة المصفوفة في Java؟ " اتضح أنه إذا لم نستخدم مصفوفة، فيجب علينا استخدام شيء ينفذ Iterable . على سبيل المثال:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
هنا يمكنك فقط أن تتذكر أننا إذا استخدمنا المجموعات ( java.util.Collection ) ، فبفضل هذا نحصل على Iterable تمامًا . إذا كان الكائن يحتوي على فئة تنفذ Iterable، فإنه ملزم بتوفير مكرر، عند استدعاء أسلوب التكرار، والذي سوف يتكرر على محتويات ذلك الكائن. الكود أعلاه، على سبيل المثال، سيحتوي على رمز بايت مثل هذا (في IntelliJ Idea يمكنك القيام بـ "عرض" -> "إظهار الرمز الثانوي":
لكل حلقة ولكل حلقة: حكاية كيف قمت بالتكرار والتكرار ولكن لم أكرر - 5
كما ترون، يتم استخدام المكرر بالفعل. إذا لم يكن الأمر كذلك بالنسبة لكل حلقة ، لكان علينا أن نكتب شيئًا مثل:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (Iterator i = names.iterator(); /* continue if */ i.hasNext(); /* skip increment */) {
	String name = (String) i.next();
	System.out.println("Name = " + name);
}

مكرر

كما رأينا أعلاه، تقول الواجهة القابلة للتكرار أنه بالنسبة لمثيلات بعض الكائنات، يمكنك الحصول على مُكرِّر يمكنك من خلاله التكرار على المحتويات. مرة أخرى، يمكن القول أن هذا هو مبدأ المسؤولية الفردية من SOLID . لا ينبغي لبنية البيانات نفسها أن تقود عملية الاجتياز، ولكنها يمكن أن توفر البنية التي ينبغي لها ذلك. التنفيذ الأساسي لـ Iterator هو أنه يتم الإعلان عنه عادةً كفئة داخلية يمكنها الوصول إلى محتويات الفئة الخارجية وتوفر العنصر المطلوب الموجود في الفئة الخارجية. فيما يلي مثال من الفصل ArrayListلكيفية إرجاع المكرر لعنصر:
public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
}
كما نرى، بمساعدة ArrayList.thisالمكرر، يتم الوصول إلى الطبقة الخارجية ومتغيرها elementData، ثم إرجاع عنصر من هناك. لذا فإن الحصول على مكرر أمر بسيط للغاية:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
يعود عملها إلى حقيقة أنه يمكننا التحقق مما إذا كانت هناك عناصر أخرى ( طريقة hasNext )، والحصول على العنصر التالي ( الطريقة التالية )، وطريقة الإزالة ، التي تزيل العنصر الأخير الذي تم استلامه من خلال next . طريقة الإزالة اختيارية وغير مضمونة التنفيذ. في الواقع، مع تطور Java، تتطور الواجهات أيضًا. لذلك، في Java 8، كانت هناك أيضًا طريقة forEachRemainingتسمح لك بتنفيذ بعض الإجراءات على العناصر المتبقية التي لم تتم زيارتها بواسطة المكرر. ما هو المثير للاهتمام حول المكرر والمجموعات؟ على سبيل المثال، هناك فئة AbstractList. هذه فئة مجردة هي أصل ArrayListو LinkedList. ومن المثير للاهتمام بالنسبة لنا وجود مجال مثل modCount . كل تغيير محتويات القائمة يتغير. إذن ما الذي يهمنا؟ وحقيقة أن المكرر يتأكد من عدم تغيير المجموعة التي يتم التكرار عليها أثناء التشغيل. كما تفهم، يقع تنفيذ مكرر القوائم في نفس المكان الذي يوجد فيه modcount ، أي في الفصل الدراسي AbstractList. دعونا نلقي نظرة على مثال بسيط:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
names.add("modcount++");
System.out.println(iterator.next());
هذا هو أول شيء مثير للاهتمام، وإن لم يكن في الموضوع. في الواقع Arrays.asListتقوم بإرجاع واحدة خاصة بها ArrayList( java.util.Arrays.ArrayList ). لا ينفذ طرق الإضافة، لذلك فهو غير قابل للتعديل. لقد تم كتابته في JavaDoc: حجم ثابت . ولكن في الواقع، هو أكثر من مجرد حجم ثابت . كما أنه غير قابل للتغيير ، أي غير قابل للتغيير؛ الإزالة لن تعمل عليها أيضًا. سنحصل أيضًا على خطأ، لأن... بعد إنشاء المكرّر، تذكرنا modcount فيه . ثم قمنا بتغيير حالة المجموعة "خارجيًا" (أي ليس من خلال المكرر) وقمنا بتنفيذ طريقة المكرر. ولذلك، حصلنا على الخطأ: java.util.ConcurrentModificationException . لتجنب ذلك، يجب إجراء التغيير أثناء التكرار من خلال المكرر نفسه، وليس من خلال الوصول إلى المجموعة:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.remove();
System.out.println(iterator.next());
كما تفهم، إذا iterator.remove()لم تفعل ذلك من قبل iterator.next()، فذلك لأنه. إذا لم يشير المكرِّر إلى أي عنصر، فسنحصل على خطأ. في المثال، سينتقل المكرّر إلى عنصر John ، وسيزيله، ثم يحصل على عنصر Sara . وهنا سيكون كل شيء على ما يرام، ولكن الحظ السيئ، مرة أخرى هناك "فروق دقيقة") java.util.ConcurrentModificationException لن يحدث إلا عندما hasNext()يعود صحيحًا . أي أنه إذا قمت بحذف العنصر الأخير من خلال المجموعة نفسها، فلن يسقط المكرر. لمزيد من التفاصيل، من الأفضل مشاهدة التقرير الخاص بألغاز Java من " #ITsubbotnik section JAVA: Java puzzles ". لقد بدأنا مثل هذه المحادثة التفصيلية لسبب بسيط وهو أن نفس الفروق الدقيقة تنطبق تمامًا عندما for each loop... يتم استخدام المكرر المفضل لدينا تحت الغطاء. وكل هذه الفروق الدقيقة تنطبق هناك أيضًا. الشيء الوحيد هو أننا لن نتمكن من الوصول إلى المُكرِّر، ولن نتمكن من إزالة العنصر بأمان. بالمناسبة، كما تفهم، يتم تذكر الحالة في لحظة إنشاء المكرر. والحذف الآمن يعمل فقط حيث يتم استدعاؤه. أي أن هذا الخيار لن يعمل:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
لأنه بالنسبة إلى iterator2، كان الحذف من خلال iterator1 "خارجيًا"، أي أنه تم إجراؤه في مكان ما بالخارج ولا يعرف شيئًا عنه. فيما يتعلق بموضوع التكرارات، أود أيضًا أن أشير إلى ذلك. تم إنشاء مكرر خاص وموسع خصيصًا لتطبيقات الواجهة List. وسموه ListIterator. فهو يسمح لك بالتحرك ليس للأمام فحسب، بل للخلف أيضًا، كما يسمح لك بمعرفة فهرس العنصر السابق والعنصر التالي. بالإضافة إلى ذلك، فهو يسمح لك باستبدال العنصر الحالي أو إدراج عنصر جديد في موضع بين موضع المكرر الحالي والموضع التالي. كما خمنت، ListIteratorيُسمح بالقيام بذلك نظرًا لأنه Listتم تنفيذ الوصول عن طريق الفهرس.
لكل حلقة ولكل حلقة: حكاية كيف قمت بالتكرار والتكرار ولكن لم أكرر - 6

جافا 8 والتكرار

لقد جعل إصدار Java 8 الحياة أسهل بالنسبة للكثيرين. كما أننا لم نتجاهل التكرار على محتويات الكائنات. لفهم كيفية عمل هذا، عليك أن تقول بضع كلمات حول هذا الموضوع. قدم Java 8 فئة java.util.function.Consumer . هنا مثال:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
المستهلك عبارة عن واجهة وظيفية، مما يعني أنه يوجد داخل الواجهة طريقة مجردة واحدة غير منفذة تتطلب تنفيذًا إلزاميًا في تلك الفئات التي تحدد أدوات هذه الواجهة. هذا يسمح لك باستخدام شيء سحري مثل لامدا. هذه المقالة لا تتحدث عن ذلك، ولكن علينا أن نفهم لماذا يمكننا استخدامها. لذا، باستخدام lambdas، يمكن إعادة كتابة المستهلك أعلاه على النحو التالي: Consumer consumer = (obj) -> System.out.println(obj); هذا يعني أن Java ترى أنه سيتم تمرير شيء يسمى obj إلى الإدخال، ثم سيتم تنفيذ التعبير بعد -> لهذا الكائن. أما بالنسبة للتكرار، يمكننا الآن القيام بذلك:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
إذا ذهبت إلى الطريقة forEach، سترى أن كل شيء بسيط للغاية. هناك واحدة المفضلة لدينا for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
من الممكن أيضًا إزالة عنصر بشكل جميل باستخدام المكرر، على سبيل المثال:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
في هذه الحالة، لا تأخذ طريقة RemoveIf مُدخلًا ، بل مُسندًا . تقوم بإرجاع منطقية . في هذه الحالة، إذا كان المسند يقول " صحيح "، فسيتم حذف العنصر. من المثير للاهتمام أنه ليس كل شيء واضحًا هنا أيضًا)) حسنًا، ماذا تريد؟ يجب منح الأشخاص مساحة لإنشاء الألغاز في المؤتمر. على سبيل المثال، لنأخذ الكود التالي لحذف كل ما يمكن للمكرر الوصول إليه بعد بعض التكرار:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
while (iterator.hasNext()) {
    iterator.next(); // Следующий элемент
    iterator.remove(); // Удалor его
}
System.out.println(names);
حسنًا، كل شيء يعمل هنا. لكننا نتذكر أن Java 8 بعد كل شيء. لذلك، دعونا نحاول تبسيط الكود:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
iterator.forEachRemaining(obj -> iterator.remove());
System.out.println(names);
هل أصبحت حقا أكثر جمالا؟ ومع ذلك، سيكون هناك java.lang.IllegalStateException . والسبب هو... وجود خطأ في جافا. اتضح أنه تم إصلاحه، ولكن في JDK 9. فيما يلي رابط للمهمة في OpenJDK: Iterator.forEachRemaining vs. مكرر.إزالة . وبطبيعة الحال، تمت مناقشة هذا بالفعل: لماذا لا يقوم iterator.forEachRemaining بإزالة العنصر في Consumer lambda؟ حسنًا، هناك طريقة أخرى مباشرة من خلال Stream API:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

الاستنتاجات

كما رأينا من جميع المواد المذكورة أعلاه، فإن الحلقة for-each loopهي مجرد "سكر نحوي" أعلى المكرِّر. ومع ذلك، يتم استخدامه الآن في العديد من الأماكن. بالإضافة إلى ذلك، يجب استخدام أي منتج بحذر. forEachRemainingعلى سبيل المثال، قد يخفي شخص غير ضار مفاجآت غير سارة. وهذا يثبت مرة أخرى أن هناك حاجة إلى اختبارات الوحدة. يمكن للاختبار الجيد تحديد حالة الاستخدام هذه في التعليمات البرمجية الخاصة بك. ما يمكنك مشاهدته/قراءته حول الموضوع: # فياتشيسلاف
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION