[Java의 정석]제3장 연산자 - 3.산술연산자

류명운

·

2014. 7. 3. 22:25

반응형

3. 산술 연산자

산술연산자인 사칙연산자(+,-,*,/), 나머지연산자(%), 쉬프트연산자(<<,>>,>>>)는 모두 두개의 피연산자를 취하는 이항연산자이며, 이항 연산자는 피연산자의 크기가 4 byte보다 작으면 4byte(int형)로 변환한 다음에 연산을 수행한다는 점을 명심해야한다.

이항연산자는 연산을 수행하기 전에 피연산자들의 타입을 일치시킨다는 사실 또한 매우 중요하다.


이항연산자는 연산을 수행하기 전에
- 크기가 4 byte이하인 자료형을 int형으로 변환한다.
- 피연산자의 타입을 일치시킨다.



3.1 사칙연산자 - +, -, *, /

이 연산자들이 프로그래밍에 가장 많이 사용되어지는 연산자들 일 것이다. 여러분들이 이미 알고 있는 것처럼, 곱셈(*), 나눗셈(/), 나머지(%) 연산자가 덧셈(+), 뺄셈(-)연산자보다 우선순위가 높다.


- int형(4 byte)보다 크기가 작은 자료형(byte, short, char)은 int형으로 변환된 후에 연산을 수행한다.
byte + short → int + int → int

- 두 개의 피연산자중 자료형의 표현범위가 큰 쪽에 맞춰서 형변환 된 후 연산을 수행한다.
int + float → float + float → float

- 정수형간의 나눗셈에서 0으로 나누는 것은 금지되어 있다.





byte + byte -> int +int -> int
byte + short -> int + int -> int
char + char -> int + int -> int
int +int -> int
float + int -> float + float -> float
long + float -> float + float -> float
float + double -> double + double -> double

[참고]위의 결과는 덧셈연산자를 포함한 모든 이항 연산자에 공통적으로 해당한다.

피연산자가 정수형인 경우, 나누는 수로 0을 사용할 수 없다. 만일 0으로 나누면, 컴파일은 정상적으로 되지만 실행 시 오류(ArithmeticException)가 발생한다.
부동소수점값인 0.0f, 0.0d으로 나누는 것은 가능하지만 그 결과는 NaN(Not A Number, 숫자 아님)이다. 나눗셈 연산자(/)와 나머지 연산자(%)의 피연산자가 무한대(Infinity) 또는 0.0인경우의 결과를 표로 정리해 놓았다. 중요한 것은 아니니 참고만 하도록 하자.



[예제3-8] OperatorEx8.java

class OperatorEx8
{
public static void main(String[] args)
{
byte a = 10;
byte b = 20;
byte c = a + b;
System.out.println(c);
}
}
[컴파일결과]
OperatorEx8.java:7: possible loss of precision
found : int
required: byte
byte c = a + b;
^
1 error

이 예제를 컴파일하면 위와 같은 에러가 발생한다. 발생한 위치는 7번째 줄(byte c=a+b;)이다. a와 b는 모두 int형보다 작은 byte형이기 때문에 +연산자는 이 두 개의 피연산자들의 자료형을 int형으로 변환한 다음 연산(덧셈)을 수행한다.

따라서 a+b의 결과는 int형(4byte)인 것이다. 4 byte의 값을 1 byte의 변수에 형변환 없이 저장하려고 했기 때문에 에러가 발생하는 것이다. 크기가 작은 자료형의 변수를 큰 자료형의 변수에 저장할 때는 자동으로 형변환(type conversion, casting)되지만, 반대로 큰 자료형의 값을 작은 자료형의 변수에 저장하려면 명시적으로 캐스트 연산자를 사용해서 변환해주어야 한다.
에러가 발생한 7번째 줄 byte c = a + b;를 byte c = (byte)(a + b);와 같이 변경해야 한다.

[참고] byte c = (byte)a + b;와 같이 하면, 역시 에러가 발생한다. 왜냐하면, 캐스트 연산자는 단항연산자이므로 연산순위가 이항 연산자인 덧셈연산자보다 높다. 그렇기 때문에, (byte)a가 먼저 수행된 다음 덧셈이 수행되므로, 캐스트 연산자에 의해서 byte형으로 형변환된 후에 다시 덧셈 연산자에 의해서 int형으로 변환된다.

[예제3-9] OperatorEx9.java

class OperatorEx9
{
public static void main(String[] args)
{
byte a = 10;
byte b = 30;
byte c = (byte)(a * b);
System.out.println(c);
}
}
[실행결과]
44

위의 예제를 실행하면 44가 화면에 출력된다. 10 * 30의 결과는 300이지만, 형변환(캐스팅, casting)에서 배운 것처럼, 큰 자료형에서 작은 자료형으로 변환하면 데이터의 손실이 발생하므로, 값이 바뀔 수 있다. 300은 byte형의 범위를 넘기 때문에 byte형으로 변환하면 데이터 손실이 발생하여 결국 44가 byte형 변수 c에 저장된다.
아래의 표에서 알 수 있듯이 byte형(1byte)에서 int형(4byte)으로 변환하는 것은 2진수 8자리에서 32자리로 변환하는 것이기 때문에 자료 손실이 일어나지 않는다.(원래 8자리는 그대로 보존하고 나머지는 모두 0으로 채운다. 음수인 경우에는 부호를 유지하기 위해 0 대신 1로 채운다.)
반대로 int형을 byte형으로 변환하는 경우 앞의 24자리를 없애고 하위 8자리(1byte)만을 보존한다.
저장된 값이 10인 경우 값이 작아서 상위 24자리를 잘라내도 원래 값을 유지하는데 지장이 없지만, byte형의 범위인 -128~127의 범위를 넘는 int형의 값을 byte형으로 변환하면, 원래의 값이 보존되지 않고 byte형의 범위 중 한 값을 가지게 된다. 이러한 값 손실을 예방하기 위해서는 충분히 큰 자료형을 사용해야 한다.



[예제3-10] OperatorEx10.java

class OperatorEx10
{
public static void main(String[] args)
{
int a = 1000000; // 1,000,000 1백만
int b = 2000000; // 2,000,000 2백만
long c = a * b; // 2,000,000,000,000 2 * 10의 12제곱
System.out.println(c);
}
}
[실행결과]
-1454759936

식 a * b의 결과 값을 담는 변수 c의 자료형이 long형이기 때문에 2 * 10의 12제곱 값을 저장하기에 충분하므로 2000000000000가 출력될 것 같지만, 결과는 전혀 다른 값이 출력된다. 그 이유는 표3-8에서 볼 수 있는 것처럼, int형과 int형의 연산결과는 int형이기 때문이다. a * b의 결과가 이미 int형 값(-1454759936)이기 때문에 long형으로 자동 변환되어서 long형 변수인 c에 저장되어도 결과는 같다. 바른 결과를 얻기 위해서는 변수 a, b의 타입을 long으로 바꾸어야 한다.
[참고] 변수 a, b중 어느 한쪽만 long형으로 바꾸어도 a * b연산을 수행하면서 둘 다 long형으로 바꿔서 연산하므로 결과는 long형이 된다.
[참고]long형의 변수가 표현할 수 있는 값의 범위는 -263~263-1이므로, 양수는 약 9 * 10의 18제곱까지 표현 가능하므로 2 * 10의 12제곱 값을 저장할 수 있다.

[예제3-11] OperatorEx11.java

class OperatorEx11
{
public static void main(String[] args)
{
char c1 = 'a'; // c1에는 문자 "a"의 문자코드(유니코드)값이 저장된다.
char c2 = c1; // c1에 저장되어 있는 값이 c2에 저장된다.
char c3 ='\u0000'; // c3를 null문자로 초기화 한다.

int i = c1 + 1; // char형이 덧셈 연산 전에 int형으로 변환되어 97+1이 수행된다.

// 덧셈연산의 결과가 int형이므로 c3에 담기위해서는 char형으로의 형변환이 필요하다.
c3 = (char)(c1 + 1);
c2++;
c2++;

System.out.println("i=" + i);
System.out.println("c2=" + c2);
System.out.println("c3=" + c3);
}
}
[실행결과]
i=98
c2=c
c3=b

c1 + 1을 계산할 때, c1이 char형이므로 int형으로 변환한 후 덧셈연산을 수행하게 된다. c1에 저장되어 있는 코드값이 변환되어 int형 값이 되는 것이다. 따라서 c1 + 1은 97 + 1이 되고 결과적으로 int형 변수 i에는 98이 저장된다.

[참고] 소문자 a는 코드값이 10진수로 97이고 16진수로는 61이다. 그래서 c1 + 1의 연산결과가 98이다. 이 값을 char형으로 변환하면, char형 변수 c3에 저장할 수 있다. 그리고 이 값은 문자의 코드값으로 인식된다. 98은 16진수로 62이며, 소문자 b를 뜻한다. 그러므로 "b"를 문자코드를 이용해서 표현하면"\u0062"이다.

c2++은 어떠한 형변환도 없이 c2에 저장되어 있는 값을 1 증가시키므로, 예제에서는 원래 저장되어 있던 값인 97이 1씩 두 번 증가되어 99가 된다. 코드값이 10진수로 99인 문자는 "c"이다. 따라서, c2를 출력하면, "c"가 화면에 나타나는 것이다.

[참고] c2++;대신에 c2=c2+1;을 사용하면 에러가 발생할 것이다. c2+1의 연산결과는 int형이며, 그 결과를 다시 c2에 담으려 하면 캐스트 연산자를 사용하여 char형으로 형변환을 해야 하기 때문이다.
따라서 c2++;대신 c2=(char)(c2+1);과 같이 하면 똑같은 결과를 얻을 수 있을 것이다.


[예제3-12] OperatorEx12.java

class OperatorEx12
{
public static void main(String[] args)
{
char c = 'a';
for(int i=0; i<26; i++) { // 블럭{} 안의 문장을 26번을 반복한다.
System.out.print(c++); // "a"부터 시작해서 26개의 문자를 출력하게 된다.
}

System.out.println();

c = 'A';
for(int i=0; i<26; i++) { // 블럭{} 안의 문장을 26번을 반복한다.
System.out.print(c++); // "A"부터 시작해서 26개의 문자를 출력하게 된다.
}

System.out.println();

c='0';
for(int i=0; i<10; i++) { // 블럭{} 안의 문장을 10번을 반복한다.
System.out.print(c++); // "0"부터 시작해서 10개의 문자를 출력하게 된다.
}
System.out.println();
}
}
[실행결과]
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789

[참고] println메서드는 값을 출력하고 줄을 바꾸지만, print메서드는 줄을 바꾸지 않고 출력한다. 매개변수없이 println메서드를 호출하면, 아무 것도 출력하지 않고 단순히 줄을 바꾸고 다음 줄의 처음으로 출력위치를 이동시킨다.

위의 예제를 실행하면, 문자 a부터 시작해서 26개의 문자를 출력하고, 또 문자 A부터 시작해서 26개의 문자, 0부터 9까지 10개의 문자를 출력한다. 소문자 a부터 z까지, 그리고 대문자 A부터 Z까지, 숫자 0부터 9까지 연속적으로 코드가 지정되어 있기 때문에 이런 결과가 나타난다.

문자 a의 코드값은 10진수로 97, b의 코드값은 98, c의 코드값은 99, ... , z의 코드값은 122이며, 문자 A의 코드값은 10진수로 65, B의 코드값은 66, C의 코드값은 67, ... , Z의 코드값은 90이다. 그리고 문자 0의 코드값은 10진수로 48이다.
이 사실을 이용하면 대문자를 소문자로 소문자를 대문자로 변환하는 프로그램을 작성할 수 있다.
[참고]대문자와 소문자간의 코드값 차이는 10진수로 32이다.
[예제3-13] OperatorEx13.java

class OperatorEx13
{
public static void main(String[] args)
{
char lowerCase = 'a';
char upperCase = (char)(lowerCase - 32);
System.out.println(upperCase);
}
}
[실행결과]
A

소문자를 대문자로 변경하려면, 대문자 A가 소문자 a보다 코드값이 32가 적으므로 소문자 a의 코드값에서 32를 빼면 되고, 반대로 대문자를 소문자로 변환하려면, 대문자의 코드값에 32를 더해주면 된다.
[참고]char형과 int형간의 뺄셈연산(-) 결과는 int형이므로, 연산 후 char형으로 다시 형변환 해야 한다는 것을 잊지 말자.

[예제3-14] OperatorEx14.java

class OperatorEx14
{
public static void main(String[] args)
{
float pi = 3.141592f;
float shortPi = (int)(pi * 1000) / 1000f;

System.out.println(shortPi);
}
}
[실행결과]
3.141

int형간의 나눗셈 int / int을 수행하면 결과가 float나 double가 아닌 int임에 주의하라. 그리고 나눗셈의 결과를 반올림을 하는 것이 아니라 버린다는 점에 유의 하도록 한다. 예를 들어 3 / 2의 결과는 1.5 또는 2가 아니라 1이다.
이 예제는 나눗셈 연산자의 이러한 성질을 이용해서 실수형 변수 pi의 값을 소수점 셋째 자리까지만 빼내는 방법을 보여 준다.


(int)(pi * 1000) / 1000f;


위의 수식에서 제일 먼저 수행되는 것은 괄호 안의 pi * 1000이다. pi가 float이고 1000이 정수형이니까 연산의 결과는 float인 3141.592f가 된다.


(int)(3141.592f) / 1000f;


그 다음으로는 단항연산자인 캐스트연산자의 형변환이 수행된다. 3141.592f를 int로 변환하면 3141를 얻는다.


3141 / 1000f;


int와 float의 연산이므로, int가 float로 변환된 다음, float와 float의 연산이 수행된다.


3141.0f / 1000f


float와 float의 나눗셈이므로 결과는 float인 3.141f가 된다.

[참고]1000f는 float형 접미사가 붙었으므로 float이며 1000.0f와 같다.
[참고]DataFormat클래스를 사용해서 소수점자리를 맞추는 것도 좋은 방법이다.

추가로 원하는 자릿수에서 반올림을 하고 나머지를 버리는 예를 하나 더 보자.

[예제3-15] OperatorEx15.java

class OperatorEx15
{
public static void main(String[] args)
{
float pi = 3.141592f;
float shortPi = Math.round(pi * 1000) / 1000f;

System.out.println(shortPi);
}
}
[실행결과]
3.142

[참고] Math.round(3141.592f)의 결과는 3142이다. 소수점 첫 째 자리에서 반올림을 하기 때문이다.

이 예제의 결과는 pi의 값을 소수점 넷째 자리인 5에서 반올림을 해서 3.142가 출력되었다. round메서드는 매개변수로 받은 값을 소수점 첫째자리에서 반올림을 하고 그 결과를 정수로 돌려주는 메서드이다.
여기서는 소수점 넷 째 자리에서 반올림 하기 위해 1000을 곱했다가 다시 1000f로 나누었다. 만일 1000f가 아닌 1000으로 나누었다면, 3.142가 아닌 3을 결과로 얻었을 것이다.

[참고]일반적으로 클래스의 메서드는 그 클래스의 객체를 만든 후에 호출할 수 있지만, round메서드와 같은 static메서드는 객체를 만들지 않고도 직접 호출할 수 있다.



3.2 나머지 연산자 - %

왼쪽의 피연산자를 오른쪽 피연산자로 나누고 난 나머지 값을 돌려주는 연산자이다. boolean형을 제외하고는 모든 기본형 변수에 사용할 수 있다. 나머지 연산자는 주로 짝수, 홀수 또는 배수 검사 등에 주로 사용된다.
나눗셈에서와 같이 피연산자가 정수형인 연산에서는 나누는 수(오른쪽 피연산자)로 0을 사용할 수 없고, 나머지 연산자 역시 0.0이나 0.0f로 나누는 것은 허용한다.

[예제3-16] OperatorEx16.java

class OperatorEx16
{
public static void main(String[] args)
{
int share = 10 / 8;
int remain = 10 % 8;
System.out.println("10을 8로 나누면, ");
System.out.println("몫은 " + share + "이고, 나머지는 " + remain + "입니다.");
}
}
[실행결과]
10을 8로 나누면,
몫은 1이고, 나머지는 2입니다.

[예제3-17] OperatorEx17.java

class OperatorEx17
{
public static void main(String[] args)
{
for(int i=1; i <=10; i++) { // i가 1부터 10이 될 때까지, {}안의 문장을 반복 수행한다.
if(i%3==0) { // i가 3으로 나누어 떨어지면, 3의 배수이므로 출력한다.
System.out.println(i);
}
}
}
}
[실행결과]
3
6
9

조건문과 반복문을 사용해서, 1과 10사이의 정수에서 3의 배수인 숫자만 출력하는 예제이다. 반복문for는 i값을 1부터 10까지 1씩 증가시키면서 괄호{} 안의 문장들을 반복해서 수행한다. 조건문
if는 조건이 만족하는 경우만 괄호{}안의 문장들을 수행한다. 따라서, i%3의 결과가 0인 경우만 i의 값을 화면에 출력하는 것이다.

[예제3-18] OperatorEx18.java

class OperatorEx18
{
public static void main(String[] args)
{
System.out.println(-10%8);
System.out.println(10%-8);
System.out.println(-10%-8);
}
}
[실행결과]
-2
2
-2

피연산자 중에 음의 부호가 있는 경우에 어떤 결과를 얻게 되는지를 보여 주는 예제이다. 결과에서 알 수 있듯이, %연산자의 왼쪽에 있는 피연산자(나눠지는 수)의 부호를 따르게 된다.
간단히 말해서 피연산자의 부호를 모두 무시하고 나머지 연산을 한 결과에 나눠지는 수의 부호를 붙이면 된다.



3.3 쉬프트연산자 - <<, >>, >>>

쉬프트연산자는 정수형 변수에만 사용할 수 있는데, 피연산자의 각 자리(2진수로 표현했을 때)를 오른쪽 또는 왼쪽으로 이동(shift)한다고 해서 쉬프트연산자(shift operator)라고 한다. 오른쪽으로 n자리를 이동하면, 피연산자를 2n로 나눈 것과 같은 결과를 얻을 수 있고, 왼쪽으로 n자리를 이동하면 2n으로 곱한 것과 같은 결과를 얻을 수 있다.


x << n는 x * 2n의 결과와 같다.
x >> n는 x / 2n의 결과와 같다.


"<<"연산자의 경우, 피연산자의 부호에 상관없이 자리를 왼쪽으로 이동시키며 빈칸을 0으로만 채우면 되지만, ">>"연산자는 오른쪽으로 이동시키기 때문에 음수인 경우 부호를 유지시켜주기 위해서 음수인 경우 빈자리를 1로 채우게 된다. 반면에 ">>>"연산자는 부호에 상관없이 항상 0으로 빈자리를 채운다.
그렇기 때문에, 음수에 ">>>"연산을 행한 후, 10진수로 변환해 보면, 뜻밖의 결과를 얻게 될 것이다. "<<", ">>"연산자와는 달리 ">>>"연산자의 결과는 10진수 보다는 2진수로 표현했을 때 기호로서 더 의미를 가지므로, 10진 연산보다는 비트연산(2진 연산)에 주로 사용된다.

[참고]여기서 숫자 n의 값이 자료형의 bit수 보다 크면, 자료형의 bit수로 나눈 나머지 만큼만 이동한다. 예를 들어 int형의 경우, 4byte(32bit)이므로 자리수를 32번 바꾸면 결국 제자리로 돌아오기 때문에,
1000 >> 32는 수행하면 아무 일도 하지 않아도 된다. 1000 >> 35는 35를 32로 나눈 나머지인 3만큼만 이동하는 1000 >> 3을 수행한다.




[참고]쉬프트연산자 역시 int보다 작은 타입의 변수는 int로 변환한 다음에 연산을 수행하므로 8은 4byte(32bit), 따라서 32자리의 2진수로 표현된다.

곱셈이나 나눗셈 연산자를 사용하면 같은 결과를 얻을 수 있는데, 굳이 쉬프트연산자를 제공하는 이유는 속도 때문이다.
예를 들어 8 >> 2 의 결과는 8 / 4의 결과와 같다. 하지만, 8 / 4를 연산하는데 걸리는 시간보다 8 >> 2를 연산하는데 걸리는 시간이 더 적게 걸린다. 다시 말하면, ">>" 또는 "<<" 연산자를 사용하는 것이 나눗셈(/) 또는 곱셈(*) 연산자를 사용하는 것 보다 더 빠르다.
쉬프트연산자보다 곱셈 또는 나눗셈연산자를 주로 사용하고, 보다 빠른 실행속도가 요구되어지는 곳에만 쉬프트연산자를 사용하도록 한다.

[예제3-20] OperatorEx20.java

class OperatorEx20 {
public static void main(String args[]) {
int temp; // 계산 결과를 담기 위한 변수

System.out.println(-8);
System.out.println(Integer.toBinaryString(-8)); // -8을 2진수 문자열로 변경한다.
System.out.println(); // 줄바꿈을 한다.

temp = -8 << 1;
System.out.println( "-8 << 1 = " + temp);
System.out.println(Integer.toBinaryString(temp));
System.out.println();

temp = -8 << 2;
System.out.println( "-8 << 2 = " + temp);
System.out.println(Integer.toBinaryString(temp));
System.out.println();

System.out.println();
System.out.println(-8);
System.out.println(Integer.toBinaryString(-8));
System.out.println();

temp = -8 >> 1;
System.out.println( "-8 >> 1 = " + temp);
System.out.println(Integer.toBinaryString(temp));
System.out.println();

temp = -8 >> 2;
System.out.println( "-8 >> 2 = " + temp);
System.out.println(Integer.toBinaryString(temp));
System.out.println();

System.out.println();
System.out.println(-8);
System.out.println(Integer.toBinaryString(-8));
System.out.println();

temp = -8 >>> 1;
System.out.println( "-8 >>> 1 = " + temp);
System.out.println(Integer.toBinaryString(temp));
System.out.println();

temp = -8 >>> 2;
System.out.println( "-8 >>> 2 = " + temp);
System.out.println(Integer.toBinaryString(temp));
System.out.println();
}
}
[실행결과]
-8
11111111111111111111111111111000

-8 << 1 = -16
11111111111111111111111111110000

-8 << 2 = -32
11111111111111111111111111100000


-8
11111111111111111111111111111000

-8 >> 1 = -4
11111111111111111111111111111100

-8 >> 2 = -2
11111111111111111111111111111110


-8
11111111111111111111111111111000

-8 >>> 1 = 2147483644
1111111111111111111111111111100

-8 >>> 2 = 1073741822
111111111111111111111111111110
[참고] -8 >>> 1과 -8 >>> 2의 결과에서 맨 앞의 0은 생략된 것이다.

반응형