JavaRush /Blog Java /Random-VI /Vòng lặp For và For-Each: câu chuyện về cách tôi lặp lại,...
Viacheslav
Mức độ

Vòng lặp For và For-Each: câu chuyện về cách tôi lặp lại, được lặp lại nhưng không được lặp lại

Xuất bản trong nhóm

Giới thiệu

Vòng lặp là một trong những cấu trúc cơ bản của ngôn ngữ lập trình. Ví dụ: trên trang web của Oracle có một phần “ Bài học: Khái niệm cơ bản về ngôn ngữ ”, trong đó các vòng lặp có một bài học riêng “ The for Statement ”. Hãy làm mới lại những điều cơ bản: Vòng lặp bao gồm ba biểu thức (câu lệnh): khởi tạo (khởi tạo), điều kiện (kết thúc) và tăng dần (tăng):
Vòng lặp For và For-Each: câu chuyện về việc tôi đã lặp đi lặp lại, lặp đi lặp lại nhưng không lặp lại - 1
Điều thú vị là chúng đều là tùy chọn, nghĩa là chúng ta có thể, nếu muốn, viết:
for (;;){
}
Đúng, trong trường hợp này chúng ta sẽ có một vòng lặp vô tận, bởi vì Chúng tôi không chỉ định điều kiện để thoát khỏi vòng lặp (chấm dứt). Biểu thức khởi tạo chỉ được thực hiện một lần trước khi toàn bộ vòng lặp được thực thi. Điều đáng ghi nhớ là một chu kỳ có phạm vi riêng của nó. Điều này có nghĩa là việc khởi tạo , kết thúc , tăng dần và nội dung vòng lặp có cùng các biến. Phạm vi luôn dễ dàng được xác định bằng cách sử dụng dấu ngoặc nhọn. Mọi thứ bên trong dấu ngoặc không hiển thị bên ngoài dấu ngoặc, nhưng mọi thứ bên ngoài dấu ngoặc đều hiển thị bên trong dấu ngoặc. Khởi tạo chỉ là một biểu thức. Ví dụ: thay vì khởi tạo một biến, bạn thường có thể gọi một phương thức sẽ không trả về bất cứ thứ gì. Hoặc chỉ cần bỏ qua, để lại một khoảng trống trước dấu chấm phẩy đầu tiên. Biểu thức sau đây xác định điều kiện kết thúc . Chỉ cần nó đúng thì vòng lặp sẽ được thực thi. Và nếu sai , lần lặp mới sẽ không bắt đầu. Nếu bạn nhìn vào hình bên dưới, chúng ta sẽ gặp lỗi trong quá trình biên dịch và IDE sẽ phàn nàn: biểu thức của chúng ta trong vòng lặp không thể truy cập được. Vì chúng ta sẽ không có một lần lặp nào trong vòng lặp nên chúng ta sẽ thoát ngay lập tức, bởi vì SAI:
Vòng lặp For và For-Each: câu chuyện về cách tôi lặp đi lặp lại, lặp lại nhưng không lặp lại - 2
Bạn nên để ý đến biểu thức trong câu lệnh kết thúc : nó trực tiếp xác định liệu ứng dụng của bạn có vòng lặp vô tận hay không. Tăng là biểu thức đơn giản nhất. Nó được thực thi sau mỗi lần lặp thành công của vòng lặp. Và biểu thức này cũng có thể được bỏ qua. Ví dụ:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Như bạn có thể thấy trong ví dụ, mỗi lần lặp của vòng lặp, chúng ta sẽ tăng theo gia số là 2, nhưng chỉ miễn là giá trị outerVarnhỏ hơn 10. Ngoài ra, vì biểu thức trong câu lệnh tăng thực ra chỉ là một biểu thức, nên nó có thể chứa bất cứ thứ gì Do đó, không ai cấm sử dụng số giảm thay vì số tăng, tức là giảm giá trị. Bạn phải luôn theo dõi việc ghi số tăng. +=thực hiện phép tăng trước rồi đến phép gán, nhưng nếu trong ví dụ trên chúng ta viết ngược lại, chúng ta sẽ nhận được một vòng lặp vô hạn, vì biến outerVarsẽ không bao giờ nhận giá trị đã thay đổi: trong trường hợp này nó =+sẽ được tính sau phép gán. Nhân tiện, việc tăng lượt xem cũng tương tự như vậy ++. Ví dụ: chúng tôi có một vòng lặp:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
Chu trình đã hoạt động và không có vấn đề gì. Nhưng rồi người tái cấu trúc đã đến. Anh ta không hiểu sự gia tăng và chỉ làm điều này:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Nếu dấu tăng xuất hiện phía trước giá trị, điều này có nghĩa là trước tiên nó sẽ tăng lên và sau đó quay trở lại vị trí được chỉ định. Trong ví dụ này, chúng ta sẽ ngay lập tức bắt đầu trích xuất phần tử ở chỉ mục 1 từ mảng, bỏ qua phần tử đầu tiên. Và khi đó ở chỉ số 3 chúng ta sẽ gặp lỗi " java.lang.ArrayIndexOutOfBoundsException ". Như bạn có thể đoán, điều này trước đây có hiệu quả đơn giản vì số gia tăng được gọi sau khi quá trình lặp hoàn tất. Khi chuyển biểu thức này sang phép lặp, mọi thứ đã bị hỏng. Hóa ra, ngay cả trong một vòng lặp đơn giản, bạn cũng có thể tạo ra một mớ hỗn độn) Nếu bạn có một mảng, có thể có cách nào đó dễ dàng hơn để hiển thị tất cả các phần tử?
Vòng lặp For và For-Each: câu chuyện về cách tôi lặp đi lặp lại, lặp đi lặp lại nhưng không lặp lại - 3

Đối với mỗi vòng lặp

Bắt đầu với Java 1.5, các nhà phát triển Java đã cung cấp cho chúng tôi một thiết kế for each loopđược mô tả trên trang Oracle trong Hướng dẫn có tên " Vòng lặp For-Each " hoặc dành cho phiên bản 1.5.0 . Nói chung, nó sẽ trông như thế này:
Vòng lặp For và For-Each: câu chuyện về cách tôi lặp đi lặp lại, lặp đi lặp lại nhưng không lặp lại - 4
Bạn có thể đọc mô tả về cấu trúc này trong Đặc tả ngôn ngữ Java (JLS) để đảm bảo rằng nó không phải là phép thuật. Cấu trúc này được mô tả trong chương " 14.14.2. Câu lệnh nâng cao ". Như bạn có thể thấy, vòng lặp for each có thể được sử dụng với các mảng và các mảng triển khai giao diện java.lang.Iterable . Nghĩa là, nếu bạn thực sự muốn, bạn có thể triển khai giao diện java.lang.Iterableđối với mỗi vòng lặp có thể được sử dụng với lớp của bạn. Bạn sẽ ngay lập tức nói: "Được rồi, đó là một đối tượng có thể lặp lại, nhưng một mảng không phải là một đối tượng. Đại loại vậy." Và bạn sẽ sai, bởi vì... Trong Java, mảng là các đối tượng được tạo động. Đặc tả ngôn ngữ cho chúng ta biết điều này: “ Trong ngôn ngữ lập trình Java, mảng là đối tượng .” Nói chung, mảng có một chút ma thuật JVM, bởi vì... mảng được cấu trúc bên trong như thế nào vẫn chưa được biết và nằm ở đâu đó bên trong Máy ảo Java. Bất cứ ai quan tâm có thể đọc câu trả lời trên stackoverflow: " Lớp mảng hoạt động như thế nào trong Java? " Hóa ra là nếu chúng ta không sử dụng mảng thì chúng ta phải sử dụng thứ gì đó triển khai Iterable . Ví dụ:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Ở đây bạn chỉ cần nhớ rằng nếu chúng ta sử dụng các bộ sưu tập ( java.util.Collection ), nhờ điều này chúng ta sẽ nhận được chính xác Iterable . Nếu một đối tượng có một lớp triển khai Iterable, thì khi phương thức iterator được gọi, nó có nghĩa vụ cung cấp một Iterator sẽ lặp lại nội dung của đối tượng đó. Ví dụ, đoạn mã trên sẽ có mã byte giống như thế này (trong IntelliJ Idea bạn có thể thực hiện "Xem" -> "Hiển thị mã byte":
Vòng lặp For và For-Each: câu chuyện về việc tôi đã lặp đi lặp lại, lặp đi lặp lại nhưng không lặp lại - 5
Như bạn có thể thấy, một trình vòng lặp thực sự được sử dụng. Nếu không có vòng lặp for each , chúng ta sẽ phải viết đại loại như:
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);
}

Trình vòng lặp

Như chúng ta đã thấy ở trên, giao diện Iterable cho biết rằng đối với các phiên bản của một số đối tượng, bạn có thể nhận được một trình lặp mà bạn có thể lặp lại nội dung. Một lần nữa, đây có thể nói là Nguyên tắc Trách nhiệm duy nhất từ ​​SOLID . Bản thân cấu trúc dữ liệu không nên điều khiển việc truyền tải, nhưng nó có thể cung cấp một thứ cần thiết. Cách triển khai cơ bản của Iterator là nó thường được khai báo là lớp bên trong có quyền truy cập vào nội dung của lớp bên ngoài và cung cấp phần tử mong muốn có trong lớp bên ngoài. Đây là một ví dụ từ lớp ArrayListvề cách một trình vòng lặp trả về một phần tử:
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];
}
Như chúng ta có thể thấy, với sự trợ giúp của ArrayList.thismột iterator, truy cập vào lớp bên ngoài và biến của nó elementData, sau đó trả về một phần tử từ đó. Vì vậy, việc có được một iterator rất đơn giản:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Công việc của nó phụ thuộc vào việc chúng ta có thể kiểm tra xem còn phần tử nào nữa không ( phương thức hasNext ), lấy phần tử tiếp theo ( phương thức next ) và phương thức Remove , loại bỏ phần tử cuối cùng nhận được thông qua next . Phương thức loại bỏ là tùy chọn và không đảm bảo sẽ được triển khai. Trên thực tế, khi Java phát triển, các giao diện cũng phát triển. Do đó, trong Java 8, một phương thức cũng xuất hiện forEachRemainingcho phép bạn thực hiện một số hành động trên các phần tử còn lại mà trình vòng lặp không truy cập. Có gì thú vị về một trình vòng lặp và các bộ sưu tập? Ví dụ: có một lớp AbstractList. Đây là một lớp trừu tượng, là lớp cha của ArrayListLinkedList. Và nó rất thú vị đối với chúng tôi vì một lĩnh vực như modCount . Mỗi thay đổi nội dung của danh sách sẽ thay đổi. Vậy điều đó có quan trọng gì với chúng ta? Và thực tế là trình vòng lặp đảm bảo rằng trong quá trình hoạt động, bộ sưu tập mà nó được lặp lại không thay đổi. Như bạn đã hiểu, việc triển khai trình lặp cho danh sách được đặt ở cùng vị trí với modcount , nghĩa là trong lớp AbstractList. Hãy xem một ví dụ đơn giản:
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());
Đây là điều thú vị đầu tiên, mặc dù không thuộc chủ đề. Trên thực tế Arrays.asListtrả về cái đặc biệt của riêng nó ArrayList( java.util.Arrays.ArrayList ). Nó không thực hiện thêm các phương thức, vì vậy nó không thể sửa đổi được. Nó được viết trong JavaDoc: fix-size . Nhưng trên thực tế, nó còn hơn cả kích thước cố định . Nó cũng bất biến , tức là không thể thay đổi; loại bỏ cũng sẽ không hoạt động trên nó. Chúng tôi cũng sẽ gặp lỗi, bởi vì... Sau khi tạo iterator, chúng tôi nhớ modcount trong đó . Sau đó, chúng tôi đã thay đổi trạng thái của bộ sưu tập “từ bên ngoài” (tức là không thông qua trình vòng lặp) và thực thi phương thức trình vòng lặp. Do đó, chúng tôi gặp lỗi: java.util.ConcurrentModificationException . Để tránh điều này, thay đổi trong quá trình lặp phải được thực hiện thông qua chính trình vòng lặp chứ không phải thông qua quyền truy cập vào bộ sưu tập:
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());
Như bạn hiểu, nếu iterator.remove()bạn không làm điều đó trước đây iterator.next()thì bởi vì. iterator không trỏ tới phần tử nào thì chúng ta sẽ gặp lỗi. Trong ví dụ, trình vòng lặp sẽ đi đến phần tử John , loại bỏ nó và sau đó lấy phần tử Sara . Và ở đây mọi thứ sẽ ổn, nhưng xui xẻo, một lần nữa lại có “sắc thái”) java.util.ConcurrentModificationException sẽ chỉ xảy ra khi hasNext()nó trả về true . Nghĩa là, nếu bạn xóa phần tử cuối cùng thông qua chính bộ sưu tập, trình vòng lặp sẽ không rơi. Để biết thêm chi tiết, tốt hơn hết bạn nên xem báo cáo về các câu đố Java từ “ #ITsubbotnik Mục JAVA: Câu đố Java ”. Chúng tôi bắt đầu cuộc trò chuyện chi tiết như vậy vì lý do đơn giản là các sắc thái giống nhau sẽ được áp dụng khi for each loop... Trình vòng lặp yêu thích của chúng tôi được sử dụng dưới mui xe. Và tất cả những sắc thái này cũng được áp dụng ở đó. Điều duy nhất là chúng ta sẽ không có quyền truy cập vào trình lặp và chúng ta sẽ không thể xóa phần tử một cách an toàn. Nhân tiện, như bạn hiểu, trạng thái được ghi nhớ tại thời điểm trình vòng lặp được tạo. Và việc xóa an toàn chỉ hoạt động ở nơi nó được gọi. Nghĩa là, tùy chọn này sẽ không hoạt động:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Bởi vì đối với iterator2, việc xóa thông qua iterator1 là "bên ngoài", tức là nó được thực hiện ở đâu đó bên ngoài và anh ta không biết gì về nó. Về chủ đề iterator, tôi cũng muốn lưu ý điều này. Một trình lặp mở rộng, đặc biệt được tạo riêng cho việc triển khai giao diện List. Và họ đặt tên cho anh ấy ListIterator. Nó cho phép bạn không chỉ di chuyển về phía trước mà còn di chuyển lùi và cũng cho phép bạn tìm ra chỉ mục của phần tử trước và phần tử tiếp theo. Ngoài ra, nó cho phép bạn thay thế phần tử hiện tại hoặc chèn phần tử mới vào vị trí giữa vị trí lặp hiện tại và phần tử tiếp theo. Như bạn đã đoán, ListIteratornó được phép thực hiện điều này vì Listquyền truy cập theo chỉ mục được triển khai.
Vòng lặp For và For-Each: câu chuyện về cách tôi lặp đi lặp lại, lặp đi lặp lại nhưng không lặp lại - 6

Java 8 và lặp lại

Việc phát hành Java 8 đã giúp cuộc sống của nhiều người trở nên dễ dàng hơn. Chúng tôi cũng không bỏ qua việc lặp lại nội dung của các đối tượng. Để hiểu cách thức hoạt động của nó, bạn cần nói đôi lời về điều này. Java 8 đã giới thiệu lớp java.util.function.Consumer . Đây là một ví dụ:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Người tiêu dùng là một giao diện chức năng, có nghĩa là bên trong giao diện chỉ có 1 phương thức trừu tượng chưa được triển khai yêu cầu triển khai bắt buộc trong các lớp chỉ định các phần triển khai của giao diện này. Điều này cho phép bạn sử dụng một thứ kỳ diệu như lambda. Bài viết này không nói về điều đó nhưng chúng ta cần hiểu tại sao chúng ta có thể sử dụng nó. Vì vậy, bằng cách sử dụng lambdas, Consumer ở ​​trên có thể được viết lại như sau: Consumer consumer = (obj) -> System.out.println(obj); Điều này có nghĩa là Java thấy rằng một thứ gọi là obj sẽ được chuyển đến đầu vào và sau đó biểu thức sau -> sẽ được thực thi cho obj này. Đối với việc lặp lại, bây giờ chúng ta có thể làm điều này:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Nếu bạn đi đến phương pháp này forEach, bạn sẽ thấy mọi thứ thật đơn giản. Có một cái chúng tôi yêu thích for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
Cũng có thể loại bỏ một phần tử một cách đẹp mắt bằng cách sử dụng trình vòng lặp, ví dụ:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
Trong trường hợp này, phương thức RemoveIf lấy đầu vào không phải là Consumer mà là Predicate . Nó trả về boolean . Trong trường hợp này, nếu vị từ nói " true " thì phần tử sẽ bị xóa. Điều thú vị là không phải mọi thứ đều rõ ràng ở đây)) Chà, bạn muốn gì? Mọi người cần có không gian để tạo ra các câu đố tại hội nghị. Ví dụ: hãy lấy đoạn mã sau để xóa mọi thứ mà trình lặp có thể đạt được sau một số lần lặp:
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);
Được rồi, mọi thứ đều hoạt động ở đây. Nhưng rốt cuộc thì chúng tôi vẫn nhớ đến Java 8. Vì vậy, hãy cố gắng đơn giản hóa mã:
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);
Nó đã thực sự trở nên đẹp hơn chưa? Tuy nhiên, sẽ có java.lang.IllegalStateException . Và lý do là... một lỗi trong Java. Hóa ra là nó đã được sửa, nhưng trong JDK 9. Đây là liên kết đến tác vụ trong OpenJDK: Iterator.forEachRemaining vs. Iterator.remove . Đương nhiên, điều này đã được thảo luận: Tại sao iterator.forEachRemaining không loại bỏ phần tử trong lambda tiêu dùng? Chà, một cách khác là trực tiếp thông qua API Stream:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

kết luận

Như chúng ta đã thấy từ tất cả nội dung ở trên, vòng lặp for-each loopchỉ là “cú pháp đường” nằm trên một vòng lặp. Tuy nhiên, hiện nay nó được sử dụng ở nhiều nơi. Ngoài ra, bất kỳ sản phẩm nào cũng phải được sử dụng một cách thận trọng. Ví dụ, một người vô hại forEachRemainingcó thể che giấu những điều bất ngờ khó chịu. Và điều này một lần nữa chứng minh rằng việc kiểm thử đơn vị là cần thiết. Một thử nghiệm tốt có thể xác định trường hợp sử dụng như vậy trong mã của bạn. Những gì bạn có thể xem/đọc về chủ đề này: #Viacheslav
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION