<Java> 연산자 1

  • 연산자라함은 ‘연산을 수행하는 기호’라고 정의되어 있습니다.저희가 살면서 익숙하게 사용했던 더하기(+),빼기(-) 등등 익숙한 수학적 기호들이 존재하는데 이들을 모두 연산자라 할 수 있습니다.
  • 자바에는 사칙연산을 위한 연산자부터 비트 연산자까지 다양한 연산자들을 가지고 있습니다. 처음 자바를 접할 경우에는 사칙연산부터 익숙해지고 그 외의 디테일한 연산자들을 공부하면 될 것 같습니다.

연산자와 피연산자

  • Operator(연산자) : 연산을 수행하는 기호(+,- 등등)
  • Operand(피연산자) : 연산자의 작업 대상(변수,상수,리터럴…)
  • x + 3이라는 식은 +라는 연산자 하나와 x와 3이라는 피연산자로 구성되어 있습니다.
  • +와 같은 모든 연산자들은 피연산자로 연산을 수행하고 항상 결과를 반환합니다.

대입연산자

  • 지금껏 해당 포스팅에서도 줄기차게 사용한 연산자 =입니다.
  • 기존의 수학에서 equal로 알려진 =은 좌변과 우변이 동일하다라는 뜻으로 연산을 진행하지만, 프로그래밍에서는 사뭇 다른 기능을 수행합니다.
  • =는 자신을 기준으로 오른쪽에 있는 값을 왼쪽에 대입시켜주는 기능을 수행합니다.예를 들어 int x = 3;와 같은 코드는 x라는 int 자료형에 3이라는 값을 대입하는 동작을 수행합니다.

연산자의 종류

기능에 따른 분류

  • 연산자의 경우 아래와 같이 4가지 기능으로 연산자를 분류할 수 있습니다.
  • 산술 연산자 : 사칙연산과 나머지 연산
  • 비교 연산자 : 크고 작음과 같고 다름을 비교
  • 논리 연산자 : And와 Or을 조건으로 연결
  • 대입 연산자 : 우변의 값을 좌변에 저장
  • 기타 : 형변환,삼항,instanceof

피연산자의 갯수에 따른 분류

  • 해당 분류기준은 연산자가 연산을 수행하는데 필요한 피연산자의 개수로 연사자를 분류합니다.
  • 단항 연산자의 경우 하나의 피연산자, 이항은 두개, 삼항은 세개를 필요로 합니다.
  • if(x > y)?(x = y):(x = 0)의 경우 자바에서 단 하나밖에 없는 삼항 연산자입니다.?:연산자라 하며 조건이 맞을 시 앞의 조건으로 조건이 아닐 경우 뒤의 조건을 수행합니다.
  • 위와 같은 분류들로 연산자들을 구분하는 이유는 바로 뒤에 배울 연산자의 우선순위 때문입니다. 수학과 마찬가지로 각각의 연산자들은 우선순위, 즉 먼저 실행되는 순서를 가지고 있습니다.

연산자의 우선순위

  • 하나의 식에서 사용된 연산자가 둘 이상인 경우, 연산자의 우선순위에 의해서 연산순서가 결정됩니다. 대표적으로 곱셈과 나눗셈이 덧셈과 뺄셈보다 우선순위가 높다는 예가 있습니다.이와 같이 대부분은 직관적으로 식을 읽어보면 유추가 가능합니다.
-x + 3 // 단항 - 연산자 먼저실행, 후 덧셈 연산자 실행
x + 3 * y // 곱셈먼저하고 나서 덧셈진행
x > 3 && x < 5 // 비교 연산자가 먼저 연산되고 나서 논리 연산자 수행
  • 이와 반대로 주의해야 할 우선순위인 경우도 존재합니다.
  • 아래와 같이 AND 논리 연산과 OR 논리 연산을 동시에 수행 할 경우, AND연산이 우선 순위가 더 높습니다. 두개의 논리 연산을 동시에 사용 할 경우 괄호를 사용하여 우선 순위를 확실해 주는 것이 바람직합니다.
x < -1 || x > 3 && x <5 // &&이 먼저 수행된다.
x < -1 || (x > 3 && x < 5)

형 변환(산술 변환)

  • 이항 연산자는 두 피연산자의 타입이 일치해야 연산이 가능하므로, 피연산자의 타입이 다른 경우에는 이를 일치시키는 동작이 필요합니다.쉽게 말하면 double과 int를 더하는 경우 double + double 또는 int + int로 일치 시켜줘야합니다.
  • 즉, 형변환(Type Casting)을 진행하여 현재 존재하는 primitive type을 바꿔야 할 타입으로 변경시켜 줍니다. 형변환의 과정은 아래와 같이 바꾸고자 하는 타입을 리터럴 앞에 괄호와 함께 적어주면 됩니다.
int x = 3;
float y = 3.2f;
float res = (float)x + y;
  • 하지만, 실제로 위의 코드는 float res = x + y;라 코딩해도 동작하는데 이는 산술 변환(자동 형변환)이 발생하기 때문입니다. 바꾸려는 타입이 작은 타입에서 큰 타입으로 변환을 할 경우, 자동적으로 형변환되므로 형변환 연산자를 생략 할 수 있습니다.
  • 아래와 같이 산술 변환의 규칙 또한 존재합니다.

  1. 두 피연산자의 타입을 같게 일치시킨다.(보다 큰 타입으로 일치)
  • long + int -> long + long -> long
  • double + int -> double + double -> double
  • double + float -> double + double -> double
  1. 피연산자의 타입이 int보다 작은 타입이면 int로 변환된다.
  • byte + short -> int + int -> int
  • char + short -> int + int -> int

  • 결국 산술 변환이란, 연산직전에 자동적으로 발생하는 형변환일 뿐이다.

단항 연산자

증감 연산자

  • 증감 연산자는 피연산자에 저장된 값을 1 증가 또는 감소시킵니다. 일반적인 단항 연산자와 달리 피연산자의 왼쪽/오른쪽 두 군데 모두 위치 가능합니다.
  • 피연산자의 왼쪽에 위치하면 전위형(prefix), 오른쪽에 위치하면 후위형(postfix)이라고 합니다.
  • 전위형은 값이 참조되기 전에 증가/감소시키고, 후위형은 값이 참조된 후에 증가/감소시킵니다. 아래의 코드를 통해 동작을 이해해봅시다.
public class OperatorEx1 {
    public static void main(String[] args) {
        int i = 1;
        i++ ;
        System.out.println("i = " + i);

        i = 1;
        ++i;
        System.out.println("i = " + i);
    }
}
  • 위의 예제처럼 증감연산자가 수식이나 메서드 호출에 포함되지 않고 독립적인 하나의 문장으로 쓰인 경우에는 전위형과 후위형의 차이가 없습니다.
public class OperatorEx2 {
    public static void main(String[] args) {
        int i = 5, j = 0;
        j = i++;
        System.out.println("i = " + i);
        System.out.println("j = " + j);
        System.out.println("======================");
        i = 5;
        j = 0;

        j = ++i;
        System.out.println("i = " + i);
        System.out.println("j = " + j);
    }
}
  • 하지만 두번째 예제는 i와 j의 값이 전위형과 후위형 사이에서 다르게 출력됩니다.
  • 이는 해당 주제의 서두에 언급된 참조하고나서 증감이냐 또는 증감하고 나서 참조냐의 차이 때문에 발생합니다.
  • 참조(Reference)라는 단어가 프로그래밍을 처음하는 분들에게 어색하게 느껴질 수 있습니다.저희는 Reference Type을 배우며 참조라는 단어를 가장 먼저 접했을 것입니다. 당시 참조형 변수들은 데이터 값이 아닌 주소값을 값으로 가지며 호출 시 해당 주소값에 존재하는 객체를 참조한다라는 표현을 읽으셨을 것입니다.이러한 경우와 함께 현재 전위형/후위형처럼 값을 참조하는 경우 두가지 경우 모두에서 참조란 어떠한 주솟값에 있는 어떠한 값 또는 객체를 읽어온다라고 이해하시면 좋을 것 같습니다.
  • 이해를 돕기 위해 하나의 예제를 더 준비했습니다.
public class OperatorEx3 {
    public static void main(String[] args) {
        int i = 5, j = 5 ;
        System.out.println("i++ = " + i++);
        System.out.println("++j = " + ++j);
        System.out.println("i = " + i + "/ j = " + j);
    }
}
  • 첫번째 출력의 경우, 참조후 증가를 시키는 후위 연산자를 사용했기에 i값이 5로 출력될 것 입니다.

부호 연산자

  • 부호 연산자의 경우 숫자 리터럴 앞에 붙는 - 기호라고 보시면 됩니다.
  • 부호 연산자 ‘-’는 피연산자의 부호를 반대로 변경한 결과를 반환합니다. 수학에서 마이너스 기호와 동일하다고 생각하면 됩니다.
  • 아래의 간단한 예제를 통해 이해할 수 있습니다.
public class OperatorEx4 {
    public static void main(String[] args) {
        int i = -10;
        i = +i;
        System.out.println("i = " + i);

        i = -10;
        i = -i;
        System.out.println("i = " + i);
    }
}

산술 연산자

  • 산술 연산자에는 사칙 연산자와 나머지 연산자 두 가지가 있습니다.
  • 대체적으로 익숙한 연산들이기에 이해하기에 어렵지 않을 것입니다.

사칙 연산자

  • 흔히 알고 있는 더하기, 빼기, 곱하기, 나누기를 연산해주는 연산자들입니다.
  • 나누기에서 주의할 점을 말씀드린다면 0으로 나누는것은 수학적으로 모순이기에 나누는 수로 0을 사용할 수 없습니다.
  • 아래의 예제를 통해 쉽게 이해 할 수 있습니다. 예제의 출력방식은 포맷팅을 활용한 방식입니다.%d, a인 경우, a는 %d에 포맷팅되어 출력됩니다. %d는 정수형을 취급하기에 a가 int 자료형이어야합니다. %f는 소수형을 취급합니다.
public class OperatorEx5 {
    public static void main(String[] args) {
        int a = 10;
        int b = 4;

        System.out.printf("%d + %d = %d\n",a,b, a + b);
        System.out.printf("%d - %d = %d\n",a,b, a - b);
        System.out.printf("%d * %d = %d\n",a,b, a * b);
        System.out.printf("%d / %d = %d\n",a,b, a / b);
        System.out.printf("%d / %d = %f\n",a,b, (float)a / b);
    }
}
  • 5번째 출력문을 보면 정확한 결과괎을 얻기 위해 a의 자료형을 float로 형변환 해준 것을 볼 수 있다.즉, int로 나눗셈을 진행할 경우, 정수부분만 출력되기에 부정확한 결과가 도출된다. 그래서 float나 double을 횔용해서 나누기 연산을 수행하면 정확한 값을 얻을 수 있다.
public class OperatorEx6 {
    public static void main(String[] args) {
        byte a = 10;
        byte b = 20;
        byte c = (byte) (a + b);
        System.out.println(c);
    }
}
  • 위의 예제 또한 형변환과 관련된 예제이다.
  • byte 타입에 대해서 자동으로 산술변환이 동작하여 a,b를 int 형으로 바꿔주었기에 byte c = a + b;라는 코드는 에러를 일으킨다.
  • 위와 같이 (byte)로 명확하게 타입을 명시해줘야 값을 대입 시킬 수 있다.
public class OperatorEx7 {
    public static void main(String[] args) {
        byte a = 10;
        byte b = 30;
        byte c = (byte) (a * b);
        System.out.println("c = " + c);
    }
}
  • 위의 예제는 큰 범위의 자료형에서 작은 범위의 자료형으로 형변환을 진행할 경우 생기는 Data Loss(손실)에 대한 예제입니다.
  • byte c의 선언과 초기화 코드를 보면 300이 당연히 저장되어야 하지만 실제로 출력을 통해 확인해보면 44라는 엉뚱한 숫자가 저장됨을 볼 수 있습니다.
  • 왜 이럴까요? 기본적으로 byte 변수인 a,b는 초기화가 진행 된 후 곱하기 연산자에 의해서 산술변환이 적용됩니다. 그래서 곱하기 연산을 수행 할때는 int형인 상태에서 연산이 완료됩니다. 이후 강제적으로 (byte) 연산자로 형변환을 진행시키는데 이러한 과정에서 데이터 손실이 발생하게 됩니다.
  • 내부적인 메커니즘을 설명해보겠습니다.byte 타입은 8비트는 공간만을 가지고 int는 32비트만큼의 공간을 가지고 있습니다.산술변환으로 인해 byte에서 int로 변환 될 경우 (기존에 가지고 있는 8비트) + (0으로 구성된 24비트)로 형변환이 진행됩니다.이후 곱셈을 통해 300이라는 수를 32비트의 공간에 2진수로 가지게 됩니다.
  • 최종적으로 32비트짜리 300이라는 수를 byte타입으로 형변환하게 되면 32비트에서 24비트만큼 잘라내게 됩니다. 그러면 남은 비트는 8비트 뿐이며 나머지 부분의 데이터는 손실이 발생 한 것입니다.
  • byte로 변환 할 경우, 손실을 발생시키지 않으려면 -128에서 127까지의 범위 내의 정수를 변환해야 가능합니다.
public class OperatorEx8 {
    public static void main(String[] args) {
        int a = 1_000_000;
        int b = 2_000_000;
        long c = a * b;
        System.out.println("c = " + c);
    }
}
  • 다음으로 볼 예제는 연산에 의한 overflow를 경험해보는 예제입니다.
  • 위와 같이 int형의 변수끼리 곱하기 연산을 진행 한뒤, 해당 값을 c에 저장합니다.현재 코드에 저장된 값의 경우 int값이 가질 수 있는 범위를 넘어서기에 이상한 값이 저장됩니다.(-1454759936)
  • 올바른 값을 얻어내기 위해서는 long c = (long)a * b;로 형변환을 진행해주어야 합니다.
public class OperatorEx9 {
    public static void main(String[] args) {
        long a = 1_000_000 * 1_000_000;
        long b = 1_000_000 * 1_000_000L;

        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
}
  • 9번예제 또한 비슷한 맥락입니다.변수 a는 long타입에 int 타입의 곱을 대입하였고 변수 b는 long타입에 long타입과 int타입의 곱을 대입했습니다.a는 overflow가 생겨 부정확한 값이 출력되고 b의 경우 리터럴에 자료형을 명시적으로 언급을 하였기에 정확한 값이 출력됩니다.
public class OperatorEx10 {
    public static void main(String[] args) {
        int a = 1000000;

        int res1 = a * a / a;
        int res2 = a / a * a;

        System.out.println("res1 = " + res1);
        System.out.println("res2 = " + res2);
    }
}
  • 10번 예제는 연산자의 순서에 의해 값이 달라진다라는 것을 보여주는 예제입니다.
public class OperatorEx11 {
    public static void main(String[] args) {
        char a = 'a';
        char d = 'd';
        char zero = '0';
        char two = '2';

        System.out.printf("'%c' - '%c' = %d\n",d,a,d - a);
        System.out.printf("'%c' - '%c' = %d\n",two,zero,two - zero);
        System.out.printf("'%c' = %d\n",a,(int)a);
        System.out.printf("'%c' = %d\n",d,(int)d);
        System.out.printf("'%c' = %d\n",zero,(int)zero);
        System.out.printf("'%c' = %d\n",two,(int)two);
    }
}
  • 위의 예제는 사칙연산의 피연산자가 숫자뿐만 아니라 문자도 가능하다는것을 보여줍니다.
  • 기본적으로 char a = 'a';와 같이 문자형을 선언하고 초기하는 경우 해당 문자는 그에 알맞은 숫자값으로 변환됩니다. 문자의 사칙연산 또한 이러한 변환을 거친후 진행됩니다.
  • 이러한 숫자와 문자간의 변환 규칙을 나타내는 코드를 유니 코드라합니다.구글에 아스키 테이블을 검색하시면 원하는 문자에 알맞은 숫자값들이 나열되어있음을 알 수 습니다.
  • 사칙 연산의 다양한 예제들은 깃헙을 참고하시기 바랍니다.

나머지 연산자

  • “%” 연산자로 알려져 있는 나머지 연산자는 왼쪽의 피연산자를 오른쪽의 피연산자로 나누고 난 나머지 값을 결과로 반환하는 연산자입니다.
  • 이 또한 나누기 연산자와 동일하게 0으로 나누면 에러가 발생합니다.
  • 예제를 통해 간단하게 알아보겠습니다.
public class OperatorEx19 {
    public static void main(String[] args) {
        int x = 10;
        int y = 3;

        System.out.printf("%d를 %d로 나누면, \n", x, y);
        System.out.printf("몫은 %d이고 나머지는 %d입니다. \n", x/y, x%y);
    }
}
  • 굉장히 간단하게 이해가능할 것입니다.
public class OperatorEx20 {
    public static void main(String[] args) {
        System.out.println(-10 % 8);
        System.out.println(10 % -8);
        System.out.println(-10 % -8);
    }
}
  • 위의 예제는 나머지 연산자가 음수도 허용한다는 것을 보여줍니다.
  • 하지만 계산시 오른쪽 피연산자, 즉 나누는 수의 부호는 무시되므로 결과는 음수의 절대값으로 나눈 나머지와 동일합니다.

Ref: 자바의 정석 - 남궁성