java/java

[Java] 연산자

danuri 2021. 6. 23. 19:00

https://github.com/whiteship/live-study

 

whiteship/live-study

온라인 스터디. Contribute to whiteship/live-study development by creating an account on GitHub.

github.com

백기선님 자바 기초 스터디 3주차

 

목표

자바가 제공하는 다양한 연산자를 학습하세요.

학습할 것

  • 산술 연산자
  • 비트 연산자
  • 관계 연산자
  • 논리 연산자
  • instanceof
  • assignment(=) operator
  • 화살표(->) 연산자
  • 3항 연산자
  • 연산자 우선 순위
  • (optional) Java 13. switch 연산자

 

산술 연산자


산술 연산자에는 사칙 연산자(+,-,*,/)와 나머지 연산자(%)가 있다. 사칙연산은 일상생활에서 자주 사용해 익숙하기 때문에, 자바에서 사칙연산시 주의해야할 사항 위주로 알아보자.

0. 산술 변환

산술 변환: 연산 수행 직전에 발생하는 피연산자의 자동 형변환

  1. 두 피연산자의 타입을 같게 일치시킨다(보다 큰 타입으로 일치)
  2. 피연산자의 타입이 int보다 작은 타입이면 int로 변환된다.

-> 산술 변환은 산술 연산자 뿐만 아니라 앞으로 나올 모든 연산자의 적용될 규칙이다.

1. int / int = int

1) 나누기 연산자(/)의 두 피연산자가 모두 int타입인 경우, 연산결과 역시 int 타입이다.

ex) 10 / 4 -> 2

실제 연산결과는 2.5일지라도 소수점 이하는 버리고 int타입의 값인 2를 결과로 얻는다.

 

2) 올바른 연산결과를 얻기 위해서는 두 피연산자 중 어느 한쪽을 실수형으로 형변환해야 한다.

ex) 10 / 4.0f -> 10.0f / 4.0f -> 2.5f

 

3) 나누기 연산자는 0으로 나누는 경우, ArithmeticException, 0.0으로 나누는 경우, Infinity를 결과로 갖는다.

2. char = char + ...

1)

char c1 = 'a';

c1 = (char)(c1 + 1);
c1 = c1 + 1; //에러

c1 + 1에서 피연산자 c1의 타입(char)은 int보다 작기 때문에 산술 변환에 의해서 int로 변환된다. ('a' -> 97)

c1 + 1 -> 97 + 1 -> 98

연산 결과 int타입을 다시 char타입에 대입할 수 없기 때문에 char타입으로 강제 형변환을 해주어야 한다.

 

2)

char c1 = 'a' + 1; //에러X

다만 이렇게 리터럴 간의 연산은 컴파일러가 알아서 계산해주기 때문에 에러가 발생하지 않는다.

3. 나머지 연산자 %

나머지 연산자는 왼쪽의 피연산자를 오른쪽 피연산자로 나누고 난 나머지 값을 결과로 반환한다.

System.out.println(10%8);
System.out.println(-10%8);
System.out.println(10%-8);
System.out.println(-10%-8);

실행결과

2
-2
2
-2

피연산자의 부호를 모두 무시하고, 나머지 연산을 한 결과에 왼쪽 피연산자의 부호를 붙이면 된다.

 

비트 연산자


  • 비트 연산자는 피연산자를 이진수로 표현했을 때, 각 비트 단위로 연산한다. (피연산자는 정수(문자 포함)만 허용된다)

1. | (OR 연산), & (AND 연산), ^ (XOR 연산)

1) OR 연산

OR 연산은 주로 특정 비트의 값을 변경할 때 사용한다.

아래의 식은 피연산자 0xAB의 마지막 4bit를 'F'로 변경하는 방법을 보여준다.

2진수 16진수
0xAB | 0xF = 0xAF 1 0 1 0 1 0 1 1
0 0 0 0 1 1 1 1
0xAB
0xF
1 0 1 0 1 1 1 1 0xAF

 

2) AND 연산

AND 연산은 주로 특정 비트의 값을 Qhqdksof 때 사용한다.

아래의 식은 피연산자 0xAB의 마지막 4bit가 어떤 값인지 알아내는데 사용되었다. 

2진수 16진수
0xAB & 0xF = 0xB 1 0 1 0 1 0 1 1
0 0 0 0 1 1 1 1
0xAB
0xF
1 0 1 0 1 0 1 1 0xB

 

2) XOR 연산

XOR 연산은 두 피연산자의 비트가 다를 때만 1이 된다.

그리고 같은 값으로 다시 XOR 연산을 수행하면 원래의 값으로 돌아오는 특징이 있어서 간단한 암호화에 사용된다.

2진수 16진수
0xAB ^ 0xF = 0xA4 1 0 1 0 1 0 1 1
0 0 0 0 1 1 1 1
0xAB
0xF
0xA4 ^ 0xF = 0xAB 1 0 1 0 0 1 0 0
0 0 0 0 1 1 1 1

1 0 1 0 1 0 1 1
0xA4
0xF

0xAB

2. 비트 전환 연산자 ~

~ 연산자는 피연산자를 2진수로 표현했을 때, 0은 1로, 1은 0으로 바꾼다.

2진수
0xA 0 0 0 0 1 0 1 0
~ 0xA 1 1 1 1 0 1 0 1

3. 쉬프트 연산자 << >>

쉬프트 연산자는 피연산자를 2진수로 표현했을 때, 각 자리를 왼쪽(<<) 또는 오른쪽(>>)으로 이동한다.

 

1) << 연산자

2진수
0x8 0 0 0 0 1 0 0 0
0x8 << 2 0 0 1 0 0 0 0 0

자리이동으로 인해 저장 범위를 벗어난 값은 버려지고, 빈자리는 0으로 채워진다.

 

2. >> 연산자

2진수
0x8 0 0 0 0 1 0 0 0
0x8 >> 2 0 0 0 0 0 0 1 0
2진수
0xF8 1 1 1 1 1 0 0 0
0xF8 >> 2 1 1 1 1 1 1 1 0

부호있는 정수는 부호를 유지하기 위해 왼쪽 피연산자가 음수인 경우 빈자리를 1로 채운다. 물론 양수일 때는 0으로 채운다.

 

+) 자바에서는 추가로 >>> 연산자를 제공한다. 이는 음수, 양수 상관 없이 무조건 빈자리를 0으로 채우는 연산자이다.

 

>>> 연산자를 적용한 예시를 보자. (중앙값 계산)

int start = 2_100_000_000;
int end = 2_100_000_002;

int mid1 = (start + end) / 2; //오버플로우
int mid2 = start + (end - start) / 2;
int mid3 = (start + end) >>> 1;
  1. 보통 가장 흔하게 사용하는 방법이 1번 방식인데, 이 방법은 int의 크기를 벗어나면 오버플로우가 발생할 수 있다.
  2. 그래서 2번 방식을 사용하는 것이 직관적이고 명확하다. 가장 권장하는 방식이다.
  3. 3번 방식은 면접 때 활용하거나 하면 좋은 멋내기(?)용 방법이다. >> 연산자를 사용하지 않는 이유는 (start + end)에서 오버플로우가 발생하여 음수가 되었을 때, 빈자리에 1을 채워 또 다시 음수가 되기 때문에, 빈자리에 0을 채우는 >>> 연산자를 사용한다.

 

관계 연산자


  • 관계 연산자는 비교 연산자라고도 하며, 두 피연산자를 비교하는 데 사용되는 연산자다.
  • 연산 결과는 오직 true와 false 둘 중의 하나이다.

 

프리미티브 타입에는 boolean 형을 제외한 나머지 자료형에 다 사용할 수 있지만 레퍼런스 타입에서는 사용할 수 없다.

비교연산자 연산결과
> 좌변 값이 크면, true 아니면 false
< 좌변 값이 작으면, true 아니면 false
>= 좌변 값이 크거나 같으면, true 아니면 false
<= 좌변 값이 작거나 같으면, true 아니면 false
== 두 값이 같으면 true 아니면 false
!= 두 값이 다르면 true 아니면 false

 

문자열의 비교

두 문자열을 비교할 때는, 비교 연산자 '==' 대신 equals()라는 메서드를 사용해야 한다.

String str = new String("abc");

boolean result = str.equals("abc"); //내용이 같으므로 true
boolean result2 = (str == "abc"); //내용은 같지만 서로 다른 객체라서 false

 

논리 연산자


  • 논리 연산자는 둘 이상의 조건을 연결하여 하나의 식으로 표현할 수 있게 해준다.
  • 논리 연산자는 피연산자로 boolean형 또는 boolean형 값을 결과로 하는 조건식만을 허용한다.

ex) x > 10 && x < 20

x y x || y x && y !x
true true true true false
true false true false false
false true true false true
false false false false true

효율적인 연산

논리 연산자는 효율적인 연산을 한다는 특징이 있다.

OR 연산 '||'의 경우, 두 피연산자 중 어느 한 쪽만 '참'이어도 전체 연산결과가 '참'이므로 좌측 피연산자가 '참'이면, 우측 피연산자의 값은 평가하지 않는다.

따라서, 연산결과가 '참'일 확률이 높은 피연산자를 연산자의 왼쪽에 놓아 더 빠른 연산결과를 얻을 수 있다.

 

ex)

('a' < ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')

위 예시는 문자 ch가 소문자 또는 대문자인지 확인하는 것인데, 사용자로부터 문자 ch를 입력받을 때, 대문자보다 소문자를 입력할 확률이 높다고 판단했기 때문에 ch가 소문자인 조건을 대문자인 조건보다 왼쪽에 놓았다.

 

AND 연산 '&&'도 마찬가지로 어느 한쪽만 '거짓'이어도 전체 연산결과가 '거짓'이므로 좌측 피연산자가 '거짓'이면, 우측 피연산자의 값은 평가하지 않는다.

 

instanceof


  • 레퍼런스 타입, 즉 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다.
  • 주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다.
  • 연산의 결과로 boolean값을 반환한다.

다음 예시를 보자.

void doWork(Car car) {
    if (c instanceof FireEngine) {
        FireEngine fe = (FireEngine) c;
        fe.water;
    } else if (c instanceof Ambulance) {
        Ambulance a = (Ambulance) car;
        a.siren();
    }
}

Car타입의 참조변수 c를 매개변수로 하는 메서드이다. 만약 Car클래스의 자손타입으로 FireEngineAmbulance클래스가 있다고 할 때, c는 실제로 어떤 타입의 인스턴스인지 메서드 내에서는 알 길이 없다.

 

그래서 intanceof 연산자를 이용해 참조변수 c가 가리키고 있는 인스턴스의 타입을 체크하고, 적절히 형변환한 다음에 작업을 해야 한다.

 

예를 들어, fe.water메서드는 Car가 아닌 FireEngine클래스에서만 호출이 가능하기 때문에

instanceof로 인스턴스 타입 확인  -> FireEngine으로 형변환 -> water 메서드 호출의 과정이 필요하다.

 

-> 어떤 타입에 대한 instanceof연산의 결과과 true라는 것은 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.

 

assignment(=) operator


1) 대입 연산자는 연산 진행 방향이 오른쪽에서 왼쪽이기 때문에 'x=y=3'에서 'y=3'이 먼저 수행되고 그 다음에 'x=y'가 수행된다.

 

2) Ivalue와 rvalue

대입 연산자의 왼쪽 피연산자를 Ivalue(left value), 오른쪽 피연산자를 rvalue(right value)라고 한다.

 

rvalue는 변수뿐만 아니라 식이나 상수 등이 모두 가능한 반면, lvalue는 반드시 변수처럼 값을 변경할 수 있는 것이어야 한다. 그래서 리터럴이나 상수같이 값을 저장할 수 없는 것들은 lvalue가 될 수 없다.

int i = 0;
3 = i + 3; //에러. lvalue가 값을 저장할 수 있는 공간이 아니다.
i + 3 = i; //에러. lvalue의 연산결과는 리터럴이다.(i+3->0+3->3)

복합 대입 연산자

op= =
i += 3; i = i + 3;
i -= 3; i = i - 3;
i *= 3; i = i * 3;
i /= 3; i = i / 3;
i %= 3; i = i % 3;
i <<= 3; i = i << 3;
i >>= 3; i = i >> 3;
i &= 3; i = i & 3;
i |= 3; i = i | 3;
i ^= 3; i = i ^ 3;
i *= 10 + j; i = i * (10 + j);

 

화살표(->) 연산자


화살표 연산자는 자바8부터 추가된 람다식에서 사용하는 연산자이다.

 

람다식은 메서드를 하나의 '식'으로 표현한 것이다.

메서드를 람다식으로 표현하면 메서드의 이름과 반환값을 제거하고 매개변수 선언부와 몸통{ } 사이에 화살표 연산자(->)를 추가한다.

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

//람다식
(매개변수 선언) -> {
	문장들
}

 

ex)

//메서드
int sum(int a, int b) {
    return a + b;
}

//람다식
(int a, int b) -> {
    return a + b;
}

 

3항 연산자


자바에서 삼항 연산자는 조건 연산자 하나뿐이다.

조건 연산자는 조건식, 식1, 식2 모두 세 개의 피연산자를 필요로 한다.

 

조건식 ? 식1 : 식2 -> 조건식의 결과가 true이면 식1이, false이면 식2가 연산결과가 된다.

ex)

result = (x > y) ? x : y;

식 'x > y'의 결과가 true이면, 변수 result에는 x의 값이 저장되고, false이면 y의 값이 저장된다.

 

조건 연산자를 중첩해서 사용하면 셋 이상 중의 하나를 결과로 얻을 수 있다. 아래의 식은 x의 값이 양수면 1, 0이면 0, 음수면 -1, 즉 셋 중 하나를 결과로 반환한다.

result = x > 0 ? 1 : (x == 0 ? 0 : -1);

조건 연산자를 여러 번 중첩하면 코드가 간략해지긴 하지만, 가독성이 떨어지므로 꼭 필요한 경우에 한번 정도만 중첩하는 것이 좋다.

 

연산자 우선 순위


출처: 자바의 정석

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

Java 13. switch 연산자


switch 연산자는 아니지만 자바는 switch ~ case 문법을 가지고 있다.

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

이 방식의 문제점은 다음과 같다.

  1. 수많은 case, break... 너무 장황하다.
  2. break를 빼먹을 경우 다음 분기로 넘어가게 된다.
  3. return 값이 존재할 수 없다.

 

그래서 자바 12버전부터 switch 연산자를 제공한다.

String day = "WEDNESDAY";
int result = switch (day) {
    case "MONDAY", "FRIDAY", "SUNDAY" -> 6;
    case "TUESDAY" -> 7;
    case "THURSDAY", "SATURDAY" -> 8;
    case "WEDNESDAY" -> 9;
    default -> throw new IllegalStateException("Invalid day: " + day);
};
System.out.println(result);
  1. 화살표(->) 연산자 표현으로 간결한 코드를 작성할 수 있다.
  2. break를 적지 않아도 된다.
  3. data만 존재할 경우 return이 가능하다.

 

자바 13 버전은 자바 12 버전에서 yield 명령이 추가되었다. yield x 하게 되면 x가 리턴된다.

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