본문 바로가기

Java/Chapter 04. 연산자(Operator)

[Java] 04.01 - 자바에서 제공하는 이항 연산자들

들어가며

 피연산자가 둘인 연산자를
가리켜

 '이항 연산자(binary operator)'
라고 한다.

 따라서 '+'나 '='도 이항 연산자에
속한다.

 

자바의 연산자들

 자바에서 제공하는 모든
연산자를

 하나의 표로 정리하면 다음과 같다.

그림 16 - 자바의 연산자들

 위의 표에서 연산자별 '결합 방향'과
'우선순위'를 볼 수 있는데,

 이들은 하나의 식 안에 둘 이상의
연산자가 존재하는 경우의

 연산 진행 순서를 결정하는
요소들이다.

 이와 관련하여 다음 수식의
연산과정은 다음과 같다.

그림 17 - 연산의 과정

 이렇듯 곱셈을 먼저 한
이유는

 다음 수학적 배경을 기초로
한다.

덧셈과 뺄셈보다 곱셈과 나눗셈을 먼저 계산해야 한다.

 그리고 이러한 내용을 반영한
것이 연산자의 '우선순위'이다.

 그럼 곱셈 이후의 남은 연산을
보자.

 뺄셈 연산만 둘이 있는 위의
경우에는

 다음 수학적 배경을 근거로
왼편에 있는 뺄셈을 먼저
진행해야 한다.

 덧셈, 뺄셈은 왼쪽에서부터 순서대로 계산한다.

 그리고 이러한 내용을 반영한
것이 연산자의 '결합 방향'이다.

 위의 표를 보면 결합 방향이
2가지로 표시되어 있다.

 하나는 '→'인데, 이는 왼쪽에서
오른쪽으로 연산을 진행해야 함을
의미하고,

 다른 하나는 그 반대의 의미를
갖는다.

 예를 들어서 다음 식을 계산한다고
가정해보자.

3 + 2 -  1

 '+'와 '-'는 우선순위가 같다.
따라서 결합 방향을 따져야 한다.

 그런데 결합 방향이 →이므로,

 위의 식에서는 왼편에 있는
덧셈을 먼저 진행한다.

 이렇듯 연산 순서를 결정짓는
첫 번째 요소는 '우선순위'이고,

 두 번째 요소는 '결합 방향'이다.

 즉, 우선순위가 같을 때 결합 방향을
따진다는 것이다.

 

대입 연산자와 산술 연산자

 이어서 소개하는 대입 연산자와
산술 연산자는

 대표적인 이항 연산자들이다.

그림 18 - 대입 연산자와 산술 연산자

 다음은 위 표에서 정리한
연산자들을 활용한 예제이다.

public class temp {
    public static void main(String[] args){
        int num1 = 7;
        int num2 = 3;

        System.out.println("num1 + num2 = " + (num1 + num2));
        System.out.println("num1 - num2 = " + (num1 - num2));
        System.out.println("num1 * num2 = " + (num1 * num2));
        System.out.println("num1 / num2 = " + (num1 / num2));
        System.out.println("num1 % num2 = " + (num1 % num2));
    }
}
/*
출력 결과
num1 + num2 = 10
num1 - num2 = 4
num1 * num2 = 21
num1 / num2 = 2
num1 % num2 = 1
*/

 위의 예제에서 보이듯이 특정
연산을 소괄호로 묶어주면

 이 부분이 별도로 구분이 되어
연산자의 우선순위에 상관 없이

 먼저 진행된다.

 연산자의 우선순위를 정확히
기억하고 있더라도

 이렇듯 소괄호로 예산의 순서
및 과정을 구분지어 주자.

 코드가 한결 이해하기 쉬워지니
말이다.

 

나눗셈 연산자에 대한 보충

 위의 예제에서 다음 문장을
실행하여

 나눗셈의 결과를 출력하였다.

System.out.println("num1 / num2 = " + (num1 / num2));

 출력 결과는 다음과 같았다.

 그리고 이때 num1은 7,
num2는 3이었다.

num1 / num2 = 2

 이렇듯 나눗셈의 몫이 출력된
이유는

 두 피연산자가 정수여서
정수형 나눗셈이 진행됐기
때문이다.

 만약에 위의 문장이 다음과
같았다면,

System.out.println("num1 / num2 = " + (7.0 / 3.0));

 정수형 나눗셈이 아닌 실수형
나눗셈이 진행되어

 다음의 출력 결과를 보게
된다.

 참고로, 수학적으로는 실수의
나눗셈에는 나머지가 존재하지
않는다.

num1 / num2 =2.333...

 

복합 대입 연산자

 복합 대입 연산자는 대입 연산자와
묶여서 정의된 형태의 연산자다.

 이 중 대입 연산자와 산술 연산자가
묶여서 만들어진 연산자는

 다음과 같이 총 5개이다.

그림 19 - 복합 대입 연산자

 이와 관련하여 다음 문장을 보자.

 이 문잦ㅇ에서는 덧셈의
우선순위가 제일 높으니

 덧셈의 결과가 변수 num에
저장된다.

num = num + 5;    // 변수 num에 저장된 값이 5 증가한다.

 즉, 위의 문장은 변수 num에
저장된 값을 증가시킨다.

 그런데 이 문장을 다음과 같이
하나의 연산자로 대체할 수 있다.

num += 5;

 유사하게 다음 문장은 변수
num에 저장된 값을 3배
증가시킨다.

num = num * 3;

 그리고 이 문장은 다음과
같이

 하나의 복합 대입 연산자로
대체할 수 있다.

num *= 3;

 지금 설명한 복합 대입 연산자의
구성 원리를 이해하면

[그림 16]의 표에 존재하는 다음
복합 대입 연산자들도

 어떻게 구성된 결과인지
알 수 있다.

A &= B       ↔      A = A & B
A ^= B       ↔      A = A ^ B
A <<= B      ↔      A = A << B
A >>>= B     ↔      A = A >>> B

 따라서 이후에 &, ^, <<,
>>> 연산자의 기능만 알면

 위의 복합 대입 연산자의
연산 결과가 어떻게 나타나는지
알 수 있다.

 그럼 이어서 다음 예제를 통해
복합 대입 연산자의 특징을
조금 더 관찰하자.

public class temp {
    public static void main(String[] args){
        short num = 10;
        num = (short)(num + 77L);   // 형 변환 안 하면 컴파일 오류 발생
        int rate = 3;
        rate = (int)(rate * 3.5);   // 형 변환 안 하면 컴파일 오류 발생
        System.out.println(num);
        System.out.println(rate);

        num = 10;
        num += 77L;     // 형 변환이 필요하지 않음
        rate = 3;
        rate *= 3.5;    // 형 변환이 필요하지 않음
        System.out.println(num);
        System.out.println(rate);
    }
}
/*
출력 결과
87
10
87
10
*/

 위 예제의 다음 문장을 보자.

 다음 연산에서는

 변수 num의 값이 long형으로
변환되어 덧셈 연산이 진행된다.

 따라서 그 결과에 대한

 short형으로의 명시적 형 변환은
반드시 필요하다.

num = (short)(num + 77L);

 그러나 위 예제의 다음
문장에서는

 형 변환이 필요없다.

num += 77L

 일반적으로, 위의 문장은

 다음 문장의 간략한 표현으로
생각한다.

 복합 대입 연사자의 기본에
근거하면 이는 분명히 맞다.

num = num + 77L;

 그러나 이렇게 해석이 되면
컴파일이 될 수 없다.

 따라서 컴파일러는 다음과
같이 해석한다.

num += 77L;
→ num = (short)(num + 77L);

 즉 복합 대입 연산자를 사용하면,

 형 변환을 알아서 해주는
것으로 이해할 수 있다.

 그리고 지금 설명한 내용은
위 예제의

 다음 두 문장에도 그대로
적용된다.

rate = (int)(rate * 3.5);
rate *= 3.5;	// 자동으로 형 변환

 

관계 연산자

 관계 연산자는 2개의 피연산자
사이에서

 크기 및 동등 관계를 따져주는
이항 연산자이다.

 따라서 '비교 연산자'라고도
한다.

 두 피연산자 값을 비교하기
때문이다.

그림 20 - 관계 연산자

 위의 연산자들은 연산의
결과에 따라서

 true 또는 false를 반환한다.

 즉 다음 경우에는 A와 Z의
값이 동일하면 true,

 동일하지 않다면 false가
반환된다.

A == Z

 따라서 다음 문장에서는
A와 Z의 비교 결과인

 true 또는 false가 result에
저장된다.

boolean result = (A == Z);

 그럼 다음 예제를 통해서
몇몇 관계 연산자의 용례를
보이겠다.

public class temp {
    public static void main(String[] args){
       System.out.println("3 >= 2 : " + (3 >= 2));
       System.out.println("3 <= 2 : " + (3 <= 2));
       System.out.println("7.0 == 7 : " + (7.0 == 7));
       System.out.println("7.0 != 7 : " + (7.0 != 7));
    }
}
/*
출력 결과
3 >= 2 : true
3 <= 2 : false
7.0 == 7 : true
7.0 != 7 : false
*/

 위 예제의 다음 문장을 보자.

System.out.println("7.0 == 7 : " + (7.0 == 7));

 위의 문장에 등장하는 7.0과
7은 다르다.

 그러나 '==' 연산을 위해
자동 형 변환이 일어난다.

 즉 다음과 같이 정수 7이
실수 7.0으로 변환되어

 비교 연산이 진행된다.

7.0 == 7
→ 7.0 == 7.0

 그래서 그 결과로 true가
반환되어 출력된다.

 

논리 연산자

 논리 연산자도 true또는 false를
반환하는 연산자로써,

 그 종류는 다음과 같다.

 참고로 아래의 표에서는
단항 연산자 '!'을 함께
소개하였다.

그림 21 - 논리 연산자

 논리 연산자의 연산 결과를
나타낸 표를 가리켜

 '진리표(Truth Table)'라 하는데,

 이 진리표를 보면 연산의 결과를
한 눈에 확인할 수 있다.

그림 22 - 진리표

 진리표도 제시하였으니

 다음 예제를 통해서 연산의
결과를 확인해보자.

public class temp {
    public static void main(String[] args){
      int num1 = 11;
      int num2 = 22;
      boolean result;
      
      result = (1 < num1) && (num1 < 100);
      System.out.println("1 초과 100 미만인가? " + result);

      result = ((num2 % 2) == 0) || ((num2 % 3) == 0);
      System.out.println("2 또는 3의 배수인가? " + result);

      result = !(num1 != 0);
      System.out.println("0인가? " + result);
    }
}
/*
출력 결과
1 초과 100 미만인가? true
2 또는 3의 배수인가? true
0인가? false
*/

 진리표에서도 보였지만

 논리연산자의 피연산자는
true 또는false이어야 한다.

 물론 피연산자의 위치에

 true 또는 false를 직접적으로
위치시키는 경우는 없다.

 대신 다음과 같이 true 또는
false를 반환하는 연산을
위치시킨다.

result = (1 < num1) && (num1 < 100);

 그러면 관계 연산 이후에
위의 문장은 다음과 같이
정리된다.

 따라서 변수 result에는
true가 저장된다.

result = true && true;

 그리고 다음과 같이 문장을
구성하면,

 num1이 0이면 false가
result에 저장된다.

result = (num1 != 0);

 따라서 다음과 같이 '!'
연산을 추가하면,

 num1이 0이면 true가
result에 저장된다.

result = !(num1 != 0);

 물론 num1이 0인지를 묻는
위의 문장은

 다음과 같이 구성하는 것이
좋다.

 그러나 예제에서는 연산자의
기능을 설명하기 위해서 위의
문장을 구성하였다.

result = (num1 == 0);

 

논리 연산자 사용시 주의할 점 : short-Circuit Evaluation(Lazy Evaluation)

 연산의 특성 중에 다음과
같은 녀석이 있다.

 short-Circuit Evaluation(Lazy Evaluation)
(줄여서 SCE라 하겠다.)

 이것에 대한 간단한 소개는
다음과 같다.

연산의 효율 및 속도를 높이기 위해서
불필요한 연산을 생략하는 행위

 다음 예제를 통해서 구체적인
설명을 진행하겠다.

 참고로 실행에 앞서 출력
결과를 예측해 보면

 보다 쉽게, 그리고 정확히
SCE를 이해할 수 있다.

public class temp {
    public static void main(String[] args){
      int num1 = 0;
      int num2 = 0;
      boolean result;

      result = ((num1 += 10) < 0) && (0 < (num2 += 10));
      System.out.println("result = " + result);
      System.out.println("num1 = " + num1);
      System.out.println("num2 = " + num2 + '\n');

      result = (0 < (num1 += 10)) || (0 < (num2 += 10));
      System.out.println("result = " + result);
      System.out.println("num1 = " + num1);
      System.out.println("num2 = " + num2);
    }
}
/*
출력 결과
result = false
num1 = 10
num2 = 0

result = true
num1 = 20
num2 = 0
*/

 위 예제의 다음 두 문장에는
num1, num2의 값을

 10씩 증가시키는 연산이
각각 포함되어 있다.

result = ((num1 += 10) < 0) && (0 < (num2 += 10));
result = (0 < (num1 += 10)) || (0 < (num2 += 10));

 따라서 결과가 어찌 되었든
num1, num2는

 둘 다 값이 20이 되어야 하지만
실행 결과는 그렇지 않다.

 그 이유는 SCE에서 찾을 수 있다.

그림 23 - short-Circuit Evaluation의 이해

 위 그림에서 설명하듯이,
다음 연산을 진행할 때

 &&의 왼편에 있는 연산이
먼저 진행된다.

((num1 += 10) < 0) && (0 < (num2 += 10));

 따라서 num1의 값은 증가한다.

 그리고 < 연산의 결과는
false이니

 위 문장은 다음과 같은
상태가 된다.

false && (0 < (num2 += 10));

 이제 &&의 오른편에 있는
연산을 진행할 차례인데

 &&의 왼편에 false가 왔으니
오른편에 무엇이 오든

 &&의 연산 결과는 false이다.

 따라서 오른편 연산을 진행하지
않고

 &&의 연산 결과로 false를
반환해버린다.

 결국 num2는 연산이 진행되지
않아 값이 증가하지 않게 된다.


 다음의 두 문장의 경우도
마찬가지다.

(0 < (num1 += 10)) || (0 < (num2 += 10));
true || (0 < (num2 += 10));

 즉 || 연산자의 왼편이 true이니
오른편에 무엇이 오든

 || 연산의 결과는 true가 된다.

 따라서 오른편 연사는 진행하지
않고

 ||의 연산 결과로 true가
반환된다.

 결국 이 경우에도 num2의
값은 증가하지 않는다.

 지금 설명한 연산의 특성을
가리켜

 SCE(short Circuit Evaluation)라
하며,

 이를 정리하면 다음과 같다.

  • &&의 왼쪽 피연산자가 false이면, 오른쪽 피연산자는 확인하지 않는다.
  • ||의 왼쪽 피연산자가 true이면, 오른쪽 피연산자는 확인하지 않는다.

 SCE는 불필요한 연산을
줄여주니 분명 유용하다.

 그러나 예제에서 보였듯이
부작용이 발생할 수 있다.

 따라서 문장을 구성할 때

 하나의 문장에 너무 많은
연산을 포함하는 것은
좋지 않다.

 즉 다음과 같이 코드를
나누어 작성하는 것이 좋다.

 SCE와 관련 없는 상황이라도
말이다.

num1 += 10;
num2 += 10;
result = (num1 < 0) && (0 < num2);

참고 및 출처

윤성우의 열혈 Java 프로그래밍
국내도서
저자 : 윤성우
출판 : 오렌지미디어 2017.07.05
상세보기