Posts :coffee: [Java] 이것이 자바다 #3: 연산자
Post
Cancel

:coffee: [Java] 이것이 자바다 #3: 연산자

연산자

연산식

  • 연산자와 피연산자를 이용하여 연산의 과정을 기술한 것
  • 반드시 하나의 값을 산출한다.
  • 따라서 하나의 값이 올 수 있는 곳이면 어디든지(변수에 저장되는 값 위치, 다른 연산식의 피연산자 위치) 값 대신에 연산식을 사용할 수 있다.

연산 방향과 우선순위

프로그램에서는 연산자의 연산 방향과 연산자 간의 우선 순위가 정해져 있다.

연산자연산 방향우선순위
증감(++, --), 부호(+, -), 비트(~), 논리(!)높음
산술(*, /, %) 
산술(+, -) 
쉬프트(<<,>>, >>>) 
비교(<,>, <=, >=, instanceof) 
비교(==, !=) 
논리(&) 
논리(^) 
논리(|) 
논리(&&) 
논리(||) 
조건(?:) 
대입(=, +=, -=, *=, /=, %=, &=, ^=, |=, <<=, >>=, >>>=)낮음
  • 단항, 이항, 삼항 연산자 순으로 우선순위를 가진다.
  • 산술, 비교, 논리, 대입 연산자 순으로 우선 순위를 가진다.
  • 단항과 대입 연산자를 제외한 모든 연산의 방향은 왼쪽에서 오른쪽이다(→).
  • 복잡한 연산식에는 괄호()를 사용하여 우선순위를 정해준다.

단항 연산자

부호 연산자

  • +: 피연산자의 부호 유지
  • -: 피연산자의 부호 변경
  • 피연산자 타입: boolean 타입과 char 타입 제외 기본 타입
  • 산출 타입: int 타입
1
2
3
short s = 100;
//short result = -s; //컴파일 에러
int result = -s; //ok!

증감 연산자

  • ++피연산자: 다른 연산을 수행하기 전 피연산자 값을 1 증가(해서 그 결과를 다시 피연산자에 저장한다.)
  • --피연산자: 다른 연산을 수행하기 전 피연산자 값을 1 감소
  • 피연산자++: 다른 연산을 수행한 후에 피연산자 값을 1 증가
  • 피연산자--: 다른 연산을 수행한 후에 피연산자 값을 1 감소

  • 피연산자 타입: boolean 타입 제외 기본 타입

  • ++ii = i + 1의 바이트코드는 동일하다. 따라서 연산 속도는 동일하다.

논리 부정 연산자 (!)

  • 조건문과 제어문에서 사용되어 조건식의 값을 부정하도록 해서 실행흐름을 제어할 때, 두 가지 상태를 번갈하면서 변경하는 토글(toggle) 기능 구현할 때 주로 사용된다.
1
2
3
4
5
6
7
8
boolean play = true;
System.out.println(play); //true

play = !play;
System.out.println(play); //false

play = !play;
System.out.println(play); //true

비트 반전 연산자(~)

  • 피연산자 타입: 정수타입(byte, short, int, long)
  • 산출 타입: int 타입
  • 피연산자는 연산을 수행하기 전에 int 타입으로 변환되고, 비트 반전이 일어난다.

  • 부호 반대 정수 구하기: ~피연산자 + 1
  • Integer.toBinaryString(): 정수값을 32비트 이진 문자열로 리턴하는 메서드이다(앞의 0은 생략된다).
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
32
33
public static void main(String[] args){
    int v1 = 10;
    int v2 = ~v1;
    int v3 = ~v1 + 1;
    System.out.println(toBinaryString(v1) + " (십진수: " + v1 + ")" );
    System.out.println(toBinaryString(v2) + " (십진수: " + v2 + ")" );
    System.out.println(toBinaryString(v3) + " (십진수: " + v3 + ")" );

    int v4 = -10;
    int v5 = ~v4;
    int v6 = ~v4 + 1;
    System.out.println(toBinaryString(v4) + " (십진수: " + v4 + ")" );
    System.out.println(toBinaryString(v5) + " (십진수: " + v5 + ")" );
    System.out.println(toBinaryString(v6) + " (십진수: " + v6 + ")" );

}

// 32개의 문자열 모두 얻는 메서드 만들기 (0 생략 X)
public static String toBinaryString(int value) {
    String str = Integer.toBinaryString(value);
    while(str.length() < 32) {
        str = "0" + str;
    }
    return str;
}

// 결과
//00000000000000000000000000001010 (십진수: 10)
//11111111111111111111111111110101 (십진수: -11)
//11111111111111111111111111110110 (십진수: -10)
//11111111111111111111111111110110 (십진수: -10)
//00000000000000000000000000001001 (십진수: 9)
//00000000000000000000000000001010 (십진수: 10)

이항연산자

산술연산자(+, -, *, /, %)

  • long을 제외한 정수 타입은 int 타입으로 산출되고, 피연산자 중 하나로 실수타입이면 실수 타입으로 산출된다.
    • 피연산자들이 모두 정수타입이고, int타입(4byte)보다 크기가 작은 타입일 경우 모두 int타입으로 변환 후 연산을 수행한다.
      • byte + byte => int + int => int
      • JVM이 기본적으로 32비트
    • 피연산자들이 모두 정수 타입이고, long 타입이 있을 경우 모두 long 타입으로 변환 후, 연산을 수행한다. 따라서 연산의 산출 타입은 long이다.
      • int + long => long + long => long
    • 피연산자 중 실수 타입이 있을 경우, 크기가 큰 실수 타입으로 변환 후 연산을 수행한다. 따라서 연산의 산출 타입은 실수 타입이다.
      • int + double => double + double => double
  • 자바는 리터럴 간의 연산은 타입 변환 없이 해당 타입으로 계산한다.
1
2
3
4
5
6
7
8
9
10
11
public class CharOperatorExample {
    public static void main(String[] args) {
        char c1 = 'A' + 1;
        char c2 = 'A';
        //char c3 = c2 + 1; // 컴파일 에러!
        char c4 = (char) (c2 + 1);
        System.out.println("c1: " + c1); //B
        System.out.println("c2: " + c2); //A
        System.out.println("c4: " + c4); //B
    }
}

오버플로우 탐지

  • 산술 연산 주의점: 연산 후 산출값이 산출 타입으로 표현 가능한 지
  • 메소드를 이용하여 산술연산 전에 피연산자의 값을 조사해서 오버플로우를 탐지하도록 하자.
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
// 산술연산 전에 오버플로우 탐지
public class CheckOverflowExample {
    public static void main(String[] args) {
        try {
            int result = safeAdd(2000000000, 2000000000);
            System.out.println(result);
        } catch(ArithmeticException e) {
            System.out.println("오버플로우가 발생하여 정확하게 계산할 수 없음")
        }
    }
    
    public static int safeAdd(int left, int right) {
        if (right > 0) {
            if (left > (Integer.MAX_VALUE - right)) {
                throw new ArithmeticException("오버플로우 발생");
            }
        } else {
            if (left < (Integer.MIN_VALUE - right)) {
                throw new ArithmeticException("오버플로우 발생");
            }
        }
        return left + right;
    }
}

// 실행 결과
// 오버플로우가 발생하여 정확하게 계산할 수 없음

정확한 계산은 정수 사용

1
2
3
4
5
6
7
8
9
10
11
12
int apple = 1;
double pieceUnit = 0.1;
int number = 7;

double result = apple - number * pieceUnit;

System.out.println("사과 한 개에서 0.7조각을 빼면");
System.out.println(result + "조각이 남는다.");

// 실행 결과
// 사과 한 개에서 0.7조각을 빼면 
// 0.29999999999999993조각이 남는다.

이진 포맷을 사용하는 부동소수점 타입(float, double)은 0.1을 정확히 표현할 수 없어 근사치로 처리한다.

1
2
3
4
5
6
7
8
9
10
// 정확하게 계산할 때에는 부동소수점 타입을 사용하지 않는다. 
int apple = 1;
int totalPieces = apple * 10;
int number = 7;
int temp = totalPieces - number;

double result = temp/10.0;

System.out.println("사과 한 개에서 0.7조각을 빼면");
System.out.println(result + "조각이 남는다.");

NaN과 Infinity 연산(/, %)

  • 좌측 피연산자가 정수 타입인 경우 나누는 수인 우측 피연산자는 0을 사용할 수 없다.
1
2
5 / 0 -> ArithmeticException 발생
5 % 0 -> ArithmeticException 발생
1
2
3
4
5
6
7
8
//예외처리
try {
    //int z = x / y;
    int z = x / y;
    System.out.println("z: " + z);
} catch (ArithmeticException e) {
    System.out.println("0으로 나누면 안됨");
}
  • 실수 타입인 0.0 또는 0.0f로 나누면 예외가 발생하지 않고 다음과 같은 값이 나온다.
1
2
5 / 0.0 => Infinity
5 % 0.0 => NaN
  • 이 값은 어떤 수와 산술연산 하더라도 Infinity와 NaN이 산출된다.
  • 따라서 Double.isInfinite(), Double.isNaN() 메서드를 활용하여 연산의 결과가 Infinity 또는 NaN이면 다음 연산을 수행하지 못하도록 처리한다.
1
2
3
4
5
if (Double.isInfinite(z) || Double.isNaN(z)) {
    System.out.println("값 산출 불가");
} else {
    System.out.println(z);
}

입력값의 NaN 검사

  • 부동소수점을 입력 받을 때는 반드시 NaN 검사를 해야 한다.
  • “NaN” 문자열은 Double.valueOf() 메서드에 의해 double 타입으로 변환되면 NaN가 된다. NaN은 산술연산이 가능하기 때문에 데이터가 엉망이 될 수 있다.
1
2
3
4
5
6
7
8
String userInput = "NaN";
double val = Double.valueOf( userInput );

double currentBalance = 10000.0;

currentBalance += val;
System.out.println(currentBalance); //NaN
// 원래 데이터가 없어진다!
  • 따라서 입력 받을 때 “NaN”인지 조사하고 “NaN”이라면 NaN과 산술연산을 수행하지 않도록 처리해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
String userInput = "NaN";
double val = Double.valueOf( userInput );

double currentBalance = 10000.0;

if (Double.isNaN(val)) {
    System.out.println("NaN이 입력되어 처리할 수 없음");
    val = 0.0;
}

currentBalance += val;
System.out.println(currentBalance);
// NaN이 입력되어 처리할 수 없음
// 10000.0

NaN인지 조사할 때 == 연산자를 사용하면 안 된다. NaN은 != 연산자를 제외한 모든 비교연산자를 사용할 경우 false값을 리턴하기 때문이다.

문자열 연결 연산자

  • 문자열과 숫자가 혼합된 + 연산식은 왼쪽에서부터 오른쪽으로 연산이 진행된다.
1
2
"JDK" + 3 + 3.0; //JDK33.0
3 + 3.0 + "JDK"; //6.0JDK

비교 연산자(<, <=, >, >=, ==, !=)

  • 산출 타입: boolean타입
  • 피연산자 타입
    • 대소 연산자: boolean 타입을 제외한 기본 타입
    • 동등 연산자: 모든 타입

부동소수점 비교

  • 비교연산자에서도 연산을 수행하기 전에 타입 변환을 통해 피연산자의 타입을 일치시킨다.
    • 예외: 이진 포맷의 가수를 사용하는 모든 부동소수점 타입은 0.1을 정확하게 표현할 수 없어 0.1f는 0.1의 근사값으로 표현된다.
    • 해결책: 피연산자를 모두 float타입으로 강제 타입 변환한 후 비교연산을 하거나, 정수로 변환해서 비교한다.
1
2
3
4
5
6
7
8
9
int v2 = 1;
double v3 = 1.0;
System.out.println(v2 == v3); //true

double v4 = 0.1;
float v5 = 0.1f;
System.out.println(v4 == v5); //false
System.out.println((float)v4 == v5); // true
System.out.println((int)(v4*10) == (int)(v5*10)); //true

String 타입 비교

  • String 객체의 문자열만을 비교하고 싶다면 == 연산자 대신 ` equals()` 메서드를 사용해야 한다.

논리 연산자 (&&, ||, &, |, ^, !)

  • 피연산자 타입: boolean 타입
  • 종류
    • AND(&&, &): 피연산자 모두가 true일 경우에만 연산 결과 true
    • OF( ,):피연산자 중 하나만 true이면 연산 결과는 true
    • XOR(^): 피연산자가 하나는 true이고 다른 하나가 false일 때 연산 결과는 true
    • NOT(!): 피연산자와 논리값을 바꿈
  • &&와 &는 산출 결과는 같지만 연산 과정이 다르다.
    • &&, ||: &&는 앞의 피연산자가 false라면 뒤의 피연산자를 평가하지 않고 바로 false라는 산출 결과를 낸다. ||는 앞의 피연산자가 true이면 뒤의 피연산자를 평가하지 않고 바로 true라는 산출 결과를 낸다. => 더 효율적!
    • &, |: 두 피연산자 모두 평가하여 산출 결과를 낸다.
1
2
3
4
5
6
7
8
9
10
int charCode = 'A';

if ( (charCode >= 65) & (charCode <= 90))
    System.out.println("대문자입니다.");

if ( (charCode >= 97) && (charCode <= 122))
    System.out.println("소문자입니다.");

if ( !(charCode < 48) && !(charCode > 57))
	System.out.println("0~9 숫자입니다.");

비트 연산자

  • 피연산자 타입: 정수 타입
    • 0과 1로 표현이 가능한 정수 타입만 비트 연산이 가능하다. 실수 타입(float, double)은 비트연산을 할 수 없다.

비트 논리 연산자 (&, |, ^, ~)

  • 종류
    • &: 두 비트 모두 1일 경우에만 연산 결과가 1
    • |: 두 비트 중 하나만 1이면 연산 결과는 1
    • ^: 두 비트 중 하나는 1이고 다른 하나가 0일 경우 연산 결과는 1
    • ~: 보수
  • 피연산자(byte, short, char)를 int타입으로 자동 타입 변환 후 연산을 수행한다.

비트 이동 연산자(<<<, >>, >>>)

  • 종류
    • a << b: 정수 a의 비트를 b만큼 왼쪽으로 이동(빈자리는 0으로 채워진다.)
    • a >> b: 정수 a의 비트를 b만큼 오른쪽으로 이동 (빈자리는 정수 a의 최상위 부호 비트(MSB)와 같은 값으로 채워진다.)
    • a >>> b: 정수 각 비트를 b만큼 오른쪽으로 이동 (빈자리는 0으로 채워진다.)

대입 연산자(=, +=, -=, *=, /=, %=, &=, ^=, |=, «=, »=, »>=)

  • 종류
    • 단순 대입 연산자: 단순히 오른쪽 피연산자의 값을 변수에 저장
    • 복합 대입 연산자: 정해진 연산을 수행한 후 결과를 변수에 저장
  • 모든 연산자들 중 우선순위가 가장 낮다: 제일 마지막에 수행된다.
  • 진행방향이 오른쪽에서 왼쪽이다.

삼항 연산자

  • 조건식(피연산자1) ? 값 또는 연산식(피연산자2) : 값 또는 연산식(피연산자3)

  • 삼항연산자는 if문으로 변경해서 작성할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
int score = 95;

// 삼항연산자 사용
char grade = (score > 90) ? 'A' : 'B';

// if문 사용
char grade;
if(score > 90)
    grade = 'A';
else
    grade = 'B';
This post is licensed under CC BY 4.0 by the author.

:coffee: [Java] 이것이 자바다 #2: 변수와 타입

:coffee: [Java] LinkedList 구현하기

Loading comments from Disqus ...