산술 연산자인 사칙 연산자, 나머지 연산자, 쉬프트 연산자는 이항 연산자이다. 이항 연산자는 피연산자의 크기가 4 byte 보다 작으면 4 byte(int 형) 로 변환한 다음 연산을 수행한다. (byte, char, short → int)

또, 연산을 수행하기 전에 피연산자들의 타입을 일치시킨다.

 

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

1. int 형(4 byte) 보다 크기가 작은 자료형은 int 형으로 형변환 후에 연산을 수행한다.

    - byte + short → int + int → int

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

    - int + float → float + float → float

3. 정수형 간의 나눗셈에서 0으로 나누는 것은 금지된다.

    - 0으로 나누면, 컴파일은 정상적으로 되지만 실행 시 오류가 발생한다.

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

 

public class ex2 {
	public static void main(String[]args) {
		boolean power = false;
		byte a = 10;
		byte b = 20;
		byte c = (byte)(a + b); //컴파일 에러가 발생하기 때문에 명시적 형변환 필요
		System.out.println(c);
	}
}
[실행 결과]
30

큰 자료형의 값을 작은 자료형의 변수에 저장하려면 반드시 명시적 형변환을 해야 한다.

[참고] byte c = (byte)a+b; 라고 해도 에러가 발생하는데, 그 이유는 캐스트 연산자는 단항 연산자이므로 이항 연산자인 덧셈 연산자보다 연산 순위가 높다. 그래서 (byte)a 가 먼서 수행된 다음 덧셈이 수행되므로, (byte)a 가 캐스트 연산자에 의해서 byte 형으로 형변환된 후에 다시 + b 의 덧셈 연산자에 의해서 int 형으로 변환된다.

 

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

10*30 의 결과는 300이지만, 큰 자료형에서 작은 자료형으로 변환하면 데이터의 손실이 발생하여 값이 바뀔 수 있다. 300은 byte 형의 범위를 넘기 때문에 데이터 손실이 발생하여 44가 출력된다.

byte 형에서 int 형으로 변환하는 것은 2진수 8자리에서 32자리로 변환하는 것이기 때문에 자료 손실이 일어나지 않는다. 기존의 8자리는 유지하고 나머지는 모두 0으로 채운다. 음수인 경우에는 부호를 유지하기 위해 0 대신 1로 채운다.

반대로 int 형을 byte 형으로 변환하는 경우 앞의 24자리를 없애고 하위 8자리만 유지한다.

byte 형의 범위인 -128 ~ 127 의 범위를 넘는 int 형을 byte 형으로 변환하면 원래의 값이 유지되지 않고 byte 형의 범위 중 한 값을 가지게 된다. 이러한 값 손실을 예방하기 위해서는 큰 자료형을 사용해야 한다.

 

public class ex2 {
	public static void main(String[]args) {
		int a = 1000000;
		int b = 2000000;
		long c = a*b;
		System.out.println(c);
	}
}
[실행 결과]
-1454759936

int 형과 int 형의 연산결과는 int 형이기 때문에 long 형으로 자동 변환되어서 long 형 변수인 c 에 저장되어도 결과를 같다. 올바른 결과를 얻기 위해서는 a, b 모두 long 타입으로 바꿔야 한다.

[참고] a, b 중 하나만 long 으로 바꿔도 결과는 long 으로 나온다

 

public class ex2 {
	public static void main(String[]args) {
		long a = 1000000 * 1000000;
		long b = 1000000 * 1000000L;
		System.out.println(a);
		System.out.println(b);
	}
}
[실행 결과]
-727379968
1000000000000

b 는 int 와 long 의 연산이기 때문에 그 결과가 long 이다.

 

public class ex2 {
	public static void main(String[]args) {
		int a = 1000000 * 1000000 / 1000000;
		int b = 1000000 / 1000000 * 1000000;
		System.out.println(a);
		System.out.println(b);
	}
}
[실행 결과]
-727
1000000

먼저 곱하는 경우 int 의 범위를 넘어선다. 이처럼 연산의 순서에 따라서 결과가 달라질 수 있다.

 

public class ex2 {
	public static void main(String[]args) {
		char c1 = 'a'; //c1에는 'a'의 코드값인 97 저장
		char c2 = c1; //c1에 저장되어 있는 값이 c2에 저장
		char c3 = ' '; //c3을 공백으로 초기화
		
		int i = c1 + 1; //'a'+1 >>97+1>>98
		
		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 형으로 변환한 후 덧셈연산을 수행한다. 

c2++ 은 형변환 없이 값을 1씩 두 번 증가되어 99가 된다. 99는 문자 'c' 이기 때문에 'c' 가 출력된다.

[참고] c2++ 대신 c2=c2+1 을 사용하게 되면 c2+1 의 연산결과가 int 형이기 때문에 에러가 난다. 결과를 다시 c2 에 담으려면 캐스트 연산자를 사용하여 char 형으로 형변환해야 한다.

 

public class ex2 {
	public static void main(String[]args) {
		char c1 = 'a';
		//char c2 = c1+1; 컴파일 에러 발생
		char c2 = 'a'+1;
		System.out.println(c2);
	}
}
[실행 결과]
b

int 보다 작은 타입의 피연산자를 int 로 자동형변환하지 않고, char c2 = (char)('a'+1); 로 형변환하지 않았는데도 에러가 없는 이유는 'a'+1 이 리터럴 간의 연산이기 때문이다. 상수 또는 리터럴 간의 연산은 실행과정동안 변하는 값이 아니기 때문에 컴파일 시, 컴파일러가 계산해서 그 결과로 대체함으로써 코드를 효율적으로 만든다.

컴파일러가 미리 덧셈연산을 수행하기 때문에 실행 시에는 덧셈 연산이 수행되지 않는다. 덧셈연산결과인 'b'를 변수 c2에 저장할 뿐이다.

그러나 c1+1 처럼 수식에 변수가 들어가 있는 경우에는 컴파일러가 미리 계산을 할 수 없기 때문에 char c2 = (char)(c1+1); 처럼 형변환을 해주어야 한다.

 

상수 또는 리터럴 간의 연산을 할 때 '86400' 이라는 값 대신 '60*60*24' 라고 풀어서 써도 컴파일러에 의해서 미리 계산되기 때문에 실행 시의 성능차이는 없다.

 

public class ex2 {
	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' 부터 26개의 문자 출력
		}
		System.out.println(); //줄바꿈
	}
}
[실행 결과]
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789

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

[참고] 대문자와 소문자 간의 코드값 차이는 10진수로 32이다.

 

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

소문자를 대문자로 변경하려면 대문자와 소문자의 코드값 차이가 32이므로 소문자 a 의 코드값에서 32를 빼면 된다.

[참고] char 형과 int 형간의 뺄셈연산 결과는 int 형이므로 char 형으로 형변환해야 한다.

 

public class ex2 {
	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 가 나온다. 그리고 나눗셈의 결과를 버림하여 출력한다. 

위의 코드는 실수형 변수 pi 의 값을 소수점 셋째 자리까지만 빼내는 방법을 보여 준다.

shortPi 부분에서 가장 먼저 수행되는 것은 (pi * 1000) 이다. pi 가 float 이고, 1000이 정수형이니까 연산의 결과를 float 인 3141.5921 이 된다.

그 다음으로는 (int)(3141.592f) / 1000f; 가 되는데, 단항연산자인 캐스트연산자의 형변환이 수행된다. 3141.592f 를 int 로 변환하면 3141을 얻고, 소수점 이하는 반올림없이 버려진다.

다음엔 3141/1000f; 가 되는데, int와 float의 연산이므로, int가 float로 변환된 다음, float와 float의 연산이 수행된다.

마지막으로 3151.0f/1000f → 3.141f 는 float 와 float의 나눗셈이므로, 결과를 float로 나오게 된다.

[참고] 1000f는 1000.0f 와 같다.

 

public class ex2 {
	public static void main(String[]args) {
		float pi = 3.141592f;
		float shortPi = Math.round(pi*1000)/1000f;
		System.out.println(shortPi);
	}
}
[실행 결과]
3.142

round 메서드는 매개변수로 받은 값을 소수점 첫째자리에서 반올림하고 그 결과를 정수로 돌려준다. 위 코드는 소수점 넷째 자리에서 반올림하기 위해 1000을 곱했다가 다시 1000f로 나누었다.

 

2. 나머지 연산자 %

왼쪽의 피연산자를 오른쪽 피연산자로 나누고 난 나머지 값을 돌려주는 연산자이다. boolean 형을 제외한 모든 기본형 변수에 사용할 수 있고, 짝수, 홀수, 배수 검사 등에 주로 사용된다.

정수형 연산에서는 오른쪽 피연산자를 0으로 사용할 수 없지만, 0.0이나 0.0f로 나누는 것은 허용된다.

public class ex2 {
	public static void main(String[]args) {
		int share = 10/8;
		int remain = 10%8;
		System.out.println("몫은 "+share+" 나머지는 "+remain);
	}
}
[실행 결과]
몫은 1 나머지는 2
public class ex2 {
	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

 

 

public class ex2 {
	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. 쉬프트 연산자 <<, >>, >>>

쉬프트 연산자는 정수형 변수에만 사용할 수 있는데, 피연산자를 2진수로 표현했을 때, 각자리를 오른쪽 또는 왼쪽으로 이동시킨다. 오른쪽으로 n자리를 이동하면 피연산자를 $2^n$ 으로 나눈 결과와 왼쪽으로 n자리를 이동하면 $2^n$ 으로 곱한 것과 같은 결과를 얻는다.

 

x << n = x * $2^n$

x >> n = x / $2^n$

<< : 피연산자의 부호에 상관없이 자리를 왼쪽으로 이동시키며 빈칸을 0으로 채운다.

>> : 오른쪽으로 이동시키기 때문에 음수인 경우 부호를 유지시켜주기 위해서 음수인 경우, 빈자리를 1로 채운다.

>>> : 부호에 상관없이 항상 0으로 빈자리를 채운다. 결과가 10진수보다 2진수로 표현했을 때 기호로서 더 의미를 가지므로, 10진 연산보다는 비트연산(2진 연산)에 주로 사용.

[참고] 숫자 n의 값이 자료형의 bit수 보다 크면, 자료형의 bit수로 나눈 나머지만큼만 이동한다. 

 

곱셈이나 나눗셈 연산자를 사용하면 같은 결과를 얻을 수 있는데, 굳이 쉬프트 연산자를 사용하는 이유는 속도 때문이다.

public class ex2 {
	public static void main(String[]args) {
		int temp;
		System.out.println(-8);
		//-8을 2진수 문자열로 변경
		System.out.println(Integer.toBinaryString(-8));
		temp = -8 << 2;
		System.out.println("-8 << 2 = "+temp);
		System.out.println(Integer.toBinaryString(temp));
		temp = -8 >> 2;
		System.out.println("-8 >> 2 = "+temp);
		System.out.println(Integer.toBinaryString(temp));
		temp = -8 >>> 2;
		System.out.println("-8 >>> 2 = "+temp);
		System.out.println(Integer.toBinaryString(temp));
	}
}
[실행 결과]
-8
11111111111111111111111111111000
-8 << 2 = -32
11111111111111111111111111100000
-8 >> 2 = -2
11111111111111111111111111111110
-8 >>> 2 = 1073741822
111111111111111111111111111110

 

'JAVA > 연산자' 카테고리의 다른 글

2. 단항 연산자  (0) 2020.02.04
1. 연산자 (operator)  (0) 2020.01.28

+ Recent posts