본문 바로가기

Programing/JVM(Java, Kotlin)

[이럴수가] 바이트 배열을 숫자로 바꾸다 발견한 점

우선 퀴즈, 다음 코드를 실행했을 때 어떤 글자가 화면에 출력될까?

class Test {

public static void main(String[] args) {

byte b = (byte)0xe9;

int i1 = b;

int i2 = b & 0xff;

System.out.println(i1);

System.out.println(i2);

}

}


정답은 아래에서 확인해보고...


아래서 확인한 내용과 머리로 생각했던 결과가 다르다면 이 글을 읽어보아라.


이런 현상은 왜 나타나게 된 것일까?

byte 배열을 정수로 바꾸는 기능을 java.nio.ByteBuffer를 이용해서 구현을 했다.

import java.nio.ByteBuffer;


class ByteToInteger {

public static int ToInt(byte[] source) {

return ByteBuffer.wrap(source).getInt();

}


public static void main(String[] args) {

byte[] src = { 0x01, 0x02, 0x03, 0x04 };    // 0x01020304는 십진수로 16,909,060 이다.

int output = ToInt(src);

System.out.println(output);                    // 16909060

}

}

그런데 문제가 생겼다. 코드가 동작하는 환경이 Java 1.4 ME(Micro Edition)이었던 것이다. 보통 개발환경은 SE(Standard Edition)에서 진행되는데 ME에는 java.nio.ByteBuffer 클래스가 없었는지 ClassNotFound 예외가 발생했다.


그래서 해당 기능을 직접 구현하게 되었다.


byte[4] -> int 로 바꾸는 알고리즘은 아래와 같다.


JVM에서는 int가 빅엔디안이므로 그냥 순서대로 shift 연산을 통해 32비트 정수로 만들어주면 되는 것이었다.

코드로 나타내면 아래와 같다.

public static int ToInt(byte[] source) {

int result =

source[0] << 24 |

source[1] << 16 |

source[2] << 8 |

source[3];

return result;

}

하지만 이 코드에는 심각한 문제가 숨어있다.

입력 배열의 각 단위의 값이 128 (0x80)을 넘으면 발생한다.

예를들어, 배열 { 0x01, 0x02, 0x03, 0xe4 } 는 십진수로 16909284라는 값이다.

하지만 위의 코드로 돌려보면 -28이라는 엉뚱한 숫자가 나온다.


사실 배열에 128 이상의 값을 넣으려고 하면 에러가 발생한다.

ByteToInteger.java:14: error: possible loss of precision

                byte[] src = { 0x01, 0x02, 0x03, 0xe4 };

                                                            ^

  required: byte

  found:    int

1 error


자바에서는 unsigned와 같이 부호 없는 타입이 별도로 없기 때문에 발생하는 일이다.

따라서 byte는 -128 ~ 127까지의 범위를 갖는다. 0xe4는 228라는 값으로 표현 범위를 넘어서는 일이다.

그래서 자바에서는 228이라는 타입을 byte가 아닌 int로 간주한다.

byte에 128~256까지를 넣기 위해서는 강제 형 변환을 사용해야 한다. (아래와 같이)

byte[] src = { 0x01, 0x02, 0x03, (byte)0xe4 };


하지만 이렇게 들어간 값을 출력을 해보면 음수로 나옴을 알 수 있다.

public static void main(String[] args) {

byte b = (byte)0xe4;

System.out.println(b);    // -28이 출력된다.

}


그러면 올바로 동작하는 코드를 만들기 위해서는 어떻게 해야 할까?

아래와 같이 byte를 int로 형변환을 해주어야 할까?

public static int ToInt(byte[] source) {

int result =

source[0] << 24 |

((int)source[1]) << 16 |

((int)source[2]) << 8 |

((int)source[3]);

return result;

}

접근 방법은 맞지만 구현 방법은 틀렸다. 왜냐하면 byte의 제일 왼쪽 비트, 즉 MSB가 1로 설정이 되어 있기에 음수가 나오는 것이다. 제일 왼쪽 1비트를 사인(sign)비트가 아닌 데이터 자체로써 인식 시켜야 한다.

방법으로는 0xff라는 일종의 111111111로 되어있는 비트 마스크를 통해 int로 형변환을 발생하게 하는 트릭이 있다.


수정한 전체 코드는 아래와 같다.

class ByteToInteger {

public static int ToInt(byte[] source) {

int result =

source[0] << 24 |

(source[1] & 0xff) << 16 |

(source[2] & 0xff) << 8 |

(source[3] & 0xff);

return result;

}


public static void main(String[] args) {

byte[] src = { 0x01, 0x02, 0x03, (byte)0xe4 };

int output = ToInt(src);

System.out.println(output);

}

}

한 가지 주의해야 할 점은 제일 왼쪽비트가 있는 좌로 24 번 시프트(shift) 연산을 하는 source[0]에 대해서는 sign bit를 살려두고 있다는 것이다. 이렇게 안하면 음~양이 표시가 안되기 때문이다.


최초 문제 답)