들어가며
int[] arr = {1,2,3,4,5};
int sum = 0;
// for문
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
// 향상된 for문
for (int i : arr) {
sum += i;
}
향상된 for문은 JAVA 5에서 추가되었습니다. 이전 for문 보다 가독성이 좋아졌고, 더욱 간결해졌습니다.
이번 글에서는 for문과 향상된 for문에 어떤 차이가 있는지 알아보도록 하겠습니다.
for문
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
이 부분에 대해서 bytecode를 확인해보겠습니다.
L3
FRAME APPEND [[I I I]
ILOAD 3 // i 값 로드
ALOAD 1 // 배열 arr 로드
ARRAYLENGTH // 배열 길이를 가져옴
IF_ICMPGE L4 // i가 배열 길이와 같거나 크면 L4로 점프
L5
ILOAD 2 // sum 값 로드
ALOAD 1 // 배열 arr 로드
ILOAD 3 // i 값 로드
IALOAD // 배열 arr[i] 값 로드
IADD // sum + arr[i] 계산
ISTORE 2 // 결과를 sum에 저장
L6
IINC 3 1 // i++
GOTO L3 // 다시 조건 검사로 이동
바이트코드를 해석해본다면 인덱스 i를 매번 증가시키고, 배열의 해당 인덱스에 접근해서 값을 더하는 방식입니다.
중요한건 인덱스를 통해 값을 가져오는 방식이라는 것입니다. 인덱스 개념이 없는 HashMap 과 같은 데이터에서는 사용할 수 없습니다. 배열구조를 가진 데이터에서 데이터를 로드할 수 있습니다.
향상된 for문
L3
FRAME FULL [[Ljava/lang/String; [I I [I I I] []
ILOAD 5 // 인덱스 로드
ILOAD 4 // 배열 길이 로드
IF_ICMPGE L4 // 인덱스가 배열 길이와 같거나 크면 L4로 점프
ALOAD 3 // 배열 arr 로드
ILOAD 5 // 인덱스 로드
IALOAD // 배열 arr[인덱스] 값 로드
ISTORE 6 // 로컬 변수 6에 저장 (배열 요소)
L5
ILOAD 2 // sum 값 로드
ILOAD 6 // 로컬 변수 6(배열 요소) 로드
IADD // sum + 배열 요소 계산
ISTORE 2 // 결과를 sum에 저장
L6
IINC 5 1 // 인덱스 증가
GOTO L3 // 다시 조건 검사로 이동
여기에서 눈 여겨볼 부분은 로컬 변수 6입니다. 일반 for문은 인덱스를 통해 데이터를 바로 sum에 저장하는 방식이었습니다.
하지만 향상된 for문은 로컬변수6에 arr[인덱스] 를 저장하고 로컬변수6을 sum에 저장하는 방식입니다.
왜 이런 번거로운 과정이 추가된걸까요?
Iterator 안전성
향상된 for문이 좋은점은 배열 뿐만이 아니고 Iterator 에서 동작할 수 있다는 것입니다. Iterable 은 Collection 인터페이스에서 상속 받고 있기때문에 컬렉션(List, Set, Map 등) 에서도 향상된 for문을 동작할 수 있다는 것이죠. 이를 위해 Iterator나 내부적인 인덱스 처리로 컬렉션을 순회하는데, 컬렉션의 상태가 중간에 변경될 가능성을 방지하는 안전한 구조가 필요하기 때문입니다.
List<Integer> arr = List.of(1,2,3,4,5);
int sum = 0;
// 이 코드는
for (int i : arr) {
sum += i;
}
// Iterator 와 동일하다
Iterator<Integer> iterator = arr.iterator();
while (iterator.hasNext()) {
sum += iterator.next();
}
List의 향상된 for문
L3
FRAME APPEND [java/util/List I java/util/Iterator]
ALOAD 3
INVOKEINTERFACE java/util/Iterator.hasNext ()Z (itf)
IFEQ L4
ALOAD 3
INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object; (itf)
CHECKCAST java/lang/Integer
INVOKEVIRTUAL java/lang/Integer.intValue ()I
ISTORE 4
L5
LINENUMBER 15 L5
ILOAD 2
ILOAD 4
IADD
ISTORE 2
L6
LINENUMBER 16 L6
GOTO L3
List의 Iterator
L3
LINENUMBER 13 L3
FRAME APPEND [java/util/List I java/util/Iterator]
ALOAD 3
INVOKEINTERFACE java/util/Iterator.hasNext ()Z (itf)
IFEQ L4
L5
LINENUMBER 14 L5
ILOAD 2
ALOAD 3
INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object; (itf)
CHECKCAST java/lang/Integer
INVOKEVIRTUAL java/lang/Integer.intValue ()I
IADD
ISTORE 2
GOTO L3
향상된 for문은 컴파일 시 iterator를 사용합니다.
재사용과 성능최적화
for (int i : arr) {
System.out.println(i);
sum += i;
}
for 문 안에서 i의 값을 여러번 사용된다면 어떻게 될까요?
L3
FRAME FULL [[Ljava/lang/String; [I I [I I I] []
ILOAD 5
ILOAD 4
IF_ICMPGE L4
ALOAD 3
ILOAD 5
IALOAD
ISTORE 6
L5
LINENUMBER 11 L5
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ILOAD 6
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L6
LINENUMBER 12 L6
ILOAD 2
ILOAD 6
IADD
ISTORE 2
L7
LINENUMBER 10 L7
IINC 5 1
GOTO L3
L3에서 ISTORE 6 에 i 값을 저장하고
L5와 L6에서 ILOAD 6 을 통해 로컬변수 6을 로드해 재사용하고 있는 것을 확인할 수 있었습니다. 로컬변수6 데이터를 저장하므로써 단 한번 배열에 접근하는 것으로 다음의 데이터를 모두 처리할 수 있게되었습니다.
읽기전용
향상된 for문에서 배열이나 컬렉션의 요소를 순회할 때, 해당 요소는 로컬변수에 저장되기때문에 읽기 전용상태로 처리됩니다. 즉 복사본을 사용하기때문에 안전한 처리가 가능해집니다.
int[] arr = {1,2,3,4,5};
for (int i : arr) {
i = 100;
}
i 는 로컬변수에서 로드된 데이터이기 때문에 i에 직접 할당할 수 없습니다.
다만 참조타입인 경우 얕은복사로 인해 내부데이터의 변경은 가능합니다.
public class Item {
public int value;
public Item(int value) {
this.value = value;
}
}
public static void main(String[] args) {
List<Item> items = List.of(new Item(1), new Item(2));
for (Item item : items) {
item.value = 100;
}
for (Item item : items) {
System.out.println(item.value); // 100
}
}
일관성
향상된 for문은 모든 배열과 Iterable 객체에서 동작하게 설계되었습니다.따라서 배열이 아닌 다른 컬렉션 객체를 다룰 때도 일관성 있게 순회하기 위해, 배열의 요소를 먼저 임시 변수에 저장하고 그 후에 작업을 수행하는 것이 통일된 방식이라고 할 수 있겠습니다.
마무리
향상된 for문은 시작, 끝 인덱스를 지정할 수 없습니다. 무조건 전체 순회만 가능하다는 것이죠.
그리고 순회 중에는 데이터를 삭제가 불가능합니다. 시작할 때 전체 길이를 저장하는데 중간에 전체길이가 달라지면 예외가 발생합니다.
일반 for문, 향상된 for문간에는 성능차이가 있을까요? 우리는 위에 bytecode를 직접 확인해봤습니다. 향상된 for문에서 추가적으로 로컬변수를 사용하는 차이가 있지만 이는 무시할 정도이고, 로컬변수 도입으로 인한 재사용성으로 인한 최적화도 무시할 정도라고 생각이 됩니다. 따라서 성능차이는 없다고 생각합니다.
결론은 향상된 for문은 Iterable 객체를 손쉽게 쉽게 사용하기 위해 만들어졌다! 그리고 컴파일 시에 로컬변수를 사용된다!
'Language > JAVA' 카테고리의 다른 글
[JAVA] 상속(Inheritance)과 복합(Composition) (0) | 2024.11.12 |
---|---|
[JAVA] String + 연산을 왜 쓰지말아야할까 (0) | 2024.09.24 |
[JAVA] If-else 와 switch 중에 어느게 더 빠를까? (0) | 2024.09.24 |
[JAVA] StringBuilder, StringBuffer 의 차이점과 주의사항 (0) | 2024.09.23 |
[JAVA] String의 불변성(immutability) (0) | 2024.09.23 |