카페에 올라온 질문이다.
import java.util.ArrayList;
import java.util.List;
public class LoopTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i=0; i<10; i++) {
list.add(""+i);
}
// 1
list.forEach(str-> {
System.out.println(str);
});
// 2
for(String str : list) {
System.out.println(str);
}
}
}
1번과 2번의 차이점은 무엇인가요?
코드리뷰
일단 차이점을 알아보기 전에 코드를 리뷰해보자.
Review 1. ArrayList 객체 생성
일단 ArrayList를 List 인터페이스로 받은 것은 잘한 것 같다.
왜 그런지는 이펙티브자바 3/E "아이템 64. 객체는 인터페이스를 사용해 참조하라"를 읽어보자.
다만 자바 7부터라면 다이아몬드 연산자(diamond operator)가 추가되었는데 아래와 같이 쓸 수 있다.
이미 자바 8의 기능(feature)인 Consumer가 코드에 보이므로 언급을 했다.
List<String> list = new ArrayList<>();
만약 자바 10이상이라면 JEP 286(Local Variable Type Inference)이 추가 되었으니 아래와 같이 더욱 타이핑을 줄일 수 있다.
var list = = new ArrayList<String>();
다만 왼쪽의 var는 List 인터페이스가 아닌 우변(LHS)의 ArrayList로 추론이 된다.
Review 2. 숫자의 문자 변환(캐스팅; casting)
아래와 같이 0부터 10까지 숫자를 리스트에 넣기 위해 for 반복문을 이용하고 있다.
그런데 i의 타입은 int 인데 List의 원소는 String이다. 그래서 정수형을 문자형으로 변환을 해서 add를 하고 있다.
list.add("" + i);
"" + i 문장을 보면서 이 문장을 작성한 사람은 JavaScript를 해본 사람이라는 느낌이 들었다.
자바스크립트에서는 문자형(string)과 숫자형(number)을 + 연산으로 수행하면 결과가 string이 된다.
연산의 수행의 세부 내용은 ECMAScript 언어사양의 12.8.3The Addition Operator 를 참고한다.
하지만 자바의 경우는 어떻게 동작할까?
자바 버전이나 컴파일러에 따라 다르지만 나의 "11.0.2" 2019-01-15 LTS 컴파일러의 경우 아래와 같이 결과를 만들었다.
LINENUMBER 11 L4
ALOAD 1
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC ""
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z (itf)
POP
다시 자바코드로 옮기면 아래와 같다.
for (int i = 0; i < 10; i++) {
// list.add("" + i); 는 아래와 같이 수행된다.
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("");
stringBuilder.append(i);
list.add(stringBuilder.toString());
}
결국 형변환을 위해 루프의 개수만큼 StringBuilder 객체가 생기고 append 연산을 두 번해서 문자열을 만들고 있다.
개선 1안: List<Integer>
테스트를 위한 코드이기 때문에 애초에 List<String>대신에 List<Integer>로 했으면 어떨까하는 생각이 든다.
public class LoopTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
// 1
list.forEach(System.out::println);
// 2
for (Integer str : list) {
System.out.println(str);
}
}
}
이렇게 되면 list.add(i) 부분은 아래와 같이 컴파일이 되는데
LINENUMBER 11 L4
ALOAD 1
ILOAD 2
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z (itf)
POP
동일한 자바코드로 옮기면 아래와 같다.
for (int i = 0; i < 10; i++) {
list.add(Integer.valueOf(i));
}
Primitive 타입인 int 가 Boxed 타입인 Integer로 오토박싱이 되는 정도의 코드가 추가되는 셈이다.
개선 2안: List<String> 으로 유지한다면?
int 를 String으로 바꾸는 것으로 볼 수 있을 것이다.
크게 두 가지가 있다.
1) String.valueOf 을 사용
for (int i = 0; i < 10; i++) {
list.add(String.valueOf(i));
}
2) Integer.toString 을 사용
for (int i = 0; i < 10; i++) {
list.add(Integer.toString(i));
}
다만 둘 중에 어떤 것을 쓸지 선택을 해야 하는데,
String.valueOf(int i) 의 경우는 내부적으로 Integer.toString(int i)를 호출한다.
package java.lang;
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// ...
public static String valueOf(int i) {
return Integer.toString(i);
}
// ..
public final class Integer extends Number implements Comparable<Integer> {
// ...
public static String toString(int i) {
int size = stringSize(i);
if (COMPACT_STRINGS) {
byte[] buf = new byte[size];
getChars(i, size, buf);
return new String(buf, LATIN1);
} else {
byte[] buf = new byte[size * 2];
StringUTF16.getChars(i, size, buf);
return new String(buf, UTF16);
}
}
// ..
차이점
이제 원래 질문인 차이점을 살펴보자.
2번
먼저 자바8 이전부터 있던 2번을 먼저 보자.
이런 형태는 자바 규격에서는 향상된 for 문(The enhanced for statement)이라고 정의하고 있다.
for (String str : list) {
System.out.println(str);
}
이것은 아래 코드처럼 동작한다.
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
1번
1번의 forEach 정의는 일단 Iterable이라는 인터페이스에 정의되어 있다.
참고로 ArrayList부터의 클래스 계층도를 살펴보면 아래와 같다.
이 문단 제일 앞에 'forEach의 정의'라는 표현이 있었는데 혹시 어색함을 느꼈어야 한다.
어색함을 느끼지 못했다면
1) 이미 자바8에 대해 잘 알고 있던지 아니면 2) 자바8 이전의 인터페이스의 특성을 모르고 있던 중 둘 중 하나일 것이다.
이 언급을 하는 이유는 forEach가 Iterable에 아래와 같이 정의 되어 있기 때문이다.
public interface Iterable<T> {
Iterator<T> iterator();
/**
* ...
* @since 1.8
*/
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
// ..
최상위 인터페이스 답게 이 인터페이스를 구현(implements)하기 위해서는,
Iterator을 돌려주는 iterator() 메서드만 정의하면 된다. (첫줄)
자바 8부터 default method 라는 이름과 함께 인터페이스에 메서드 바디를 가질 수 있게 되었다.
그 이전에서는 인터페이스에 메서드를 만드는 것이 아예 불가능했다.
forEach의 구현을 보면 아까 2번에서 설명했던 것과 거의 유사하다.
// A
for (String str : list) {
System.out.println(str);
}
// vs
// B
for (T t : this) {
action.accept(t);
}
다만 좀 다른 것은 B의 경우는 반복문 바디가 인터페이스 내부에 정의되어 있다는 점이다.
따라서 루프를 직접 타이핑할 필요가 없고 다만 action이라는 수행해야 할 동작을 파라미터로 전달하면 되는 것이다.
혹시 자바스크립트나 C언어를 해보았다면 이런 비슷한 개념을 이미 접해보았을 것 같다.
전자에서는 함수를 파라미터로 넘기는 일이 빈번하게 일어난다. C언에서는 함수 포인터라는 것을 통해 유사하게 할 수 있다.
여기에서 예제와 비슷하게 자바스크립트로 만들어보면 아래와 같다.
class Test {
constructor(arr) {
this.arr = arr;
}
forEach(fn) {
for (let elem of this.arr) {
fn(elem);
}
}
}
const test = new Test([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
test.forEach(console.log);
다만 Java는 JavaScript와 달리 함수가 1급 시민이 아니다.
그래서 Consumer라는 인터페이스를 전달받아서 내부적으로는 accept라는 메서드를 실행하도록 구조가 되어 있다.
참고로 Consumer 인터페이스는 함수형 인터페이스(Functional Interface)의 한 종류로 아래와 같이 선언되어 있다.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
// ...
결국 위에서 아래와 같은 문장은
list.forEach(str-> {
System.out.println(str);
});
아래와 유사하다고 볼 수 있다.
list.forEach(new PrintWithLinefeed());
//..
class PrintWithLinefeed implements Consumer<String> {
@Override
public void accept(String str) {
System.out.println(str);
}
}
아래는 익명클래스를 이용한 인터페이스의 구현이다.
list.forEach(new Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
});
바로 위의 코드는 아래 처럼 람다로 바꿀 수 있다.
list.forEach(str -> System.out.println(str));
자, 원래 질문자의 코드가 되었다.
사실 자바8에는 한 단계가 더 있다. 메서드 레퍼런스라는 것을 이용하면 람다 표현이 아래와 같이 바꿀 수 있다.
list.forEach(System.out::println);
'Programing > JVM(Java, Kotlin)' 카테고리의 다른 글
성지순례와 인터페이스 (0) | 2019.10.08 |
---|---|
[Java] Wrapper 클래스 vs Overload 메서드 중 우선순위는? (1) | 2019.10.06 |
[Java] 인스턴스 변수 초기화 (0) | 2019.09.18 |
ThreadLocalRandom and Random (0) | 2019.08.27 |
[Java] JRE update 업데이트된 라이센스 조항 (0) | 2019.07.18 |