티스토리 뷰

JAVA

[자바 스터디] 3주차 : 연산자

짜비 2022. 1. 7. 23:39

산술 연산자

사칙 연산 (+,-,*,/)

  • 타입에 유의하자

    int a = 1_000_000;
    int b = 2_000_000;
    long c = a * b;
    System.out.println("c = " + c); //c = -1454759936

    a*b 를 수행하는 과정에서 이미 overflow가 발생하기 때문에 예상과 달리 음수값이 나온다.

  int a = 1_000_000;
  int b = 2_000_000;
  long c = (long)a * b;
  System.out.println("c = " + c); //c = 2000000000000

해결방법은, 한 쪽에 casting을 걸어서 연산 과정 중에 자동 형변환이 이루어지도록 하면 된다.

  • 리터럴 연산은 연산 중간에 형변환되지 않는다.

    char c1 = 'a';
    
    char c2 = c1+1;        //컴파일 에러
    char c3 = 'a'+1;    

    char c3 = 'a'+1; 는 왜 컴파일 에러가 뜨지 않을까?
    리터럴 연산의 경우, 컴파일러가 미리 계산을 완료하기 때문에 실행할 때 덧셈 연산이 수행되지 않는다. 따라서 연산 도중에 자동형변환이 일어날 일이 없기 때문에 자동형변환에 따른 타입 불일치 에러가 발생하지 않는 것이다.

비트 연산자

and( & ), or ( | ), xor( ^ )

  • 피연산자로 정수만 사용 가능

  • 정수를 이진수로 바꿔서 각각의 자리에 있는 숫자끼리 비트 연산을 수행

  • 비트연산

    x y x|y x&y x^y
    1 1 1 1 0
    1 0 1 0 1
    0 1 1 0 1
    0 0 0 0 0
        int x = 0xAB;   //10101011
        int y = 0xF;    //00001111
        long z = 0xF;        //00001111 (마지막 8비트만 표기)

        System.out.println(Integer.toBinaryString(x | y));    //10101111
        System.out.println(Integer.toBinaryString(x & y));    //1011
        System.out.println(Integer.toBinaryString(x ^ y));    //10100100
        System.out.println(Integer.toBinaryString(~(x ^ y)));    //11111111111111111111111101011011
        System.out.println(Long.toBinaryString(~(x ^ z)));                     
                //1111111111111111111111111111111111111111111111111111111101011011

System.out.println(Long.toBinaryString(~(x ^ z))); 의 결과를 보면, 비트 연산도 산술 연산과 마찬가지로 연산 과정 중간에 자동형변환이 일어나는 것을 확인할 수 있다.

not ( ~ )

  • 피연산자를 이진수로 바꿔서 0은 1로, 1은 0으로 바꾼다.
  • 피연산자의 타입이 int보다 작으면(byte, short) int로 자동형변환 후 연산한다. 즉, 연산결과는 항상 32자리의 2진수이다.
        byte p = 10; //1010

        System.out.println(Integer.toBinaryString(p));  //1010 
        System.out.println(Integer.toBinaryString(~p)); //11111111111111111111111111110101
        System.out.println(Integer.toBinaryString(~p + 1));   //11111111111111111111111111110110
        System.out.println(Integer.toBinaryString(~~p));    //1010

shift ( >> , << )

  • 2진수로 변환한, 피연산자의 각 자리에 있는 수를 오른쪽 혹은 왼쪽으로 이동시킴
  • '<<' 연산자
    • 왼쪽으로 이동시킨 후 빈 자리를 0으로 채움
    • x << n == x * 2^n
  • '>>'
    • 오른쪽으로 이동
      • 양수일 경우, 빈 자리를 0으로 채움
      • 음수일 경우, 빈 자리를 1로 채움
    • x >> n == x / 2^n
        int n = 8;
        System.out.println(Integer.toBinaryString(n));  //1000
        System.out.println(Integer.toBinaryString(n >> 1)); //0100
        System.out.println(Integer.toBinaryString(n >> 2)); //0010
        System.out.println(Integer.toBinaryString(n >> 3)); //0001
        System.out.println(Integer.toBinaryString(n >> 4)); //0000
        System.out.println(Integer.toBinaryString(n >> 5)); //0000

                //1000 --> 우측 피연산자가 bit수 이상일 경우, (우측 피연산자 % bit수) 만큼 자릿수를 이동
        System.out.println(Integer.toBinaryString(n >> 32)); 

        System.out.println(Integer.toBinaryString(n >> 33)); //0100

관계 연산자 (비교 연산자)

두 피연산자를 비교하는 데 사용되는 연산자.

  • 두 피연산자의 타입이 서로 다를 경우에는 자료형 범위가 큰 쪽으로 자동형변환 발생
  • 대소비교 연산자 ('>', '<', '>=', "<=")
    • boolean을 제외한 Primitive type 사용 가능
    • Reference type 사용 불가
  • 등가비교 연산자 ('==', '!=')
    • Primitive type, Reference type 모두 사용 가능
      • Primitive type : 변수에 저장되어 있는 값이 같은지 비교
      • Reference type : 두 참조 변수가 같은 객체를 가리키고 있는지 비교
        System.out.println(10==10.0f); //true (10 -> 10.0f 형변환)

        System.out.println(0.1==0.1f); //false

System.out.println(0.1==0.1f); 은 왜 false로 출력될까?

그 이유는 실수형이 저장될 때 근사값으로 저장되어 오차가 발생할 수 있기 때문이다.

그렇다면 float와 double을 올바르게 비교하려면 어떻게 해야 할까?

  • Double -> float 타입으로 형변환
  • 어느 정도 오차는 감수하고, 앞에서 몇 자리만 잘라서 비교

문자열 비교

  • '==' 비교 : 문자열이 같은 객체인지 비교 -> 문자열이 같더라도 다른 객체라면 비교 결과값이 false로 나올 수 있음.
  • equals() 메소드 : 문자열 내용이 같은지 비교
        String str1 = "abc";
        String str2 = new String("abc");

        System.out.println(str1.equals("abc")); //true
        System.out.println(str2.equals("abc")); //true
        System.out.println(str1 == "abc");  //true
        System.out.println(str2 == "abc");  //false -> 다른 객체이기 때문

instanceof

  • Reference type이 참조하고 있는 instance의 실제 타입을 알아보기 위해 사용 (주로 조건문)
  • 참조변수 instanceof 타입(클래스명)
  • 참조변수의 타입이 아니라, 참조변수가 가리키고 있는 실제 객체의 타입에 의해 결과값이 결정된다.
  • LSP(리스코프 치환 원칙) 를 위반하는 코드에서 주로 나타나는 연산자
    -> instanceof 가 자주 등장한다면, 리팩토링의 대상이 아닌지 점검해보는 것이 좋다.
  • instanceof 결과가 참이라는 것은 참조변수가 검사한 타입으로 변환될 수 있음을 의미한다.
public class InstanceofTest {
    public static void main(String[] args) {
        FireEngine fe = new FireEngine();

        if (fe instanceof FireEngine) {
            System.out.println("FireEngine");
        }

        if (fe instanceof Car) {
            System.out.println("Car");
        }

        if (fe instanceof Object) {
            System.out.println("Object");
        }
    }

    static class Car{}
    static class FireEngine extends Car{}
}
FireEngine
Car
Object

위 조건문이 모두 참이므로, FireEngine은 Car, Object로 변환가능하다. 바꿔 말하면, Car 참조변수와 Object 참조변수가 FireEngine instance를 담을 수 있다.

논리 연산자

둘 이상의 조건을 연결해서 하나의 식으로 표현할 수 있게하는 연산자. 피연산자로는 boolean 혹은 조건문이 올 수 있다.

  • && (And) 연산자 : 두 피연산자가 모두 true일 경우에만 true 반환
  • || (Or) 연산자 : 두 피연산자 중 어느 한 쪽이 true이면 true 반환
  • !(논리 부정) 연산자 : 하나의 피연산자의 boolean 값을 toggle

short circuit evaluation (효율적인 연산)

  • 만약 하나의 피연산자 만으로 논리 연산의 결과값을 도출할 수 있는 경우, 나머지 피연산자를 확인하지 않고 논리 연산을 종료
  • short circuit evaluation 특징을 잘 활용하면 연산 속도 향상 가능
    • 가령 '||' 연산의 경우, 하나의 조건문만 참이어도 최종 결과값이 참이 되기 때문에, 두 개의 조건문 중 참이 될 가능성이 높은 조건문을 왼쪽에 배치해서 연산 속도를 빠르게할 수 있다.
    • '&' , '|' 연산자는 short circuit evaluation 을 하지 않고, 두 조건문을 모두 체크한다.
        int a = 5;
        int b = 0;
        System.out.println(a != 0 || ++b!=0);   //true
        System.out.println(b);                  //0 (위에서 ++b 연산이 생략된 것을 알 수 있음)

assignment(=) operator

변수와 같은 저장 공간에 값 또는 수식의 연산결과를 저장하는 연산자

  • 오른쪽 피연산자(rvalue)의 값을 왼쪽 피연산자(lvalue)에 저장
  • 연산 결과로는 저장된 값을 반환한다.
  • 연산 진행 방향 : 오른쪽 -> 왼쪽
    • x = y = 3
      1. y = 3 저장
      2. y = 3 연산 결과로 3 반환
      3. 연산의 결과 값인 3이 x에 저장
  • lvalue는 값을 변경할 수 있는 변수만 올 수 있다.
    • 즉, 리터럴이나 final이 붙은 변수는 lvalue가 될 수 없다.
        int a;
        System.out.println(a = 5);  //5

        final int b = 3;
        b = 5;  // 컴파일 에러

화살표(->) 연산자

메서드로 전달할 수 있는 익명함수를 단순화한 것.

  • 람다 파라미터 -> 람다 바디

  • 동적 파라미터를 이용할 때 익명 클래스 등 판에 박힌 코드를 작성할 필요가 없어진다. 즉, 간결하게 코드를 작성할 수 있다.

            //익명 클래스
                    Comparator<Apple> byWeight = new Comparator<Apple>() {    
                @Override
                public int compare(Apple a1, Apple a2) {
                    return a1.getWeight().compareTo(a2.getWeight());
                }
            };
    
                    //람다식
                    Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

익명 클래스란, local class와 비슷한 개념으로, 클래스 선언과 인스턴스화를 동시에 할 수 있는 문법이다.

동적 파라미터 : 파라미터를 받은 메서드 내부에서 다양한 동작을 수행할 수 있도록, 기능이 확정되지 않은 코드블록을 파라미터로 넘긴다. 이렇게 어떤 코드블록이 오는지에 따라 다양한 역할을 수행할 수 있는 파라미터를 동적 파라미터라고 한다.

더 자세한 내용은 15주차 : 람다식 을 공부할 때 다루도록 하자.

3항 연산자

  • 3개의 피연산자를 필요로 하는 연산자

  • 조건식 ? 식1 : 식2

    • 조건식이 참일 경우, 식1의 연산 결과가 최종 결과값이 된다.
    • 조건식이 거짓일 경우, 식2의 연산 결과가 최종 결과값이 된다.
        int x = -5;
        int absX = (x >= 0) ? x : -x;
        System.out.println("absX = " + absX); // absX = 5

연산자 우선 순위

종류 연산순서 연산자 우선순위
단항 연산자 <--- ++ -- + - ~ ! (type) 높음
산술 연산자 * / %
+ -
<< >>
비교 연산자 < > <= >= instanceof
== !=
논리 연산자 &
^
|
&&
||
삼항 연산자 ?:
대입 연산자 <--- = += -= ... 낮음

(optional) Java 13. switch 연산자

기존 switch statement

public enum Day { SUNDAY, MONDAY, TUESDAY,
    WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; }

// ...
    int numLetters = 0;
    Day day = Day.WEDNESDAY;
    switch (day) {
        case MONDAY:
        case FRIDAY:
        case SUNDAY:
            numLetters = 6;
            break;
        case TUESDAY:
            numLetters = 7;
            break;
        case THURSDAY:
        case SATURDAY:
            numLetters = 8;
            break;
        case WEDNESDAY:
            numLetters = 9;
            break;
        default:
            throw new IllegalStateException("Invalid day: " + day);
    }
    System.out.println(numLetters);

// https://docs.oracle.com/en/java/javase/13/language/switch-expressions.html

문제점

  • break 누락으로 인한 human fault 발생 가능성
  • 값을 return할 수 없음
  • 장황한 코드

Java 12, 새로운 switch expression

    Day day = Day.WEDNESDAY;    
    System.out.println(
        switch (day) {
            case MONDAY, FRIDAY, SUNDAY -> 6;
            case TUESDAY                -> 7;
            case THURSDAY, SATURDAY     -> 8;
            case WEDNESDAY              -> 9;
            default -> throw new IllegalStateException("Invalid day: " + day);
        }
    );    

// https://docs.oracle.com/en/java/javase/13/language/switch-expressions.html

바뀐 점

  • '->' 연산자 사용, break 사용하지 않음.
  • 여러 옵션을 한 줄에 표현 가능
  • Expression 이기 때문에 결과값을 바로 사용할 수 있음

Java 13, yield 추가

    int numLetters = switch (day) {
        case MONDAY, FRIDAY, SUNDAY -> {
            System.out.println(6);
            yield 6;
        }
        case TUESDAY -> {
            System.out.println(7);
            yield 7;
        }
        case THURSDAY, SATURDAY -> {
            System.out.println(8);
            yield 8;
        }
        case WEDNESDAY -> {
            System.out.println(9);
            yield 9;
        }
        default -> {
            throw new IllegalStateException("Invalid day: " + day);
        }
    };  

// https://docs.oracle.com/en/java/javase/13/language/switch-expressions.html

바뀐 점

  • yield 예약어를 통해 case 별, switch expression의 결과값을 반환 가능

Java 13에서도 switch 내부에서 ' : '(semi colon) 을 사용할 수 있다. 하지만 이 경우, break 를 명시해야 한다.

따라서, break를 사용할 필요가 없는 ' -> ' 을 쓰도록 하자.

백기선님 리뷰 영상

  • int mid = start + (end - start) / 2; : overflow 방지 가능
  • int mid = (start + end) >>> 1; : unsigned shift
  • xor 을 이용해서 array 중 한 번만 등장하는 element 찾기

References

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함