오늘은 어디로 갈까...

숫자(Number) 0x00 본문

낙서

숫자(Number) 0x00

剛宇 2009. 2. 23. 21:00
 숫자 유틸리티를 만들어 보기로 하자.
 자바의 숫자형은 크게 정수형과 실수형으로 구분한다.

1. 정수형
 - 자바의 기본 정수형은 unsigned type이 존재하지 않는다. 그래서 해당 정수형의 바이트(byte)수에 의해서 -2(비트수-1) 부터 2^(비트수-1)-1의 범위를 가지게 된다. 비트(bit)수에서 1비트를 빼는 이유는, signed type 즉 부호 비트로 최상위 1비트를 사용하기 때문이고, 최대값에 -1을 해주는 이유는 0부터 시작하기 때문이다.
 byte  1byte  -128  ~ 127 (-27 ~ 27-1)
 short  2byte  -32768  ~ 32767 (-215 ~ 215-1)
 int  4byte  -2147483648 ~ 2147483647 (-231 ~ 231-1)
 long  8byte   -9223372036854775808 ~ 9223372036854775807 (-263 ~ 263-1)

 - 자, 아주 무서운 프로그램을 한번 만들어보자.
		for (byte b = 0x00; b < 0xff; b++) {
			System.out.println(b);
		}
 얼핏 보기에는 별 문제 없어 보이지만, 실행해보면 무한루프가 되어버린다. for 조건문이 b < 0xff 즉 256보다 작을때까지이다. 그런데 byte의 최대값은 127. 즉 영원히 돌아버리게 된다.
 byte형의 최대값 127에서 1을 더하면 어떻게 될까? 127을 비트로 표현해보면 01111111이 된다. 여기에 1을 더하면 10000000이 되는것이다. 그런데 맨앞자리는 부호비트라고 했다. 부호 비트가 0에서 1로 변하면서 +127에서 -128로 변해버린것이다. (부호 비트가 1일 경우 2의 보수를 취해야한다.)
 잠깐 산으로 가볼까....
 인간의 입장에서 보면 맨 앞자리가 부호 비트라 했으니 10000000 은 -0, 10000001은 -1로 생각하는게 더 쉽게 받아들여진다. (나만 그런가...) 그런데 갑자기 2의 보수란 놈이 나오다니!!! 이 보수란 놈은 컴퓨터가 바보(덧샘밖에 못한다는 소문이...)라서 나오게 된것이다.
 8비트도 너무 많으니 4비트의 숫자가 있다고 하자. 당연 최상위 비트는 부호 비트이다.
 인간답게 생각하면 2은 0010, -1은 1001이다.
 두 수를 더해볼까? 2 + (-1) = 1이다.
 컴퓨터는 비트 밖에 인식을 못하니 비트로 더해보자. 0010 + 1001 = 1011 이 나온다.
 답은 0000이 나와야하는데, 1011이라는 이상한 값이 나온다. 그래서 나온것이 1의 보수이다.
 1의 보수란, 음수 표현시에 0을 1로 1을 0으로 바꿔주는것이다.
 1의 보수로 변환한 다음 다시 계산해 보자. 감수를 1의 보수를 취한다.
 0010 + 1110(1의 보수로 변환한 값) = 1 0000 (올림수 1이 발생한다.)
 올림수가 발생하면 최하위수에 1을 더해준다. 즉, 0010 + 1110 = 0001이 된다.(원하던 1의 값이 나왔다.)
 만약, 올림수가 발생하지 않으면 더한 결과 값에 1의 보수를 취한 다음 부호를 -로 바꿔주면 된다.
 그런데 이 1의 보수에는 한가지 문제점이 있다. 1 + (-1)을 계산해보자.
 0001 + 1110 = 1111
 이번에는 이상한 1111 이라는 값이 나온다. 그 이유는 1의 보수가 0000, 1111 두가지의 0 값을 가지기 때문이다. 즉 +0, -0이 존재하는것이다. 4이 2개라니 정말 헷갈린다. 그래서 생겨난게 2의 보수이다.
 2의 보수란 0을 1로, 1을 0으로 바꾼다음 1을 더하는 방법이다.
 자, 다시 계산해볼까
 0001 + 1111(-1의 2의보수) = 1 0000
 최상위 비트가 1이지만, 범위를 벗아나기 때문에 무시한다. 그래서 0000, 우리가 원하는 값이 나온것이다.
 살펴본바와 같이, 1의 보수나 2의 보수를 사용하면 감사기가 필요없이, 더하기 만으로 뺄셈이 가능하다.
 1의 보수는 올림수 처리를 해줘야하지만, 2의 보수는 무시해버리면 되기 때문에 다른 처리를 할 필요가 없다.
 
이처럼 숫자형을 사용할 때는 항상 최대값을 염두해 주고 사용해야한다. 특히, 돈계산하는곳에서는 최대값을 넘겨버려 엉뚱한 값을 뱉어내면 참 난감하니까 말이다. long보다 큰 정수형이 필요할 경우 java.math.BigInteger 형을 사용하면 된다. BigInteger 클래스는 불변형 클래스임에 주의해야한다.
		BigInteger bi = BigInteger.valueOf(0);
		for (int i = 1; i <= 10; i++) {
			bi.add(BigInteger.valueOf(i));
		}
		System.out.println(bi);
 1-10까지 더했으니 55란 값이 나올까? 불행히도 0이란 값이 나온다. 원하는 값을 구하려면 아래처럼 작성해야한다.
		BigInteger bi = BigInteger.valueOf(0);
		for (int i = 1; i <= 10; i++) {
			bi = bi.add(BigInteger.valueOf(i));
		}
		System.out.println(bi);
 

  쉽게 범할 수 있는 실수니, 주의해야겠다.

2. 실수형
  - 자바의 기본 실수형은 float(4byte)와 double (8byte) 2가지가 있다.
  - 실수형은 부호, 지수, 소수부(유효숫자)로 구성되어 있다.
  - float는 1비트의 부호비트와, 8비트의 지수부, 23비트의 소수부를 가진다.
  - double은 1비트의 부호비트와, 11비트의 지수부, 52비트의 소수부를 가진다.
  - float형을 예로 들어 설명해 보도록 하자.

123000.0이란 숫자를 정규화(normalization)하면 1.23×105 이 된다.
(정규화란 소수점 앞에 0이 아닌 한자리 수만으로 나타낸 것이다.)
간혹 프로그래밍 중에  x.xxxEyy 형식, 즉 1.7976931348623157E308 이런식으로 출력하는 숫자를 봤을것이다.
이 숫자가 정규화표현식으로 나타낸것이다. 1.7976931348623157E308는 1.7976931348623157 × 10308 이라는 것이다.
10진수는 인간을 위한것이니 2진수로 예를 들어보자
7.5 -> 111.1 -> 1.111×22
부호는 양수이고, 지수는 2, 유효숫자는 1111이다. 이 유효숫자는 양옆의 0을 제거한 값이다. 즉, 001111000 이라면 1111 이라는 것이다.
자, 이 값들을 지정된 영역에 저장해 볼까?
양수이니 부호비트에 0을 저정한다.
지수값은 지수부분에 저장을 하게되는데,  바이어스(bias)라 불리는 127이란 값을 더해서 저장한다. 이 값을 더하는 이유는 음의 지수값을 양수로 표현하기 위해서이다. (127이란 값은, float의 지수바가 8비트라서 256개의 상태를 표현가능한데, 음수와 양수를 절반씩 나누면 128개 가능한데, 0의 지수가 포함되므로 127로 잡아서 양의 지수를 하나 더 표현할 수 있게 한것이다. 즉, 지수의 표현 가능 범위는 -(27 - 1) ~  27 이 된다. 그런데, -127과 128은 특별한 용도로 쓰이기때문에 실제로는 -126~127이 된다.)
현재는 지수값이 2이므로, 2+127=129, 즉 10000001 이 저장된다.
유효숫자는 1111중 맨 앞의 1을 제외한 나머지 부분을 소수 부분에 저장하게 된다.
이렇게 하는 이유는 앞에 설명한것처럼 양옆의 0을 제거한 값이기에 무조건 1로 시작하게 되므로,
맨 앞의 1을 제외함으로서 유효숫자를 하나라도 더 저장하기 위함이다.
0 10000001 11100000000000000000000 이란 값이 되는것이다.
우리가 만든 비트값이 진짜 7.5가 맞는지 테스트 해보자.
		int bits = Integer.parseInt("01000000111100000000000000000000", 2);
		System.out.println(Integer.toHexString(bits));
		float f = Float.intBitsToFloat(bits);
		System.out.println(f);
  오~ 신가하다. 40f00000 가 7.5 라니~~~

 자, 그럼 이 float 의 숫자 범위는 어떻게 될까? 양수일경우, 최소값은 지수가 -126이고 소수가 다 0일 경우이고, 최대값을 지수가 127이고 소수가 다 1일 경우가 된다. 즉, 2-126×1.0 ~ 2127×(2-ε)이다. (소수가 다 0인데, 왜 1.0이냐고 묻지말자. 모조건 1로 시작~~한다고 앞아서 얘기했다. 1.11111111111111111111111은 10진수로 계산하면 2에 아주 가까운 값이다. 그래서 ε(입실론)이라는 임의의 값을 뺀것으로 표현한것이다.) 음수일 경우는 반대가 되겠다. –B~-A, A~B. 즉, 10진수로 변환하면 -3.40282347× 1038 ~ -1.17549435×10-38 1.17549435×10-38 ~ 3.40282347× 1038의 범위를 가지게 된다.
 그런데, 0을 포함해서 -A부터 A까지 사이의 수는 표현할 수 없다는것을 알 수 있다. 이 사이의 수를 subnormal number이라고 하는데, 특별한 용도로 쓰인다는 지수부 -127 값을 사용하여 표현한다. 즉, 바이어스를 더해서 0이 될때 소수부분의 비트열의 앞에 1을 더하지 않고, 소수 그 자체로 계산함으로서 최소값과 0 사이에 나타내지 못했던 숫자를 표현할 수 있게 하는것이다. 결론적으로 말하면 이 범위를 포함하면 float형의 숫자 범위는 -3.4×1038 ~ 3.4×038가 되는것이다.
 그러면 지수부 128은 어디에 쓰이는것일까? 이 놈은 무한대의 값이나 무리수를 표현하기 위해서 쓰인다. 소수부가 모두 0이고 부호비트가 0이면 양의 무한대, 소수부가 모두 0이고 부호비트가 1이면 음의 무한대, 그 외는 무리수로 표현하는것이다.
  0 11111111 00000000000000000000000  -> 양의 무한대
  1 11111111 00000000000000000000000  -> 음의 무한대
  (자바의 Float 클래스를 보면 Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY 상수로 무한대 값이 정의되어 있다.)

 위에서 본것처럼 이 실수형을 사용하게 되면, 아주 작은 숫자나, 아주 큰 숫자를 표현할 수 있게 된다. 하지만, 정밀도의 한계를 가지게 된다. 즉, float일 경우 소수부가 23비트이므로, 2-23보다 작은 정밀도의 값을 나타낼 수 없다.
		float f;
		for (int i = 83886080; i < 83886090; i++) {
			f = (float)i;
			System.out.println(i + " --> " + f + " --> " + (int)f);
		}

* 실행 결과
83886080 --> 8.388608E7 --> 83886080
83886081 --> 8.388608E7 --> 83886080
83886082 --> 8.388608E7 --> 83886080
83886083 --> 8.388608E7 --> 83886080
83886084 --> 8.388608E7 --> 83886080
83886085 --> 8.3886088E7 --> 83886088
83886086 --> 8.3886088E7 --> 83886088
83886087 --> 8.3886088E7 --> 83886088
83886088 --> 8.3886088E7 --> 83886088
83886089 --> 8.3886088E7 --> 83886088

실행 결과를 보자. 놀랍지 아니한가? 정밀도의 한계로 인해 83886080, 83886081, 83886082 모두다 83886080의 값을 가져버리게 되었다. 그러니 실수형을 사용할때는 항상 주의가 필요하겠다.
보다 정밀한 계산이 필요할 때는 java.math.BigDecimal 클래스를 사용하면 된다. 이 클래스는 나누기를 사용할때 특히 주의를 해야한다. 1을 0.3으로 나눠볼까. 3.33333333333333333333333333333333333... 일 것이다. 기본형인 double형으로 계산해보자
		double d1 = 1.0;
		double d2 = 0.3;
		System.out.println(d1 / d2);

* 출력 결과
3.3333333333333335
한계 정밀도 까지 알아서 끊어준다.
		BigDecimal bd1 = BigDecimal.valueOf(1.0);
		BigDecimal bd2 = BigDecimal.valueOf(0.3);
		System.out.println(bd1.divide(bd2));

실행해보면 에러가 발생할것이다.
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
 즉, 계산이 끝나지 않고 무한대~로 간다는것이다. 그래서 아래처럼 정밀도를 지정해줘야한다.
		BigDecimal bd1 = BigDecimal.valueOf(1.0);
		BigDecimal bd2 = BigDecimal.valueOf(0.3);
		System.out.println(bd1.divide(bd2, BigDecimal.ROUND_HALF_UP));
		System.out.println(bd1.divide(bd2, BigDecimal.ROUND_UP));
		System.out.println(bd1.divide(bd2, 32, BigDecimal.ROUND_HALF_UP));
		System.out.println(bd1.divide(bd2, new MathContext(32, RoundingMode.HALF_EVEN)));
		System.out.println(bd1.divide(bd2, MathContext.DECIMAL32));
		System.out.println(bd1.divide(bd2, MathContext.DECIMAL64));
		System.out.println(bd1.divide(bd2, MathContext.DECIMAL128));

* 출력결과
3.3
3.4
3.33333333333333333333333333333333
3.3333333333333333333333333333333
3.333333
3.333333333333333
3.333333333333333333333333333333333