[이펙티브 자바 아이템.61] 박싱된 기본 타입보다는 기본 타입을 사용하라

박상준·2024년 7월 5일
0

이펙티브 자바

목록 보기
17/19

자바의 데이터 타입

  1. 기본 타입
    • int, double, boolean
  2. 참조 타입
    • String, List
  3. 박싱된 기본 타입
    • 각 기본 타입에 대응하는 참조 타입이 있음
    • int → Integer
    • double → Double
    • boolean → Boolean

기본 타입과 박싱된 기본 타입의 차이점

  1. 식별성(IDENTITY)
    • 기본 타입 : 값만 가진다
    • 래퍼 타입: 값과 식별성을 가진다
      • 동일한 값을 가지는 두 인스턴스도 서로 다른 것으로 식별될 수 있다
  2. null 허용
    1. 기본 타입은 null 불가
    2. 래퍼 타입은 null 가능
  3. 효율
    1. 기본 타입이 시간과 메모리면에서 더 효율적이다

잘못된 비교자 구현

Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
public class Main {
    public static void main(String[] args) {
        Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
        
        System.out.println(naturalOrder.compare(new Integer(42), new Integer(42)));
    }
}
  • 같은 숫자라도 1 을 반환한다.
  • Integer 내부에서
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

equals 의 경우에는 value 값 끼리 비교를 하도록 되어 있지만,

  • 두 번째 검사 (i == j)
    • 객체 참조의 식별성을 검사 ( 서로의 메모리상의 주소를 비교 )
    • 서로 다른 Integer 인스턴스라면 false 를 반환
    • 결과적으로 1 을 반환하게 된다.

권장 사항

  • 기본 타입을 다루는 비교자가 필요한 경우 Comparator.naturalOrder() 를 사용하라.

이해가 되지 않는 점.

Comparator.naturalOrder() 말고도 스트림API 에서의 String 등에서 sorted() 로도 동일한 결과를 출력한다

명시적으로 naturalOrder() 를 사용해야할 이유가 있는지 솔직히 의문임.

직접 비교자를 만들어야 하는 경우

  1. 비교자 생성 메서드를 사용한다
  2. 기본 타입을 받는 정적 compare 메서드를 사용한다

박상된 기본 타입 비교시 - 문제 해결

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
      int i = iBoxed, j = jBoxed; // 언박싱
      return i < j ? -1 : (i == j ? 0 : 1);
  };
  • 박싱된 기본타입을 언박싱을 통해 value 비교로 변경

주의해야할 상황

1. null 참조 문제 발생

public class Main {
    static Integer i;
    
    public static void main(String[] args) {
        if (i == 42)
            System.out.println("믿을 수 없군!");
    }
}

//Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "아이템61.Main.i" is null
  • Integer 값은 기본적으로 null 로 초기화 된다.
    • if 문 비교시 NPE 가 발생함.
  • Integer 값말고 int 값으로 사용할 것

근데.. 엔티티나 VO 에서는 보통 래퍼타입을 사용하는 경우가 많다

  • 그 이유는 JPA 같은 경우 엔티티 필드를 매핑하는 경우 객체 타입을 선호하는 경우가 많다고 한다
    • 이유
      1. DB 의 NULLABLE 컬럼을 표현할 수 있으며
      2. 객체 지향적인 설계
        • JPA 는 객체지향적 패러다임을 DB 에 매핑하는 것이 목표라고 한다
        • Java 의 객체지향적 특성을 더 잘 활용할 수 있기에, 엔티티에는 래퍼타입을 좀 더 선호한다고 한다
        • 물론 NULL 값이 들어가지 않는 경우는 기본타입을 두는 게 더 나을 수 있다고 생각한다.
          • count 같은 값은 기본적으로 0 부터 시작하는 경우가 있다.
      3. 타입의 안정성
        1. 제네릭과 함께 사용하는 경우 타입 안정성을 부여가능하다
      4. 리플렉션 활용
        1. JPA 의 경우 내부적으로 리플렉션을 많이 사용한다고 한다.

          엔티티 자체에 변수가 private 으로 선언된 경우가 많기에, getter/setter 없이 필드값을 읽고 쓰기 위해 → 리플렉션을 자체적으로 사용한다고 함.

          // 엔티티 클래스의 모든 필드를 가져옴
          Field[] fields = User.class.getDeclaredFields();
          • 이런식으로 모든 필드를 가져와서
            • 컬럼 어노테이션이 존재하는지, 필드 값을 읽고 필드 타입을 확인하는 등의 절차를 거친다고함.
        2. 기본타입보다는 래퍼타입이 리플렉션 API 와 더 친하다고 한다.

2. 성능 문제

public class Main {
    public static void main(String[] args) {
        // 래퍼 클래스 Long 사용
        long startTime1 = System.currentTimeMillis();
        Long sumObject = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++) {
            sumObject += i;
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println("Long 결과: " + sumObject);
        System.out.println("Long 실행 시간: " + (endTime1 - startTime1) + "ms");
        
        // 기본 타입 long 사용
        long startTime2 = System.currentTimeMillis();
        long sumPrimitive = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++) {
            sumPrimitive += i;
        }
        long endTime2 = System.currentTimeMillis();
        System.out.println("long 결과: " + sumPrimitive);
        System.out.println("long 실행 시간: " + (endTime2 - startTime2) + "ms");
    }
}
Long 결과: 2305843008139952128
Long 실행 시간: 4184ms
long 결과: 2305843008139952128
long 실행 시간: 459ms
  1. 박싱과 언박싱의 과정을 매 연산마다 수행

    • Long 에서 += long value 를 수행하는 경우
    • i 의 long 값을 → Long 으로 박싱을 수행 후
    • sum = Long.valueOf(sum.longValue() + i ) 와 유사한 과정을 통해 연산을 매번 수행
  2. 객체 생성 오버헤드 발생

     Long.valueOf(1L);

    @IntrinsicCandidate
    public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }
    • Long 값이 -128 ~ 127 의 값이라면 별도의 캐싱이 되어 기존의 Long 값을 반환하겠지만,
    • 그외의 값의 경우 new Long 을 통해 매번 객체를 생성하게 된다.
  3. 메모리 사용

    • 일단 Long 객체는 메모리상에 별도 공간사용으로 메모리 효율성 DOWN

그럼 박싱된 기본 타입은 언제 쓰는데

  1. 컬렉션의 원소, 키 ,값으로 사용

    • 어차피 컬렉션에서는 기본 타입을 사용하지 못함
  2. 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로 사용

    매개변수화 메서드?

    • Parameterized Method
    • 이라고 함.
    • 제네릭 메서드에 타입 인자를 넣어서 구체화한 메서드를 말한다.
    • ex
      • <T> void printArr(T[] array)
    • 책에서는 ThreadLocal<T> 의 케이스를 예시로 들었는데
    • ThreadLocal 의 경우도 제네릭클래스이기에 기본 타입을 사용불가능하다..
      • 그래서 걍 해당 예시를 든거같다.
  3. 리플렉션을 통한 메서드 호출의 경우

    1. 자바에서 리플렉션은 그냥 객체를 다루도록 설계가 되어 있다.
profile
이전 블로그 : https://oth3410.tistory.com/

0개의 댓글