공부방/JAVA

연산자 - 백기선 자바라이브스터디

EVO. 2023. 7. 28. 17:42

산술 연산자


산술연산자에는 사칙 연산자(+,-,*,/)와 나머지 연산자(%)가 있습니다. 

이 장에선 몇 가지 주의할 사항들에 대해 설명하겠습니다.

이는 저번 시간에 배웠던 타입 캐스팅과 타입프로모션(자동형변환)을 알아야 쉽게 이해되는 예제들 입니다.

 

int a = 10;
int b = 4;

System.out.printf("%d / %d = %d",   a, b, a / b);

두 변수 a와 b에 10과 4를 저장하고나서 사칙연산 / 을 수행한 결과가 2.5가 아닌 2 입니다.

나누기 연산자의 두 피연산자가 모두 int 타입인 경우, 연산 결과 역시 int타입 입니다. int 타입은 소수점을 저장하지 못하므로 정수만 남고 소수점 이하는 버려집니다. 

 

int a = 10;
int b = 4;

System.out.println("%d / %f = %f" ,  a, (float)b, a / (float)b);

다음 연산은 10 / 4.0f  -> 10.0f / 4.0f -> 2.5f 로 두 피연산자 중 어느 한 쪽을 실수형으로 형변환 해놓으면 다른 한쪽도 같이 자동 형변환이 되어 결국 실수형의 값을 결과로 얻습니다.

 

byte a = 10;
byte b = 20;
byte c = a + b; // compile error!

이번에는 byte + byte입니다. 연산자 '+'은 a와 b가 모두 int형보다 작은 byte이기 때문에 두개의 피연산자들의 자료형을 int형으로 바꾼다음 연산을 수행합니다. 그래서 'a+b'의 연산 결과는 int형이 되고 byte형인 c에 명시적으로 형변환을 써주지 않았기 때문에 컴파일 오류가 생겼습니다.

 

byte c = (byte)(a + b); 와 같이 변경해야 컴파일 에러가 발생하지 않습니다. 

 

byte a = 10;
byte b = 30;
byte c = (byte)(a * b);
System.out.println(c); // 44

10x30은 300이지만 300은 byte형의 범위를 넘어서기 때문에 byte형으로 명시적으로 바꾸면 데이터 손실이 일어납니다. 

 

손실을 예방하기 위해 byte나 short형 같이 작은 자료형을 사용하는 것을 지양하는 편이 좋을 것 같습니다.

 

int a = 1_000_000; // 100만
int b = 2_000_000; // 200만

long c = a * b;

System.out.println(c); // -1454759936

변수 c의 자료형은 8 byte 이기 때문에 충분히  2x10^12를 저장하고 출력할 수 있을 것 같지만 결과는 전혀 다른 값이 출력됩니다. 이는 int타입과 int타입의 연산결과는 int타입이라고 방금 위에서 설명했습니다. a*b의 결과가 int타입으로 저장되므로 -1454759936 이므로 long형으로 자동 형변환 되어도 값은 변하지 않습니다. 

 

올바른 결과를 얻으려면 변수 a 또는 b의 타입을 'long'으로 형변환을 해야합니다.

long c = 1_000_000L * 2_000_000;

 

 

비트 연산자


비트 연산자는 피연산자를 비트단위로 논리 연산을 합니다. 피연산자를 이진수로 표현했을 때의 각 자리를 아래의 규칙에 따라 연산을 수행하며, 피연산자로 실수는 허용하지 않습니다. 

 

| (OR)은 피연산자 중 한 쪽의 값이 1이면, 1을 결과로 얻는다. 그외는 0을 얻는다.

& (AND)은 피연산자 양 쪽이 모두 1이여만 1을 결과로 얻는다.

^ (XOR)은 피연산자의 값이 서로 다를 때만 1을 결과로 얻는다. 

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

 

쉬프트 연산자 << >>

'<<' 연산자는 각 자리를 왼쪽으로 이동시키며 빈칸을 0으로만 채웁니다.

//8<<2에 대한 계산 입니다.
00001000
//8<<2는 10진수 8의 2진수를 왼쪽으로 2자리 이동시킵니다.
001000__
//자리이동으로 인해 저장범위를 벗어난 값은 버려지고 빈자리는 0으로 채워집니다.
00100000(10진수로 32)

'>>'연산자의 경우, 오른쪽으로 이동시키는데 부호있는 정수는 부호를 유지하기 위해 왼쪽 피연산자가 음수인 경우 빈자리를 1로 채웁니다. 물론 양수일때는 0으로 채웁니다.

 

8>>0일때
00000000 00000000 00000000 00001000 10진수: 8
8>>1일때
00000000 00000000 00000000 00000100 10진수: 4
8>>2일때
00000000 00000000 00000000 00000010 10진수: 2

8<<0일때
00000000 00000000 00000000 00001000 10진수: 8
8<<1일때
00000000 00000000 00000000 00010000 10진수: 16
8<<2일때
00000000 00000000 00000000 00100000 10진수: 32

 

 2진수 n자리를 왼쪽으로 이동하면 피연산자를 2의 n승 곱한 결과를,

오른쪽으로 이동하면 피연산자를 2의 n승 나눈 결과라는 것을 알 수 있습니다.

 

곱셈이나 나눗셈 연산을 사용하는 것과 같은 결과를 얻지만 쉬프트 연산자를 사용하는 이유는 속도때문입니다. 

다만 가독성면에서는 떨어지기 때문에 곱셈또는 나눗셈을 주로 사용하는 곳에 사용하는 것이 좋습니다.

 

관계연산자


두 피연산자를 비교하는 데 사용되는 연산자입니다. 주로 조건문과 반복문의 조건식에 사용되며, 연산결과는 오직 true와 false 둘 중의 하나입니다.

 

대소비교연산자 : 기본형중에서는 boolean형을 제외한 나머지 자료형에 다 사용가능하지만 참조형에서는 불가능 합니다.

>,<,>=,<=

 

등가비교연산자 : 기본형은 물론, 참조형에서도 사용가능합니다. 참조형의 경우 객체의 주소값을 저장하기 때문에 두 개의 객체의 주소값을 비교하여 같은 객체인지 알 수 있습니다. 

==, !=

 

비교연산자는 비교하는 피연산자의 타입이 서로 다를 경우에는 자료형의 범위가 큰 쪽으로 자동 형변환하여 피연산자의 타입을 일치시킨 후에 비교합니다.

10 == 10.0f
=> 10.0f == 10.0f
=> true

하지만 다음 코드는 false입니다.

0.1 == 0.1f
=>false

실수형은 근사값으로 저장이 되므로 0.1f는 무한소수여서 저장할 때 2진수로 변환하는 과정에서 오차가 발생합니다. 

 

실수간의 비교

다음은 실수간 비교입니다. 

float f = 0.1f;
double d = 0.1;
d == f
=> d == (double)f;
=> 0.10000000000000001 == (double)0.0000000149011612
=> 0.10000000000000001 == 0.0000000149011612

float 타입의 값을 double타입으로 형변환하면, 부호와 지수는 달라지지 않고 그저 가수의 빈자리를 0으로 채울 뿐이므로 0.1f를 double타입으로 형변환해도 뒤에 0이 채워지는것이기 때문에 값이 그대로 입니다. 

 

다시말해 float타입의 값을 정밀도가 더 높은 double타입으로 형변환한다고 오차가 적어지는게 아닙니다. 

 

같은 소수인 float와 double타입을 비교하려면 double타입을 float타입으로 형변환한다음 비교해야 올바른 결과를 얻을 수 있습니다.

(float)d = f;
=>(float)0.10000000000000001 = 0.1000000149011612
=>0.10000000149011612 == 0.1000000149011612
=>true

문자열의 비교

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

 

논리 연산자


|| , | (OR연산자)은 피연산자 중 어느 한쪽만 true이면 true를 결과로 얻는다.

&&, &(AND연산자)은 피연산자 양쪽 모두 true이어야 true를 결과로 얻는다.

!논리적인 부정을 뜻하며, true를 false로, false를 true로 바꿔준다.

 

효율적인 연산

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

&&의 경우 어느 한쪽만 false이어도 전체 연산결과가 false이므로 좌측 피연산자가 거짓이면, 우측 피연산자는 평가하지 않습니다.

즉 , 같은 조건식이라도 피연산자의 위치에 따라서 연산속도가 달라질 수 있습니다. 

 

instanceof


Java instanceof 연산자는 객체가 지정된 유형(클래스 또는 서브클래스 또는 인터페이스)의 인스턴스인지 테스트하는 데 사용됩니다.

 

objectName instanceOf className;

 

여기서 객체 이름이 클래스 이름의 인스턴스인 경우 연산자는 true을 반환합니다. 그렇지 않으면 false를 반환합니다.

class Main {

  public static void main(String[] args) {

    String name = "test";
    
    // checks if name is instance of String
    boolean result1 = name instanceof String;
    System.out.println("name is an instance of String: " + result1); //print true
  }
}

 

이뿐만이 아니고 instanceof를 사용하여 서브클래스의 객체가 슈퍼클래스의 인스턴스이기도 한지 확인할 수 있습니다.

// Java Program to check if an object of the subclass
// is also an instance of the superclass

// superclass
class Animal {
}

// subclass
class Dog extends Animal {
}

class Main {
  public static void main(String[] args) {

    // create an object of the subclass
    Dog d1 = new Dog();

    // checks if d1 is an instance of the subclass
    System.out.println(d1 instanceof Dog);        // prints true

    // checks if d1 is an instance of the superclass
    System.out.println(d1 instanceof Animal);     // prints true
  }
}

Dog클래스는 Animal클래스를 상속받은 상태이며 Dog는 서브 클래스 Animal은 슈퍼클래스라 할 수 있습니다. 

d1 instanceof Animal

여기서는 instanceof를 사용해 d1이 슈퍼클래스 Animal의 인스턴스인지 확인합니다.

 

 

또한 인터페이스 클래스를 제외한 모든 클래스는 Object 객체를 상속받기 때문에 

d1 instanceof Object

true가 나옵니다. 

 

instanceof 연산자를 사용한 다운캐스팅

먼저 다음 코드를 이해할 수 있어야 합니다.

Dog d=new Animal();//Compilation error

타입프로모션(자동형변환)은 작은 타입이 큰타입으로 바꿀 때만 가능합니다. 그리고 Dog같은 경우 Animal를 상속받으므로 Animal(부모=슈퍼클래스) , Dog(자식=서브클래스) 이고 부모객체는 자식 객체에 상속 받고 있으니 더 상위 요소로 판별될 수 있습니다. 

 

Dog d=(Dog)new Animal();  
//Compiles successfully but ClassCastException is thrown at runtime

문제를 해결하기 위해 명시적 형변환을 통해 다운캐스팅을 진행했지만 런타임에 ClassCastException이 발생합니다.

왜냐하면 ClassCastException은 객체를 인스턴스가 아닌 서브클래스로 형 변환하려고 시도했음을 나타내기 위해 발생합니다.

 

instanceof 연산자로 다운캐스팅이 가능한 예시를 살펴보겠습니다.

class Animal { }  
  
class Dog3 extends Animal {  
  static void method(Animal a) {  
    if(a instanceof Dog3){  
       Dog3 d=(Dog3)a;//downcasting  
       System.out.println("ok downcasting performed");  
    }  
  }  
   
  public static void main (String [] args) {  
    Animal a=new Dog3();  
    Dog3.method(a);  
  }  
    
 }

 다운 캐스팅의 목적은 업캐스팅한 객체를 되돌리는데 있습니다. 그래서 아까와 같이 업캐스팅되지 않은 생 부모 객체 (Animal) 일 경우 , 이를 다운 캐스팅 하면 예외가 발생됩니다.(ClassCastException)이 발생됩니다.

특히 이 예외는 컴파일때 발생되지 않고 런타임때 발생되므로 주의해야 합니다.

 

업캐스팅과 다운캐스팅에 대한 더 자세한 내용은 해당 글에 있으니 참고하면 좋을 것 같습니다.

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%97%85%EC%BA%90%EC%8A%A4%ED%8C%85-%EB%8B%A4%EC%9A%B4%EC%BA%90%EC%8A%A4%ED%8C%85-%ED%95%9C%EB%B0%A9-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

 

☕ JAVA 업캐스팅 & 다운캐스팅 - 완벽 이해하기

자바의 참조형 캐스팅 하나의 데이터 타입을 다른 타입으로 바꾸는 것을 타입 변환 혹은 형변환(캐스팅) 이라고 한다. 자바의 데이터형을 알아보면 크게 두가지로 나뉘게 된다. 기본형(primitive ty

inpa.tistory.com

 

assignment(=) operator


할당 연산자는 일반적으로 두 가지 유형이 있습니다. 

1. 단순 할당 연산자: 단순 할당 연산자는 왼쪽이 피연산자로 구성되고 오른쪽이 값으로 구성되는 "=" 기호와 함께 사용됩니다. 오른쪽의 값은 왼쪽에 정의된 것과 동일한 데이터 유형이어야 합니다.

2. 복합 할당 연산자: 복합 연산자는 +,-,* 및 /가 = 연산자와 함께 사용되는 경우에 사용됩니다.

예 : += , -= , *= , /= , %=,^=,|=,<<=,>>=,

 

3. reference type의 경우 주소값을 할당 

 

 

화살표(->) 연산자 


람다 표현식을 만드는 데 사용되는 화살표 연산자는 Java 8에 람다 표현식 기능이 추가되면서 함께 도입되었습니다.

화살표 연산자는 표현식 본문과 인수를 구분합니다.

(parameters) -> { statements; } // 람다 표현식

 

Java 코드를 더 이해하기 쉽고 간결하게 만들기 위해 Java 8에서 도입된 람다 표현식을 익명 클래스에 대신 사용할 수 있습니다.

Java 8 이전 버전의 Java를 사용하여 익명 클래스를 빌드하는 방법을 보여주는 그림은 다음과 같습니다.

 

Runnable r1 = new Runnable() {  
        @Override  
        public void run() {  
            System.out.print(" method Run ");  
        }  
};

람다 표현식을 사용하여 앞서 언급한 작업을 수행하는 방법은 다음과 같습니다.

Runnable r1 = ()-> System.out.print(" method Run ");

 

또 다른 예시는 다음과 같습니다.

interface Drawable{    
    public void draw();    
}    
public class M {  
    public static void main(String[] args) {  
        int w = 20;    
        // arrow operator    
        Drawable d=()->{    
            System.out.println(" Drawing width is   "+w);    
        };    
        d.draw();    
    }  
}

Drawable 인터페이스의 draw() 메서드는 람다 식과 화살표 연산자를 사용하여 구현되었습니다.

 

람다식을 활용해 함수형 프로그래밍이 가능해지고 매우 간결한 표현이 가능해졌습니다. 추후에 람다식에 대해 더 자세히 알아보겠습니다.

 

 

3항 연산자


Java 삼항 연산자는 피연산자가 3개인 유일한 조건 연산자입니다.

if-then-else 문을 한 줄로 대체할 수 있으며 Java 프로그래밍에서 많이 사용됩니다.

삼항 연산자를 if-else 조건 대신 사용하거나 중첩된 삼항 연산자를 사용하여 조건을 전환할 수도 있습니다.

if-else 문과 동일한 알고리즘을 따르지만 조건 연산자는 공간을 덜 차지하며 가능한 한 짧은 방법으로 if-else 문을 작성하는 데 도움이 됩니다.

 

3항 연산자 표현식:

variable = Expression1 ? Expression2(true인 경우): Expression3(false인 경우)

if-else-then 표현식:

if(Expression1)
{
    variable = Expression2;
}
else
{
    variable = Expression3;
}

 

 

연산자 우선 순위


1순위 : expr++, expr--

2순위 : ++expr, --expr, +expr, -expr, ~ , !

3순위 : * , / , %

4순위 : +, -

5순위 : << , >> , >>>

6순위 : < , > , <= , >= , instanceof

7순위 : == , !=

8순위 : &

9순위 : ^

10순위 : |

11순위 : && 

12순위 : ||

13순위 : ? : 

14순위 : = , += , -= , *= , /= , %= , &= , ^= , |= , <<= , >>= , >>>=

 

  • 할당 연산을 제외한 연산자들은 왼쪽에서 오른쪽으로 연산을 진행합니다.
  • 동일 선상에 있는 연산자들은 동일한 우선순위를 가집니다.

Java 13. switch 연산자


일단 저희가 알고 있었던 switch 문으로 만든 코드를 보겠습니다.

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);

이처럼 요일의 이름의 개수를 출력하기 위해 numLetter라는 변수에 저장을 하고 break합니다.

 

요일 이름의 길이를 변수에 저장하는 대신 스위치 표현식을 사용하여 '반환'할 수 있다면 더 좋을 것입니다.

또한, 작성하기 힘들고 잊어버리기 쉬운 중단 문을 사용하지 않는 것이 더 좋을 것입니다.

이와 같은 해결책으로 나온 switch 연산자가 도입되었습니다.

 

- case 문 내에 여러 개의 값을 이용할 수 있습니다.

 

이게 바로 Java 13에서 나온 표현 식 입니다.

    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);
        }
    );

 

case label_1, label_2, ..., label_n -> expression;|throw-statement;|block

 

Java 런타임은 화살표 왼쪽에 있는 레이블 중 하나라도 일치하면 화살표 오른쪽에 있는 코드를 실행하고 스위치 표현식(또는 문)의 다른 코드는 실행하지 않습니다. 화살표 오른쪽에 있는 코드가 표현식인 경우 해당 표현식의 값이 스위치 표현식의 값이 됩니다.

 

break로 값을 반환하던 것이 yield를 이용해서 반환

또한 Java SE 13에는 yield 문이 도입되었습니다. 이 문은 스위치 표현식에서 대/소문자 레이블이 생성하는 값인 하나의 인수를 받습니다.

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

 

마지막으로 추천하는 방식은 

"화살표 ->" 레이블을 사용하는 것이 좋습니다. "콜론 : " 레이블을 사용할 때 break 또는 yield 문을 삽입하는 것을 잊어버리기 쉬우며, 그렇게 하면 코드에 의도하지 않은 내용이 포함될 수 있습니다.
"화살표 대/소문자" 레이블의 경우 표현식이나 throw 문이 아닌 여러 문이나 코드를 지정하려면 블록으로 묶어 지정하세요. 대/소문자 레이블이 산출하는 값을 산출문과 함께 지정합니다

    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);
        }
    };

 

 

 

 

참고 문헌


 

Java instanceof (With Examples)

The instanceof operator in Java is used to check whether an object is an instance of a particular class or not. Its syntax is objectName instanceOf className; Here, if objectName is an instance of className, the operator returns true. Otherwise, it returns

www.programiz.com

 

Java instanceof - javatpoint

The Java instanceof operator is used to test if the object or instance is an instanceof the specified type (class or subclass or interface), how downcasting in java is possible with instanceof operator:

www.javatpoint.com

 

Arrow Operator in Java - Javatpoint

Arrow Operator in Java with java tutorial, features, history, variables, programs, operators, oops concept, array, string, map, math, methods, examples etc.

www.javatpoint.com