본문 바로가기

Java

[Java] Wrapper class

기본형의 한계

자바는 객체 지향 언어입니다. 그런데 자바 안에 객체가 아닌 것이 있습니다.

바로 int, double 같은 기본형(Primitive Type) 입니다. 기본형은 객체가 아니기 때문에 한계가 있습니다.

 

객체가 아님: 기본형 데이터는 객체가 아니기 때문에, 객체 지향 프로그래밍의 장점을 살릴 수 없습니다.

  • 예를 들어 객체는 유용한 메서드를 제공할 수 있는데, 기본형은 객체가 아니므로 메서드를 제공할 수 없다.
  • 객체 참조가 필요한 컬렉션 프레임워크를 사용할 수 없다. 그리고 제네릭도 사용할 수 없다.

null 값을 가질 수 없음: 기본형 데이터 타입은 null 값을 가질 수 없습니다. 

  • 때로는 데이터가 없음 이라는 상태를 나타내야 할 필요가 있다. 
  • 기본형은 항상 값을 가지기 때문에 이런 표현을 할 수 없다.
public class MyIntegerMethodMain0 {

    public static void main(String[] args) {
        int value = 10;
        int i1 = compareTo(value, 5);
        int i2 = compareTo(value, 10);
        int i3 = compareTo(value, 20);
        System.out.println("i1 = " + i1);
        System.out.println("i2 = " + i2);
        System.out.println("i3 = " + i3);
    }
    private static int compareTo(int value, int target) {
        if (value > target) {
            return 1;
        } else if (value < target) {
            return -1;
        } else {
            return 0;
        }
    }
}
i1 = 1
i2 = 0
i3 = -1
  • value와 비교 대상 값을 compareTo( ) 라는 외부 메서드를 사용해서 비교한다.
  • 자기 자신인 value 값을 연산하는 것이기 때문에 항상 자신의 값인 value가 사용된다.
  • value가 객체라면 value 객체 스스로 자기 자신의 값과 다른 값을 비교하는 메서드를 만드는 것이 더 효율적이다.
<예시>
value.compareTo(5);
value.compareTo(10);
value.compareTo(20);

 

 

직접 만드는 래퍼 클래스

int를 클래스로 만들어봅시다. int는 클래스는 아니지만, int 값을 가지고 클래스를 만들면 됩니다.

다음 코드는 마치 int를 클래스로 감싸서 만드는 것 처럼 보입니다. 이렇게 특정 기본형을 감싸서(Wrap) 만드는 

클래스를 래퍼 클래스(Wrapper class) 라고 합니다.

public class MyInteger {

    private final int value;

    public MyInteger(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
    public int compareTo(int target) {
        if (value > target) {
            return 1;
        } else if (value < target) {
            return -1;
        } else {
            return 0;
        }
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}
  • MyInteger는 int value 라는 단순한 기본형 변수를 하나 가지고 있다.
  • 그리고 이 기본형 변수를 편리하게 사용하도록 다양한 메서드를 제공한다.
  • 앞에서 본 compareTo( ) 메서드를 클래스 내부로 캡슐화 하였다.
  • 이 클래스는 불변으로 설계했다.

MyInteger 클래스는 단순한 데이터 조각인 int를 내부에 품고, 메서드를 통해 다양한 기능을 추가했습니다.

덕분에 데이터 조각에 불가한 int를 MyInteger를 통해 객체로 다룰 수 있게 되었습니다.

public class MyIntegerMethodMain1 {

    public static void main(String[] args) {
        MyInteger myInteger = new MyInteger(10);

        int i1 = myInteger.compareTo(5);
        int i2 = myInteger.compareTo(10);
        int i3 = myInteger.compareTo(20);

        System.out.println("i1 = " + i1);
        System.out.println("i2 = " + i2);
        System.out.println("i3 = " + i3);
    }
}
  • myInteger.compareTo( ) 는 자기 자신의 값을 외부의 값과 비교한다.
  • MyInteger는 객체이므로 자신이 가진 메서드를 편리하게 호출할 수 있다.
  • 참고로 int는 기본형이기 때문에 스스로 메서드를 가질 수 없다.

 

기본형과 null

 

기본형은 항상 값을 가져야 합니다. 하지만 때로는 데이터가 없음 이라는 상태가 필요할 수 있습니다.

public class MyIntegerNullMain0 {

    public static void main(String[] args) {
        int[] intArr = {-1, 0, 1, 2, 3};
        System.out.println(findValue(intArr, -1)); // -1
        System.out.println(findValue(intArr, 0)); // 0
        System.out.println(findValue(intArr, 1)); // 1
        System.out.println(findValue(intArr, 100)); // -1
    }
    public static int findValue(int[] intArr, int target) {
        for (int value : intArr) {
            if (value == target) {
                return value;
            }
        }
        return -1;
    }
}
  • findValue( )는 배열에 찾는 값이 있으면 해당 값을 반환하고, 찾는 값이 없으면 -1을 반환한다.
  • findValue( )는 결과로 int를 반환한다. int와 같은 기본형은 항상 값이 있어야 한다.
  • 여기서도 값을 찾지 못한다면 숫자 중에 하나를 반환해야 하는데 보통 -1 또는 0을 사용한다.
-1
0
1
-1
  • 실행 결과를 보면 입력값이 -1일 때 -1을 반환한다. 그런데 배열에 없는 값인 100을 입력해도 같은 -1을 반환한다.
  • 입력값이 -1 일 때를 생각해보면, 배열에 -1값이 있어서 -1을 반환한 것인지, 아니면 -1이 없어서 -1을 반환한 것인지 명확하지 않다.

 

객체의 경우에는 데이터가 없다는 null 값이 명확히 존재합니다.

public class MyIntegerNullMain1 {

    public static void main(String[] args) {
        MyInteger[] intArr = {new MyInteger(-1), new MyInteger(0)
                , new MyInteger(1), new MyInteger(-1)};
        System.out.println(findValue(intArr, -1)); // -1
        System.out.println(findValue(intArr, 0)); // 0
        System.out.println(findValue(intArr, 1)); // 1
        System.out.println(findValue(intArr, 100)); // -1
    }
    public static MyInteger findValue(MyInteger[] intArr, int target) {
        for (MyInteger myInteger : intArr) {
            if (myInteger.getValue() == target) {
                return myInteger;
            }
        }
        return null;
    }
}
-1
0
1
null
  • 앞서 만든 MyInterger 래퍼 클래스를 사용했다.
  • 참조 값을 리턴하지만 MyInteger에 오버라이딩한 toString( ) 에 의해 String.valueOf(value)이 반환된다.
  • 실행 결과를 보면 -1을 입력했을 때는 -1을 반환한다.
  • 100을 입력했을 때는 값이 없다는 null을 반환한다.

기본형은 항상 값이 존재해야 합니다. 숫자의 경우 0, -1 같은 값이라도 항상 존재해야 합니다.

반면에 객체인 참조형은 값이 없다는 null을 사용할 수 있습니다.

물론 null 값을 반환하는 경우 잘못하면 nullPointerException이 발생할 수 있기 때문에 주의해서 사용해야 합니다.

 

 

래퍼 클래스

 

래퍼 클래스는 기본형을 객체로 감싸서 더 편리하게 사용하도록 도와주기 때문에 상당히 유용합니다.

쉽게 이야기해서 래퍼 클래스는 기본형의 객체 버전인 것 입니다.

 

자바는 기본형에 대응하는 래퍼 클래스를 기본으로 제공합니다.

  • byte → Byte
  • short → Short
  • int → Integer
  • long → Long
  • float → Float
  • double → Double
  • char → Character
  • boolean → Boolean

그리고 자바가 제공하는 기본 래퍼 클래스는 다음과 같은 특징을 가지고 있습니다.

  • 불변이다.
  • equals 로 비교해야 한다.
public class WrapperClassMain {

    public static void main(String[] args) {
        Integer newInteger = new Integer(10); // 미래에 삭제 예정, 대신에 valueOf() 사용
        //Integer newInteger = Integer.valueOf(10); // -128 ~ 127 자주 사용하는 숫자 값 재사용(like 문자열 풀)
        Integer integerObj = Integer.valueOf(10); // -128 ~ 127 자주 사용하는 숫자 값 재사용, 불변
        Long longObj = Long.valueOf(100);
        Double doubleObj = Double.valueOf(10.5);
        System.out.println("integerObj = " + integerObj);
        System.out.println("longObj = " + longObj);
        System.out.println("doubleObj = " + doubleObj);

        System.out.println("내부 값 읽기");
        int intValue = integerObj.intValue();
        System.out.println("intValue = " + intValue);
        long longValue = longObj.longValue();
        System.out.println("longValue = " + longValue);

        System.out.println("비교");
        System.out.println("==: " + (newInteger == integerObj));
        System.out.println("equals: " + (newInteger.equals(integerObj)));
    }
}
integerObj = 10
longObj = 100
doubleObj = 10.5
내부 값 읽기
intValue = 10
longValue = 100
비교
==: false
equals: true

 

래퍼 클래스 생성 - 박싱(Boxing)

  • 기본형을 래퍼클래스로 변경하는 것을 마치 박스에 물건을 넣은 것 같다고 해서 박싱(Boxing)이라 한다.
  • new Integer(10)은 직접 사용하면 안된다. 작동은 하지만, 향후에 자바에서 제거될 예정이다.
  • 대신에 Integer.valueOf(10)를 사용하면 된다.
  • 내부에서 new Integer(10)을 사용해서 객체를 생성하고 돌려준다.
  • 추가로 Integer.valueOf( ) 에는 성능 최적화 기능이 있다. 개발자들이 일반적으로 자주 사용하는 -128 ~ 127 범위의 Integer 클래스를 미리 생성해준다. 해당 범위의 값을 조회하면 미리 생성된 Integer 객체를 반환한다. 해당 범위의 값이 없으면 new Integer( ) 를 호출한다. 그래서 -128 ~ 127 의 값을 가진 객체를 생성했을 때 == 을 사용하면 동일하다고 나온다.
  • 마치 문자열 풀과 비슷하게 자주 사용하는 숫자를 미리 생성해두고 재사용한다.

intValue( ) - 언박싱(Unboxing)

  • 래퍼 클래스에 들어있는 기본형 값을 다시 꺼내는 메서드이다.
  • 박스에 들어있는 물건을 꺼내는 것 같다고 해서 언박싱(Unboxing) 이라 한다.

비교는 equals( ) 사용

  • 래퍼 클래스는 객체이기 때문에 == 비교를 하면 인스턴스의 참조값을 비교한다.
  • 래퍼 클래스는 내부의 값을 비교하도록 equlas( )를 재정의 해두었다. 따라서 값을 비교하려면 equlas( )를 사용해야 한다.

참고로 래퍼 클래스는 객체를 그대로 출력해도 내부에 있는 값을 문자로 출력하도록 toString( )을 재정의 해두었다.

 

 

래퍼 클래스 - 오토 박싱

자바에서 int를 Integer로 변환하거나, Integer를 int로 변환하는 부분

  • vauleOf( ) : 박싱
  • intValue( ) : 언박싱
public class AutoBoxing {

    public static void main(String[] args) {
        // Primitive Type -> Wrapper
        int value = 7;
        Integer boxedValue = Integer.valueOf(value);

        // Wrapper -> Primitive Type
        int unboxedValue = boxedValue.intValue();

        System.out.println("boxedValue = " + boxedValue);
        System.out.println("unboxedValue = " + unboxedValue);
    }
}

 

개발자들이 오랜기간 개발을 하다 보니 기본형을 래퍼 클래스로 변환하거나 또는 래퍼 클래스를 기본형으로 변환하는

일이 자주 발생했습니다. 그래서 많은 개발자들이 불편을 호소함에 따라 자바는 이런 문제를 해결하기 위해 자바 1.5 부터 

오토 박싱(Auto-Boxing), 오토 언박싱(Auto-Unboxing)을 지원하고 있습니다.

 

오토 방식, 언박싱

public class AutoBoxingMain2 {

    public static void main(String[] args) {
        // Primitive Type -> Wrapper
        int value = 7;
        Integer boxedValue = value; // 오토 박싱(Auto-boxing)

        // Wrapper -> Primitive Type
        int unboxedValue = boxedValue; // 오토 언박싱(Auto-unboxing)

        System.out.println("boxedValue = " + boxedValue);
        System.out.println("unboxedValue = " + unboxedValue);
    }
}
  • 오토 박싱과 오토 언박싱은 컴파일러가 개발자 대신 valueOf, xxxValue( ) 등의 코드를 추가해주는 기능이다.
  • 덕분에 기본형과 래퍼형을 서로 편리하게 변환할 수 있다.
Integer boxedValue = value; // 오토 박싱(Auto-boxing)
Integer boxedValue = Integer.valueOf(value); // 컴파일 단계에서 추가

int unboxedValue = boxedValue; // 오토 언박싱(Auto-unboxing)
int unboxedValue = boxedValue.intValue(); // 컴파일 단계에서 추가

 

 

레퍼 클래스 - 주요 메서드와 기능

 

public class WrapperUtilsMain {

    public static void main(String[] args) {
        Integer i1 = Integer.valueOf(10); // 숫자, 래퍼 객체 반환
        Integer i2 = Integer.valueOf("10"); // 문자열, 래퍼 객체 반환
        int intValue = Integer.parseInt("10"); // 문자열 전용, 기본형 반환

        //비교
        int compareResult = i1.compareTo(20);
        System.out.println("compareResult = " + compareResult);

        //산술 연산
        System.out.println("sum: " + Integer.sum(10, 20));
        System.out.println("min: " + Integer.min(10, 20));
        System.out.println("max: " + Integer.max(10, 20));
    }
compareResult = -1
sum: 30
min: 10
max: 20
  • valueOf( ) : 래퍼 타입을 반환한다. 숫자, 문자열 모두 지원한다.
  • parseInt( ) : 문자열을 기본형으로 변환한다.
  • compareTo( ) : 내 값과 인수로 넘어온 값을 비교한다. 내 값이 크면 1, 같으면 0, 작으면 -1을 반환한다.
  • Integer.sum( ), Integer.min( ), Integer.max( ) : static 메서드이다. 간단한 덧셈, 작은 값, 큰 값 연산을 수행한다.

 

parseInt( ) vs valueOf( )

  • vauleOf("10") 은 래퍼 타입을 반환한다.
  • parseInt("10") 은 기본형을 반환한다.
  • Long.parseLong( ) 처럼 각 타입에 parseXxx( ) 가 존재한다.

 

 

래퍼 클래스와 성능

래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공합니다.

그렇다면 더 좋은 래퍼 클래스만 제공하면 되지 기본형을 제공하는 이유는 무엇일까요?

public class WrapperVsPrimitive {

    public static void main(String[] args) {
        int iterations = 1_000_000_000; // 반복 횟수 설정, 10억
        long startTime, endTime;

        // 기본형 long 사용
        long sumPrimitive = 0;
        startTime = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            sumPrimitive += i;
        }
        endTime = System.currentTimeMillis();
        System.out.println("sumPrimitive = " + sumPrimitive);
        System.out.println("기본 자료형 long 실행 시간: " + (endTime - startTime) + "ms");
        
        // 래퍼 클래스 Long 사용
        Long sumWrapper = 0L;
        startTime = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            sumWrapper += i; //오토 박싱 발생
        }
        endTime = System.currentTimeMillis();
        System.out.println("sumWrapper = " + sumWrapper);
        System.out.println("래퍼 클래스 Long 실행 시간: " + (endTime - startTime) + "ms");
    }
}
sumPrimitive = 499999999500000000
기본 자료형 long 실행 시간: 443ms
sumWrapper = 499999999500000000
래퍼 클래스 Long 실행 시간: 4134ms
  • 기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지한다. 예를 들어 int는 보통 4바이트의 메모리를 사용한다.
  • 래퍼 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용한다. 자바 버전과 시스템마다 다르지만 대략 8~16 바이트의 메모리를 추가로 사용한다.

기본형 or 래퍼 클래스 ?

  • CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만~수십만 이상 연속해서 연산을 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려하자.
  • 그렇지 않은 일반적인 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 된다.

 

유지보수 vs 최적화

 

유지보수와 최적화를 고려해야 하는 상황이라면 유지보수 하기 좋은 코드를 먼저 고민해야 한다. 특히 최신 컴퓨터는 매우

빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많다.

  • 코드 변경 없이 성능 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단숨함 보다는 복잡함을 요구한다. 최적화를 위해 유지보수 해야하는 코드가 더 늘어날 것이다. 그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있다.
  • 특히 웹 어플리케이션의 경우 메모리 안에서 발생하는 연산보다 네크워크 호출 한 번이 많게는 수십만배 더 오래 걸린다. 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것 보다, 네크워크 호출 한번을 더 줄이는 것이 더 효과적인 경우가 많다.
  • 권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것이다.

 

'Java' 카테고리의 다른 글

[Java] System 클래스  (0) 2024.04.11
[Java] Class 클래스??  (0) 2024.04.05
[Java] 가변 String  (1) 2024.04.04
[Java] String 클래스 주요 메서드를 살펴보자!  (0) 2024.04.02
[Java] String 클래스가 불변 객체 라고?  (0) 2024.04.02