연산자
연산자는 연산을 수행하는 기호이다. 연산자가 연산을 수행하기 위해서는 연산의 대상이 있어야 하는데, 이를 연산자와 피연산자로 나눌 수 있다.
- 연산자(operator) : 연산을 수행하는 기호 (X + 3) 에서 +
- 피연산자(operand) : 연산자의 작업 대상 (X + 3) 에서 X 와 3
그리고 식이란, 이 연산자와 피연산자의 조합이다.
연산자의 종류
종류 연산자 설명
산술 연산자 | +, -, *, /, % , <<, >> | 사칙연산과 나머지 연산(%) |
비교 연산자 | >, <, ≥, ≤, ==, ≠ | 크고 작음과 같고 다름을 비교 |
논리 연산자 | &&, | |
대입 연산자 | = | 우변의 값을 좌변에 저장 |
기타 | (type), ?:, instanceof | 형변환 연산자, 삼항 연산자, instanceof 연산자 |
연산자의 우선순위와 결합규칙
- 우선순위
- 산술 > 비교 > 논리 > 대입
- 단항(1) > 이항(2) > 삼항(3)
- 단항 연산자와, 대입 연산자를 제외한 모든 연산의 진행방향은 왼쪽에서 오른쪽이다.
- 결합규칙종류 결합규칙 연산자 우선순위
단항 연산자 <ㅡㅡㅡㅡㅡㅡ ++, --, +, -, ~, ! , (type) 높음 산술 연산자 ㅡㅡㅡㅡㅡㅡ> *, /, % +, - << , >> 비교 연산자 ㅡㅡㅡㅡㅡㅡ> < , > , ≤ , ≥, instanceof == , ≠ 논리연산자 ㅡㅡㅡㅡㅡㅡ> & ^ && 삼항연산자 ㅡㅡㅡㅡㅡㅡ> ?: 대입 연산자 <ㅡㅡㅡㅡㅡㅡ =, +=, -=, *=, /=, %=, <<=, >>=, &=, ^=, =
산술 변환
연산 전에 피연산자 타입의 일치를 위해 자동 형변환 되는 것을 말한다.
산술 변환의 규칙
- 두 피연산자의 타입을 같게 일치시킨다. (보다 큰 타입으로 일치) → 피 연산자의 값 손실 최소화
- long + int -> long + long -> long float + int -> float + float -> float double + float -> double + double -> double
- 피연산자의 타입이 int보다 작은 타입이면 int로 반환된다.
- byte + short -> int + int -> int char + short -> int + int -> int
참고 연산결과의 타입은 피연산자의 타입과 일치하기 때문에, int / int = int 를 반환하여 소수점 이하가 버려진다. 따라서, 소수점 이하 결과값을 얻기 위해서는 정수 타입을 실수 타입으로 하나는 형변환을 해줘야 한다.
단항 연산자
증감 연산자
- 증가 연산자(++) : 피연산자의 값을 1 증가시킨다.
- 감소 연산자(--) : 피연산자의 값을 1 감소시킨다.
이때, 피 연산자의 앞에 위치하면 전위형(prefix), 뒤에 위치하면 후위형(postfix)이다. 전위형과 후위형이 수식이나 메서드 호출에 포함되지 않고 독립적이면, 큰 차이가 없다.
int i = 5;
i++;
System.out.println(i); // 6
i = 5;
++i;
System.out.println(i); // 6
그러나 단독으로 사용되지 않을 경우에는 다르다.
int i = 5, j = 0;
j = i++;
System.out.println(i + ", " + j); // 6, 5
i = 5;
j = 0;
j = ++i;
System.out.println(i + ", " + j); // 6, 6
이걸 나눠서 보면 다음과 같다.
// 1. j = ++i; // 전위형
++i; // 증가 후
j = i; // 대입
// 2. j = i++; // 후위형
j = i; // 대입 후
i++; // 증가
// 전위는 증가후에 대입이고, 후위는 대입후에 증가이다. 이를 잊지말자.
증감 연산자를 x = x++ - ++x; 와 같이 사용할 수 있다. 그러나 이는 가독성이 떨어져서 권장하지 않는다.
부호 연산자
부호 연산자는 boolean형과 char형을 제외한 기본형에만 사용할 수 있다. -는 피연산자의 부호를 반대로 변경한 결과를 반환하지만, +는 하는 일이 없다.
산술 연산자
사칙 연산자
피연산자가 정수형인 경우, 나누는 수로 0을 사용할 수 없고, 정수를 실수로 나누게 되면 Infinity가 출력된다.
1. 연산자 연산 주의
byte a = 10;
byte b = 20;
byte c = a + b; // 컴파일 에러 발생 byte c = (int)a + (int)b 와 같음
// 피연산자의 값이 int보다 작기 때문에 int로 자동 형변환 된 것
System.out.println(c);
따라서, 큰 자료형의 값을 작은 자료형의 변수에 저장하려면 명시적으로 형변환 연산자를 사용해서 변환해주어야 한다.결과값을 300을 예상했겠지만, 44가 나왔다. byte는 1바이트의 크기를 가지고 있고, 이는 8비트, 즉 2^8을 갖는다. 따라서 256을 넘어 다시 44가 리턴 된다.
byte a = 10;
byte b = 20;
byte c = a + b; // 컴파일 에러 발생 byte c = (int)a + (int)b 와 같음
// 피연산자의 값이 int보다 작기 때문에 int로 자동 형변환 된 것
System.out.println(c);
결과값을 300을 예상했겠지만, 44가 나왔다. byte는 1바이트의 크기를 가지고 있고, 이는 8비트, 즉 2^8을 갖는다. 따라서 256을 넘어 다시 44가 리턴 된다.
2. 연산자 범위 주의
int a = 1000000;
int b = 2000000;
long c = a * b // -145479936
두 int형 자료형의 계산 값을 long타입에 저장함으로 정상적으로 2000000000000이 출력될 것 같지만 그렇지 않다. 연산자의 우선순위를 생각해보면 이해가 편한데, 대입 연산자는 가장 마지막에 이루어진다. 따라서 int형 자료형인 a와 b가 먼저 계산이 되고, 예상한 결과값은 int 타입의 범위를 넘어서기 때문에, 오버플로우가 발생하여 이상한 값이 출력되게 된다. 우리가 예상한 값으로 출력하기 위해서는 다음과 같은 작업이 필요하다.
// 1.
long a = 1000000;
int b = 2000000
long c = a * b
// 2.
int a = 1000000;
int b = 2000000;
long c = a * (long)b
3. 사칙연산은 문자끼리도 가능하다.
char a = 'a';
char d = 'd';
System.out.println(d - a) = 3;
char 자료형은 실제로는 유니코드 값이 저장이 된다. 그리고 0~9, a ~ z, A ~ Z 는 연속적으로 배치가 되어 있다. 따라서 위와 같은 연산이 가능한데 만약 해당 char 숫자의 원 값을 구하고 싶다면, 첫 값을 빼면 원 값이 나온다.
System.out.println('3' - '0'); // 3
4. 리터럴 연산과, 형변환
char c1 = 'a';
char c2 = c1;
char c3 = ' ';
int i = c1 + 1;
c3 = (char)(c1 + 1); // 98
c2++;
System.out.println(c2); // 98
c2 = c2 + 1; // 컴파일 에러
c2 = 'a' + 1; // 컴파일 에러 발생 X
여기서 c2 = c2 + 1은 컴파일 에러가 발생하는 것을 볼 수 있다. 이유는 형변환때문에 발생한 것인데, c2는 char이고 1은 int형이다. 따라서 두 타입의 계산결과는 int형이 반환되지만, char형에 값을 담으려고 하기 때문에 컴파일 에러가 발생한다.
참고로 대문자와 소문자 간의 코드값 차이는 10진수로 32이다.
5. 정수, 소수 연산
float pi = 3.141592f;
float shortPi = (int)(pi * 1000) / 1000f;
// 3141.592 -> (int) 형변환 -> 3141 -> 형변환 -> 3.141
System.out.println(shortPi); // 3.141
이때, int형으로 형변환을 하게 되면 그 수까지의 자릿수만 받아들이고 다음 자릿수는 내림으로 바뀌는 것을 볼 수 있다.
따라서 이때는 Math.round() 를 사용하거나, 0.5를 더해주어야 한다.
(int)(pi * 1000 + 0.5) / 1000.0;
Math.round(pi * 1000) / 1000.0;
나머지 연산자
피 연산자로 정수만 허용한다.
int x = 10;
int y = 8;
// 몫 : x / y : 1 , 나머지 : x % y = 2
비교 연산자
대소비교 연산자
대소비교 연산에는 >, <, >=, <= 이 있다.
등가비교 연산자
기본형의 경우에는 변수에 저장되어 있는 값을 확인 하기 위해서 ==, != 를 사용할 수 있고, 기본형과 참조형은 서로 형변환이 가능하지 않기 때문에, 기본형과 참조형은 비교할 수 없다.
여기서 주의할점은 다음과 같다.
float f = 0.1f;
double d = 0.1;
double d2 = (double)f;
10.0 == 10.0f // true
0.1 == 0.1f // false
10.0, 10.0f 는 true를 반환하고, 0.1, 0.1f 는 false를 반환하는 모습을 볼 수 있습니다. 이는 부동 소수점, 정밀도 때문에 이진수 반환 과정에서 오류가 발생한 것인데 사실 위에 0.1에 저장된 값은 다음과 같습니다.
float f = 0.1f; // 0.10000000149011612
double d = 0.1 // 0.10000000000000001
따라서 d = f를 하더라도, 형변환되기전에 이미 값이 float 형태로 들어가있기 때문에, double 타입으로 형변환해도 값이 변하지 않았음에 주목하자.
- 문자열의 비교팁 대소문자를 구별하지 않고 비교하고 싶을 땐, equalsIgnoreCase()를 사용하자.
- String은 Class 이므로 == 으로 문자가 같은지에 대해서 판단할 수 없다. (클래스는 객체의 주소 값으로 비교하기 때문이다.) 따라서, equals() 라는 메서드를 사용해야 한다. equals() 는 같으면 true, 틀리면 false를 반환한다.
논리 연산자
논리연산자에는 둘 이상의 조건을 AND, OR로 연결하여 하나의 식으로 표현할 수 있게 해준다.
|| (OR결합) 피연산자 중 어느 한 쪽만 true이면 true를 결과로 얻는다.
&& (AND결합) 피연산자 양쪽 모두 true이어야 true를 결과로 얻는다.
여기서 주의할 점은 || 보다 &&이 우선순위가 더 높다는 것이다.
i % 2 == 0 || i % 3 == 0 && i % 6 != 0
-> i % 2 == 0 || (i % 3 == 0 && i % 6 != 0)
우선순위로 인해 뒤에가 먼저 연산되는 것을 볼 수 있다. 따라서, OR을 먼저 연산하고자 한다면 괄호를 통해 명확히 표현을 해주어야 한다.
효율적인 연산
효율적인 연산(short circuit evaluation)은 좌측에 대한 결과에 대해서 후에 있을 연산을 하지 않는 것이다.
- OR 연산
- OR 연산의 경우 좌측 피연산자가 참이라면 무조건 참이 나온다. 따라서 우측 피연산자는 평가하지 않는다.
- AND 연산
- AND 연산의 경우 좌측 피연산자 거짓이라면 무조건 거짓이 나온다. 따라서 우측 피연산자는 평가하지 않는다.
('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')
다음과 같은 코드를 살펴보자. 위 코드는 해당 코드가 소문자인지, 대문자인지 판별하는 코드이다. 소문자인지 확인하는 코드를 먼저 앞에 둔 것을 볼 수 있는데, 소문자를 입력할 가능성이 더 높기 때문이다. 좌측이 참으로 확인되면 우측은 확인하지 않는다는 특징을 사용한 것이다.
int a = 5;
int b = 0;
if(a != 0 || ++b != 0)
System.out.println(a + ", " + b); // 5, 0
앞 조건이 참이기 때문에 뒷 조건을 검사하지 않아, 단항 연산자가 수행되지 않았기 때문에 b의 값이 ++ 되지 않았다.
논리 부정 연산자 !
true이면 false를, false면 true를 반환하는 연산자이다. 이를 이용해 toggle button 같은 것을 이용할 수 있다.
ch < 'a' || ch > 'z' // 1
!('a' <= ch && ch <= 'z') // 2
위 식은 문자 ch가 소문자인지 아닌지 판별하는 조건 식인데, 1번 식보다, 2번 식을 사용하는 것이 가독성이 더 좋다.
비트 연산자
| (OR연산자) 피연산자 중 한 쪽 값이 1이면, 1을 결과로 얻는다. 그 외에는 0을 얻는다.
& (AND연산자) 피연산자 양 쪽이 모두 1이어야만 1을 결과로 얻는다. 그 외에는 0을 얻는다.
^ (XOR연산자) 피연산자의 값이 서로 다를 때만 1을 결과로 얻는다. 같을 때는 0을 얻는다.
언제 사용하는 지에 대해서 아는 것이 중요할 것 같다.
- 비트OR연산자 |
- 특정 비트의 값을 변경할 때 사용한다.
- 0xAB | 0xF → 0xAF
- 비트AND연산자 &
- 특정 비트의 값을 뽑아낼 때 사용한다.
- 0xAB & 0xF → 0XB
- 비트XOR연산자 ^
- 값은 값으로 두고 XOR 연산을 수행하면 원래 값으로 돌아온다. 이 특성을 이용하여 간단한 암호화가 가능하다.
- 0xAB ^ 0xF = 0xA4 ^ 0xF = 0xAB
비트 전환 연산자 ~
- 피연산자를 2진수로 표현했을 때, 1은 0으로, 0은 1로 바꾸는 연산자이다.
쉬프트 연산자 << , >>
- 피연산자를 이진수로 표현했을 때 각 자리를 << 왼쪽, >> 오른쪽 으로 이동시키는 연산자이다.
<< 연산자
- 8 << 2 는 왼쪽 피연산자인 8을 왼쪽으로 2자리 이동한다.
- 00001000 → 00100000 = 32
- << 연산자는 피연산자의 부호에 상관없이 빈칸을 0으로만 채우면 된다.
>> 연산자
- 8 >> 2 는 왼쪽 피연산자인 8을 오른쪽으로 2자리 이동한다.
- 00001000 → 00000010 = 2
- 연산자는 부호비트가 있기 때문에, 음수일 경우 1을 채우고, 양수일 경우 0을 채운다.
팁 : x << n 연산자는 $x * 2^n$ 과 같고, x >> n 연산자는 $x / 2^n$ 의 결과와 같다.
추가적으로 쉬프트 연산자는 일반적인 곱셈 나눗셈보다 연산속도가 빠르다.
하지만 가독성을 생각하긴 해야한다. 따라서, 빠른 실행속도가 요구되어지는 곳에만 쉬프트 연산자를 사용하는 것이 좋다.
그 외의 연산자
조건 연산자
조건식 ? 식1 : 식2
삼항 연산자라고도 부른다. 조건이 참이면 식1, 거짓이면 식2를 반환한다.
result = x > 0 ? 1 : x==0 ? 0 : -1
다음과 같은 식이 있을 때, 조건 연산자의 결합규칙은 오른쪽에서 왼쪽이기 때문에 오른쪽이 먼저 수행된다. 삼항 연산 내부에서는 왼쪽에서 오른쪽이다.
if x == 3
// 1. x > 0 ? 1 : 3 == 0 ? 0 : -1
// 2. x > 0 ? 1 : false ? 0 : -1
// 3. x > 0 ? 1 : -1
// 4. 3 > 0 ? 1 : -1
// 5. true ? 1 : -1
// 6. 1
다음과 같은 과정으로 수행된다.
대입 연산자
대입연산자 = 는 가장 낮은 수선순위를 가지고 있어서 가장 나중에 수행된다.
x = y = 3
// 1. y = 3
// 2. x = y
대입 연산자는 결합 규칙이 오른쪽에서 왼쪽이기 때문에 오른쪽이 먼저 수행되고, 수행된 값을 왼쪽 값에 대입한다.
- op= (복합 대입 연산자)
op = | = |
i += 3; | i = i + 3; |
i -= 3; | i = i - 3; |
i *= 3; | i = i * 3; |
i /= 3; | i = i / 3; |
i %= 3; | i = i % 3; |
i <<= 3; | i = i << 3; |
i >>= 3; | i = i >> 3; |
i &= 3; | i = i & 3; |
i ^= 3; | i = i ^ 3; |
i | = 3; |
i *= 10 + j; | i = i * (10 + j) |
'Language > Java' 카테고리의 다른 글
7-2 참조변수 super, 생성자 super() (0) | 2023.03.22 |
---|---|
7-1 상속과 오버라이딩 (0) | 2023.03.22 |
6. 객체지향언어 1 (0) | 2023.03.22 |
2. 변수 (0) | 2023.03.21 |
1. 자바 (0) | 2023.03.21 |