JavaRush /Java Blog /Random-KO /For 및 For-Each 루프: 반복했지만 반복되었지만 반복되지 않은 방법에 대한 이야기
Viacheslav
레벨 3

For 및 For-Each 루프: 반복했지만 반복되었지만 반복되지 않은 방법에 대한 이야기

Random-KO 그룹에 게시되었습니다

소개

루프는 프로그래밍 언어의 기본 구조 중 하나입니다. 예를 들어, Oracle 웹사이트에는 루프에 별도의 " The for 문 " 단원이 있는 " 강의: 언어 기본 " 섹션이 있습니다 . 기본 사항을 다시 살펴보겠습니다. 루프는 초기화 (초기화), 조건 (종료) 및 증분 (증분) 의 세 가지 표현식(문)으로 구성됩니다 .
For 및 For-Each 루프: 반복하고 반복했지만 반복하지 않은 방법에 대한 이야기 ​​- 1
흥미롭게도 모두 선택 사항입니다. 즉, 원할 경우 다음과 같이 작성할 수 있습니다.
for (;;){
}
사실, 이 경우 무한 루프가 발생합니다. 루프 종료(종료) 조건을 지정하지 않습니다. 초기화 표현식은 전체 루프가 실행되기 전에 한 번만 실행됩니다. 주기에는 자체 범위가 있다는 점을 항상 기억할 가치가 있습니다. 이는 초기화 , 종료 , 증가 및 루프 본문이 동일한 변수를 참조함을 의미합니다. 중괄호를 사용하면 범위를 쉽게 결정할 수 있습니다. 괄호 안의 모든 것은 괄호 밖에서는 보이지 않지만, 괄호 밖의 모든 것은 괄호 안에 보입니다. 초기화 는 단지 표현식일 뿐입니다. 예를 들어, 변수를 초기화하는 대신 일반적으로 아무것도 반환하지 않는 메서드를 호출할 수 있습니다. 아니면 그냥 건너뛰고 첫 번째 세미콜론 앞에 공백을 남겨두세요. 다음 식은 종료 조건을 지정합니다 . true 이면 루프가 실행됩니다. false 인 경우 새 반복이 시작되지 않습니다. 아래 그림을 보면 컴파일 중에 오류가 발생하고 IDE가 다음과 같이 불평합니다. 루프의 표현식에 접근할 수 없습니다. 루프에서 단일 반복이 없으므로 즉시 종료합니다. 거짓:
For 및 For-Each 루프: 반복하고 반복했지만 반복하지 않은 방법에 대한 이야기 ​​- 2
종료문의 표현식 을 주의 깊게 관찰할 가치가 있습니다 . 이는 애플리케이션이 무한 루프를 가질지 여부를 직접적으로 결정합니다. 증가는 가장 간단한 표현입니다. 루프가 성공적으로 반복될 때마다 실행됩니다. 그리고 이 표현은 건너뛸 수도 있습니다. 예를 들어:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
예제에서 볼 수 있듯이 루프를 반복할 때마다 2씩 증가하지만 값이 outerVar10보다 작은 경우에만 증가합니다. 또한 increment 문의 표현식은 실제로 표현식일 뿐이므로 무엇이든 포함될 수 있습니다. 따라서 누구도 증분 대신 감소를 사용하는 것을 금지하지 않습니다. 가치를 줄입니다. 증분 기록을 항상 모니터링해야 합니다. +=먼저 증가를 수행한 다음 할당을 수행하지만 위의 예에서 반대를 작성하면 변수가 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 " 오류로 인해 충돌이 발생합니다 . 짐작할 수 있듯이 이전에는 반복이 완료된 후 증분이 호출되었기 때문에 이는 작동했습니다. 이 표현식을 반복으로 옮기면 모든 것이 깨졌습니다. 알고 보니 간단한 루프에서도 엉망이 될 수 있습니다.) 배열이 있다면 모든 요소를 ​​표시하는 더 간단한 방법이 있을까요?
For 및 For-Each 루프: 반복하고 반복했지만 반복하지 않은 방법에 대한 이야기 ​​- 3

각 루프마다

Java 1.5부터 Java 개발자는 " For-Each 루프 " 또는 버전 1.5.0for each loop 이라는 가이드의 Oracle 사이트에 설명된 디자인을 제공했습니다 . 일반적으로 다음과 같습니다.
For 및 For-Each 루프: 반복하고 반복했지만 반복하지 않은 방법에 대한 이야기 ​​- 4
JLS(Java 언어 사양)에서 이 구성에 대한 설명을 읽어서 이것이 마법이 아닌지 확인할 수 있습니다. 이 구성은 " 14.14.2. 향상된 for 문 " 장에 설명되어 있습니다 . 보시다시피 for Each 루프는 배열 및 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을 구현하는 클래스가 있는 경우 반복자 메서드가 호출될 때 해당 객체의 내용을 반복하는 Iterator를 제공해야 합니다. 예를 들어 위의 코드에는 다음과 같은 바이트코드가 있습니다(IntelliJ Idea에서는 "View" -> "Show bytecode"를 수행할 수 있습니다:
For 및 For-Each 루프: 반복하고 반복했지만 반복하지 않은 방법에 대한 이야기 ​​- 5
보시다시피 반복자가 실제로 사용됩니다. foreach 루프 가 아니었다면 다음과 같이 작성해야 합니다.
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 메서드 ), 다음 요소를 가져오고( next 메서드 ), next를 통해 수신된 마지막 요소를 제거하는 제거 메서드를 수행할 수 있다는 사실로 귀결됩니다 . 제거 메소드 는 선택사항이며 구현이 보장되지 않습니다. 실제로 Java가 발전함에 따라 인터페이스도 발전합니다. 따라서 Java 8에는 반복자가 방문하지 않은 나머지 요소에 대해 일부 작업을 수행할 수 있는 메서드도 있었습니다 . 반복자와 컬렉션의 흥미로운 점은 무엇입니까? 예를 들어 클래스가 있습니다 . 이는 및 의 부모인 추상 클래스입니다 . 그리고 modCount 와 같은 필드 때문에 우리에게 흥미롭습니다 . 각 변경 목록의 내용이 변경됩니다. 그렇다면 그것이 우리에게 무슨 상관이 있습니까? 그리고 반복자가 작업 중에 반복되는 컬렉션이 변경되지 않도록 한다는 사실입니다. 아시다시피 목록에 대한 반복자의 구현은 modcount 와 동일한 위치 , 즉 클래스에 있습니다 . 간단한 예를 살펴보겠습니다. forEachRemainingAbstractListArrayListLinkedListAbstractList
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: 고정 크기 에 대해 기록되어 있습니다 . 그러나 실제로는 고정 크기 이상입니다 . 또한 불변(immutable) , 즉 변경할 수 없습니다. 제거도 작동하지 않습니다. 또한 오류가 발생합니다. 왜냐하면... 반복자를 생성한 후 우리는 그 안에 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은 true를hasNext() 반환할 때만 발생합니다 . 즉, 컬렉션 자체를 통해 마지막 요소를 삭제하면 반복자가 빠지지 않습니다. 자세한 내용은 " #ITsubbotnik 섹션 JAVA: Java 퍼즐 " 에서 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을 통한 삭제가 "외부"였기 때문입니다. 즉, iterator2는 외부 어딘가에서 수행되었으며 이에 대해 아무것도 모릅니다. 반복자에 관해서도 이 점에 주목하고 싶습니다. 특별한 확장 반복자는 인터페이스 구현을 위해 특별히 만들어졌습니다 List. 그리고 그들은 그 사람의 이름을 지었습니다 ListIterator. 앞으로 뿐만 아니라 뒤로도 이동할 수 있으며, 이전 요소와 다음 요소의 인덱스도 확인할 수 있습니다. 또한 현재 요소를 바꾸거나 현재 반복자 위치와 다음 요소 사이의 위치에 새 요소를 삽입할 수 있습니다. 짐작하셨겠지만, 인덱스에 의한 접근이 구현되었기 ListIterator때문에 이런 일이 허용됩니다 List.
For 및 For-Each 루프: 반복하고 반복했지만 반복하지 않은 방법에 대한 이야기 ​​- 6

Java 8 및 반복

Java 8의 출시로 많은 사람들의 삶이 더 쉬워졌습니다. 우리는 또한 객체의 내용에 대한 반복을 무시하지 않았습니다. 이것이 어떻게 작동하는지 이해하려면 이에 대해 몇 마디 말해야 합니다. Java 8에는 java.util.function.Consumer 클래스가 도입되었습니다 . 예는 다음과 같습니다.
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
소비자 는 기능적 인터페이스입니다. 즉, 인터페이스 내부에는 이 인터페이스의 구현을 지정하는 클래스에서 필수 구현이 필요한 구현되지 않은 추상 메서드가 하나만 있습니다. 이를 통해 람다와 같은 마법 같은 것을 사용할 수 있습니다. 이 글은 그것에 관한 것이 아니지만 왜 우리가 그것을 사용할 수 있는지 이해해야 합니다. 따라서 람다를 사용하면 위의 Consumer를 다음과 같이 다시 작성할 수 있습니다. Consumer consumer = (obj) -> System.out.println(obj); 이는 Java가 obj라는 항목이 입력으로 전달된 다음 -> 뒤의 식이 이 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를 입력으로 사용합니다 . 부울을 반환합니다 . 이 경우 조건자가 " true "이면 해당 요소가 제거됩니다. 여기서도 모든 것이 명확하지 않다는 것이 흥미 롭습니다.)) 글쎄요, 무엇을 원하세요? 회의에서 사람들에게 퍼즐을 만들 수 있는 공간이 제공되어야 합니다. 예를 들어, 반복자가 일부 반복 후에 도달할 수 있는 모든 것을 삭제하기 위해 다음 코드를 사용하겠습니다.
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이 발생합니다 . 그 이유는... Java의 버그입니다. 이는 수정된 것으로 밝혀졌지만 JDK 9에서는 다음과 같습니다. OpenJDK의 작업에 대한 링크는 다음과 같습니다. Iterator.forEachRemaining 및. 반복자.제거 . 당연히 이것은 이미 논의되었습니다. iterator.forEachRemaining이 소비자 람다에서 요소를 제거하지 않는 이유는 무엇입니까? 또 다른 방법은 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