타입 변환 (Type Conversion)
하나의 타입을 다른 타입으로 바꾸는 것을 타입 변환 혹은 형변환 이라고 한다. 예를 들어 byte 타입을 int 타입으로 변환하거나 반대로 int 타입을 byte 타입으로 변환하는 행위를 말한다.
프로그램에서 값의 대입이나 연산을 수행할 때는 같은 타입끼리만 가능하다. 그래서 같은 정수라도 타입이 다르면 연산을 수행하기 전에 같은 타입으로 만들어야 하기 때문에 형변환 작업이 필요한 것이다.
타입 변환에도 규칙이 있는데, 메모리에 할당받은 바이트의 크기가 상대적으로 작은 타입에서 큰 타입으로의 타입 변환은 생략할 수 있다. 하지만 메모리에 할당받은 바이트의 크기가 큰 타입에서 작은 타입으로의 타입 변환은 데이터의 손실이 발생하게 된다. (용량이 다르니까)
따라서 상대적으로 바이트의 크기가 작은 타입으로 타입 변환을 할 경우 자바 컴파일러는 오류를 발생시킨다.
short small_bowl; // 작은 그릇
int big_bowl; // 큰그릇
big_bowl = 200_000_000; // 2억
small_bowl = (short) big_bowl; // 큰 그릇을 작은 그릇에 강제로 물을 옮긴다.
System.out.println(small_bowl); // -15872 물이 넘쳐(데이터 손실) 이상한 값이 되었다.
small_bowl = 1000;
big_bowl = (int) small_bowl; // 작은 그릇의 물을 큰 그릇에 담는다.
System.out.println(big_bowl); // 1000 데이터 손실 없이 물을 잘 옮겨 담았다.
자바의 형변환 방법에는 크게 두가지 종류로 나뉜다.
개발자가 지정하지 않아도 자동적으로 이루어지는 자동(묵시적) 형변환과, 개발자가 명시해야만 이루어지는 강제(명시적) 형변환이 있다. 이 두 형변환 종류는 다른 자료형 간의 연산시 어떤 자료형 크기에 맞추느냐에 따라 사용처가 달라지게 된다.
- 낮은 자료형으로 맞출 시 : 낮은 자료형으로 강제 형변환
- 높은 자료형으로 맞출 시 : 높은 자료형으로 자동 형변환
자동 형변환 (Promotion)
자동 타입 변환은 묵시적 / 암시적 형변환 이라고도 불리우며, 프로그램 실행 도중에 컴파일러가 자동적으로 타입 변환이 일어나는 것을 말한다.
단, 작은 크기를 가지는 타입이 큰 크기를 가지는 타입에 저장될 때만 자동 타입 변환이 발생한다.
자바에서는 데이터의 손실이 발생하지 않거나, 데이터의 손실이 최소화되는 방향으로 자동 타입 변환을 진행하기 때문이다.
즉, 기존의 값을 최대한 보존할 수 있는 큰 타입으로 자동 형변환이 가능하다고 이해하면 된다.
예를 들어 byte 타입은 1byte 크기를 가지고, int 타입은 4byte 크기를 가지므로 int 타입이 큰 크기 타입이고, byte 타입이 작은 크기 타입이다.
즉, 아래 코드와 같이 서로 다른 타입의 데이터라도 낮은 자료형과 높은 자료형을 연산할 경우 자동으로 형변환이 되어 계산된다.
byte small_bowl = 10; // 작은 그릇
int big_bowl = 10000; // 큰그릇
int result = big_bowl - small_bowl; // small_bowl이 int형으로 자동 형변환 되어 int 끼리 계산된다.
System.out.println(result); // 9990
byte a = 32;
// 1 byte로 저장된 값을 2 byte로 변환
short b = a;
// 2 byte로 저장된 값을 4 byte로 변환
int c = b;
// 4 byte로 저장된 값을 8 byte로 변환
long d = c;
int a = 1234;
float b = a; // 작은 범위의 타입을 큰 타입으로 넣을 때는 자동으로 형변환을 컴파일러가 해줌
float c = 3.14f;
int d = c; // 에러!!! - 큰 범위의 타입을 작은 타입으로 하려면 자동 형변환 해주지 않는다. 값 손실 발생
char ch1 = 'a';
int charNumber = ch1; // 문자형은 int 타입으로 자동 형변환
System.out.println("charNumber : " + charNumber); // charNumber : 97
작은 타입과 큰 타입을 구분하는 기준은 각 데이터 타입의 메모리 크기(byte) 이다. 위의 사진과 같이 int타입은 메모리 크기가 4바이트고, short 타입은 2바이트이므로 int가 short보다 큰 타입이 되게 된다. 반면 long은 8바이트이므로 int가 long보다는 작은 타입이 된다.
long 과 float 타입 크기
위 그림에서 long 타입은 8바이트이고 float은 4바이트이기 때문에 따라서 float가 long 보다는 작은 타입이 되어야 하지만, 그림상 long 보다 float이 더 크다고 되어있다.
왜냐하면 일반적으로 메모리 설계상 정수 타입보다 실수 타입이 더 크게 되어 있기 때문이다.
다음 타입 크기 범위를 직접 비교해보자.
long 타입의 최대 크기인 9223372036854775808 와 float 타입의 최대 크기인 3.4 x 10^38 를 비교해 볼 것이다.
double a = 9223372036854775808.0; // 비교를 위해 실수로 표현
double b = 3.4 * (Math.pow(10, 38)); // 3.4 x 10^38
System.out.println(a); // 9.223372036854776 x 10^18
System.out.println(b); // 3.4 x 10^38
System.out.println(a < b); // true
지수 표현식으로 둘을 비교해 보니 꽤나 큰 차이를 보인다는 것을 확인 할 수 있다.
4바이트 float이 더 큰 수를 표현할수 있는 이유는 바로 부동소수점 방식으로 표현하기 때문이다.
부동 소수점 방식에서는 지수부와 가수부를 나누고, 가수부에는 실제 값을 지수부에는 2제곱을 얼마나 곱할지 표현하기 때문에 수의 표현 범위는 long 타입보다 더 커지게 되는 것이다.
char 와 byte 타입 크기
이외에도 타입 크기 비교에 있어 추가적인 예외가 존재한다.
char 타입과 byte 타입은 둘 다 정수형 타입이고, char타입은 2바이트의 크기, byte타입은 1바이트의 크기를 가진다.
※ 참고
char 타입은 문자 자료형이지만, 아스키 코드 숫자를 저장하기에 사실상 정수형 타입이다.
char 타입이 byte 타입보다 더 크니까 담을수 있어보이지만, 실제로 byte 타입을 변환하여 char타입에 저장할 수는 없다.
왜냐하면 char타입은 음수를 표현할 수 없기 때문이다.
char타입은 유니코드와 아스키코드를 표현할 목적을 가지고 있기 때문에 음수는 필요가 없어서 음수를 표현할 조합까지 양수를 더 많이 조합하는 것에 사용한다.
하지만 byte는 비록 표현할 수 있는 데이터 크기는 작아도 음수까지 표현하기 때문에, 음수를 표현할 수 없는 char에는 저장할 수 없는 것이다. (대신 short에는 담을수 있다)
연산식 자동 형변환
데이터 연산은 기본적으로 같은 타입의 피연산자 간에만 수행되기 때문에, 서로 다른 타입의 피연산자가 있을 경우 두 피연산자 중 크기가 큰 타입으로 자동 변환된 후 연산을 수행한다.
예를 들어 int 타입 피연산자와 double 타입 피연산자를 덧셈 연산하면, int타입이 먼저 double 타입으로 변환되고 연산이 수행되게 된다. 그리고 결과는 double 타입이 된다.
int num1 = 10;
double num2 = 11.52;
// num1이 double형으로 자동 변환되어 계산됨
double result = num1 + num2; // 21.52
자바는 피연산자를 4바이트 단위로 저장하기 때문에, 정수 연산일 경우 int 타입을 기본으로 하는 특징이 있다.
그래서 2바이트 char 타입이라도 둘이 연산을 하게되면 자동으로 int 타입으로 변환되어 계산 된다.
char ch1 = 'A';
char ch2 = 'B';
int result1 = ch1 + ch2; // 'A'의 유니코드 값과 'B'의 유니코드 값을 더한 값 저장
// char result2 = ch1 + ch2; - 에러 발생 !!!
byte byteValue1 = 10;
byte byteValue2 = 20;
// byte byteValue3 = byteValue1 + byteValue2; - 에러
int intValue1 = byteValue1 + byteValue2;
강제 형변환 (Casting)
강제 형변환은 명시적 형변환이라고 불리우며, 사용자가 타입 캐스트 연산자 (double)135 를 사용하여 값의 타입을 강제적으로 변환을 수행하는 것을 말한다.
작은 크기의 타입은 큰 크기의 타입으로 자동 형변환이 되지만, 반대로 큰 크기의 타입은 작은 크기의 타입으로 자동 타입 변환을 할 수 없다.
예를 들어, 4byte인 int타입을 1byte인 byte 타입에 담을 수 없다. 마치 큰 그릇의 물을 작은 그릇 안에 모두 넣을 수 없는 것과 동일한 이치이다. 큰 그릇에 있는 물의 양에 따라 물이 넘칠수도 있고 보존될 수도 있다. 마찬가지로 큰 데이터 타입에서 작은 데이터 타입으로 옮길 때 데이터의 손실이 발생할 수도 아닐수도 있다. 하지만 필요에 의해서 int타입을 byte 타입으로 처리해주어야 할 필요성이 생긴다면 강제 형변환을 고려해야 한다.
int num = 60000;
byte value = (byte)num; // 데이터 손실 발생
System.out.println(value); // 96
int num2 = 10;
byte value2 = (byte)num2; // 데이터 손실 발생하지 않음
System.out.println(value2); // 10
주의할 점은, 강제적인 동작이므로 만일 데이터의 손실이 일어난다면 정확한 연산을 수행할 수 없기 때문에 예상하지 못한 결과를 얻을 수 있다.
byte b = 100;
byte b = (byte)100; // byte의 범위는 -128 ~ 127이기 때문에 데이터 손실 없이 대입이 가능하고 컴파일러가 자동 형변환을 해준다.
---------------------------------------
int i = 100;
byte b = i; // 에러, 변수이기 떄문에 상수일 때랑 다르다. 컴파일러는 변수 안에 값을 알지 못하기 때문에
byte b = (byte)i;
---------------------------------------
byte b = 1000; // 에러, byte 타입의 범위를 넘었기 때문에 자동 형변환 x
byte b = (byte)1000; // b에 -24가 대입된다.
실수 타입을 정수 타입으로 강제 타입 변환하면 소수점 이하의 데이터들은 버려진다.
double dbNum = 12.3456;
int num = (int)dbNum; // 12
캐스트 연산자를 사용한 형변환은 일시적이기 때문에 피연산자의 자체 자료형에는 영향을 주지 않는다.
그리고 기본 자료형 중 boolean을 제외하면 모든 자료형은 형변환이 가능하다.
float 과 int 형변환 주의점
강제 타입 변환에서 데이터 자체 손실 외에도 또 다른 주의점이 있다. 정수 타입을 실수 타입으로 변환할 때 정밀도 손실을 피해야 한다. 다음 예제를 보자.
int num1 = 123456780;
int num2 = 123456780;
float num3 = num2;
System.out.println(num3); // 1.23456784 * E8
num2 = (int)num3;
System.out.println(num2); // 123456784
int result = num1 - num2;
System.out.println(result); // -4
int 타입 변수 num1과 num2에 동일한 123456780 값을 저장시키고, num2를 float 타입으로 변환시킨 후, 다시 int 타입으로 변환해서 num2에 저장했다. 그리고 num1에서 num2를 뺀 결과를 변수 result에 저장하고 콘솔에 출력한다.
동일한 값을 뺐기 때문에 당연히 0이 출력되어야 할 것이다. 하지만 실행 결과를 보면 엉뚱하게도 0이 나오질 않는다. 이러한 결과가 나온 이유는 int 값을 float 타입으로 자동 변환하면서 문제가 발생했기 때문이다.
float 타입은 다음과 같이 비트 수가 할당되어 있다.
int 값을 손실 없이 float 타입의 값으로 변환할 수 있으려면 가수 23비트로 표현 가능한 값이어야 한다. 하지만 가수(23bit)가 최대로 표현할수 있는 값은 16,777,215 이다. 때문에 123,456,780은 23비트로 표현할 수 없기 때문에 근사치로 변환된다.
즉 정밀도 손실이 발생한 것이다. 그래서 위 코드에서 float 값을 다시 int 타입으로 변환했어도 원래의 int 값을 얻지 못한 것이다.
이에 대한 해결책은 모든 int 값을 실수 타입으로 안전하게 변환시키는 double 타입을 사용하는 것이다. double 타입은 다음과 같이 가수부가 52비트나 할당되어 있으니 int형을 충분히 커버하고도 남는다.
int num1 = 123456780;
int num2 = 123456780;
double num3 = num2;
System.out.println(num3); // 1.2345678 * E8
num2 = (int)num3;
System.out.println(num2); // 123456780
int result = num1 - num2;
System.out.println(result); // 0
형변환 안전장치
강제 형변환의 문제점인 데이터 손실을 방지하기 위해, 자바는 코드에서 데이터 값을 검사하기 위한 모든 기본 타입에 대해 최대값(max)과 최소값(min)을 다음과 같이 상수로 제공하고 있다. (boolean과 char 타입 제외)
기본 타입 | 최대값 상수 | 최소값 상수 |
byte | Byte.MAX_VALUE | Byte.MIN_VALUE |
short | Short.MAX_VALUE | Short.MIN_VALUE |
int | Integer.MAX_VALUE | Integer.MIN_VALUE |
long | Long.MAX_VALUE | Long.MIN_VALUE |
float | Float.MAX_VALUE | Float.MIN_VALUE |
double | Double.MAX_VALUE | Double.MIN_VALUE |
이를 이용하여 어떤 정수값과 실수값을 다른 타입으로 변환하고자 할 때, 변환될 타입의 최소값과 최대값을 벗어나는지 검사하고, 만약 벗어난다면 타입 변환을 하지 않는 식으로 타입 변환 가드를 칠 수 있다.
int i = 128;
if( (i < Byte.MIN_VALUE) || (i > Byte.MAX_VALUE) ) {
System.out.println("byte 타입으로 변환 할 수 없음"); // 실행 (byte의 최대 크기는 127)
} else {
byte b = (byte) i;
System.out.println(b);
}
참고