3장. 연산자

7 minute read

목표

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

학습할 것

산술 연산자

우리가 기본적으로 우리 세계에서 자주 만나고 사용했던 수학 연산자라고 생각하면 편할 것 같다. 기본적으로 +, -, *, /, % 연산자를 말할 수 있다.

+

public int add(int a, int b) {
    return a + b;
}

println(add(1, 2)); // 3

기본적인 수학이기에 따로 설명하지 않겠습니다.

-

public int subtract(int a, int b) {
    return a - b;
}
기본적인 수학이기에 따로 설명하지 않겠습니다.

println(subtract(1, 2)); // -1

*

public int multiply(int a, int b) {
    return a * b;
}

println(multiply(1, 2)); // 2

기본적인 수학이기에 따로 설명하지 않겠습니다.

/

public int divide(int a, int b) {
    return a / b;
}

println(divide(1, 2)); // 0

여기서 중요한 것은 타입이 int이기 때문에 1 / 20이 된다. int는 기본적으로 소수점을 표현하지 변수 타입이기 때문에 이 부분에서 값이 사라진다.

저번 스터디 강의에서 들은 내용으로 만약 소수점을 저장하기 위해서 어떤 변수 타입을 사용해야 되냐는 답을 해주셨다.

일단 doublefloat는 기본적으로 부동소수점 정확도 문제가 발생하기 때문에 값의 변형이 일어날 수 있는 위험이 있다.

그래서 BigDecimal이라는 변수 타입을 사용하는 것을 권장한다고 합니다.

BigDecimal을 객체로 선언해서 만들어야 되면 +, -, *, / 이런 식으로 연산자를 사용하는 것이 불가능하다.

BigDecimal a = new BigDecimal(100);
BigDecimal b = new BigDecimal(3);

a.add(b);
a.subtract(b);
a.multiply(b);
a.devide(b);

위와 같은 연산으로 사용할 수 있다. 추가적으로 밑에 같은 경우가 있다.

BigDecimal a = new BigDecimal(100);
BigDecimal b = new BigDecimal(3);

a.devide(b); // => 무한대로 가는 경우 exception 발생
a.devide(b, RoundingMode.HALF_EVEN); // => 값을 어떤 식으로 처리할 것인지에 대해서 지정할 수 있다.

%

public int modular(int a, int b) {
    return a % b;
}

println(modular(1, 2)); // 1

이거는 기본 수학에서 만나지 못하는 연산자일텐데요. 대학에서 정수론에서 배운 모듈러 연산과 같은 방식으로 동작합니다.

a % b를 했을 경우 ab로 나눴을 때 a가 b보다 작아지기 위해서 b의 배수를 빼는 과정이 진행됩니다.

5 % 2
5 - 2 * ??? // 5와 가장 가까운 4가 되기 위해 ???는 2가 됩니다.
5 - 4

=> 1

---
10 % 7 = 3
144 % 2 = 0
145 % 2 = 1
...

이런 과정으로 연산이 진행된다고 생각하면 될 것 같습니다.

비트 연산자 & 논리 연산자

기본적으로 bit라는 개념을 알아야 합니다. bit는 데이터를 저장하는 단위라고 생각하면 될 것 같으며 01을 기억합니다.

여기서 이 bit라는 것을 8개 모으게 되면 byte라는 개념을 만들 수 있습니다.

한마디로 8bit는 1byte라는 단위가 되는 것입니다. (1000m = 1km, 8bit = 1byte)

0 0 0 0 0 0 0 0 => 1byte
|             |
여기가 07번째 비트|
        여기가 0번째 비트

그런데 이 1byte, 8bit를 우리가 아는 일반적인 숫자로 표현이 가능합니다.

0번째 bit는 2^0, 1번째 bit는 2^1, 2번째 bit는 2^2, …, 7번째 bit는 2^7의 값을 가지고 있습니다.

만약 bit가 켜져있다면(값이 1이라면) 해당 비트에 해당하는 곱합니다.

1 0 0 0 0 0 0 1 => 1 * 2^7 + 0 * 2^6 + ... + 1 * 2^0 = 128 + 1 = 129

위와 같은 식으로 계산이 가능합니다.

이젠 연산에 대해서 설명 드리겠습니다.

& AND

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

해당 값들은 단순히 1을 의미하는 것이 아닌 bit의 값을 표현하고 있습니다. 한마디로 서로 해당 비트의 값이 저런 형식으로 표현할 수 있다는 것입니다. AND 연산자는 두 개 값 모두가 1이어야지만 1이 나오는 연산을 진행합니다.

5 & 3 
---
5 => 1 0 1
3 => 0 1 1
---
5 & 3 = 0 0 1 = 1

위에 예제처럼 값은 1로 되어버립니다.

| OR

0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1

OR 연산자는 모두 중에 하나만 1이어도 1을 반환하게 됩니다.

5 | 3 
---
5 => 1 0 1
3 => 0 1 1
---
5 | 3 = 1 1 1 = 7

위에 예저에서 보면 값은 7이 됩니다.

<<, >> SHIFT

shift 연산자는 해당 비트를 움직이게 만듭니다. 간단하게 예를 들면

5 => 1 0 1
5<<1 => 1 0 1 0 => 10
5<<2 => 1 0 1 0 0 => 20

5>>1 => 0 1 0 => 2
5>>2 => 0 0 1 => 1

5라는 값을 한번 왼쪽으로 shift를 진행하고 오른쪽 끝에 0번째 비트에 0인 값으로 생성된다.

단순히 보게되면 왼쪽으로 shift를 진행할 때 마다 2배씩 커지게 됩니다.

이것과 반대로 오른쪽으로 쉬프트를 하게 되면 2배씩 작아지게 됩니다.

제가 설명한 연산자들 말고도 ^(XOR), ~(toggle) 연산자와 같이 여러 다른 연산자가 존재합니다.

관계 연산자

관계 연산자는 기본적으로 2개의 수를 가지고 어떤 관계를 가지고 있는지에 대한 결과를 만들어냅니다.

우리가 보통 알고 있는 <, >, ==가 여기에 해당된다.

primitive type

5 < 4  // true
5 > 4  // false
5 == 4 // false
5 != 4 // true

객체

자바에는 primitive type을 제외하고는 모두 객체라고 말해도 된다. 이런 부분을 생각했을 때 ==는 어떻게 동작하는 지 보자.

객체는 내부적으로 여러 값을 가질 수 있습니다. 그리고 그것이 모두 같은 경우 같다고 볼 수 있습니다. (상황에 따라 다르지만 이런 경우에 보통 같다고 할 수 있을 것 같습니다.)

public class Test {

    static class Student {
        int number;
        String name;

        public Student(int number, String name) {
            this.number = number;
            this.name = name;
        }
    }

    public static void main(String[] args) {
        Student s1 = new Student(1, "배지");
        Student s2 = new Student(1, "배지");

        if (s1 == s2) System.out.println("Same");
        else {
            System.out.println("Not Same");
            System.out.println(s1);
            System.out.println(s2);
        }

        if (s1.equals(s2)) System.out.println("Same");
        else System.out.println("Not Same");
    }
}

---
Not Same
test.Test$Student@56cbfb61
test.Test$Student@1134affc
Not Same

위와 같은 결과가 나왔습니다. 일반적으로 우리는 같은 친구일 것이라고 생각하는데 그렇지 않습니다. 이유는 객체에서 ==을 활용해서 값을 비교할 때에는 해당 주소값이 같은지를 확인하기 때문에 내부 원소가 같은지를 비교하지 않습니다.

그리고 equals()을 활용해서 비교를 해도 같지 않다고 나오는데 이 부분은 기본적으로 Object을 상속받아서 만들어지기 때문에 모두 equals()메소드를 상속받습니다. 내부 구조를 보게 되면

public boolean equals(Object obj) {
    return (this == obj);
}

이런 구조인데 위에 ==하는 것도 똑같다고 생각합니다.

이 부분을 해결하기 위해서는 equals()를 오버라이딩해야 됩니다. intellij에서는 해당 메소드 오버라이딩을 쉽게 할 수 있기 때문에 바로 만들 수 있습니다. 혹은 롬복을 사용하면 됩니다. 롬복은 사용하는데 있어서 유의사항이 있습니다.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return number == student.number &&
            Objects.equals(name, student.name);
}

---

Not Same
test.Test$Student@179770
test.Test$Student@179770
Same

위와 같은 코드를 추가해서 동작하게 되면 단순 ==을 하는 부분은 여전히 같다고 나오지 않지만 equals()을 통해서 진행하는 로직에서는 같다고 나옵니다.

기존 C++을 공부했을 때 연산자 오버라이딩이라는 개념으로 클래스끼리 연산자를 정의할 수가 있었는데 Java에서는 지원하지 않습니다.

instanceof

해당 키워드는 형 변환이 가능한지, 해당 class와 같은 타입인지 혹은 자식 타입인지를 확인하는 키워드입니다.

a(변수) instanceof A(class) 이런 식으로 사용됩니다.

a instanceof Object에서 a에 어떤 타입을 넣어도 true가 반환되는 이유는 Object는 모든 객체의 부모 클래스이기 때문입니다.

예외로는 null instanceof Object는 false가 나오게 됩니다.

assignment(=) operator

우리는 어떤 값 (value)를 특정 변수에 주입하기 위해서 =을 사용하게 됩니다.

여기서 활용할 수 있는 것은 이 =와 함께 사용되는 친구들이 존재한다는 것입니다. +=, -=, >>= 이런 식으로 표현할 수가 있습니다.

int a = 5;
a += 30;
---
int a = 5;
a = a + 30;
a = 5 + 30;
a = 35;

위와 같은 전개로 진행된다고 생각하면 될 것 같습니다.

화살표(->) 연산자

주로 stream()을 활용해서 map, filter을 활용하는 경우에 자주 볼 수 있는 연산자입니다.

해당 연산자는 기본적으로 인터페이스를 직접 구현하는데 있어서 사용된다고 생각하면 됩니다. 자세하게 설명드리면

public interface hello {
    int get(int number);
}

이런 interface가 있다고 가정했을 때 우리는 이 interface를 구현해야지 실제 해당 메소드를 사용할 수 있습니다. (default method는 여기서는 배제하겠습니다.)

class helloImp implements hello {
    @Override
    pubilc get (int number) {
        return something;
    }
}
---
main() {
    hello h1 = new hello() {
        @Override
        pubilc get (int number) {
            return something;
        }
    }
}
 

이런 식으로 구현을 해서 해당 메소드를 사용할 수 있을텐데요. 혹은 두번쨰 방식인 바로 구현해서 사용하는 방식이 있을 것 입니다. 하지만 이렇게 구현하게 되면 굉장히 번거롭고 가독성도 떨어트릴 수 있습니다. 이런 부분을 보완하기 위해서 나왔다는 생각이 듭니다.

해당 연산자를 사용하게 되면 위와 같은 경우는 아래와 같이 변경해서 사용할 수 있습니다.

hello h1 = (number) -> return somthing;

3항 연산자

해당 로직 움직이는 것 + 컨벤션 우리는 if을 활용해서 어떤 같이 true이면 A로직, false이면 B로직을 동작하도록 많이 만듭니다. 이런 부분을 아주 간단하게 만들어준 것이 바로 3항 연산자입니다.

boolean flag = false;

String result;
if(flag) {
    result = "true";
} else {
    result = "false";
}

---
boolean flag = false;

String result = flag ? "true" : "false";

위와 아래는 모두 같은 결과물을 만들어 낸다. ?는 3항 연산자의 시작이라고 생각하면 됩니다. 현재 예제에서 flag라는 변수의 true, false 값을 가지고 3항 연산자가 진행하겠다는 이야기입니다. :를 기준으로 왼쪽은 true인 경우에 해당하는 값이고 오른쪽은 false에 해당하는 값이다.

연산자 우선 순위

표 넣기

(optional) Java 13. switch 연산자

In live study

  • 논리 연산자는 피연산자가 boolean이냐 아니냐에 따라 다르다.

  • java 13에서 사용되는 switch operator (statement는 X)부터 아무 설정없이 사용가능하게 됨.

  • if문을 진행하면서 한쪽이 참이면 그 뒤에 있는 조건을 확인하지 않는다.

  • 중간 값을 구할 때는 어떻게 할까요? ``` int start = 0; int end = 10;

int mid = (start + end) / 2 // 이 부분에서 오버 플로우가 날수가 있음.

안전한 방식

int mid = start + (end - start) / 2; // why?

int mid = (start + end) »> 1; // 비트 연산자 관련 ‘»>’. 양수에서만. 음수에서는 ㄴㄴ


- 내 눈에는 한 줄이지만 실제로 바이트 코드 상에서는 두 줄, 세 줄이 될 수 있다. 그것으로 인해서 멀티 프로그래밍 환경에서 문제가 발생할 수 있는 것이다.

- 문제: 배열에 하나 숫자를 제외하고 나머지 숫자들은 모두 2개씩 들어있다. 어떻게 해야될까요?
``` java
XOR 사용~~~

기선님이 추천한 글 모임

https://hsm622.blog.me/222150928707

https://catch-me-java.tistory.com/

https://b-programmer.tistory.com/226

https://velog.io/@uhan2/Java-Operator

https://whereishq.blogspot.com/2020/11/3.html

https://yadon079.github.io/2020/java%20study%20halle/week-03

Leave a comment