오늘은 뭘 할려고 하는건 아니고 자바와 관련해서 몇가지 이야기를 해보려고 한다. 많이 중요한 내용은 아니지만(물론 알면 좋지만) 다시 처음으로 돌아가는 의미로 하는 것이다. 맨날 쉽지 않은(?) 이야기만 하다보니 처음으로 돌아가자는 의미로 몇가지 살펴보도록 하자.

Integer 오토박싱

int를 오토박싱 할 때 자바 컴파일러는 내부적으로 Integer.valueOf() 메서드를 호출한다.

public static void main(String[] args) {
  Integer i = 10;
}

이 경우 자바 컴파일러는 Integer.valueOf() 메서드를 호출해서 오토박싱을 한다. 정말로 그렇게 되는지 바이트코드를 살펴보자.

public static main([Ljava/lang/String;)V
L0
 LINENUMBER 9 L0
 BIPUSH 10
 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
 ASTORE 1
 L1
 LINENUMBER 10 L1
 RETURN
 L2
 LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
 LOCALVARIABLE i Ljava/lang/Integer; L1 L2 1
 MAXSTACK = 1
 MAXLOCALS = 2

5번째 줄을 보면 java/lang/Integer.valueOf 를 호출하는 것을 알 수 있다. 물론 다른 Number 타입들도 {Number}.valueOf() 메서드를 동일하게 호출한다.

Integer Identity

동일한 값 Integer로 Identity 비교시에 항상 동일한 결과를 리턴하는 것은 아니다.

public static void main(String[] args) {
  Integer a = 100;
  Integer b = 100;
  System.out.println(a == b);
}

a 와 b 각각 100이라는 숫자를 넣고 Identity 비교를 해보자. 이 때 비교시에은 true를 리턴하지만 아래의 경우에는 false를 리턴한다.

public static void main(String[] args) {
  Integer c = 200;
  Integer d = 200;
  System.out.println(c == d);
}

위 코드가 false를 리턴하는 이유는 Integer의 내부에 -128 부터 127까지 캐싱처리를 하여 동일한 값으로 판단한다. -129 이하 이거나 128 이상일 경우에는 동일성의 결과가 다를 수 있다. 만약에 그 캐싱의 값들을 늘리고 싶다면 java.lang.Integer.IntegerCache.high 속성을 사용해서 늘리거나 줄일 수 있다. 그래도 버그를 줄이기 위해 오토박싱한 Integer는 equals를 이용해서 비교하도록 하자!

Iterable과 for-each Loop

자바의 Iterable 인터페이스를 구현하면 for-each Loop를 사용할 수 있다. 자바의 컬렉션 최상위 인터페이스인 Iterable은 for-each Loop를 사용할 수 있도록 해준다. 구현할건 아니고 되는지만 확인해보도록 하자.

public class ExampleIterable<T> implements Iterable<T>{

  @Override
  public Iterator<T> iterator() {
    return null;
  }
}

위와 같이 Iterable를 구현했으면 아래와 같이 for each를 사용할 수 있다.

public static void main(String[] args) {
  ExampleIterable<String> example = new ExampleIterable<>();
  for(String e : example){

  }

}

forEach와 Iterator

Iterator는 forEach보다 느리지 않다. 실제로 예전에 이런 이야기가 있었는데 아무 근거 없는 이야기이다. 실제 자바 컴파일러는 forEach Loop문을 Iterator로 바꾼다. 실제로 어떻게 되는지 바이트코드로 살펴보자.

public static void main(String[] args) {

  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

  Iterator<Integer> iterator = numbers.iterator();
  while (iterator.hasNext()) {
   System.out.println(iterator.next());
  }
}

위의 코드는 Iterator를 사용해서 순차적으로 element를 가져오는 코드이다. 위의 코드를 바이트코드로 살펴보자.

...
L1
 LINENUMBER 18 L1
 ALOAD 1
 INVOKEINTERFACE java/util/List.iterator ()Ljava/util/Iterator;
 ASTORE 2
L2
 LINENUMBER 19 L2
 FRAME APPEND [java/util/List java/util/Iterator]
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.hasNext ()Z
 IFEQ L3
L4
 LINENUMBER 20 L4
 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
 ALOAD 2
 INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
 GOTO L2
...

바이트코드가 조금 길어서 위 아래 생략하고 중요한 부분만 살펴보면 위와 같다. 그럼 forEach Loop을 사용해보자.

public static void main(String[] args) {

  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

  for (Integer i : numbers) {
    System.out.println(i);
  }

}

우리가 흔히 쓰는 forEach Loop문이다. 이것 또한 바이트코드를 살펴보면 다음과 같다.

L1
  LINENUMBER 18 L1
  ALOAD 1
  INVOKEINTERFACE java/util/List.iterator ()Ljava/util/Iterator;
  ASTORE 2
L2
  FRAME APPEND [java/util/List java/util/Iterator]
  ALOAD 2
  INVOKEINTERFACE java/util/Iterator.hasNext ()Z
  IFEQ L3
  ALOAD 2
  INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
  CHECKCAST java/lang/Integer
  ASTORE 3
L4
  LINENUMBER 19 L4
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  ALOAD 3
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V

내부적으로는 Iterator를 사용해서 element 가져오는 것을 확인 할 수 있다. 그래도 코드를 작성할 때 특별한 경우가 아니라면 Iterator보다는 forEach문을 사용하는게 낫다.

Stream과 Iterable의 forEach

위는 일반 forEach Loop문을 이야기 한거지만 지금 이야기할 것은 java8의 나온 forEach() 메서드를 말한다. Stream의 forEach 메서드와 collection의 forEach 메서드는 다르다. 예로 List의 forEach 문은 자바의 최상위 인터페이스인 Iterable 의 default 메서드로 구현되어 있다. 그래서 우리는 List의 forEach문만 사용한다면 다음과 같이 코드를 작성할 필요는 없다.

public static void main(String[] args) {

  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
  numbers.stream().forEach(System.out::println);

}

위 처럼 굳이 Stream으로 만들 필요 없이 바로 forEach문을 사용하면 된다. Stream을 만드는 비용도 있으니 더욱 위의 코드처럼 사용하지는 말아야 한다.

public static void main(String[] args) {

  List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
  numbers.forEach(System.out::println);

}

String intern

객체화된 String을 상수로 변경하기 위해 intern() 메서드를 권장하지 않는다. 그냥 String 리터럴을 사용해라. 쓸때 없이 객체형 String을 만들필요는 더더욱 없고 그걸 또한 상수로 변경하기 위해 intern() 메서드를 호출하는 것은 더욱 권장하지 않는다. 예로 String을 객체화 할 때도 비용이 들지만 String pool에서도 해당 String을 찾느라 시간이 더욱걸린다.

public static void main(String[] args) {

  long startTime = System.currentTimeMillis();
  for ( int i = 0 ; i < 10000000; i++ ) {
    String str = "wonwoo";
  }
  System.out.println(System.currentTimeMillis() - startTime);

  long startTime1 = System.currentTimeMillis();
  for ( int i = 0 ; i < 10000000; i++ ) {
    new String("wonwoo");
  }
  System.out.println(System.currentTimeMillis() - startTime1);

  long startTime2 = System.currentTimeMillis();
  for ( int i = 0 ; i < 10000000; i++ ) {
    new String("wonwoo").intern();
  }
  System.out.println(System.currentTimeMillis() - startTime2);
}

String 리터럴과 new String(), new String().intern() 을 각각 천만번 돌려봤다. 결과는 다음과 같다.

6
9
2612

위는 정확한 벤치마킹은 아니지만 어느정도 비슷할 듯 싶다. 실제로 new String().intern()을 사용한게 제일 느리다. 근데 String 리터럴과 new String()은 그렇게 많은 차이는 보이지 않았다. 그래도 굳이 객체화된 String을 만들 필요는 없다.

이렇게 오늘은 java와 관련된 이야기를 몇가지 해봤다. 아직 java에 대해서 모르는게 많다. 한 언어를 완벽하게 소화하는 건 쉬운 일이 아니다. 완벽하게 하지는 못해도 어느정도 깊이는 알아야 되지 않을까 싶다.