1. float와 double
float와 double은 자바에서 0.12, 0.333 과 같은 실수를 표현하는 실수형 자료형이다. 그리고 두 실수형이 표현할수있는 범위는 다음과 같다.
위 표는 ‘양의 범위’만 적은 것으로 - 부호를 붙이면 ‘음의 범위’가 된다. 예를 들어 float 타입으로 표현 가능한 음의 범위는 -1.4 x 10 ^45 ~ -3.4 X 10^38이다. 따라서 float 타입의 표현범위는 -3.4 X 10^38 ~ 3.4 X10^38 이지만, -1.4 X10^45 ~1.4 X 10^-45범위의 값은 표현할 수 없다. (0 제외)
실수형은 소수점을 표현해야하므로 ‘얼마나 큰 값을 표현할 수 있는가 ’뿐만 아니라 ‘얼마나 0에 가깝게 표현할 수 있는가’도 중요하다.
2. 실수의 표현방식
컴퓨터에서 실수를 표현하는 방식으로는 고정소수점과 부동소수점 방식이 있다.
2.1 고정 소수점
단어 그대로 소수점의 위치를 고정시켜 놓고 수를 표현하는 방식이다.
5.625라는 숫자가 있을 때, 5.625 = 4 + 1+ 0.5 + 0.125 = 2^2 + 2^0 + 2^(-1) + 2^(-3) = 101.101(2) 로 표현가능하다.
부호에 0, 정수부에 101이라는 2진수, 소수부에 101이라는 2진수로 표현된다. 이 방법은 직관적으로 실수를 표현할수있는다는 장점이 존재한다. 하지만 표현 가능한 범위가 매주 적다는 단점이 존재한다. 뿐만 아니라 0.12라는 실수가 존재할때 이 0.12를 표현하기 위해 16비트를 모두 사용한다. 이는 매우 메모리가 낭비되는 방식으로 이러한 문제를 해결하기 위해 부동소수점 방식을 사용한다.
2.2 부동 소수점
자바의 실수 표현방식으로 단어의 부동(Floating - point)에서 알수 있듯이 소수점이 떠다닌다는 의미로 표현할 수 있는 값의 범위를 최대한 넓혀 오차를 줄이는 방식이다.
그림에서 볼수있듯이 부호(S) , 가수(M), 지수(E)로 이루어져있는데 실수를 2의 제곱을 곱한 형태 ( M X 2^E)로 저장해 큰 범위의 값을 저장 할 수 있다.
1.333 * 2^n 이라고 표현된 숫자가 있다면 앞의 숫자를 가수(M), 뒤의 숫자를 지수(E)라고 한다
부동소수점 방식은 실수의 값을 가수부에 넣어 표현하기 때문에 큰 비트의 범위를 가지게 되며 고정소수점과 같이 정수부가 크든 소수부가 크든 상관없이 가수부내에서 전체 실수를 다 표현하기 때문에 공간낭비 문제도 해결된다.
기호 의미 설명
S | 부호(Sign bit) | 0이면 양수, 1이면 음수 |
E | 지수(Exponent) | 부호있는 정수. 지수의 범위는 float는 -127 ~ 128 , double은 -1024 ~ 1024이다. |
M | 가수(Mantissa) | 실제값을 저장하는 부분 |
- 부호 :0이면 양수, 1이면 음수를 의미하고 ‘2의 보수법’을 사용하지 않아 양의 실수에서 음의 실수로 바꿀때 부호비트만 바꾸면 된다
- 지수 :float의 경우 8bit 즉 32byte를 가지고 있다. 따라서 모두 2^8= 256개의 값을 저장할 수 있으므로 ‘-127 ~ 128 ‘의 값이 저장된다. 여기서 -127과 128은 ‘숫자 아님’과 ‘양의 무한대’.’음의 무한대’와 같이 특별한 값의 표현을 위해 저장되어있으므로 실제로 사용가능한 범위는 -126 ~ 127이다.
- 가수 : 실제 값인 가수를 저장하는 부분으로 float의 경우 2진수의 23자리를 저장할 수 있다. 2진수의 23자리로는 약 7자리의 10진수를 저장할 수 있는데 이것이 바로 뒤에 나오는 정밀도이다. 따라서 float의 정밀도는 7자리를 가지며,double은 52비트로 float의 약 2배로 15자리 정도의 정밀도를 가진다.
즉, float의 경우 지수는 23자리를 저장할 수 있는데 이때 2진수 23자리는 10진수는 7자리이므로 정밀도가 7이된다.
지수 표기법
지수 표기법은 큰 숫자를 간단하게 표기하는 방법으로 숫자를 지수 형태로 표기해 무의미한 0이나 소수점을 생략할 수 있다. 지수표현법에서 E = 10을 의미하며 10^2에서 2를 지수라고 한다.
0.000000000002를 지수로 표기하면 2 * 10^(-12 ) = 2E-12이다. 0.00000314 를 지수로 표현하면 3.14 * 10 ^( -6) = 3.14E-6 이다.
2.3 정밀도
정밀도란 실제값과 저장된 값이 얼마나 정확한지를 나타내는 기준으로 해당 자릿수까지 변수에 저장된 값을 보장하는 것을 정밀도(precision)라고한다. float는 7자리, doubledms 15자리까지의 정확한 값을 보장한다.
예를 들어 실수 32.12345678 가 있을 때 float의 정밀도는 7자리로 32.123456789, double은 32.12345678000000000 까지이다. 따라서 실수형은 정수형과 달리 오차의 가능성이 존재한다.
2.4 부동 소수점의 저장방식 및 계산 방법
실수중에는 3.1415192.. 같은 파이와 같은 무한 소수가 존재하며, 정수와 달리 실수를 저장할 때는 오차가 발생할 수 있다. 또한 10진수가 아닌 2진수로 저장되기 때문에 10진수로는 유한소수더라도, 2진수로 변환하면 무한소수가 되는 경우가 있다.
예를 들어 9.1234567 라는 실수는 10진수로는 유한소수이나 2진수로는1001.100101101011010000111… (2)으로 무한소수이다. 즉, 2진수로 10진수를 정확히 표현하지 못한다는 이야기이다. 가수를 저장할 수있는 자리수가 한정되어있으므로 저장되지 못하고 버려지는 값들이 있으면 오차가 발생한다.
부동 소수점 계산 방법
- 9.1234567 2진수로 변환한다. 이때 구하는 방법은 정수와 소수부를 나누어 2진수로 바꾼다. 즉 9 =1001, 0.1234567 = 100101101011010000111
- 1001.10010110101101000011.. 를 ‘1.xxxx * 2^n’ 으로 형태로 변환한다. 따라서 1001. 100101101011010000111… (2) → 1.001100101101011010000111… * 2^3 이 과정을 정규화라고한다. 정규화된 2진 실수는 항상 1로 시작하므로 1을 제외한 나머지 23자리의 2진수가 가수자리에 저장되고 그 이후는 잘려나간다. 이때 점은 무시한다.
- 지수는 기저법으로 저장되기때문에 지수 3의 기전인 127을 더한 130이 2진수로 변환되어 저장되어 지수영역에저장된다. 10진수 130은 2진수 10000010(2)이다.
- float의 경우 가수 자리는 23비트인데 이때 가수자리에 1을 제외한 00110010110101101000011 를 저장하고 이는 2^-23 즉 10진수로 약 10 ^-7기때문제 float의 정밀도를 7자리라고 하는것이다. 같은 말로 소수점 이라 6자리 라고 하는데 이것은 소수점이하의 자릿수만을 센 것으로 같은 말이다.
즉, 2 ^(-23) = 0.0000001192 = 10^(-7) 이다.
기저법(bias)
‘2의 보수법’처럼 부호있는 정수를 저장하는 방법이다. 저장할때 특정값(기저)를 더했다가 읽어올때 다시 뺀다.
float의 지수부 공간은 8비트이므로 2^8 총 256개 0~ 255까지 표현할 수 있다. 하지만 지수가 음수일 경우가 존재하기 때문에 bias를 더한다. 예를들어 2^(-3)이면 -2을 지수부에 저장할 수 없기때문에 이를 위해 고정값으로 -3에 127을 더하고 지수부에 저장한다. 따라서 실제지수가 127이면 127을 더해 0으로 128이면 127을 더해 255으로 만든다.
2.5 숫자계산에 쓸수 없는 이유
위에서 언급한 것과 같이 float와 double은 부동소수점으로 값을 표현 그리고 정밀도를 벗어난 소수점 이하의 값이 정확하게 표현이 안되는 오류가 발생한다. 큰 숫자를 다를 수있지만 근사값이 계산되기 때문에 돈 계산에 사용하면 안 된다.
3. 해결법 - BigDecimal
이를 해결하기 위해 BigDecimal 클래스를 이용하면된다. BigDecimal 는 java.math 패키지 안에있는 숫자를 다루는 클래스이다.
BigDecimal bigDecimal = new BigDecimal("1000.12345"); // 인자로 String으로 넘겨주어야한다.
먼저 BigDecimal은 원시 타입 자료형이 아니기때문에 연산을 하기 위해서는 add, muitiply, divide와 같은 메서드를 사용해서 계산해야하며 ,불변성(Immutable)을 가지고 있는것이 특징이다. 이 불변은 객체의 연산시 객체의 값이 변경되는 것이 아니라 새로운 객체가 생성되는것이 특징이다.
그렇다면 BigDecimal을 사용해야하는 이유는 무엇일까?
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal; //정수, 수를 구성하는 전체 자리수
private final int scale; // 전체 소수점 자리수
private transient int precision; //정밀도
public BigDecimal(String val) {
this(val.toCharArray(), 0, val.length());
}
}
BigDecimal 클래스를 이용해 객체를 생성하면 내부적으로 int[]에 저장해 값을 다룬다. 따라서. 이 방식으로 자리수 부족으로 인한 float와 double문제를 해결했다. 돈과 큰 소수점을 다룬다면 BigDecimal 을 사용해야한다. 하지만 동시에 성능상으로 느리고 메소드를 이용해서 연산해야하는 단점이 존재한다.
'Java' 카테고리의 다른 글
StringBuilder는 어떻게 내부 크기를 늘릴까? (0) | 2024.02.07 |
---|---|
[JAVA] ArrayList의 로드팩터는 1인데 HashMap의 로드팩터는 0.75일까? (0) | 2024.01.26 |
[JAVA] GC 과정에서 왜 survivor 한곳을 비워야할까? (0) | 2024.01.19 |
[JAVA] 자바의 hashcode는 무엇이고, 어디에 사용할까? (0) | 2024.01.18 |
[JAVA] == 와 equals()의 차이 (1) | 2024.01.05 |