java, 타입 변환(promotion, casting)과 규모의 손실(Loss of magnitude)

타입 변환

  • 타입 변환이란 처음 선언되고 초기화된 데이터 타입을 새로운 타입으로 변환하고 새로운 변수에 할당하는 행위를 의미한다.
  • 타입 변환은 기본타입과 참조타입 모두 가능하다. 현재 글에서는 기본 타입을 기준으로 설명한다.

자동 타입 변환 (Promotion)

정수 간 변환

  • 리터럴 중 정수 타입을 길이를 기준으로 정렬하면 다음과 같다.
  • byte(1byte) < short(2byte) < int(4byte) < long(8byte).
  • 네 가지의 데이타 타입은 모두 정수이며 길이의 차이만을 가진다. byte가 short으로 변환이란 더 큰 용량에 옮기는 행위이다. 문제가 발생할 수 없다. 이런 경우를 자동 타입 변환이라 하며 특정한 코드를 작성할 필요가 없다.
Byte byte1 = 10; 
int int1 = byte1; // 10
double do2 = byte1; // 10.0

정수에서 실수로의 변환

  • 정수를 실수로 변경할 경우 자동 타입 변환이 가능하다. 다만 초과된 값에 대해서는 지수로 표현한다.
  • 아래의 코드는 컴파일 에러 없이 동작한다.
byte b = Byte.MAX_VALUE;
float bToF = b;
double bToD = b;

short s = Short.MAX_VALUE;
float sToF = s;
double sToD = s;

int i = Integer.MAX_VALUE;
float iToF = i;
double iToD = i;

long l = Long.MAX_VALUE;
float lToF = l;
double lToD = l;
  • 한편 위의 변환은 의문을 가진다. 왜냐하면 일정 범위를 넘어서는 float과 double은 지수로 표현하기 때문이다. 그 말은 long의 온전한 값을 변환하지 못한다는 의미와 같다. 실제로 아래와 같이 테스트가 가능하다.
// import static org.assertj.core.api.Assertions.assertThat;

Long l1 = 9223372036854775807L;
Long l2 = 9223372036854775806L;
assertThat(l1).isNotEqualTo(l2);

Double d1 = l1.doubleValue();
Double d2 = l2.doubleValue();
assertThat(d1).isEqualTo(d2);

long ll1 = d1.longValue();
long ll2 = d2.longValue();
assertThat(ll1).isEqualTo(ll2);
  • 위와 같이 동작하는 이유는 무엇일까? 이러한 경우는 강제 타입 변환을 해야하는 것 아닐까?
  • 규모의 손실(Loss of magnitude)은 캐스팅을 강제할 필요가 있지만 정밀도의 손실(Loss of precision)은 수용 가능하다고 판단했기 때문이다. 실제로 Long의 최대값은 float이 표현할 수 있는 범위(규모) 이내이다. 구체적인 내용은 아래 링크를 참고하자.
  • https://stackoverflow.com/questions/1293819/why-does-java-implicitly-without-cast-convert-a-long-to-a-float

강제 타입 변환 (Casting)

정수 간 변환

  • 자동 타입 변환과 강제 타입 변환은 값이 변경될 경우 사용한다.
  • 길이가 긴 타입을 그것보다 짧은 타입으로 변경하거나, 실수를 정수로 변경할 때 발생한다.
long long2 = 99999999999L;
int int2 = (int) long2; ...........(1)
long long3 = 1234L;
int int3 = (int) long3; ............(2)
  • (1) long2의 값이 int의 가용범위를 넘어서기 때문에 기존의 값을 잃어버렸다.
  • (2) long2의 값이 int의 가용범위를 넘지 않기 때문에 기존의 값을 보존했다.
  • (1)의 경우 데이터가 손실되는 치명적인 문제가 발생한다. 이러한 문제를 해소하기 위한 장치를 마련해야 한다. 해당 코드는 아래와 같다.
long long4 = 99999999999999999L;

if(long4>Integer.MAX_VALUE || long4<Integer.MIN_VALUE){
    System.out.println("long 변수의 값이 int가 저장할 수 있는 범위를 초과하여 변경할 수 없습니다.");
}else{
    int int4 = (int) long4;
    System.out.println("해당 변수가 다음과 같이 변경되었습니다 : " + int4);
}

실수에서 정수로의 변환

  • 실수에서 정수는 강제 타입 변환을 해야 한다.
double dou5 = 10.123; // 10 
int int5 = (int) dou5; // 10 
  • 규모의 손실과 정밀도의 손실은 실수에서 정수로 변환할 때 잘 드러난다.
// 여기서는 큰 문제가 없어 보인다.
float f1 = 1f;
long f1ToL = (long) f1;

// float이 표현할 수 있는 숫자의 규모는 long이 표현 범위를 아득히 넘어선다. 
// 그러므로 실수를 자연수로 변환할 때는 반드시 강제 타입 변환이 필요하다.
float f2 = Float.MAX_VALUE; // 3.4028235E38
long f2ToL = (long) f2; // 9223372036854775807
long maxL = Long.MAX_VALUE; // 9223372036854775807

연산에서의 타입 변환

  • 정수타입을 연산할 때 사용되는 가장 작은 타입은 int이다.
  • byte와 short의 경우 int로 자동 타입 변환한 후 연산한다. 결과값 역시 int이다.
  • 그러므로 연산 시 byte와 char를 사용하는 것은 성능 상 지양한다.
byte by1 = 23;
byte by2 = 10;

int sum2 = by1 + by2; ..........(1)
byte sum1 = (byte) (by1 + by2); ........(2) 
  • (1) byte 간 연산한 결과의 값은 int 타입이므로 캐스팅을 필요로 하지 않는다.
  • (2) int의 결과값을 byte로 출력하기 위해서는 캐스팅을 해야 한다.

  • 정수 타입과 실수 타입 간 연산 시 그 결과는 실수 타입으로 자동 변환된다.
int intA = 10;
double douA = 5.5;
double douSum = intA + douA; // 15.5
int intSum = intA + (int)douA; // 15

char의 타입 변환

  • char는 숫자와 문자로 변환 가능하다.
char var1 = 'a';
char var2 = 97;
System.out.println(var1==var2); // true