본문 바로가기

Development/Java

Java Wrapper Class 의 캐싱

Wrapper Class


Java에는 Primitive Type을 Reference Type으로 사용하기 위해서 만든 Wrapper Class가 있습니다.

Primitive TypeWrapper Class
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean
voidVoid

Primitive Type 값을 Wrapper Class의 인스턴스로 변환하여 쓰는 과정을 Boxing, 그 반대를 Unboxing 이라고 하는데 Java 1.5 부터는 이를 자동으로 해주는 AutoBoxing/AutoUnBoxing을 지원해 줍니다.


Boxing/Unboxing

  Integer n1 = 1;
  Integer n1 = new Integer(1);
  
  int n2 = n1;
  int n2 = n1.intValue();

여기 까지가 Wrapper Class에 대한 간단한 설명이었습니다.


그래서 뭐?


문득 java.lang 패키지의 Integer 소스를 보는데 신기한 소스가 있었습니다.

IntegerCache라는 Inner Class가 있습니다.

Integer.java

  ...
  /**
    * Cache to support the object identity semantics of autoboxing for values between
    * -128 and 127 (inclusive) as required by JLS.
    *
    * The cache is initialized on first usage.  The size of the cache
    * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
    * During VM initialization, java.lang.Integer.IntegerCache.high property
    * may be set and saved in the private system properties in the
    * sun.misc.VM class.
    */
  private static class IntegerCache {
          static final int low = -128;
          static final int high;
          static final Integer cache[];
  
          static {
              // high value may be configured by property
              int h = 127;
              String integerCacheHighPropValue =
                  sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
              if (integerCacheHighPropValue != null) {
                  try {
                      int i = parseInt(integerCacheHighPropValue);
                      i = Math.max(i, 127);
                      // Maximum array size is Integer.MAX_VALUE
                      h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                  } catch( NumberFormatException nfe) {
                      // If the property cannot be parsed into an int, ignore it.
                  }
              }
              high = h;
  
              cache = new Integer[(high - low) + 1];
              int j = low;
              for(int k = 0; k < cache.length; k++)
                  cache[k] = new Integer(j++);
  
              // range [-128, 127] must be interned (JLS7 5.1.7)
              assert IntegerCache.high >= 127;
          }
  
          private IntegerCache() {}
      }
  
  ...


위 코드와 주석을 보면 오토박싱을 서포팅하는 캐시를 가지는 것을 알 수 있습니다. low ~ high(기본값은 -128 ~ 127) 사이의 value를 가지는 Integer 인스턴스를 미리 생성하고 있네요. 또한 캐싱할 Integer 범위는 VM option으로 설정이 가능한 것도 알 수 있습니다.

  
  /**
   * Returns an {@code Integer} instance representing the specified
   * {@code int} value.  If a new {@code Integer} instance is not
   * required, this method should generally be used in preference to
   * the constructor {@link #Integer(int)}, as this method is likely
   * to yield significantly better space and time performance by
   * caching frequently requested values.
   * ...
   * This method will always cache values in the range -128 to 127,
   * inclusive, and may cache other values outside of this range.
   * ...
   */
  
  public static Integer valueOf(int i) {
      if (i >= IntegerCache.low && i <= IntegerCache.high)
          return IntegerCache.cache[i + (-IntegerCache.low)];
      return new Integer(i);
  }

또한, valueOf 메소드를 봐도 캐싱된 값을 사용하는 것을 알 수 있으며, 반드시 새 인스턴스가 필요한 것이 아니라면 valueOf 를 이용해서 인스턴스를 할당받는 것을 권장합니다.


Test


자, 테스트 코드를 볼까요 ? VM option을 따로 주지 않았기 때문에 Integer 캐싱 범위는 -128 ~ 127 입니다.

  @Test
  public void IntegerTest1() throws Exception {
      Integer n1 = 1;
      Integer n2 = 1;
  
      assertTrue(n1 == n2);
  }
  
  @Test
  public void IntegerTest2() throws Exception {
      Integer n1 = 300;
      Integer n2 = 300;
  
      assertTrue(n1 == n2);
  }


IntegerTest1,2 모두 테스트는 실패해야 하는 것으로 보입니다. 왜냐하면 Reference Type에 대하여 '==' 연산은 reference 비교이니까요. 그런데 IntegerTest1은 성공하고 IntegerTest2는 실패합니다. 이것이 위에서 언급된 오토박싱에 대한 캐싱때문이지요.


Integer class 는 클래스 로드시 -128~127 값에 대한 인스턴스를 미리 생성해놓고 오토박싱시에 이 범위의 값이면 미리 생성된 인스턴스를 리턴합니다. 그래서 캐싱 범위에 속하는 1에 대해서는 미리 생성된 같은 인스턴스를 가지게 되는 것이고, 캐싱 범위 밖인 300에 대해서는 각각 새로운 인스턴스를 받게 되는 것이지요.

  @Test
  public void IntegerTest3() throws Exception {
      Integer n1 = Integer.valueOf(1);
      Integer n2 = Integer.valueOf(1);
  
      assertTrue(n1 == n2);
  }
  
  @Test
  public void IntegerTest4() throws Exception {
      Integer n1 = Integer.valueOf(300);
      Integer n2 = Integer.valueOf(300);
  
      assertTrue(n1 == n2);
  }


똑같이 캐싱된 값을 이용하는 valueOf 메소드에 대해서도 테스트 결과는 같습니다. test3 은 성공하고 test4 는 실패합니다.

자, 마지막으로 다음과 같은 경우는 어떨까요?

  @Test
  public void IntegerTest5() throws Exception {
      Integer n1 = new Integer(1);
      Integer n2 = new Integer(1);
  
  
      assertTrue(n1 == n2);
  }


이 테스트는 실패합니다.

  //Integer Constructor
  public Integer(int value) {
      this.value = value;
  }

왜냐하면, 위와 같이 생성자를 통한 객체 생성에서는 캐싱된 값을 사용하고 있지 않고 있기 때문이지요.

당연히, Integer Class뿐만 아니라 Byte, Short, Long, Character 역시 자체적으로 캐싱을 하고 있습니다. Float나 Double에서 쓰지 않는 것은, 자주 쓰는 범위를 예측하기에는 너무 어렵고, 그 사이에 너무 많은 값이 존재할 수 있어서 일까요?