Java

원시 자료형 (Primitive Type) 과 참조 자료형 (Reference Type)

재심 2024. 10. 3. 21:27

목차

    자바에서는 자료형을 2가지로 구분할 수 있다.

     

    • 원시 자료형 (Primitive Type)
    • 참조 자료형 (Reference Type)

     

    원시 자료형 (Primitive Type)

    보통의 int, boolean, float 같은 타입이다.

    C나 자바 같은 전통적인 프로그래밍 언어들은 원시자료형을 제공한다.

     

    특징

    • 객체가 아님: 객체가 아니기 때문에 메모리에서의 위치를 통해 값을 직접 다루며 이 때문에 참조형에 비해 더 효율적이다.
    • 자동 형변환/암시적 형변환: 원시 자료형끼리 자동 타입 변환이 지원된다. (int를 long에 할당할 수 있다. 하지만 반대로는 명시적인 형변환이 필요)
    • 기본값: 원시 자료형은 기본 값을 가진다. int는0 boolean은 false, char는 null일 가진다

     

    참조 자료형 (Reference Type)

    원시형이 아닌 자료형 모두를 의미한다.

     

    주요 참조 자료형

    • 객체 (Object): String, Integer..
    • 배열: 배열도 객체로 간주되므로 참조자료형이다.
    • 인터페이스: 인터페이스도 참조 자료형으로 간주된다.

    원시형에 대응되는 참조 자료형

     

    특징

    NULL이 될 수 있다.

    메모리 주소를 가리킴

    Collection은 참조형만 가능하다.

     

    참조형이 데이터를 참조하는 방식

    JVM에서 메모리 영역은 스택과 힙으로 나눌 수 있다.

     

    • 스택: 메서드 호출 시 생성되는 지역변수와 참조형 변수의 메모리를 저장
    • 힙: 객체와 배열이 생성되는 메모리 공간. 즉, 모든 객체는 힙에 저장된다. 객체의 생명은 명시적으로 힙에서 제거될 때 까지 유지된다.

    그래서 객체를 아래처럼 생성하면 객체는 힙에 저장된다.

    Dog dog = new Dog("Buddy");

     

    이렇게 생성된 객체의 주소가 dog이라는 참조형 변수에 저장된다. 실제 데이터가 아닌 객체의 메모리 주소를 가리킨다.

    그래서 다른 참조형 변수도 동일한 객체를 가리킬 수 있다.

    Dog anotherDog = dog; // dog와 동일한 객체를 참조
    anotherDog.bark(); // "Buddy says Woof!" 출력
    
    anotherDog.name = "Max"; // 객체의 속성 변경
    dog.bark(); // "Max says Woof!" 출력 (변경 사항 반영)

     

    GC가 일어나면 사용하지 않는 객체를 자동으로 메모리에서 제거한다.

    사용하지 않는 객체라면 참조형 변수가 null로 설정되거나 다른 객체를 참조하면 이전 객체는 더 이상 참조하지 않게되어 GC에 의해 회수 된다.

     

    박싱과 언박싱

    • 박싱: 원시 자료형을 참조 자료형으로 변환하는 과정. (int -> Integer)
    • 언박싱: 참조 자료형을 원시 자료형으로 변환하는 과정 (Integer -> int)

    박싱

    AutoBoxing, 즉 원시 자료형이 자동으로 참조 자료형으로 변환될 수 있다. (Java5 부터 지원) 이 때 박싱과정에서 객체를 생성하기 때문에 메모리 오버헤드가 발생할 수 있다.

    int primitiveInt = 5;
    Integer boxedInt = primitiveInt; // 자동 박싱

     

    언박싱

    Unboxing은 참조형이 원시형으로 자동으로 변환되는 것을 의미한다. (Java5 부터 지원) 이 때 참조형이 null이면 언박싱하다가 NPE가 발생할 수 있다.

    Integer boxedInt = null;
    int primitiveInt = boxedInt; // NullPointerException 발생

     

    박싱 과정에서 결국은 객체를 생성하여 힙에 할당하고 (그 객체가 차지하는 리소스도 더 큼) 힙에 할당한 객체들은 사용하지 않을 경우 GC의 대상이 되기 때문이다.

    정리하면 반복적인 박싱/언박싱은 성능 저하를 일으킬 수 있어서 주의할 필요가 있다.

     

     

     

     

    원시형과 참조형의 속도 비교

    동일한 동작을 원시형과 참조형을 비교해서 속도비교를 해본다.

    1억 개의 element를 원시형 int[]에 삽입하고 다시 그 값을 찾는 속도를 측정해본다.

     

            // int[]에 1억개 삽입
            long start = System.currentTimeMillis();
            int[] intElements = new int[100000000];
            for (int i = 0; i < 100000000 - 1; i++) {
                intElements[i] = 1;
            }
            intElements[100000000 - 1] = 2;
    
            // int[] 1억 개 중 찾기
            int idx = 0;
            while (2 != intElements[idx]) {
                idx++;
            }
    
            long end = System.currentTimeMillis();
            System.out.println(end - start + "ms");

     

    결과: 228ms

     

            long start = System.currentTimeMillis();
            Integer[] intElements = new Integer[100000000];
            for (int i = 0; i < 100000000 - 1; i++) {
                intElements[i] = 1;
            }
            intElements[100000000 - 1] = 2;
    
            // int[] 1억 개 중 찾기
            int idx = 0;
            while (2 != intElements[idx]) {
                idx++;
            }
    
            long end = System.currentTimeMillis();
            System.out.println(end - start + "ms");

     

    결과: 707ms

     

    결과적으로 3배 이상의 차이가 발생한다. 하지만 참조형의 경우 편리한 기능을 많이 제공하기 때문에 필요하다면 사용하는게 맞고 무분별하게 참조형을 사용하지 않도록 한다.

     

    메모리 사용 측면에서도 int (원시형) 은 32비트 즉 4바이트를 사용하며, Integer(참조형)은 128비트 즉 16바이트를 사용한다.

    참조형은 값이 보관되는 영역은 4바이트로 동일하지만 나머지 12바이트는 여러 부가정보를 보관하는 헤더로 구성된다고 한다.

     

    오토 박싱과 언박싱을 피하는 방법

    • 기본형 타입을 많이 사용하기
    • 컬렉션을 사용할 때는 IntStream 같은 스트림API 사용을 고려해본다. (스트림API는 박싱/언박싱을 회피하려는 설계를 가지고 있다고 한다) 
    // 객체 스트림을 사용할 경우
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    int sum = list.stream().mapToInt(Integer::intValue).sum();  // 언박싱이 발생
    
    // 기본형 스트림을 사용할 경우
    IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
    int sum = intStream.sum();  // 박싱/언박싱 없이 처리

     

     

    원시 타입, 참조 타입 어떤 것을 가지고 싶은지에 따라 parseXX, valueOf 구분하기

    int 타입을 예로 들면 parseInt, valueOf 둘 다 int 타입으로 받을 수 있다.

    하지만 결국 내부적으로 valueOf를 통해 int 타입으로 변환한다면 내부적으로 오토 언박싱이 발생한다고 한다.

     

    => int 타입으로 받으면 결국 둘 다 내부적으로 오토 언박싱이 발생함.

     

    하지만 valueOf의 경우 Integer 타입으로 받는다면 캐싱을 기대할 수 있다. 캐싱 범위 안이라면 새로운 객체를 만들지 않고 캐싱된 결과를 돌려주어 성능 향상을 좀 더 기대할 수 있다고 한다.

     

    참조

    https://f-lab.kr/insight/java-auto-boxing-and-unboxing

     

    자바 성능 최적화: 오토 박싱과 언박싱 이해하기

    자바에서 오토 박싱과 언박싱의 개념을 이해하고, 이들이 성능에 미치는 영향을 최소화하는 방법에 대해 설명합니다.

    f-lab.kr

    https://khnemu.tistory.com/13

     

    Integer.valueOf VS Integer.parseInt

    들어가기전에 int형으로 바꿔주는게 valueOf말고 parseInt가 또 있네?? 알고리즘 문제를 풀다보면 문자열을 숫자로 바꿔야 하는 일이 정말 많다. 특히 입력 값을 받을 때 숫자로 받아야 하는 일이 대

    khnemu.tistory.com

     

    'Java' 카테고리의 다른 글

    컬렉션 프레임워크  (2) 2024.10.05
    Java가 빌드되고 실행되는 과정  (3) 2024.10.03
    함수형 인터페이스 (Functional Interface) 에 대해  (2) 2024.10.03
    제네릭 (Generic) 에 대해  (1) 2024.10.03
    Java의 주요 특징  (1) 2024.10.03