최진영 2021. 3. 16. 15:12

산술 연산자

 연산자라고 특이한 무언가가 있는 것은 아니고 흔히 사칙연산에서 사용하는 덧셈, 뺄셈, 곱셈, 나눗셈과 더불어 나머지 연산을 포함한 연산을 말한다. 나눗셈 연산의 경우 소숫점은 무조건 버림이 되니 유의해서 사용해야한다.

 

package Week3;

public class Exam1 {
    public static void main(String[] args) {
        int a = 12;
        int b = 5;

        System.out.println("a + b = " +  (a+b));
        System.out.println("a - b = " +  (a-b));
        System.out.println("a * b = " +  (a*b));
        System.out.println("a / b = " +  (a/b));
        System.out.println("a % b = " +  (a%b));
    }
}
//[출력]
//a + b = 17
//a - b = 7
//a * b = 60
//a / b = 2
//a % b = 2

 

  단 계산의 결과는 우리가 흔히 아는 연산과 동일하지만 주의해야할 것은 타입 변환이다. 캐스팅의 경우에는 강제 형변환을 본인이 확인을 하고 진행을 하면 크게 문제 될 것이 없지만 항상 타입 프로모션으로 자동 형변환이 타입 데이터 크기에 따라 변동된다는 것을 잊으면 안된다. (아래 코드에서 int가 double로 자동 형변환되어서 계산된 것을 볼 수 있다.)

 

package Week3;

public class Exam2 {
    public static void main(String[] args) {
        int a = 12;
        double b = 5;

        System.out.println("a + b = " +  (a+b));
        System.out.println("a - b = " +  (a-b));
        System.out.println("a * b = " +  (a*b));
        System.out.println("a / b = " +  (a/b));
        System.out.println("a % b = " +  (a%b));
    }
}
//[출력]
//a + b = 17.0
//a - b = 7.0
//a * b = 60.0
//a / b = 2.4
//a % b = 2.0

 

비트 연산자

 비트 연산자는 특별하게 일상생활에서나 코드에서 사용할 일은 없지만 이진수를 다룰 때 같은 등과 같은 상황이 발생한다.

 비트 연산은 말한대로 이진수에 기반하여 계산되는 것으로 0과 1로 계산되는 연산법이다. 하나하나 알아보자.

 

&(AND)

input output
0 0 0
1 0 0
0 1 0
1 1 1

|(OR)

input output
0 0 0
1 0 1
0 1 1
1 1 1

^(XOR)

input output
0 0 0
1 0 1
0 1 1
1 1 0

~(NOT)

input output
0 1
1 0

 

package Week3;

public class Exam3 {
    public static void main(String[] args) {
        int a = 12;
        int b = 15;

        System.out.println("a : " + Integer.toBinaryString(a));
        System.out.println("b : " + Integer.toBinaryString(b));

        System.out.println("========================================");

        System.out.println("a&b : " + Integer.toBinaryString(a&b));
        System.out.println("a|b : " + Integer.toBinaryString(a|b));
        System.out.println("a^b : " + Integer.toBinaryString(a^b));
        System.out.println("~a  : " + Integer.toBinaryString(~a));
        System.out.println("integer ~a : " + ~a);
    }
}
//[출력]
//a : 1100
//b : 1111
//========================================
//a&b : 1100
//a|b : 1111
//a^b : 11
//~a  : 11111111111111111111111111110011
//integer ~a : -13

 다른 비트 연산자를 보면 생각한대로 결과가 나왔다고 생각하는데 ~ 연산자가 조금 독특하다. 분명 1100을 넣었다고 생각하고 ~ 연산자를 사용했으니까 0011이 나와야하는 것이 아니었을까...? 아니다(단호)

 지난 주차에서 변수를 배울 때 int형이 32bits의 데이터 크기로 되어있다는 것을 배웠다. 그럼 a의 표현 범위가 32개의 박스로 되어있은 것을 기억할 것이다. 따라서 a는 32개의 0과 1로된 4byte범위의 값으로 이진수의 길이가 32가 되는 것이다. 실제로 1100인 값을 넣었지만 내부는 0000_0000_0000_0000_0000_0000_0000_1100 이 ~으로 뒤집어 진 것이다.

 

 시프트 연산자의 경우 화살표의 방향대로 우측 변수 만큼 좌측 변수의 비트를 이동하는 것을 말한다.

 

 시프트 연산자에는 >>, <<, >>>가 있으며 화살표가 하나 다 붙은 것에는 MSB처리의 유무에 따라서 갈린다. int의 범위는 보통 -2^31 ~ 2^31-1이다. 32bits의 범위가 아닌 31bits의 범위가 된 이유는 제일 처음 bit가 MSB가 되어 음수처리를 해주기 때문이다. <<는 이 MSB를 살려주는 시프트 연산자로 <<로 옮기더라도 제일 처음 bit에는 영향이 가지 않고 움직인다.

 

 단, >>>의 경우 우측으로 옮긴 후 1로 채우는 것이 아닌 MSB를 고려하지 않고 0으로 채운다. <<도 MSB를 고려하지 않는다. 예제를 살펴보면 감을 잡을 수 있다.

package Week3;

public class Exam5_2 {
    public static void main(String[] args) {
        int num = 15;
        System.out.println("num : " + Integer.toBinaryString(num));

        System.out.println("num << 2 : " + Integer.toBinaryString(num<<2));
        num = 15;
        System.out.println("num >> 2 : " + Integer.toBinaryString(num>>2));


        num = -2;
        System.out.println("num : " + Integer.toBinaryString(num));
        System.out.println("num >>> 2 : " + Integer.toBinaryString(num>>>2));
        System.out.println("num >> 2 : " + Integer.toBinaryString(num>>2));

        num = Integer.MAX_VALUE;
        System.out.println("num : " + Integer.toBinaryString(num));
        System.out.println("num << 2 : " + Integer.toBinaryString(num<<2));
    }
}
//[출력]
//num : 1111
//num << 2 : 11_1100
//num >> 2 : 0011
//num       : 1111_1111_1111_1111_1111_1111_1111_1110
//num >>> 2 : 0011_1111_1111_1111_1111_1111_1111_1111
//num >> 2  : 1111_1111_1111_1111_1111_1111_1111_1111
//num       : 0111_1111_1111_1111_1111_1111_1111_1111
//num << 2  : 1111_1111_1111_1111_1111_1111_1111_1100

 

 

관계 연산자

관계 연산자는 두 변수를 비교하는 연산자로 결과는 boolean (true, false) 로 나온다.

== : 같음
!= : 같지 않음
>  : 보다 큼
>= : 보다 크거나 같음
<  : 보다 작음
<= : 보다 작거나 같음

익숙하지 않다면 <=와 >=의 '=' 위치를 주의하면서 사용해야한다.

package Week3;

public class Exam4 {
    public static void main(String[] args) {
        System.out.println("1 == 1 : " + (1==1));
        System.out.println("1 == 2 : " + (1==2));
        System.out.println("--------------------");
        System.out.println("1 != 1 : " + (1!=1));
        System.out.println("1 != 2 : " + (1!=2));
        System.out.println("--------------------");
        System.out.println("2 > 1 : " + (2>1));
        System.out.println("1 > 2 : " + (1>2));
        System.out.println("--------------------");
        System.out.println("2 >= 1 : " + (2>=1));
        System.out.println("1 >= 2 : " + (1>=2));
        System.out.println("--------------------");
        System.out.println("1 < 2 : " + (1<2));
        System.out.println("2 < 1 : " + (2<1));
        System.out.println("--------------------");
        System.out.println("1 <= 2 : " + (1<=2));
        System.out.println("2 <= 1 : " + (2<=1));
    }
}
//[출력]
//1 == 1 : true
//1 == 2 : false
//--------------------
//1 != 1 : false
//1 != 2 : true
//--------------------
//2 > 1 : true
//1 > 2 : false
//--------------------
//2 >= 1 : true
//1 >= 2 : false
//--------------------
//1 < 2 : true
//2 < 1 : false
//--------------------
//1 <= 2 : true
//2 <= 1 : false

 

논리 연산자

 관계 연산자처럼 둘을 비교하는 연산자이면서 비트 연산자와 비슷하게 생겼다.

 결과는 관계 연산자처럼 boolean으로 나오며 비교하는 대상 또한 boolean이다. 두 가지의 관계 연산자의 결과에 대해서 동일한지 동일하지 않은지를 연산자의 옵션에 따라 검사한다.

&&(&) : 둘 다 true인지
||(|) : 둘 중 하나라도 true인지

 찾아보면서 사실 부끄럽지만 처음 알았던 것은 논리 연산자를 항상 &&로 두 개만 사용했는데 연산자 &로 하나만 사용할 수도 있다는 것을 이제 알았다는 것이고 둘의 기능 차이를 이제야 정리하게 되었다는 것이다..

 and 논리 연산으로 예를 들면 연산자를 && 두개를 사용하였을 때는 만약 첫번째 조건이 false이면 두번째 조건이 true이건 false이건 확인하지 않고 결과값을 false로 return 한다. 하지만 &를 사용하였을 경우에는 첫번째 조건이 false라도 두번째 조건까지 확인하므로 성능을 따졌을 때 논리 연산자는 &&로 두개를 사용하는 것이 맞다.

(or까지 이야기하면 첫번째 조건이 true이면 두번째 조건으로 가지 않고 true를 return 한다.)

package Week3;

public class Exam5 {
    public static void main(String[] args) {
        if(true && true) System.out.println("true && true 는 true");
        if(true & true) System.out.println("true & true 는 true");
        if(true && false) System.out.println("ture && false 는 false");
        if(true & false) System.out.println("ture & false 는 false");

        if(true || false) System.out.println("true || false 는 true");
        if(true | false) System.out.println("true | false 는 true");
    }
}
//[출력]
//true && true 는 true
//true & true 는 true
//true || false 는 true
//true | false 는 true

 

 

instance of

 참조변수가 참조하고있는 인스턴스의 타입을 알아보기 위한 용도로 사용된다.

(참조변수) instanceof (클래스 명)  //결과값은 true, false

 instance of로 true가 나왔다는 것은 참조변수가 클래스 타입이거나 그 하위의 구현체라는 것을 의미한다. 따라서 참조한 변수가 해당 클래스로 타입 변환이 가능하다는 것을 의미한다. 다음 코드를 보자.

package Week3;

public class Exam6 {
    public static void main(String[] args) {
        Car car = new Car();
        FireEngine fireEngine = new FireEngine();

        System.out.println("car instanceof Car : " + (car instanceof Car));
        System.out.println("car instanceof FireEngine : " + (car instanceof FireEngine));
        System.out.println("car instanceof booster : " + (car instanceof Booster));

        System.out.println("fireEngine instanceof Car : " + (fireEngine instanceof Car));
        System.out.println("fireEngine instanceof booster : " + (fireEngine instanceof Booster));
        System.out.println();
        
        Car newCar = new FireEngine();
        System.out.println("newCar address : " + newCar);
    }
}

class Car implements Booster {
}

class FireEngine extends Car {
}

interface Booster {
}
//[출력]
//car instanceof Car : true
//car instanceof FireEngine : false
//car instanceof booster : true
//fireEngine instanceof Car : true
//fireEngine instanceof booster : true
//
//newCar address : Week3.FireEngine@60e53b93

 

fireEngine instance Cartrue이기 때문에 FireEngine은 Car의 하위 구현체로 되어있음을 확인할 수 있다.

 

assignment(=) operator

 대입 연산자이다. 대입의 순서는 오른쪽 피연산자를 왼쪽 피연산자로 대입한다. 값을 초기화하는데에도 사용하며 값을 변환하는데에도 사용한다. 따라서 오른쪽 피산자는 대입을 할 수 있는 리터럴이거나 리터럴을 가진 변수가 위치하여야 한다.

 대입 연산자는 단순 대입만으로도 사용하지만 이외의 이항 연산자와 결합하여 사용하기도 한다. 별다른 특이사항은 없고 변수 자신에 이항 연산자로 오른쪽 피연산자를 대입하는 것이다. 아래 예를 보면 무슨 뜻인지 완전히 이해할 수 있다.

package Week3;

public class Exam7 {
    public static void main(String[] args) {
        int input = 5;
        int output = 0;

        output = input;
        System.out.println(" = 연산 : " +  output);   //output = 5;

        output += input;
        System.out.println(" += 연산 : " + output);   //output = output + input;

        output -= input;
        System.out.println(" -= 연산 : " + output);   //output = output - input;

        output /= input;
        System.out.println(" /= 연산 : " + output);   //output = output / input;

        output %= input;
        System.out.println(" %= 연산 : " + output);   //output = output % input;

        input = 15;  // input : 1111
        input &= 2; // 2 : 0010
        System.out.println(" &= 연산 : " + Integer.toBinaryString(input));
      //input = input & 2;

        input = 15; // input : 1111
        input |= 2; // 2 : 0010
        System.out.println(" |= 연산 : " + Integer.toBinaryString(input));
      //input = input | 2;

        input = 15; // input : 1111
        input ^= 2; // 2 : 0010
        System.out.println(" ^= 연산 : " + Integer.toBinaryString(input));
      //input = input ^ 2;

        input = 15; // input : 1111
        input <<= 2;
        System.out.println(" <<= 연산 : " + Integer.toBinaryString(input));
      //input = input << 2;

        input = 15; // input : 1111
        input >>= 2;
        System.out.println(" >>= 연산 : " + Integer.toBinaryString(input));
      //input = input >> 2;

        input = 15; // input : 1111
        input >>>= 2;
        System.out.println(" >>>= 연산 : " + Integer.toBinaryString(input));
      //input = input >>> 2;
    }
}
//[출력]
//= 연산 : 5
//+= 연산 : 10
//-= 연산 : 5
///= 연산 : 1
//%= 연산 : 1
//&= 연산 : 10
//|= 연산 : 1111
//^= 연산 : 1101
//<<= 연산 : 111100
//>>= 연산 : 11
//>>>= 연산 : 11

 

화살표(->) 연산자

 자바 8부터 지원되는 lambda 식에 사용하기 위한 연산자이다.

반환타입 메서드이름(매개변수 선언) {
	문장들
}


(매개변수 선언) -> {
	문장들
}

 반환 타입과 메서드이름을 삭제하고 선언된 매개변수 다음에 화살표 연산자를 활용해 람다식을 완성한다. 화살표 연산자로 람다식을 사용할 경우에 return문을 대신할 수도 있다. 끝에 ';'가 빠진 것을 유의하자.

(int a, int b) -> {return a>b ? a:b;}
(int a, int b) -> a>b ? a:b

 구체적인 람다에 대한 내용은 15주차에 람다식에 대한 내용에 대해 공부할 때 정리하기로하고 화살표 연산자는 예제 코드로보면서 이해한다.

package Week3;

public class Exam8 {
    public static void main(String[] args) {
        int a = 4;
        Calculator calculator = new Calculator() {
            @Override
            public int cal(int num) {
                return num+1;
            }
        };
        System.out.println(calculator.cal(a));

      //람다식 사용
        Calculator calculator1 = (int num) -> {return num+1;};
        System.out.println(calculator1.cal(a));
    }

}
interface Calculator {
    int cal(int num);
}
//[출력]
//5
//5

 

3항 연산자

 3항 연산자 말그대로 연산자 3개를 이용해서 나타낸 식으로 엄밀히 말하면 if를 축약한 식이다. if문과 마찬가지로 중첩해서 사용할 수 있다.

(조건식) ? (true일 때 실행) : (false일 때 실행)
package Week3;

public class Exam9 {
    public static void main(String[] args) {
        int a = 5;
        int b = 6;

        if(a==b){
            System.out.println(true);
        }
        else{
            System.out.println(false);
        }

        System.out.println((a==b)?true:false);
    }
}

 개인적으로 중첩될 경우 가독성이 매우 떨어진다고 생각한다. if문보다 짧게 쓸 수 있기 때문에 향후 리팩토링

(아직 제대로 알진 못하지만..)

에 쓰이지 않을까 싶다.

 

연산자 우선 순위

 괄호의 우선순위가 제일 높고, 그 다음으로는 산술 > 비교 > 논리 > 대입 순이며, 단항 > 이항 > 삼항 순이다. 연산자의 진행방향은 왼쪽 > 오른쪽 이라고 생각하면 되는데 단, 단항 연산자와 대입 연산자의 경우 오른쪽에서 왼쪽으로 수행된다.

우선순위 연산자
1 (), []
2 !, ~, ++, --
3 *, /, %
4 +, -
5 <<, >>, >>>
6 <, <=, >, >=
7 ==, !=
8 &
9 ^
10 |
11 &&
12 ||
13 ?:
14 =, +=, -=, *=, /=, %=, <<=, >>=, >>>=, &=, ^=, ~=

 일단 외우기 매우매우 까다롭다. 연산자의 수가 굉장히 많기 때문에 자주쓰는 연산자가 아닌 경우 실수하기 쉽상이다. 따라서 괄호를 상황에 맞게 써주면 읽기도 편하고 실수도 줄어드니 괄호를 적절하게 사용하도록 한다.

(그렇다고 남발하면 가독성이 떨어진다..)

 

(optionial) Java 13. switch 연산자

 switch 연산자는 조건값들이 연속적으로 나타나기보단 case별로 불연속적으로 분기되어있는 경우 효율적으로 사용하기 위해 사용하는 연산자이다.

package Week3;

public class Exam10 {
    public static void main(String[] args) {
        int month = 4;

        switch (month) {
            case 12:
            case 1:
            case 2:
                System.out.println("winter");
                break;
            case 3:
            case 4:
            case 5:
                System.out.println("spring");
                break;
            case 6: case 7: case 8:
                System.out.println("summer");
                break;
            case 9: case 10: case 11:
                System.out.println("fall");
                break;
            default:
                System.out.println("error");
        }
    }
}

 보다시피 불필요하게 복잡하다... 같은 case를 묶어놔도 그렇게 깔끔해 보이지는 않는다.

 

 자바에서는 12와 13을 걸쳐서 switch 연산자에 대한 편의성을 증대시켜주었다고 한다.

 

java 12 switch

  • ,(쉼표)로 같은 case를 묶을 수 있다.
  • break를 return으로 사용할 수 있다.
  • 람다식을 사용할 수 있다.
public static String CalSeason(int month) {
    String season = switch (month) {
      case 12, 1, 2:
        break  "winter";
      case 3, 4, 5:
        break "spring";
      case 6, 7, 8:
        break "summer";
      case 9, 10, 11:
        break "fall";
      default:
        break "error";
    };
    return season;
}

public static String CalSeason(int month) {
		String season = switch (month) {
        case 12, 1, 2 -> "winter";
        case 3, 4, 5 -> "spring";
        case 6, 7, 8 -> "summer";
        case 9, 10, 11 -> "fall";
        default -> "error";
    };
  	return season;
}
자바 8 이상에서의 switch를 볼 기회가 없었는데 이전 버전보다 좀 더 깔끔해진 느낌이다

 

java 13 switch

  • break를 yield로 변경하였다.
public static String CalSeason(int month) {
    String season = switch (month){
        case 12, 1, 2:
          yield  "winter";
        case 3, 4, 5:
          yield "spring";
        case 6, 7, 8:
          yield "summer";
        case 9, 10, 11:
          yield "fall";
        default:
          yield "error";
    };
    return season;
}

 

 자바 12에서 13까지 변경되는 과정에서 가장 눈에 띈 업데이트는 return이 가능하다는 것이다.

 자바 8까지 사용하던 switch의 경우 switch의 행위에 따른 return되는 결과값이 없어서 변수에 필요로 할 때 case문마다 변수 대입을 하는 구문을 넣는 등 길어지고 복잡했다. 근데 자바 12부터 break를 case결과에 따른 return처럼 사용하기 시작하여 switch 연산에 대한 결과를 받아 낼 수 있다.

(지금은 switch를 자주 사용하진 않지만 이렇게 깔끔해지면 자바 12 이상 버전이 표준이 되는 날에는 자주 쓰지 않을까싶다.)