[자바의 정석] 연산자

Jiwon An·2023년 10월 8일
0

Java

목록 보기
6/9

연산자와 피연산자

연산자는 연산을 수행하는 기호(+,-,*,/ 등)이고, 피연산자는 연산자의 작업 대상(변수, 상수, 리터럴, 수식)을 의미합니다. 연산자는 피연산자로 연산을 수행하고 나면 항상 결과값을 반환합니다.

연산자의 종류

종류연산자설명
산술 연산자+ - * / % << >>사칙 연산(+,-,*,/)과 나머지 연산(%)
비교 연산자> < >= <= == !=크고 작음과 같고 다름을 비교
논리 연산자&& || ! & | ^ ~'그리고(AND)'와 '또는(OR)'으로 조건을 연결
대입 연산자=우변의 값을 좌변에 저장
기타(type) ?: instanceof형변환 연산자, 삼항 연산자, instanceof 연산자

연산자의 우선순위

식에 사용된 연산자가 둘 이상인 경우, 연산자의 우선순위에 의해서 연산순서가 결정됩니다.
또한 하나의 식에 같은 우선순위의 연산자들이 여러 개 있는 경우, 연산자의 결합규칙에 의해 처리되는 순서가 결정됩니다.

종류결합규칙연산자우선순위
단항 연산자<------++ -- + - ~ ! (type)높음
산술 연산자------>* / %
------>+ -
------><< >>
비교 연산자------>< > <= >= instanceof
논리 연산자------>&
------>&
------>^
------>|
------>&&
------>||
삼항 연산자------>?:
대입 연산자<------= += -= *= /= %=
<<= >>= &= ^= |=
낮음
  1. 산술 > 비교 > 논리 > 대입. 대입은 제일 마지막에 수행된다.
  2. 단항(1) > 이항(2) > 삼항(3). 단항 연산자의 우선순위가 이항 연산자보다 높다.
  3. 단항 연산자와 대입 연산자를 제외한 모든 연산의 진행방향은 왼쪽에서 오른쪽이다.

산술 변환

이항 연산자는 두 피연산자의 타입이 일치해야 연산이 가능하므로, 피연산자의 타입이 서로 다르다면 연산 전에 형변환 연산자로 타입을 일치시켜야 합니다. 대부분의 경우, 두 피연산자의 타입 중에서 더 큰 타입으로 일치시키는데, 그 이유는 작은 타입으로 형변환하면 원래의 값이 손실될 가능성이 있기 때문입니다. 이처럼 연산 수행 직전에 피연산자 타입의 일치를 위해 자동 형변환되는 것을 '산술 변환' 또는 '일반 산술 변환'이라 합니다.

산술 변환의 규칙

  1. 두 피연산자의 타입을 같게 일치시킨다. (보다 큰 타입으로 일치)
    • 피연산자의 값손실을 최소화하기 위한 것이다.
    • long + int \rarr long + long \rarr long
  2. 피연산자의 타입이 int보다 작은 타입이면 int로 변환된다.
    • 정수형의 기본 타입인 int가 가장 효율적으로 처리할 수 있는 타입이기 때문이고 int보다 작은 타입의 표현범위가 좁아서 연산중에 오버플로우가 발생할 가능성이 높기 때문이다.
    • byte + short \rarr int + int \rarr int

단항 연산자

증감 연산자 ++ --

이름형태설명
증가 연산자++피연산자의 값을 1 증가
감소 연산자--피연산자의 값을 1 감소

대부분의 연산자는 피연산자의 값을 읽어서 연산에 사용할 뿐, 피연산자의 타입이나 값을 변경시키지 않습니다. 오직 대입연산자와 증감연산자만 피연산자의 값을 변경합니다.

일반적으로 단항 연산자는 피연산자의 왼쪽에 위치하지만, 증가 연산자 '++'와 감소 연잔자 '--'는 양쪽 모두 가능합니다. 피연산자의 왼쪽에 위치하면 '전위형(prefix)', 오른쪽에 위치하면 '후위형(postfix)'이라고 합니다.

타입설명
전위형값이 참조되기 전에 증가
감소 연산자값이 참조된 후에 증가

증감연산자를 사용하면 코드가 간결해지지만, 지나치면 코드가 복잡해서 이해하기 어려워지기도 한다. 그래서 하나의 식에서 증감연산자의 사용을 최소화하고, 식에 두 번 이상 포함된 변수에 증감연산자를 사용하는 것은 피해야 한다.

  x = x++ - ++x;

부호 연산자 + -

부호 연산자는 boolean형과 char형을 제외한 기본형에만 사용할 수 있습니다.

타입설명
-피연산자의 부호를 반대로 변경한 결과 반환
+하는 일 없으며, 쓰이는 경우도 거의 없음

산술 연산자

산술 연산자에는 사칙 연산자(+,-,*,/)와 나머지 연산자(%)가 있습니다.

사칙 연산자 + - * /

피연산자가 정수형인 경우, 나누는 수로 0을 사용할 수 없다. 만일 0으로 나누면, 컴파일은 정상적으로 되지만 실행 시 오류(ArithmeticException)가 발생한다. 부동 소수점값인 0.0f, 0.0d로 나누는 것은 가능하지만 그 결과는 Infinity(무한대)이다.

피연산자가 유한수가 아닌 경우의 연산결과

xyx/yx%y
유한수±0.0\pm0.0±Infinity\pm InfinityNaN
유한수±Infinity\pm Infinity±0.0\pm0.0x
±0.0\pm0.0±0.0\pm0.0NaNNaN
±Infinity\pm Infinity유한수±Infinity\pm InfinityNaN
±Infinity\pm Infinity±Infinity\pm InfinityNaNNaN

크기가 작은 자료형의 변수를 큰 자료형의 변수에 저장할 때는 자동으로 형변환되지만, 반대로 큰 자료형의 값을 작은 자료형의 변수에 저장하려면 명시적으로 형변환 연산자를 사용해서 변환해주어야 합니다.

byte a = 10;
byte b = 20;
byte c = (byte) a + b; // 형변환 안해주면 컴파일 에러

int타입과 int타입의 연산결과는 int타입이기 때문에 연산결과가 int타입의 최대값을 넘으면 오버플로우가 발생한다. 그래서 이미 오버플로우가 발생한 값을 아무리 long타입의 변수에 저장을 해도 소용이 없다.

long a = 1_000_000 * 1_000_000; // 오버플로우 발생

사칙연산의 피연산자로 숫자뿐만 아니라 문자도 가능합니다. 문자는 실제로 해당 문자의 유니코드(부호없는 정수)로 바뀌어 저장되므로 문자간의 사칙연산은 정수간의 연산과 동일합니다. 주로 문자간의 뺄셈을 하는 경우가 대부분이며, 문자 '2'를 숫자로 변환하려면 문자 '0'을 빼주면 됩니다.

char c1 = 'a';

char c2 = c1 + 1; // 컴파일 에러 발생, 형변환 해줘야함
char c2 = 'a' + 1; // 컴파일 에러 없음

'c1+1'을 계산할 때, c1이 char형이므로 int형으로 변환한 후 덧셈연산을 수행하게 됩니다. c1에 저장되어 있는 코드값이 변환되어 int형 값이 되는 것입니다. 이렇듯 덧셈 연산자와 같은 이항 연산자는 int보다 작은 타입의 피연산자를 int로 자동 형변환합니다.

하지만 'a'+1 같은 리터럴간의 연산은 형변환을 해주지 않아도 됩니다. 상수 또는 리터럴 간의 연산은 실행 과정동안 변하는 값이 아니기 때문에, 컴파일 시에 컴파일러가 계산해서 그 결과로 대체함으로써 코드를 보다 효율적으로 만듭니다. 컴파일러가 미리 덧셈연산을 수행하기 때문에 실행 시에는 덧셈 연산이 수행되지 않습니다.

컴파일 전의 코드컴파일 후의 코드
char c2 = 'a+1;
int sec = 60 * 60 * 24;
char c2 = 'b';
int sec = 86400;

그래서 수식에 변수가 들어가 있는 경우에는 컴파일러가 미리 계산을 할 수 없기 때문에 형변환을 해줘야 합니다.

일부러 뻔한 리터럴 연산을 풀어쓸 필요는 없지만, 코드의 가독성과 유지보수를 위해서 그렇게 하는 경우가 있습니다. int 타입의 변수 sec에 하루(day)를 초(second) 단위로 변환한 값을 저장하는 코드에서, 86400이라는 값보다는 '606024'와 같이 적어주는 것이 이해하기도 쉽고 오류가 발생할 여지가 적습니다. 나중에 반나절(12시간)로 값을 변경해야한다면 계산할 필요없이 '606012'로 변경하면 되기 때문입니다. 이렇게 풀어써도 결국 컴파일러에 의해서 미리 계산되기 때문에 실행 시의 성능차이는 없습니다.

또한 int형 간의 나눗셈 'int / int'를 수행하면 결과가 float나 double이 아닌 int임에 주의해야 합니다. 그리고 나눗셈의 결과를 반올림 하는 것이 아니라 버립니다.

나머지 연산자 %

나머지 연산자는 왼쪽의 피연산자를 오른쪽 피연산자로 나누고 난 나머지 값을 결과로 반환하는 연산자입니다. 그리고 나눗셈에서처럼 나누는 수(오른쪽 피연산자)로 0을 사용할 수 없다는 점에 주의하자. 나머지 연산자는 주로 짝수, 홀수 또는 배수 검사 등에 주로 사용됩니다.

비교 연산자

비교 연산자는 두 피연산자를 비교하는 데 사용되는 연산자입니다. 주로 조건문과 반복문의 조건식에 사용되며, 연산결과는 오직 true와 false 둘 중의 하나입니다.
비교 연산자의 역시 이항 연산자이므로 비교하는 피연산자의 타입이 서로 다를 경우 자료형의 범위가 큰 쪽으로 자동 형변환하여 피연산자의 타입을 일치시킨 후에 비교한다는 점에 주의하자.

대소비교 연산자 < > <= >=

두 피연산자의 값의 크기를 비교하는 연산자입니다. 기본형 중에서는 boolean형을 제외한 나머지 자료형에 다 사용할 수 있지만 참조형에는 사용할 수 없습니다.

비교연산자연산결과
>좌변 값이 크면, true 아니면 false
<좌변 값이 작으면, true 아니면 false
>=좌변 값이 크거나 같으면, true 아니면 false
<=좌변 값이 작거나 같으면, true 아니면 false

등가비교 연산자 == !=

두 피연산자의 값이 같은지 또는 다른지를 비교하는 연산자입니다. 대소비교 연산자와는 달리, 기본형은 물론 참조형, 즉 모든 자료형에 사용할 수 있습니다. 기본형의 경우 변수에 저장되어 있는 값이 같은지를 알 수 있고, 참조형의 경우 객체의 주소값을 저장하기 때문에 두 개의 피연산자(참조변수)가 같은 객체를 가리키고 있는지를 알 수 있습니다.
기본형과 참조형은 서로 형변환이 가능하지 않기 때문에 등가비교 연산자로 기본형과 참조형을 비교할 수 없습니다.

비교연산자연산결과
==두 값이 같으면, true 아니면 false
!=두 값이 다르면, true 아니면 false
System.out.println(10.0==10.0f); // true
System.out.println(0.1==0.1f); // false

'10.0==10.0f'는 true인데 '0.1==0.1f'는 false라니 무슨? 정수형과 달리 실수형은 근사값으로 저장되므로 오차가 발생할 수 있기 때문입니다. 10.0f는 오차없이 저장할 수 있는 값이라서 double로 형변환해도 그대로 10.0이 되지만, 0.1f는 저장할 때 2진수로 변환하는 과정에서 오차가 발생합니다. double타입의 상수인 0.1도 저장되는 과정에서 오차가 발생하지만, float타입의 리터럴인 0.1f보다 적은 오차로 저장됩니다.

float f = 0.1f; // f에 0.10000000149011612로 저장됨
double f = 0.1; // d에 0.10000000000000001로 저장됨

float타입의 값을 double타입으로 형변환하면, 부호와 지수는 달라지지 않고 그저 가수의 빈자리를 0으로 채울 뿐이므로 0.1f를 double타입으로 형변환해도 그 값은 전혀 달라지지 않습니다. 즉, float타입의 값을 정밀도가 더 높은 double타입으로 형변환했다고 해서 오차가 적어지는 것이 아니라는 것입니다.

float타입의 값과 double타입의 값을 비교하려면 어떻게 해야할까? double타입의 값을 float타입으로 형변환한 다음에 비교해야 합니다. 그래야만 올바른 결과를 얻을 수 있습니다. 또는 어느 정도의 오차는 무시하고 두 타입의 값을 앞에서 몇 자리만 잘라서 비교할 수도 있습니다.

(float)d == f
-> (float)0.10000000000000001 == 0.10000000149011612
-> 0.10000000149011612 == 0.10000000149011612
-> true

문자열의 비교

두 문자열을 비교할 때는, 비교 연산자 '=='대신 equals()라는 메서드를 사용해야 합니다. 비교 연산자는 두 문자열이 완전히 같은 것인지 비교할 뿐이므로, 문자열의 내용이 같은지 비교하기 위해서는 equals()를 사용하는 것입니다.

논리 연산자

논리 연산자는 둘 이상의 조건을 '그리고(AND)'나 '또는(OR)'으로 연결하여 하나의 식으로 표현할 수 있게 해줍니다.

논리 연산자 - &&, ||, !

연산자설명
||(OR결합)피연산자 중 어느 한 쪽만 true이면 true를 결과로 얻음
&&(AND결합)피연산자 양쪽 모두 true이어야 true를 결과로 얻음
!피연산자가 true이면 false, false면 true를 반환

효율적인 연산
논리 연산자의 또 다른 특징은 효율적인 연산을 한다는 것입니다. OR연산'||'의 경우, 두 피연산자 중 어느 한 쪽만 '참'이어도 전체 연산결과가 '참'이므로 좌측 피연산자가 'true(참)'이면, 우측 피연산자의 값은 평가하지 않습니다.

AND연산'&&'의 경우도 마찬가지로 어느 한쪽만 '거짓(false)'이어도 전체 연산결과가 '거짓(false)'이므로 좌측 피연산자가 '거짓(false)'이면, 우측 피연산자는 평가하지 않습니다.

이렇듯 같은 조건식이라도 피연산자의 위치에 따라서 연산속도가 달라질 수 있습니다. OR연산'||'의 경우에는 연산결과가 '참'일 확률이 높은 피연산자를 연산자의 왼쪽에 놓아야 더 빠른 연산결과를 얻을 수 있습니다.

비트 연산자 & | ^ ~ << >>

연산자설명
|(OR연산자)피연산자 중 한 쪽의 값이 1이면, 1을 결과로 얻음 그외에는 0을 얻음
&(AND연산자)피연산자 양 쪽이 모두 1이어야만 1을 결과로 얻음 그외에는 0을 얻음
^(XOR연산자)피연산자의 값이 서로 다를 때만 1을 결과로 얻음 같은 때는 0을 얻음
~피연산자를 2진수로 표현했을 때, 0은 1로, 1은 0으로 바꿈
<<(쉬프트 연산자)피연산자를 2진수로 표현했을 때, 왼쪽으로 이동
>>(쉬프트 연산자)피연산자를 2진수로 표현했을 때, 오른쪽으로 이동

주로 사용되는 곳

연산자사용
|특정 비트의 값을 변경할 때 사용
&특정 비트의 값을 뽑아낼 때 사용
^같은 값으로 두고 XOR연산을 수행하면 원래의 값으로 돌아오는 특징
-> 간단한 암호화에 사용
~1의 보수를 얻는 데 사용
<<x<<n은 xx * 2n2^{n}의 결과와 같음
>>x>>n은 xx / 2n2^{n}의 결과와 같음

비트 전환 연산자 ~

비트 전환 연산자'~'에 의해 '비트 전환'되고 나면, 부호있는 타입의 피연산자는 부호가 반대로 변경됩니다. 즉, 피연산자의 '1의 보수'를 얻을 수 있습니다. 또한 p에 대한 음의 정수는 ~p+1 로, p에 대한 양의 정수는 ~(n-1)로 얻을 수 있다. 물론 부호 연산자'-'를 사용하면 되기 때문에 참고로만 알아두자.

쉬프트 연산자

'<<'연산자의 경우, 피연산자의 부호에 상관없이 각 자리를 왼쪽으로 이동시키며 빈칸을 0으로만 채우면 되지만, '>>'연산자는 오른쪽으로 이동시키기 때문에 부호있는 정수는 부호를 유지하기 위해 왼쪽 피연산자가 음수인 경우 빈자리를 1로 채웁니다. 물론 양수일 때는 0으로 채웁니다.

비트 연산에서도 피연산자의 타입을 일치시키는 '산술 변환'이 일어날 수 있습니다. 쉬프트 연산자의 좌측 피연산자는 산술변환이 적용되어 int보다 작은 타입은 int타입으로 자동 변환되고 연산결과 역시 int타입이 됩니다. 그러나 쉬프트 연산자는 다른 이항연산자들과 달리 피연산자의 타입을 일치시킬 필요가 없기 때문에 우측 피연산자에는 산술변환이 적용되지 않습니다.

'x << n'또는 'x >> n'에서, n의 값이 자료형의 bit수 보다 크면, 자료형의 bit수로 나눈 나머지만큼만 이동합니다. 예를 들어 int타입이 4byte(=32bit)인 경우, 자리수를 32번 바꾸면 결국 제자리로 돌아오기 때문에, '8 >> 32'는 아무 일도 일어나지 않습니다. '8 >> 34'는 32를 32로 나눈 나머지인 2만큼만 이동하는 '8 >> 2'를 수행합니다. 당연히 n은 정수만 가능하며 음수인 경우, 부호없는 정수로 자동 변환됩니다.

곱셈이나 나눗셈 연산자를 사용하면 같은 결과를 얻을 수 있는데, 굳이 쉬프트 연산자를 제공하는 이유는 속도 때문입니다. 예를 들어 '8 >> 2'의 결과는 '8 / 4'의 결과와 같습니다. 하지만, '8 / 4'를 연산하는데 걸리는 시간보다 '8 >> 2'를 연산하는데 걸리는 시간이 더 적게 걸립니다. 즉, '>>' 또는 '<<' 연산자를 사용하는 것이 나눗셈'/'또는 곱셈'*' 연산자를 사용하는 것 보다 더 빠릅니다.
그러나 프로그램의 실행속도도 중요하지만 프로그램을 개발할 때 코드의 가독성도 중요합니다. 쉬프트 연산자가 속도가 빠르기 해도 곱셈이나 나눗셈 연산보다는 가독성이 떨어질 것입니다. 쉬프트 연산자보다 곱셈 또는 나눗셈 연산자를 주로 사용하고, 보다 빠른 실행속도가 요구되어지는 곳만 쉬프트 연산자를 사용하는 것이 좋습니다.

그 외의 연산자

조건 연산자 ?:

조건 연산자는 조건식1, 식1, 식2 모두 세 개의 피연산자를 필요로 하는 삼항 연산자이며, 삼항 연산자는 조건 연산자 하나뿐입니다.

조건식 ? 식1 : 식2

조건 연산자는 첫 번째 피연산자인 조건식의 평가결과에 따라 다른 결과를 반환합니다. 조건식의 평가결과가 true이면 식1이, false이면 식2가 연산결과가 됩니다. 가독성을 높이기 위해 조건식을 괄호()로 둘러싸는 경우가 많지만 필수는 아닙니다.

result = (x > y) ? x : y;

조건 연산자는 조건문인 if문으로 바꿔 쓸 수 있으며, if문 대신 조건 연산자를 사용하면 코드를 보다 간단히 할 수 있습니다.

if (x > y)
	result = x;
else
	result = y;

조건 연산자를 여러 번 중첩하면 코드가 간략해지긴 하지만, 가독성이 떨어지므로 꼭 필요한 경우에 한번 정도만 중첩하는 것이 좋습니다. 조건 연산자의 식1과 식2 ,이 두 피연산자의 타입이 다른 경우, 이항 연산자처럼 산술 변환이 발생합니다.

대입 연산자 = op=

대입 연산자는 변수와 같은 저장공간에 값 또는 수식의 연산결과를 저장하는데 사용됩니다. 이 연산자는 오른쪽 피연산자의 값(식이라면 평가값)을 왼쪽 피연산자에 저장합니다. 그리고 저장된 값을 연산결과로 반환합니다.

System.out.println(x = 3); 변수 x에 3이 저장되고
System.out.println(3);     연산결과인 3이 출력됨

대입 연사자의 왼쪽 피연산자를 'lvalue(left value)'이라 하고, 오른쪽 피연산자를 'rvalue(right value)'라고 합니다.

대입연산자의 rvalue는 변수뿐만 아니라 식이나 상수 등이 모두 가능한 반면, lvalue는 반드시 변수처럼 값을 변경할 수 있는 것이어야 합니다. 그래서 리터럴이나 상수같이 값을 저장할 수 없는 것들은 lvalue가 될 수 없습니다.

복합 대입 연산자

대입 연산자는 다른 연산자(op)와 결합하여 'op='와 같은 방식으로 사용될 수 있습니다. 예를 들면, 'i = i + 3'은 'i += 3'과 같이 표현될 수 있습니다. 그리고 결합된 두 연산자는 반드시 공백없이 붙여 써야 합니다.

profile
🚀 백엔드 2년차 개발자입니다 🚀 성장의 즐거움으로 아자자자!🤣

0개의 댓글