java 연산자

1. 연산자와 연산식

  • 자바는 데이타를 처리하고 결과를 산출하는 과정을 연산(Operations)이라고 한다. 연산을 위한 연산자(operator)와 연산하는 대상으로의 피연산자(opreand)가 있다. 이러한 코드를 연산식(expression)이라 한다.
  • 연산자의 종류에 따라 연산의 우선 순서와 방향이 차이를 가진다. 괄호 ()는 최우선 순위이므로 필요에 따라 적절하게 사용한다.
  • 연산자는 다른 연산자를 합쳐서 하나의 새로운 연산자를 만들 수 있다(+, ++, <, <=)
  • 연산자의 경우 단항연산자(!, –), 이항연산자(a+b), 삼항연산자(a?b:c) 등, 연산식에 사용하는 연산자의 갯수에 따라 구분한다.

2. 산술 연산자

2.1 산술 연산자란

  • 산술연산자는 +,-,*,/,% 로 이뤄져 있다.
  • 정수끼리 연산을 하면, 피연산자 중에 long이 있을 경우 다른 피연산자는 long으로 변환 후 수행되며, 그 미만의 정수는 int 변환된 후 수행한다.
  • 실수와 정수간 연산을 하면, 실수가 아닌 정수는 실수로 변환 후 수행한다.
  • 정수 간 연산을 한 후 실수로 데이타를 변환한다 하더라도 소수점은 버림이 된 상태로 값을 출력한다.

2.2 산술연산자의 오버플로우 방지

  • 오버플로우는 연산의 결과가 해당 데이타 타입의 수용가능한 범위를 초과할 때 발생한다. 오버플로우에 대응하는 코드는 아래와 같음.
int a = 1000000000;
int b = 1000000000;
System.out.println(a*b);  // -1486618624

if(a>=Integer.MAX_VALUE/b){
    System.out.println("오버플로우 발생");  // 오버플로우 발생
}

2.3 산술연산자의 부동소수점 문제

System.out.println(7*0.1); // 0.7000000000000001
  • 위의 코드에서 우리가 기대하는 값은 0.7이다. 그러나 실제로는 0.7000000000000001 을 출력한다. 숫자 처리를 엄밀하게 해야할 경우 문제가 발생할 수 있다.
  • 이러한 문제가 발생하는 이유는 부동소수점 타입(double, float) 때문이라 한다. 해결책은 bigDecimal을 통해 가능하다. 예시는 아래와 같다.
BigDecimal c = BigDecimal.valueOf(7.0);
BigDecimal d = BigDecimal.valueOf(0.1);
System.out.println(c.multiply(d)); // 0.70

2.4 NaN과 infinity, ArithmeticException

  • 0으로 나누는 경우 아래와 같은 오류가 발생한다.
1/0 // ArithmeticException
1/0.0 // Infinity
1%0.0 // NaN
  • 산술예외에 대해서는 예외처리를, Infinity와 NaN에 대해서는 Double.isInfinity(), Double.isNan()을 통해 true와 false 값을 받는 것으로 해결 가능하다.
  • 하지만 NaN의 경우 그 값이 String이라 하더라도 Double로 인식된다. 이에 대한 대응을 필요로 하며 그것은 아래와 같다.
Double val = Double.valueOf("NaN"); // 컴파일 오류가 발생하지 않는다.
System.out.println(val+3); // NaN

if(val.isNaN()){
    val=0.0;
}
System.out.println(val+3); //3.0

2.5 문자열 연결 연산자(+)

  • 숫자와 문자열을 연결할 수 있다. 한편, 숫자와 문자열이 혼합된 상태라면, 그것의 위치에 따라 산술연산이 되기도, 문자열 연결이 되기도 한다. 구체적인 내용은 아래와 같다.
System.out.println(1+2+"번"); // 3번
System.out.println(1+(2+"번")); // 12번

3. 비교 연산자

  • 피연산자를 비교하여 boolean 타입인 true/false를 산출하기 위한 연산자이다. <, <=, =, >, ==, != 등이 있다.
  • 2.3의 부동소수점 문제가 동일하게 발생할 수 있다. 그러므로 double과 float의 비교 연산은 주의를 해야 한다.
0.1==0.1f // false
  • String의 경우 문자열이 동일하면 같은 해당 변수는 같은 주소를 가르킨다. 그러므로 비교 연산자가 작동한다. 하지만 new를 통해 새로운 객체를 생성하면 그 문자열이 동일하더라도 항상 false이다. 그때는 equals() 메서드를 사용해야 한다.

4. 논리 연산자

논리곱(AND) &&,& 좌항우항 모두 true
논리합(OR) ||,| 둘 중 하나 혹은 양항 모두 true
배타적논리합(XOR) ^ 양항 중 단 하나만 true
논리부정(NOT) ! 피연산자의 논리값을 바꿈
  • 논리곱과 논리합은 각각 연산자를 두 개를 가진다. &의 경우 좌항이 false일 경우 해당 연산식을 false로 처리한다. 의 경우 좌항이 true일 경우 해당 연산식을 true로 처리한다. &&과   의 경우 좌항의 논리값과 관계 없이 우항을 처리한다.

5. 비트 연산자

  • 비트연산자는 0과 1로 이뤄진 bit 데이타를 의미한다. 데이타 타입 중 bit를 다루는 것은 자바에 존재하지 않는다. 그러므로 비트 연산자는 연산하고자 하는 정수를 int로 변환하고, 비트연산자(&, , ^, ~, «, », »>)와 비트 논리 연산자(&, , ^)를 처리한 후, int 값으로 산출한다.
  • 비트 연산자의 값을 출력하고 싶으면, Integer.toBinaryString() 매서드를 통해 String 값으로 받아야 한다. 추가적으로 비트 반전 연산자는 ~이며, 0을 1로 1을 0으로 바꾼다. 그 예시는 아래와 같다.
System.out.println(Integer.toBinaryString(7)); // 111
System.out.println(Integer.toBinaryString(~7)); // 11111111111111111111111111111000
  • 아래는 비트연산의 예시이며 그 아래의 table은 그것의 처리 과정이다.
System.out.println(6^3); // 5
4 2 1
6 1 1 0
3 0 1 1
XOR(둘 중 하나만 true) true false true
값 : 5 1 0 1
  • 비트 이동 연산자는 그 값을 좌측 혹은 우측으로 해당 값 만큼 밀어내는 것을 의미한다. 아래는 111을 좌측으로 3칸 옮기는 것을 의미한다.
System.out.println(Integer.toBinaryString(7<<3)); // 111000

6. 대입연산자(assignment operator)

  • 대입연산자(=)를 통해 우항을 좌항의 변수의 값으로 저장한다.
  • 대입연산자에 다른 연산자를 추가하여 복합 대입 연산자를 만들 수 있다. 그럴 경우 변수의 값이 좌항이 되고, 대입연산자가 아닌 연산자를 연산자로 하며, 대입하는 값을 우항으로 한다. 설명으로는 복잡한데, 아래의 코드로는 쉽게 이해 가능하다.
double assign = 10.0;
System.out.println(assign+=10); // 20.0 (assign = assign + 10.0)
System.out.println(assign/=10); // 2.0 (assign = assign / 10.0)

7. 삼항연산자

  • 삼항 연산자는 세 개의 피연산자를 통해 if문을 간략하게 처리한다. 그래서 조건 연산식이라고도 불린다.
String str = (10>6)?"10이크다":"6이크다";
System.out.println(str); // 10이크다

8. instanceof

  • 부모타입의 객체를 자식타입으로 강제 타입 변환(Casting)을 할 수 있다. 하지만 상속관계가 없는 경우 ClassCastException이 발생할 수 있다. 그러므로 instanceof를 통해 해당객체가 해당타입에 상속관계인지를 true/false를 통해 보여준다. 그 말은 타입 변환이 가능하다는 의미와 같다.
if(parent instanceof Child){
	Child child = (Child) parent;
}

9. 화살표 연산자(람다식, ->)

  • 인터페이스는 매소드는 존재하지만 구체적인 정의는 되어있지 않다. 이를 상황과 조건에 따라 정의하고 사용할 수 있다. 이때 사용하는 것이 람다식이다.
  • 인터페이스 변수 = 람다식; 으로 정의한다. 람다식은 (타입 매개변수) -> {실행문}의 구조이다. 구체적인 코드는 아래와 같다.
public interface TestInterface {
    public void method(int x);
}
public class test {
    public static void main(String[] args) {
        TestInterface test;
        test = x-> {
            System.out.println(x*5);
        };
        test.method(5); // 25
    }
}
  • TestInterface는 반드시 단 하나의 메서드를 가져야 한다. 그렇지 않으면 어떤 메서드를 호출하는지 확인할 수 없기 때문이다.
  • 인터페이스의 메서드는 리턴값의 여부(void, int)와 매개변수(int x)를 정의할 수 있다.
  • 람다에서 정의할 때는 인터페이스에서 정의한 매서드의 형식에 맞춰서 리턴과 매개변수를 정의해야 한다.