Skip to content

참고

해당 글은 백기선의 온라인 자바 기초 스터디 3주차 과제: 연산자를 바탕으로 학습한 내용입니다.

연산자 (Operator)

정의

연산자는 연산을 수행하는 기호 를 의미한다. 쉽게 사칙연산을 의미한다고 생각하면 편하다. 이때 연산을 수행하면서 연산의 대상이 되는 피연산자(operand) , 연산자와 피연산자를 조합하여 표현한 식(expression)문장(statement) 그리고 평가(evaluation) 의 개념이 생긴다. 정리하면 아래와 같다.

연산자 관련 개념 정리

연산자 (operator): 연산을 수행하는 기호 ( + , - , * , / 등) 피연산자 (operand): 연산자의 작업 대상 (변수, 상수, 리터럴, 수식) 식 (expression): 연산자와 피연산자를 조합하여 계산하고자 하는 바를 표현한 것 문장 (statement): 작성한 식의 끝에 ; 를 붙여 프로그램에서 실행될 수 있게 표현한 것 평가 (evaluation): 식을 계한하여 결과를 얻는 것

대입 연산자

위 개념을 통해서 우리는 식을 통해 얻은 결괏값을 얻는 걸 평가라고 한다는 걸 알게 되었다. 프로그래밍에서는 얻게 된 결과를 어딘가에 저장해야 이를 다시 활용할 수 있기 때문에, 다시 말해 연산의 결괏값을 메모리에 저장 해야 이를 기억하여 사용할 수 있기 때문에 대입연산자( = , assignment operator)를 사용하여 어떤 특정 변수에 저장해야 한다.

종류

연산자의 종류를 기능별로 분류하면 아래 이미지와 같다.

또한 피연산자의 개수에 의해서도 분류할 수 있다. 예를 들어 -3 - 5 와 같은 식이 있을 경우 3 앞에 붙어 있는 연산자 - 는 부호 연산자로 3 을 음수로 정의해준다. 반면 -35 사이에 있는 연산자 - 는 뺄셈 연산자로 피연산자가 두 개가 필요하다. 이처럼 동일한 기호더라도 피연산자의 개수로 인해 연산자의 역할이 구분 될 수 있다.

우선순위와 결합규칙

우선순위

식에 사용된 연산자가 둘 이상인 경우 연산자의 우선순위가 존재해야 연산순서를 결정할 수 있다. 학창시절 수학에서 덧셈( + )과 뺄셈( - )보다 곱셈( * )과 나눗셈( / )의 우선순위가 더 높다는 것을 배웠을 것이다. 이처럼 대부분의 우선순위는 상식 선에서 해결된다.

결합규칙

하나의 식에 같은 우선순위의 연산자들이 여러 개 있는 경우에도 정해진 순서가 있다. 이 규칙을 연산자의 결합규칙이라 한다. 대부분 왼쪽에서 오른쪽의 순서로 연산을 수행하며 단항 연산자와 대입 연산자만 그 반대인 오른쪽에서 왼쪽으로 연산을 수행한다.

결론

우선순위와 결합규칙을 하나의 표로 정리하면 아래 이미지와 같다.

그리고 간단하게 아래와 같이 세 가지만 기억하도록 하자.

  1. 산술 > 비교 > 논리 > 대입 연산자의 순서로 우선순위가 높다.
  2. 단항 (1) > 이항 (2) > 삼항 (3) 연산자의 순서로 우선순위가 높다.
  3. 단항 연산자와 대입 연산자를 제외한 모든 연산의 진행방향은 왼쪽에서 오른쪽이다.

산술 변환 (usual arithmetic conversion)

정의

이항 연산자는 두 피연산자의 자료형이 일치해야 연산이 가능 하다. 따라서 피연산자의 자료형이 서로 다르다면 연산 전에 형변환 연산자( (type) )를 사용하여 자료형을 일치시켜야 한다. 대부분의 형변환은 두 피연산자 중 더 큰 자료형을 기준으로 일치 시키는데 더 작은 크기의 자료형으로 일치시킬 경우 값이 손실될 가능성이 있기 때문이다.

변수 파트에서 더 큰 자료형으로 형변환을 할 경우 자동 형변환(type promotion)이 되는 걸 배웠다. 이와 같이 연산 전에 피연산자의 자료형을 일치시키기 위해 자동 형변환 되는 것을 산술 변환 또는 일반 산술 변환 이라 하며 이는 이항 연산자 뿐만 아니라 단항 연산에서도 발생한다.

규칙

이때 산술 변환에는 세 가지 규칙이 있다.

첫 번째는 앞서 정의에서 살펴봤듯이 값손실을 최소화하기 위해, 이를 테면 오버플로우(overflow)와 같은 현상을 예방하기 위해 더 큰 크기의 자료형을 기준으로 자료형을 일치시킨다는 것이다.

두 번째는 정수형의 기본 타입인 int 자료형이 가장 효율적으로 처리할 수 있는 자료형이기 때문에 그보다 작은 자료형인 byte , char , short 의 경우 int 자료형으로 변환된다.

세 번째는 연산결과의 자료형이 피연산자의 자료형과 일치하는데 이를 테면 5 / 2 식의 결괏값은 피연산자 2int 자료형이기 때문에 2.5 가 아닌 2 가 된다.

이를 정리하면 아래와 같다.

  1. 두 피연산자의 자료형을 같게 일치시키는데 이때 크기가 더 큰 자료형을 기준으로 한다.
  2. 피연산자의 자료형이 int 보다 작을 경우 int 자료형으로 변환된다.
  3. 연산결과의 자료형은 피연산자의 자료형과 일치한다.

피연산자의 개수

단항 연산자

증감 연산자

피연산자에 저장된 값을 1 증가( ++ ) 또는 감소( -- )시킨다. 이때 유의할 점은 증감 연산자의 위치인데 만약 피연산자의 왼쪽에 위치할 경우 이를 전위형(prefix)이라 하며 값이 참조되기 전에 증가 시킨다. 반대로 오른쪽에 위치할 경우 이를 후위형(postfix)이라 하며 값이 참조된 후에 증가 시킨다.

아래 이미지와 같이 변수 tmp1 의 경우 변수 x 에 증가 연산자( ++ )를 후위형으로 사용했기 때문에 tmp1 의 값은 5 가 되고 그 뒤에 값이 증가되어 x6 이 된다. 반대로 변수 tmp2 의 경우 변수 y 에 증가 연산자( ++ )를 전위형으로 사용했기 때문에 y6 이 된 이후 참조되어 tmp2 의 값도 6 이 된다. 따라서 위치에 따라 결괏값이 달라지는 것이다.

        // 증감 연산자
        int x = 5;
        int y = 5;
        int tmp1 = 0;
        int tmp2 = 0;

        tmp1 = x++;
        tmp2 = ++y;

        System.out.println(tmp1); // > 5
        System.out.println(tmp2); // > 6

부호 연산자

부호 연산자( - )는 피연산자의 부호를 반대로 변경한 결과를 반환한다. 다시 말해 피연산자가 음수면 양수로, 양수면 음수로 부호를 바꿔 반환한다. boolean 형과 char 형을 제외한 기본형에서만 사용 가능하다.

기타

이외에도 단항 연산자의 종류로는 논리부정 연산자( ! )비트전환 연산자( ~ ) 가 있지만 이는 편의상 뒤 논리 연산자 부분과 비트 연산자 부분에서 다룰 예정이다.

산술 연산자

사칙 연산자

앞서 설명한 것처럼 곱셈( * ), 나눗셈( / ), 나머지( % ) 연산자가 덧셈( + ), 뺄셈( - ) 연산자보다 우선순위가 높다. 또한 피연산자가 정수형인 경우 나누는 수로 0 을 사용할 수 없으며 아래 이미지와 같이 나누기 연산자의 두 피연산자가 모두 int 자료형일 경우 연산결과 역시 int 자료형이다. 따라서 결괏값으로 2.5 가 아닌 2 가 출력된다. 이때 결괏값이 3 이 아닌 2 인 이유는 int 자료형은 소수점을 저장하지 못하형 정수만 남고 소수점 이하는 버려지기 때문이다. 여기서 정수를 나눌 때 버림을 수행한다는 사실을 기억하자.

        // 자동 형변환
        int a = 10;
        int b = 4;
        System.out.println(a / b); // > 2

중요한 점은 자동 형변환이 되더라도 명시적으로 해주지 않으면 int 자료형끼리의 연산결과는 결국 int 자료형이라는 점이다. 아래 이미지를 한 번 같이 살펴보자. 결괏값으로 2_000_000_000_000 이 출력될 것 같지만 전혀 다른 수인 1_454_759_936 이 출력됐다. 이는 firstNumsecondNum 의 곱셈( * ) 결괏값의 자료형이 결국 int 자료형이기 때문에 오버플로우가 발생한 뒤에 long 타입 변수 res 에 저장됐기 때문이다.

        // 산술 연산자 오버 플로우
        int firstNum = 1_000_000;
        int secondNum = 2_000_000;        

        long res = firstNum * secondNum;
        System.out.println(res); // > -1454759936

사칙 연산자 중 + 연산자의 경우 리터럴 간의 연산으로도 사용할 수 있는데 아래 이미지를 한 번 살펴보자. 변수 c2 의 경우 char 타입이기 때문에 연산결과의 타입인 int 와 매칭이 되지 않아 컴파일 오류가 발생한다. 그러나 변수 c3 의 경우 리터럴 간의 연산 으로 되기 때문에 컴파일러가 이를 계산하여 미리 덧셈연산을 수행한다. 따라서 컴파일 오류가 발생하지 않는 것이다. 컴파일러는 변수를 미리 계산할 수 없기 때문에 리터럴을 사용하는 경우에만 가능하다는 걸 잊지말자.

        // 리터럴 연산
        char c1 = 'a';
        char c2 = c1 + 1; // > Type mismatch: cannot convert from int to char
        char c3 = 'a' + 1;

나머지 연산자

나머지 연산자는 왼쪽의 피연산자를 오른쪽 피연산자로 나누고 난 나머지 값을 결과고 반환하는 연산자다. 이를 공식으로 나타내면 다음과 같다. \(a - (a / b) * b\) 다시 말해 왼쪽의 피연산자인 a 에 따라서 부호가 결정된다. 아래 이미지에서 출력되는 결괏값을 보면 이를 쉽게 이해할 수 있다.

        // 나머지 연산자의 부호
        System.out.println(3 % 2); // > 1
        System.out.println(3 % -2); // > 1
        System.out.println(-3 % 2); // > -1
        System.out.println(-3 % -2); // > -1

이게 중요한 이유는 바로 홀수와 짝수를 구별할 때 발생한다. 보통 나머지 연산자( % )를 활용해서 구하게 되는데 만약 아래 이미지와 같이 나머지가 1 인 경우로 따지게 되면 왼쪽 피연산자의 부호에 따라 값이 달라지기 때문에 제대로 구별할 수 없게 된다.

    public boolean isOddFirst(int num) {
        return num % 2 == -1;
    }

그렇다면 음수의 경우에도 제대로 홀수와 짝수를 구별하려면 어떻게 해야할까? 이때는 아래 이미지와 같이 나머지 연산의 결괏값을 1 과 비교하는 것이 아닌 0 과 비교하면 된다.

    public boolean isOddSecond(int num) {
        return num % 2 != 0;
    }

물론 이 방법보다 더 효율적인 방법은 비트 연산자 중 AND ( & )를 사용하는 방법이 있다. 모든 비트의 첫 번째 자리가 곧 부호를 나타내기 때문이다. 이는 뒤에 비트 연산자에서 알아보고자 한다.

비교 연산자

정의

비교 연산자는 두 피연산자를 비교하는 데 사용되는 연산자로 관계 연산자라고도 한다. 주로 조건문 또는 반복문의 조건식에 사용되며 연산결과로 얻게 되는 값은 오직 truefalse 둘 중 하나다. 비교 연산자 역시 이항 연산자이기 때문에 피연산자의 자료형이 서로 다를 경우 자동 형변환을 통해 자료형을 일치시킨 후 비교를 수행한다는 점을 유의해야 한다.

대소비교 연산자

두 피연산자 값의 크기를 비교하는 연산자다. 이때 기준은 좌변 값이며 종류로는 큰 경우( > ), 작은 경우 ( < ), 크거나 같은 경우( >= ), 작거나 같은 경우( <= )가 있다. 기본형 중에서는 논리형인 boolean 을 제외한 나머지 자료형에서 다 사용할 수 있지만 참조형에서는 사용할 수 없다.

등가비교 연산자

두 피연산자의 값이 같은지 또는 다른지 비교하는 연산자다. 대소비교 연산자와 달리 기본형은 물론 참조형까지 포함하여 모든 자료형에서 사용할 수 있다. 기본형에 사용할 경우 변수에 저장되어 있는 값이 같은지 를 알 수 있고 참조형의 경우 객체의 주소값을 저장하기 때문에 두 피연산자(참조변수)가 같은 객체를 가리키고 있는지 알 수 있다. 종류로는 같은 경우( == )와 다른 경우( != )가 있다.

논리 연산자

논리 연산자는 둘 이상의 조건을 그리고( AND , && )나 또는( OR , || )으로 연결하여 하나의 식으로 표현할 수 있게 해준다. 이를 간단하게 표로 표현하면 아래 이미지와 같다.

효율적인 연산 (short circuit evaluation)

논리 연산자의 경우 효율적인 연산이 가능 하다. 위 이미지를 보면 알 수 있듯 || 연산자의 경우 어느 한 쪽만 참( true ) 이어도 연산결과가 참이기 때문에 좌측 피연산자가 참( true )이면 우측 피연산자의 값을 평가하지 않는다. 반대로 && 연산자의 경우 한쪽만 거짓( false ) 이어도 연산결과가 거짓이기 때문에 좌측 피연산자가 거짓( false )이면 우측 피연산자의 값은 평가하지 않는다. 따라서 이러한 위치에 따라 효율이 달라질 수 있기 때문에 어떤 연산자를 사용하여 조건을 어느 위치에 둘 것인지 고려하는 것이 효율적인 식을 표현할 수 있다.

논리 부정 연산자

논리 부정 연산자( ! )는 피연산자가 truefalse 를, falsetrue 를 결괏값으로 반환한다.

비트 연산자

논리 연산자

피연산자를 비트단위로 논리 연산한다. 피연산자를 이진수로 표현하여 아래와 같은 규칙을 통해 연산을 수행한다. 이때 중요한 점은 피연산자로 실수를 허용하지 않는다는 점이다. 정수 또는 문자만 허용된다.

  1. | ( OR 연산자)

    피연산자 중 한 쪽의 값1 이면, 1 을 결과로 얻는다. 그 외에는 0 을 얻는다. 주로 특정 비트의 값을 변경할 때 사용한다.

  2. & ( AND 연산자)

    피연산자 중 양 쪽이 모두 1 이어야만 1 을 결과로 얻는다. 그 외에는 0 을 얻는다. 주로 특정 비트의 값을 뽑아낼 때 사용한다.

  3. ^ ( XOR 연산자)

    피연산자의 값이 서로 다를 때만 1 을 결과로 얻는다. 같을 때는 0 을 얻는다. 간단한 암호화에 사용되는 경우가 많다.

정보

잠깐! 이때 2진수로 표현된 결괏값을 확인하기 위해 보통 toBinaryString() 메서드를 사용한다.

앞서 나머지 연산자( % )에서 살펴보았던 비트 연산자를 활용한 홀수-짝수 판별법을 알아보자. 아래 이미지와 같이 식을 나타낼 수 있다.

    public boolean isOddThird(int num) {
        return (num & 1) != 0;
    }

2진수에서 홀수-짝수를 결정짓는 숫자는 2^01 이다. 왜냐하면 나머지 자리는 결국 2^n 형태로 수를 표현하기 때문에 나머지는 전부 짝수이기 때문이다. 따라서 1 이 존재하면 홀수 그렇지 않으면 짝수가 된다. 따라서 위 이미지와 같이 num 을 비트 연산자 & 를 사용하여 계산하게 될 경우 만약 num10 일 경우 2진수로 표현하면 1010 이 되고 2^0 자리의 값이 0 이기 때문에 이것과 1 을 비교하면 false 가 결괏값으로 반환된다.

비트 전환 연산자

논리부정 연산자( ! )와 유사하며 01 로, 10 으로 바꾼다. 이를 통해 결국 부호가 있는 피연산자는 부호가 반대로 바뀌게 된다. 다시 말해 1의 보수를 얻게 되는 것이다. 따라서 비트 전환 연산자를 1의 보수 연산자라고도 한다. 따라서 어떤 정수 p 의 음수값을 얻고 싶을 때는 비트 전환 연사자( ~ )를 활용하여 1의 보수를 얻은 뒤에 1 을 더해줘, 다시 말해 ~p + 1 과 같은 식을 통해 값을 얻을 수 있다.

쉬프트 연산자

피연산자의 각 자리를 2진수로 표현했을 때 오른쪽( >> ) 또는 왼쪽( << )으로 이동(shift)한다고 해서 쉬프트 연산자(shift operator)라고 이름이 붙여졌다. 예를 들어 8 << 2 인 경우 피연산자인 10진수 8 을 2진수로 표현한 뒤 왼쪽으로 2 자리 이동한다. 이때 저장범위를 벗어난 값들은 버려지고 양수의 경우 빈자리는 0 으로, 음수의 경우 빈자리는 부호로 인해 1 로 채워진다.

예를 들어 아래 이미지와 같이 8 << 28 의 2진수를 왼쪽으로 2 자리 이동시키고 저장범위를 벗어난 값은 버려져 빈자리는 0 으로 채워진 뒤 결괏값을 얻게 된다.

반대로 아래 이미지와 같이 -8 >> 2 인 경우 -8 의 2진수를 오른쪽으로 2 자리 이동시키고 저장범위를 벗어난 값은 버려져 빈자리는 1 로 채워진 뒤 결괏값을 얻게 된다.

이를 식으로 표현하면 다음과 같다. x << n 의 경우 \(x * 2^n\) 과 결괏값이 동일하며 x >> n 의 경우 \(x / 2^n\) 과 결괏값이 동일하다. 곱셈이나 나눗셈 연산자보다 훨씬 빠른 연산결과를 얻을 수 있지만 가독성 때문에 실행속도가 중요한 경우에만 사용하는 게 좋다.

대입 연산자

정의

대입 연산자는 변수와 같은 저장공간에 값 또는 수식의 연산결과를 저장하는데 사용된다. 이 연산자는 오른쪽 피연산자의 값(식이라면 평가값)을 왼쪽 피연산자에 저장한다. 그리고 저장된 값을 연산결과로 반환한다. 이때 대입 연산자는 우리가 앞서 우선순위에서 살펴본 것처럼 가장 낮은 우선순위를 가지고 있기 때문에 식에서 제일 나중에 수행된다. 또한 연산 진행 방향이 오른쪽에서 왼쪽으로 진행된다.

lvalue와 rvalue

대입 연산자의 왼쪽에 위치한 피연산자를 lvalue(left value) 라 하고 오른쪽에 위치한 피연산자를 rvalue(right value) 라 한다. 이때 rvalue에는 변수 뿐만 아니라 식, 상수 등 모든 값이 올 수 있지만 lvalue에는 변수처럼 변경 가능한 값만 올 수 있다. 예를 들어 아래 이미지와 같이 final 키워드를 사용해 선언한 변수 MAX 값을 변화시키려고 시도한 경우 Cannot assign a value to final variable 'MAX' 라는 오류를 반환받는다.

        // 대입 연산자 lvalue
        final int MAX = 10;
        MAX = 10; // > Cannot assign a value to final variable 'MAX'        

복합 대입 연산자

대입 연산자는 다른 연산자와 결합하여 += 와 같은 형태로 사용할 수 있다. 이때 유의할 점은 i *= 10 + j 와 같은 식일 경우 i = (i * 10) + j 와 같은 식이 아닌 i = i * (10 + j) 와 같은 식이라는 점이다. 또한 결합된 두 연산자는 반드시 공백이 없어야 한다.

기타 연산자

조건 연산자

조건 연산자는 조건식, 첫 번째 식, 두 번째 식, 이렇게 세 개의 피연산자를 필요로 하는 삼항 연산자 다. 조건 연사자는 첫 번째 피연산자인 조건식의 평가결과에 따라 다른 결과를 반환한다. 조건의 평가결과가 true 면 첫 번째 식이 반환되고 false 면 두 번째 식이 반환된다. 이때 조건 연산자 또한 이항 연산자처럼 첫 번째 식과 두 번째 식의 피연산자 자료형이 다른 경우 산술 변환이 발생한다. 조건 연산자는 아래 이미지와 같이 표현한다.

        // 조건 연산자
        int result = 5 > 10 ? 1 : 0;
        System.out.println(result); // > 0

이외에도 람다 표현식이 등장하며 나온 화살표 연산자( -> )나 참조변수가 참조하고 있는 인스턴스의 타입을 알아볼 때 사용하는 instanceof 연산자, 그리고 switch 제어문에서 사용하는 switch 연산자( -> , yield )가 있는데 이에 대해선 앞으로 하나씩 더 자세히 알아보고자 한다.