JavaRush /وبلاگ جاوا /Random-FA /برای و برای هر حلقه: داستانی از اینکه چگونه تکرار کردم، ت...
Viacheslav
مرحله

برای و برای هر حلقه: داستانی از اینکه چگونه تکرار کردم، تکرار شد، اما تکرار نشد

در گروه منتشر شد

معرفی

حلقه ها یکی از ساختارهای اساسی زبان های برنامه نویسی هستند. به عنوان مثال، در وب سایت Oracle بخشی وجود دارد " Lesson: Language Basics "، که در آن حلقه ها یک درس جداگانه دارند " The for Statement ". بیایید اصول اولیه را تازه کنیم: حلقه از سه عبارت (گزاره) تشکیل شده است: مقداردهی اولیه (آغازسازی)، شرط (پایان) و افزایش (افزایش):
برای و برای هر حلقه: داستانی در مورد اینکه چگونه تکرار کردم، تکرار کردم، اما تکرار نکردم - 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 از آرایه می کنیم و از عنصر اول صرفنظر می کنیم. و سپس در index 3 با خطای " java.lang.ArrayIndexOutOfBoundsException " خراب می شویم. همانطور که ممکن است حدس بزنید، این کار قبلا کار می کرد زیرا افزایش پس از اتمام تکرار فراخوانی می شود. هنگام انتقال این عبارت به تکرار، همه چیز خراب شد. همانطور که مشخص است، حتی در یک حلقه ساده می توانید به هم ریختگی ایجاد کنید) اگر یک آرایه دارید، شاید راه ساده تری برای نمایش همه عناصر وجود داشته باشد؟
برای و برای هر حلقه: داستانی از اینکه چگونه تکرار کردم، تکرار کردم، اما تکرار نکردم - 3

برای هر حلقه

با شروع با جاوا 1.5، توسعه دهندگان جاوا طرحی را به ما ارائه کردند for each loopکه در سایت اوراکل در راهنما توضیح داده شده است به نام " حلقه برای هر " یا برای نسخه 1.5.0 . به طور کلی به این صورت خواهد بود:
برای و برای هر حلقه: داستانی از اینکه چگونه تکرار کردم، تکرار کردم، اما تکرار نکردم - 4
می توانید توضیحات این ساختار را در مشخصات زبان جاوا (JLS) بخوانید تا مطمئن شوید که جادویی نیست. این ساختار در فصل " 14.14.2. بیانیه ارتقا یافته برای " توضیح داده شده است. همانطور که می بینید، برای هر حلقه می توان با آرایه ها و آرایه هایی که رابط java.lang.Iterable را پیاده سازی می کنند، استفاده کرد . یعنی اگر واقعاً بخواهید می توانید رابط java.lang.Iterable را پیاده سازی کنید و برای هر حلقه می توانید با کلاس خود استفاده کنید. فوراً خواهید گفت: "بسیار خوب، این یک شی تکرارپذیر است، اما آرایه یک شی نیست. به نوعی." و شما اشتباه خواهید کرد، زیرا ... در جاوا، آرایه ها اشیایی هستند که به صورت پویا ایجاد می شوند. مشخصات زبان این را به ما می گوید: " در زبان برنامه نویسی جاوا، آرایه ها اشیا هستند ." به طور کلی، آرایه ها کمی جادوی JVM هستند، زیرا ... نحوه ساختار داخلی آرایه ناشناخته است و جایی در داخل ماشین مجازی جاوا قرار دارد. هر کسی که علاقه مند است می تواند پاسخ های مربوط به stackoverflow را بخواند: " کلاس آرایه در جاوا چگونه کار می کند؟ " معلوم می شود که اگر از آرایه استفاده نمی کنیم، باید از چیزی استفاده کنیم که Iterable را پیاده سازی کند . مثلا:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
در اینجا شما فقط می توانید به یاد داشته باشید که اگر از مجموعه ها ( java.util.Collection ) استفاده کنیم ، به لطف این، دقیقا تکرارپذیر می شویم . اگر یک شی دارای کلاسی باشد که Iterable را پیاده سازی می کند، موظف است در هنگام فراخوانی متد iterator، یک Iterator ارائه کند که روی محتویات آن شی تکرار شود. به عنوان مثال، کد بالا دارای یک بایت کد چیزی شبیه به این خواهد بود (در IntelliJ Idea می توانید "View" -> "Show bytecode" را انجام دهید:
برای و برای هر حلقه: داستانی از اینکه چگونه تکرار کردم، تکرار کردم، اما تکرار نکردم - 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);
}

اشاره گر

همانطور که در بالا دیدیم، رابط Iterable می گوید که برای نمونه هایی از یک شی، می توانید یک تکرار کننده دریافت کنید که با آن می توانید روی محتواها تکرار کنید. باز هم می توان گفت که این اصل مسئولیت واحد از 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 )، عنصر بعدی ( روش بعدی ) و متد remove را دریافت کنیم که آخرین عنصر دریافت شده از طریق next را حذف می کند . روش حذف اختیاری است و اجرای آن تضمینی نیست. در واقع، با تکامل جاوا، رابط ها نیز تکامل می یابند. بنابراین، در جاوا 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 در مورد آن نوشته شده است: fixed-size . اما در واقع، بیش از اندازه ثابت است . همچنین تغییرناپذیر است ، یعنی تغییر ناپذیر. حذف هم روی آن کار نمی کند. همچنین با خطا مواجه خواهیم شد، زیرا ... پس از ایجاد تکرار کننده، 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 تنها زمانی رخ می دهد که truehasNext() را برگرداند . یعنی اگر آخرین عنصر را از طریق خود مجموعه حذف کنید، تکرار کننده سقوط نمی کند. برای جزئیات بیشتر، بهتر است گزارش مربوط به پازل های جاوا را از « #ITsubbotnik بخش JAVA: پازل های جاوا » تماشا کنید. ما یک گفتگوی مفصل را شروع کردیم به این دلیل ساده که دقیقاً همان تفاوت های ظریف زمانی که ... تکرار کننده مورد علاقه ما در زیر کاپوت استفاده می شود. و تمام این تفاوت های ظریف در آنجا نیز اعمال می شود. تنها چیزی که وجود دارد این است که ما به تکرارکننده دسترسی نخواهیم داشت و نمی‌توانیم با خیال راحت عنصر را حذف کنیم. به هر حال، همانطور که می دانید، حالت در لحظه ایجاد تکرار کننده به خاطر می ماند. و حذف ایمن فقط در جایی کار می کند که نامیده می شود. یعنی این گزینه کار نخواهد کرد: 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 و تکرار

انتشار جاوا 8 زندگی را برای بسیاری آسانتر کرده است. همچنین تکرار بر روی محتوای اشیاء را نادیده نگرفتیم. برای اینکه بفهمید این چگونه کار می کند، باید چند کلمه در این مورد بگویید. جاوا 8 کلاس java.util.function.Consumer را معرفی کرد . در اینجا یک مثال است:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Consumer یک رابط کاربردی است، به این معنی که در داخل اینترفیس تنها 1 روش انتزاعی اجرا نشده وجود دارد که نیاز به پیاده سازی اجباری در کلاس هایی دارد که پیاده سازی های این رابط را مشخص می کنند. این به شما امکان می دهد از چنین چیز جادویی مانند لامبدا استفاده کنید. این مقاله در مورد آن نیست، اما باید بدانیم که چرا می توانیم از آن استفاده کنیم. بنابراین با استفاده از lambdas، Consumer فوق را می توان به این صورت بازنویسی کرد: Consumer consumer = (obj) -> System.out.println(obj); یعنی جاوا می بیند که چیزی به نام obj به ورودی ارسال می شود و سپس عبارت after -> برای این 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 نه یک Consumer ، بلکه یک Predicate را به عنوان ورودی می گیرد . بولی را برمی گرداند . در این حالت، اگر گزاره « درست » بگوید، عنصر حذف خواهد شد. جالب است که اینجا هم همه چیز واضح نیست)) خوب، چه می خواهید؟ باید به افراد فضایی برای ایجاد پازل در کنفرانس داده شود. به عنوان مثال، بیایید کد زیر را برای حذف هر چیزی که تکرار کننده می تواند پس از چند بار تکرار به آن برسد، در نظر بگیریم:
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);
خوب، همه چیز اینجا کار می کند. اما ما به هر حال آن جاوا 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.remove . طبیعتاً، این قبلاً مورد بحث قرار گرفته است: چرا 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