본문 바로가기
Back-end

java.lang패키지와 유용한 클래스(8)

by 신재권 2021. 7. 1.

java.math.BigInteger 클래스

정수형으로 표현할 수 있는 값의 한계가 있다. 가장 큰 정수형 타입인 long으로 표현할 수 있는 값은 10진수로 19자리 정도이다. 이 값도 상당히 큰 값이지만, 과학적 계산에서는 더 큰값을 다뤄야할 때가 있다. 그럴 떄 사용하면 좋은 것이 BigInteger이다.

BigInteger는 내부적으로 int배열을 사용해서 값을 다룬다. 그래서 long타입보다 훨씬 큰 값을 다룰 수 있는 것이다. 대신 성능은 long타입밖에 떨어질 수 밖에 없다.

final int signum; //부호 1(양수), 0, -1(음수) 셋중 하나
final int[] mag;  //값(magnitude)

위의 코드에서 알 수 있듯이, BigInteger는 String처럼 불변(immutable)이다. 그리고 모든 정수형이 그렇듯이 BigIntger역시 값을 '2의 보수'의 형태로 표현한다.

좀 더 자세히 말하면, 위의 코드에서 알 수 있듯이 부호를 따로 저장하고 배열에는 값 자체만 저장한다. 그래서 signum의 값이 -1, 즉 음수인 경우, 2의보수법에 맞게 mag의 값을 변환해서 처리한다. 그래서 부호만 다른 두 값의 mag는 같고 signum은 다르다.

BigInteger의 생성

BigInteger를 생성하는 방법은 여러가지가 있는데, 문자열로 숫자를 표현하는 것이 일반적이다. 정수형 리터럴로는 표현할 수 있는 값의 한계가 있기 때문이다.

BigInteger val;
val = new BigInteger("12345678901234567890"); //문자열로 생성
val = new BigInteger("FFFF", 16);  //n진수( radix)의 문자열로 생성
val = BigInteger.valueOf(1234567890L); //숫자로 생성

다른 타입으로의 변환

BigInteger를 문자열, 또는 byte배열로 변환하는 메서드는 다음과 같다.

String toString(); //문자열로 변환
String toString(int radix); //지정된 진법( radix)의 문자열로 변환
byte[] toByteArray();  //byte배열로 변환

BigInteger도 Number부터 상속받은 기본형으로 변환하는 메서드들을 가지고 있다.

int intValue()
long longValue()
float floatValue()
double doubleValue()

정수형으로 변환하는 메서드 중에서 이름 끝에 'Exact'가 붙은 것들을 변환한 결과가 변환한 타입의 범위에 속하지 않으면 ArithmeticException을 발생시킨다.

byte byteValueExact()
int intValueExact()
long longValueExact()

BigInteger의 연산

BigInteger에는 정수형에 사용할 수 있는 모든 연산자와 수학적인 계산을 쉽게 해주는 메서드들이 정의되어 있다. 기본적인 연산을 수행하는 메서드 몇개만 고르면 다음과 같다.

  • remainder와 mod는 둘 다 나머지를 구하는 메서드이지만, mod는 나누는 값이 음수이면 ArithmeticException을 발생시킨다는 점이 다르다.
BigInteger add(BigInteger val)  //덧셈(this+val)
BigInteger subtract(BigInteger val)  //뺼셈(this - val)
BigInteger multiply(BigInteger val) //곱셈(this * val)
BigInteger divide(BigInteger val) //나눗셈(this/val)
BigInteger remainder(BinInteger val) //나머지(this % val)

BigInteger은 불변이므로, 반환타입이 BigInteger이란 얘기는 새로운 인스턴스가 반환된다는 뜻이다. Java API를 보면, 메서드마다 연산기호가 적혀있기 때문에, 각 메서드가 어떤 연산자를 구현한 것인지 쉽게 알 수 있다.

비트 연산 메서드

워낙 큰 숫자를 다루기 위한 클래스이므로, 성능을 향상시키기 위해 비트단위로 연산을 수행하는 메서드들을 많이 가지고 있다. and, or, xor, not과 같이 비트연산자를 구현한 메서드들은 물론이고 다음과 같은 메서드들도 제공한다.

int bitCount()  //2진수로 표현했을 때, 1의 개수(음수는 0의 개수) 를 반환
int bitLength() //2진수로 표현했을 떄, 값을 표현하는데 필요한 bit수
boolean testBit(int n) //우측에서 n+1번째 비트가 1이면 true, 0이면 false
BigInteger setBit(int n)  //우측에서 n+1번째 비트를 1로 변경
BigInteger clearBit(int n) //우측에서 n+1번째 비트를 0으로 변경
BigInteger flipBit(int n) //우측에서 n+1번째 비트를 전환(1->0, 0->1)
  • n의 값은 배열의 index처럼 0부터 시작하므로, 우측에서 첫 번째 비트는 n이 0이다.

앞서 정수가 짝수인지 확인할 때, 정수를 2로 나머지 연산한 결과가 0인지 확인하는 조건식을 작성하였다. BigInteger의 경우에도 같은 식으로 작성하면 꽤 복잡해진다.

BigInteger bi = new BigInteger("4");
if(bi.remainder(new BigInteger("2")).equals(BigInteger.ZERO)) { 
...

대신 짝수는 제일 오른쪽 비트가 0일것이므로, testBit(0)으로 마지막 비트를 확인하는 것이 더 효율적이다.

BigInteger bi  = new BigInteger("4");
if(!bi.testBit(0) { //if(bi.testBit(0) == false){
	....

이처럼 가능하면 산술연산 대신 비트연산으로 처리하도록 노력해야 한다.

import java.math.BigInteger;


public class BigIntegerEx {

	public static void main(String[] args)  throws Exception {
		for(int i=1; i<100; i++){ //1!부터 99!까지 출력
			System.out.printf("%d!=%s%n",i,calcFactorial(i));
			//Thread.sleep(300); //0.3초의 지연
		}

	}
	
	static String calcFactorial(int n){
		return factorial(BigInteger.valueOf(n)).toString();
	}
	
	static BigInteger factorial(BigInteger n){
		if(n.equals(BigInteger.ZERO)) // 0일경우
			return BigInteger.ONE;  //1을 반환
		else //return n* fatorial(n-1);
			return n.multiply(factorial(n.subtract(BigInteger.ONE)));
	}

}
=====================================
1!=1
2!=2
3!=6
4!=24
5!=120
6!=720
7!=5040
8!=40320
9!=362880
....
99!=933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565182862536979208272237582511852109168640000000000000000000000

1!~99!까지 출력하는 예제이다. long타입으로는 20!까지밖에 계산할 수 없지만, BigINteger로는 99!까지 , 그 이상도 얼마든지 가능하다. BigINteger의 최대값은 ±2의 Integer.MAX_VALUE제곱인데, 10진수로는 10의 60억 제곱이다.

//6.464569929448805E8
System.out.println(Math.log10(2) * Integer.MAX_VALUE);

java.math.BigDecimal 클래스

double타입으로 표현할 수 있는 값은 상당히 범위가 넓지만, 정밀도가 최대 13자리 밖에 되지 않고 실수형의 특성상 오차를 피할 수 없다. BigDecimal은 실수형과 달리 정수를 이용해서 실수를 표현한다. 앞에서 배운것과 같이 실수의 오차는 10진 실수를 2진실루로 정확히 변환할 수 없는 경우가 있기 때문에 발생하는 것이므로, 오차가 없는 2진 정수로 변환하여 다루는 것이다. 실수를 정수와 10의 제곱의 곱으로 표현한다.

정수 x 10^-scale

scale은 0부터 Integer.MAX_VALUE사이의 범위에 있는 값이다. 그리고 BigDecimal은 정수를 저장하는데 BigInteger을 사용한다.

  • BigInteger처럼 BigDecimal도 불변(immutable)이다
private final BigIntegr intVal;  //정수(unscaled value)
private final int scale;  //지수(scale)
private transient int precision; //정밀도(precision) -정수의 자릿수

예를 들어 123.45는 12345x10^-2로 표현할 수 있으며, 이 값이 BigDecimal에 저장되면, intVal의 값은 12345가 되고 scale의 값은 2가 된다. scale은 소수점 이하의 자리수를 의미한다는 것을 알 수 있다. 그리고 precision의 값은 5가 되는데, 이 값은 정수의 전체 자리수를 의미한다.

BigDecimal val = new BigDecimal("123.45"); //12345 x 10^2
System.out.println(val.unscaledValue());  //12345
System.out.println(val.scale());  //2
System.out.println(val.precision());  //5

BigDecimal의 생성

BigDecimal를 생성하는 방법은 여러 가지가 있는데 , 문자열로 숫자를 표현하는 것이 일반적이다. 기본형 리터럴로는 표현할 수 있는 값의 한계가 있기 때문이다.

BigDecimal val; 
val  = new BigDecimal("123.4567890"); //문자열로 생성
val = new BigDecimal(123.456); //double타입의 리터럴로 생성
val = new BigDecimal(123456); //int, long타입의 리터럴로 생성가능
val = BigDecimal.valueOf(123.456);  //생성자 대신 valueOf(double)사용
val = BigDecimal.valueOf(123456); //생성자 대신 valueOf(int)사용

그리고 한 가지 주의할 점은 double타입의 값을 매개변수로 갖는 생성자를 사용하면 오차가 발생할 수 있다는 것이다.

System.out.println(new BigDecimal(0.1)); //0.100000000000000555111...
System.out.println(new BigDecimal("0.1"));  //0.1

다른 타입으로의 변환

BigDecimal을 문자열로 변환하는 메서드는 다음과 같다

String toPlainString() //어떤 경우에도 다른 기호없이 숫자로만 표현
String toString() //필요하면 지수형태로 표현할 수도 있음

대부분의 경우 이 두 메서드의 반환결과가 같지만, BigDecimal을 생성할 때, '1.0e-22'와 같은 지수형태의 리터럴을 사용했을 때 다른 결과를 얻는 경우가 있다.

BigDecimal val = new BigDecimal(1.0e-22);
System.out.println(val.toPlainString()); // 0.000000000000000000000010...
System.out.println(val.toString());  // 1.00000000000048..5E-22

BigDecimal도 Number로 부터 상속받은 기본형으로 변환하는 메서드들을 가지고 있다

int intValue()
long longValue()
float floatValue()
double doublevalue()

BigDecimal을 정수형으로 변환하는 메서드 중에서 이름 끝에 'Exact'가 붙은 것들은 변환한 결과가 변환한 타입의 범위에 속하지 않으면 ARithmeticException을 발생시킨다.

byte byteValueExact()
short shortValueExact()
int intValueExact()
long longValueExact()
BigInteger toBigIntegerExact()

BigDecimal의 연산

BigDecimal에는 실수형에 사용할 수 있는 모든 연산자와 수학적인 계산을 쉽게 해주는 메서드들이 정의되어 있다. 아래는 기본적인 연산을 수행하는 메서드 몇 개만 골라보면 아래와 같다

BigDecimal add(BigDecimal val) //덧셈(this + val)
BigDecimal subtract(BigDecimal val) //뺼셈(this-val)
BigDecimal multiply(BigDecimal val) //곱셈(this * val)
BigDecimal divide(BigDecimal val) //나눗셈(this / val)
BigDecimal remainder(BigDecimal val) //나머지(this % val)

BigInteger와 마찬가지로 BigDecimal은 불변이므로, 반환타입이 BigDecimal인 경우 새로운 인스턴스가 반환된다. Java API를 보면, 메서드마다 연산 기호가 적혀있기 떄문에, 영어를 몰라도 각 메서드가 어떤 연산자를 구현한 것인지 쉽게 알 수 있다.

한가지 알아둬야 할 것은 연산결과의 정수, 지수, 정밀도가 달라진다는 것이다.

				              //value, scale, precision
BigDecimal bd1 = new BigDecimal("123.456");//123456  3          6
BigDecimal bd2 = new BigDecimal("1.0");    //10      1          2
BigDecimal bd3 = bd1.multiply("bd2");      //1234560 4          7

곱셈에서는 두 피연산자의 scale을 더하고 , 나눗셈에서는 뺸다. 덧셈과 뺼셈에서는 둘 중에서 자리수가 높은 쪽으로 맞추기 위해서 두 scale중에서 큰 쪽이 결과가 된다.

반올림 모드-divide()와 setScale()

다른 연산과 달리 나눗셈을 처리하기 윟나 메서드는 다음과 같이 다양한 버전이 존재한다. 나눗셈의 결과를 어떻게 반올림(roundingMode)처리할 것인가와, 몇 번째 자리(scale)에서 반올림할 것인지 지정할 수 있다. BigDecimal이 아무리 오차없이 실수를 저장한다해도 나눗셈에서 발생하는 오차는 어쩔 수 없다.

BigDecimal divide(BigDecimal divisor)
BigDecimal divide(BigDecimal divisor, int roundingMode)
BigDecimal divide(BigDecimal divisor, RoundingMode roundingMode)
BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
BigDecimal divide(BigDecimal divisor, MathContext mc)

roundingMode는 반올림 처리방법에 대한 것으로 BigDecimal에 정의된 'ROUND_'로 시작하는 상수들 중에 하나를 선택해서 사용하면 된다. RoundingMode는 이 상수들을 열거형으로 정의한 것으로 나중에 추가되었다. 가능하면 열거형 RoundingMode를 사용하자

CELING : 올림

FLOOR : 내림

UP : 양수일때는 올림, 음수일때는 내림

DOWN : 양수일때는 내림, 음수일때는 올림(UP과 반대) HALF_UP : 반올림(5이상 올림, 5미만 버림)

HALF_EVEN : 반올림(반올림 자리의 값이 짝수면 HALF_DOWN, 홀수면 HALF_UP)

HALF_DOWN : 반올림(6이상 올림, 6미만 버림)

UNNECESSARY : 나눗셈의 결과가 딱 떨어지는 수가 아니면, ArithmeticException발생

올림과 내림은 수학시간에 배워서 알고 있을 거시고, 우리가 일반적으로 알고있는 반올림은 HALF_UP이다 5가 아닌 6을 기준으로 반올림하는 것이 HALF_DOWN이다.

주의해야할 점은 1.0/3.0처럼 divide()로 나눗셈한 결과가 무한소수인 경우, 반올림 모드를 지정해주지 않으면 ArithmeticException이 발생한다는 것이다

BigDecimal bigd = new BigDecimal("1.0");
BigDecimal bigd2 = new BigDecimal("3.0");

System.out.println(bigd.divide(bigd2));  // ArithmeticException 발생
System.out.println(bigd.divide(bigd2,3,RoundingMode.HALF_UP)); //0.333

java.math.MathContext

이 클래스는 반올림 모드와 정밀도(precision)을 하나로 묶어 놓은 것 일뿐 별다른 것은 없다.

한가지 주의할 점은 divide()에서는 scale이 소수점 이하의 자리수를 의미하는데, MathContext에서는 precision이 정수와 소수점 이하를 모두 포함한 자리수를 의미한다는 것이다.

BigDecimal bd1 = new BigDecimal("123.456");
BigDecimal bd2 = new BigDecimal("1.0");

System.out.println(bd1.divide(bd2, 2, HALF_UP));  //123.46
System.out.println(bd1.divide(bd2, new MathContext(2, HALF_UP))); //1.2E + 2

그래서 위의 결과를 보면 scale이 2이면 나눗셈의 결과가 두 자리까지 출력되는데, MathContext를 이용한 결과는 precision을 가지고 반올림 하므로 bd1의 precision에 scale이 반영되어 1.2E+2가 된것이다. 위의 코드에서 scale과 precision의 값을 계속 바꿔가면서 반복해서 출력해보면 쉽게 이해가 될 것이다.

scale의 변경

BigDecimal을 10으로 곱하거나 나누는 대신 scale의 값을 변경함으로써 같은 결과를 얻을 수 있다. BigDecimal의 scale을 변경하려면, setScale()을 이용하면 된다.

BigDecimal setScale(int newScale)
BigDecimal setScale(int newScale, int roundingMode)
BigDecimal setScale(int newScale, RoundingMode mode)

setScale()로 scale을 값을 줄이는 것은 10의 n 제곱으로 나누는 것과 같으므로, divide()를 호출할 때처럼 오차가 발생할 수 있고, 반올림 모드를 지정해주어야 한다.

import java.math.*;
import static java.math.BigDecimal.*;
import static java.math.RoundingMode.*;
public class BigDecimalEx {

	public static void main(String[] args) {
		BigDecimal bd1= new BigDecimal("123.456");
		BigDecimal bd2 = new BigDecimal("1.0");
		
		System.out.print("bd1="+bd1);
		System.out.print(", \tvalue="+bd1.unscaledValue());
		System.out.print(", \tscale="+bd1.scale());
		System.out.print(", \tprecision="+bd1.precision());
		System.out.println();
		
		System.out.print("bd2="+bd2);
		System.out.print(", \tvalue="+bd2.unscaledValue());
		System.out.print(", \tscale="+bd2.scale());
		System.out.print(", \tprecision="+bd2.precision());
		System.out.println();
		
		BigDecimal bd3 =bd1.multiply(bd2);
		System.out.print("bd3="+bd3);
		System.out.print(", \tvalue="+bd3.unscaledValue());
		System.out.print(", \tscale="+bd3.scale());
		System.out.print(", \tprecision="+bd3.precision());
		System.out.println();
		
		System.out.println(bd1.divide(bd2,2,HALF_UP)); // 123.46
		System.out.println(bd1.setScale(2,HALF_UP)); // 123.46
		System.out.println(bd1.divide(bd2, new MathContext(2, HALF_UP)));

	}

}
=============================
bd1=123.456, 	value=123456, 	scale=3, 	precision=6
bd2=1.0, 	value=10, 	scale=1, 	precision=2
bd3=123.4560, 	value=1234560, 	scale=4, 	precision=7
123.46
123.46
1.2E+2