자바 기초 공부를 하던 중 정확한 이해를 위해 직접 손으로 써 본 부분에 대한 기록이다.
C언어에는 unint8_t 타입이 있는데, 이 타입은 1byte 크기를 가지면서 0~255 값의 범위를 가진다. C 프로그램이 uint8_t 타입 136을 2진수로 보내면 자바는 2진수를 -120으로 읽게 된다. 그 이유는 자바는 최상위 비트가 1이면 음수로 인식하기 때문이다. -120을 복원하고 싶다면 -120과 255를 비트 논리곱(&) 연산을 수행하면 된다.
신용권, 임경균, 『이것이 자바다 』, 한빛미디어(2023), p120-121.
위 인용 파트가 잘 이해되지 않아서 직접 136과 -120을 2진수로, 그리고 다시 10진수로 변환해 보며 이해하려고 했다. 첫 번째로 136을 2진수로 변환하면 다음과 같이 나온다.
10진수 136 ▶ 2진수 10001000
1byte가 8bit이므로 크기에 딱 맞게 8자리를 모두 사용한다. 여기에서 문제가 발생하게 되는 것이다. 위 인용문에서 언급한것과 같이 최상위 비트가 1이므로 자바에서는 음수로 인식하게 된다.
여기서 음수로 인식하는 과정이 나에게는 조금 복잡했는데, 이때까지 1의 보수까지만 알고 있었기 때문인 것 같다. 따라서 2진수 10001000을 음수인 10진수로 다시 변환하기 전, 컴퓨터에서 음수인 10진수가 2진수로 변환되는 과정을 보이고자 한다. 이때는 2의 보수를 사용하는데, 2의 보수는 1의 보수를 적용한 후 마지막에 1을 더해주는 것이다. 굳이 1의 보수 대신 더 복잡해 보이는 2의 보수를 사용하는 이유는 자리올림(carry)으로 인해 발생하는 문제를 해결하기 위함이라고 한다. 여기서는 중요한 내용이 아니므로 설명은 생략한다.
그러면 -67을 예로 들어서 10진수를 2진수로 변환하는 과정을 설명하겠다. 총 세단계를 거칠 것인데, 우선 양수라고 가정하고 67을 2진수로 변환해 주고, 그 값에 1의 보수를 취해준 후, 마지막으로 1을 더해주는 것으로 마무리한다.
(0단계: 10진수 숫자) -67
(1단계: 2진수로) -01000011
(2단계: 1의 보수) 1011100
(3단계: 1 더하기) 1011101
결국 -67이 2진수 101101로 표현되는 것을 볼 수 있다. 그럼 이제 똑같은 과정을 반대로 거쳐 2진수 10001000을 10진수로 변환해줄 수 있을 것이다.
(0단계: 2진수 숫자) 10001000
(1단계: 1 빼기) 10000111
(2단계: 1의 보수 풀어주기) -01111000
(3단계: 10진수로) -120
이것이 자바에서 10001000으로 받은 136이 -120으로 변환되어 해석되는 이유이다.
그렇다면 이것을 어떻게 올바르게, 즉 C프로그램에서 받은 것과 같은 숫자로 해석해줄 수 있을까? 바로 비트 논리곱 연산을 이용해주면 된다. C프로그램에서 uint8_t 티입의 최댓값인 255와 비트 논리곱 연산을 수행하게 되면, 원래값인 136을 얻을 수 있다. 왜냐하면 자바에서는 10001000이 음수로 인식되고, 보수로 처리되어 1byte 외 앞 영역이 모두 1로 표현될 테지만, C프로그램에서의 원래 값은 앞 영역이 모두 0으로 표현되어 있었을 것이기 때문이다. 즉 아래표와 같은 차이점이 있다.
C프로그램에서 의도한 2진수 값 | 00000000 00000000 00000000 10001000 |
자바 프로그램에서 해석한 2진수 값 | 11111111 11111111 11111111 10001000 |
따라서 255와 비트 논리곱 연산을 수행하면 C프로그램에서 원래 의도한 2진수 값으 도출할 수 있다.
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 0 0 (-120)
& 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 (255)
= 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 (136)
하지만 위 과정들은 단순 이해를 위한 과정이고, 자바는 개발자 편의를 위해 Byte.toUnsignedInt()를 제공하기 때문에 이를 사용해서 간편하게 아래와 같은 코드로 해결이 가능하다.
byte receiveData = -120;
int unsignedInt = Byte.toUnsignedInt(receiveData); // 136
이와 비슷한 예로 비트 논리 부정을 어떻게 손으로 계산할 수 있는지를 보겠다. ~45가 왜 -46이 되는지에 대한 과정이다.
첫번째로 ~45를 2진수로 표현한다. ~는 논리 부정(NOT), 즉 보수에 해당하므로, 45를 2진수로 바꾸고 이에 보수를 취해주는 것이 그 과정이 된다.
(0단계: 10진수 숫자) ~45
(1단계: 45를 2진수로) ~00101101
(2단계: 보수 적용) 11010010
이 과정을 거치고 나면 ~45의 이진수 표현이 11010010 이라는 것을 알 수 있는데, 앞서 언급한 것처럼 자바는 최상위비트가 1이면 그 수를 음수로 인식한다. 따라서 ~45 역시 음수로 인식되는 것이다. 그렇다면 11010010을 다시 음수인 십진수로 변환해보겠다.
(0단계: 2진수 숫자) 11010010
(1단계: 1 빼기) 11010001
(2단계: 1의 보수 풀어주기) -00101110
(3단계: 10진수로) -46
결과적으로 ~45가 -46이 되는 것을 알 수 있다.
비슷한 내용을 이용해서 비트 이동 연산자 >>와 >>>의 차이를 알아보겠다. 비트 이동 연산자 >>와 >>>는 모두 연산자 왼쪽 정수의 각 비트를 오른쪽 정수만큼 오른쪽으로 이동시킨다는 공통점이 있지만, >> 연산자의 경우 왼쪽 빈자리르 최상위 부호 비트와 같은 값으로 채우는 반면, >>> 연산자의 경우 왼쪽 빈자리를 0으로 채운다는 차이점이 있다. 이 차이점이 실제 결괏값에 어떤 영향을 미치는지 알아보려 한다.
-8을 3만큼 오른쪽으로 비트 이동시키는 경우를 예로 들어 차이를 보겠다. 우선 -8을 위와 같은 과정으로 이진수로 나타내주겠다. 여기서 -8이 int type 변수에 저장되어 있다는 가정 하에 연산을 진행한다.
(0단계: 10진수 숫자) -8
(1단계: 2진수로) -00000000 00000000 00000000 00001000
(2단계: 1의 보수) 11111111 11111111 11111111 11110111
(3단계: 1 더하기) 11111111 11111111 11111111 11111000
위 과정에 따라, -8을 4byte의 이진수로 표현하면 11111111 11111111 11111111 11111000가 된다. 이를 바탕으로 각각 >> 연산자와 >>> 연산자를 이용한 연산을 진행해주겠다.
첫 번째로 >> 연산자를 이용한 연산을 진행해주었다.
-8 >> 3
이진수 결괏값: 11111111 11111111 11111111 11111111
십진수 결괏값:
(0단계: 2진수 숫자) 11111111 11111111 11111111 11111111
(1단계: 1 빼기) 11111111 11111111 11111111 11111110
(2단계: 1의 보수 풀어주기) -00000000 00000000 00000000 00000001
(3단계: 10진수로) -1
위와 같이 -8 >> 3을 해준 결과 -1이 도출되는 것을 볼 수 있다. 이진수 결괏값에서 3만큼 오른쪽으로 이동을 할 때 왼쪽 빈칸을 1로 채웠기 때문에 얻을 수 있는 결과이다.
두 번째로 >>> 연산자를 이용한 연산을 진행해주었다.
-8 >>> 3
이진수 결괏값: 00011111 11111111 11111111 11111111
십진수 결괏값:
(0단계: 2진수 숫자) 00011111 11111111 11111111 11111111(1단계: 1 빼기) 00011111 11111111 11111111 11111110(2단계: 1의 보수 풀어주기) -11100000 00000000 00000000 00000001
(3단계: 10진수로) 536870911
위와 같이 이전 값과는 완전히 다른 536870911이 도출되는 것을 알 수 있다. 심지어는 양수값이 도출된 것을 볼 수 있는데, 그 이유는 2단계에서 -부호가 이미 빠졌지만 맨 앞자리가 다시 1이 되면서 음수가 되기 때문에 다시 양수로 변환되는 것이다. 사실 이런 복잡한 설명 필요 없이, 이미 0단계에서 2진수 숫자의 최상위 비트가 0이 되었기 때문에 1단계, 2단계 없이 바로 양수 십진수 숫자로 변환해주면 된다.
이로서 >>와 >>>가 어떤 숫자로 왼쪽 빈자리를 채우느냐에 따른 엄청난 차이를 볼 수 있다.
'개발 언어 및 알고리즘 기초 > JAVA 기초' 카테고리의 다른 글
[Java] 스트림(Stream) 요소 처리 (0) | 2024.04.25 |
---|---|
[Java] Wrapper Class (래퍼 클래스, 포장 클래스) (0) | 2024.04.18 |
[JAVA] StringBuilder로 문자열 조작하기 (0) | 2024.04.17 |
[JAVA] Collection Framework 정리 (0) | 2024.04.12 |
[JAVA] Thread 상태 (0) | 2024.04.10 |