[WS/3주차] 연산자

자바 스터디 https://github.com/whiteship/live-study/issues/3
목표 : 자바가 제공하는 다양한 연산자를 학습하세요.

이것이 자바다 책을 보며 같이 공부했습니다.

0. 연산자란?

  • 연산은 프로그램에서 데이터를 처리하여 결과값을 산출하는 것. 연산에 사용되는 표시/기호를 연산자(operator)라고 하고 연산에 사용되는 데이터를 피연산자(operand)라고 한다. 연산자와 피연산자로 연산 과정을 기술한 걸 연산식(expressions)라고 부른다.
  • 연산식은 반드시 하나의 값을 산출한다.

1. 산술 연산자

연산식 설명
피연산자 + 피연산자 덧셈 연산
피연산자 - 피연산자 뺼셈 연산
피연산자 * 피연산자 곱셈 연산
피연산자 / 피연산자 좌측 피연산자를 우측 피연산자로 나눗셈 연산
피연산자 % 피연산자 좌측 피연산자를 우측 피연산자로 나눈 나머지를 구하는 연산

산술연산자는 boolean 타입을 제외한 모든 기본 타입에 사용할 수 있다.

산술 연산자는 피연산자들의 변수 타입이 동일하지 않을 경우 다음과 같은 규칙으로 피연산자들의 타입을 일치시킨 후 연산을 수행한다.

  1. 피연산자들이 모두 정수 타입이고 int타입(4byte)보다 크기가 작은 타입일경우 모두 int 타입으로 변환 후 연산을 수행한다. 연산 산출 결과타입은 int다. (JVM이 기본적으로 32비트 단위로 연산을 수행하기 때문.) ex) byte + byte → int + int = int
  2. 피연산자들이 모두 정수 타입이고 long 타입이 있을 경우 모두 long 타입으로 변환 후 연산을 수행한다. 연산 산출 결과 타입은 long이다. ex) int + long → long + long = long
  3. 피연산자들 중 실수 타입(float, double)가 하나라도 있을 경우, 크기가 큰 실수 타입으로 변환 후에 연산을 수행한다. 연산 결과는 실수 타입이다. ex) int + double → double + double = double
int a = 10;
int b = 4;
int result1 = a/b; // 소수점을 버리고 2만 저장된다
double result2 = a/b // 2가 나온다. 2.5의 결과를 얻고 싶다면 최소 하나는 실수 타입이어야 한다.
double result3 = a*(1.0) / b; //2.5
double result4 = (double) a / b; //2.5

char도 정수 타입이기에 산술 연산이 가능하다. 단 char로 산술 연산을 할 경우 결과값은 int 타입임을 주의해야 한다.

char a = 'A' + 1; //리터럴 간 연산은 타입 변환 없이 해당 타입으로 계산한다. 즉 B가 저장된다.
char b = 'A';
char res = b + 1; //컴파일 에러, b가 int 타입으로 변환되어 연산이 되기 때문에 산출 타입은 int다
char res2 = (char)(b+1); //char로 결과값을 얻기 위해선 캐스팅을 해야한다

산술 연산시 연산의 결과가 산출 타입의 범위값을 넘지 않는지 신경써줘야 한다. 범위를 넘을 시 쓰레기값이 발생한다. 따라서 사용자에게 입력값을 받을 땐 바로 산술 연산을 하는 게 아니라 메소드를 따로 만들어 오버플로우 탐지를 위해 예외 처리를 해주자.

public class Main {
	public static void main(String args[]) {
		try {
			int res = add(2000000000,2000000000);
			System.out.println(res);
		} catch(ArithmeticException e){
			System.out.println("오버플로우 발생");
		}
	}

	private static int add(int i, int j) {
		if(j > 0) {
			if(i>Integer.MAX_VALUE-j) //오버플로우시 i+j>Max_Value로 하면 이미 i+j가 쓰레기값이기 때문에 오른편에서 -j를 해줌
				throw new ArithmeticException("오버플로우");
		} else { //j<=0일 경우
			if(i<Integer.MIN_VALUE-j)
				throw new ArithmeticException("오버플로우");
		}
		return i+j;
	}
}

또한 정확한 계산값을 원할 땐 정수값을 사용해야 한다. 이진 포맷의 가수를 사용하는 float, double은 0.1를 정확하게 표기할 수 없어 근사치로 처리하기 때문이다.

/ 와 %를 사용할 때 나누는 수로 0은 사용할 수 없다. 3/0 혹은 3%0은 실행시 예외가 발생한다. 실수 타입인 0.0으로 나눌 경우 예외 대신 / 결과론 Infinity(무한대) 값이, % 연산 결과론 NaN(Not a Number)가 나온다. 하지만 이 값들은 어떤 수와 연산을 하더라도 Infinity 또는 NaN가 나오기 때문에 연산을 계속해선 안 된다. Double.isInfinite()Double.isNan() 메소드를 이용해 if문으로 연산 검사를 해주자. NaN은 == 연산자를 사용할 수 없다. != 연산자를 제외한 모든 비교 연산자에 false를 리턴하기 때문에 NaN 검사는 Double.isNan() 메소드를 이용해야 한다.

2. 비트 연산자

비트 연산자는 데이터를 비트(bit) 단위로 연산하기에 0과 1이 피연산자가 된다. 따라서 0과 1로 표현 가능한 정수 타입만 비트 연산이 가능하다. 기능에 따라 비트 논리 연산자(&, |, ^, ~)와 비트 이동 연산자(«, », »>)로 구분한다. 비트 논리 연산자는 0과 1을 연산하고 비트 이동 연산자는 비트를 좌측, 혹은 우측으로 이동시킨다.

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

&, |, ^는 피연산자가 boolean 타입일 경우엔 일반 논리 연산자를, 정수 타입일 경우엔 비트 논리 연산자를 수행한다. ~는 비트값인 0을 1로, 1은 0으로 반전시켜 보수를 만든다.

구분 연산식 설명
AND & 두 비트 모두 1일 경우 연산 결과 1
OR | 두 비트 중 하나만 1이면 연산 결과 1
XOR ^ 두 비트 중 하나는 1이고 다른 하나가 0일 경우 연산 결과 1
NOT ~ 보수

비트 연산자는 피연산자를 int 타입으로 자동 타입 변환 후 연산을 수행한다. 따라서 결과 타입이 int가 아니면 컴파일 에러가 난다.

비트 반전 연산자(~) 역시 연산 수행 전 int 타입으로 변환 되어, 산출 타입이 int다.
~를 이용하여 부호가 반대인 정수를 구할 수 있다. ~의 산출값에 +1을 하면 부호가 반대인 정수를 얻을 수 있다.

01

비트 이동 연산자(«, », »>)

비트 이동 연산자는 정수 데이터의 비트를 좌측 혹은 우측으로 밀어 이동시키는 연산자이다.

연산식 설명
a « b 정수 a의 각 비트를 b만큼 왼쪽으로 이동(빈자리는 0으로 채워짐)
a » b 정수 a의 각 비트를 b만큼 오른쪽으로 이동(빈자리는 정수 a의 최상위 부호 비트(MSB)와 같은 값으로 채워짐)
a »> b 정수 a의 각 비트를 b만큼 오른쪽으로 이동(빈자리는 0으로 채워짐)

3. 관계 연산자

관계(비교) 연산자는 대소(<,<=,>,>=) 또는 동등(==,!=)를 사용해 boolean 타입을 산출한다. 대소 연산자는 boolean을 제외한 기본 타입에, 동등 연산자는 모든 타입에 사용 가능하다.

관계 연산자도 연산시 피연산자의 타입이 다르면 타입 변환으로 연산을 수행한다. A==65는 A가 int 타입 65로 변환 되어 65==65가 된다. 3.0==3은 3.0은 double형, 3은 int형이기 때문에 int를 double로 바꿔 3.0==3.0으로 비교하게 된다.

하지만 0.1==0.1f는 false가 나온다. 그 이유는 이진 포맷의 가수를 사용하는 모든 부동소수점 타입은 0.1을 정확하게 표현할 수 없어 0.1f를 근사치로 계산하기 때문이다. 피연산자를 모두 float 타입으로 강제 타입 변환을 하거나 정수로 변환해서 비교해야 한다.

또한 String은 참조변수이기 때문에 객체의 주소를 값으로 갖고 있다. 객체의 주소가 다르면 문자열이 같아도 false를 반환할 수 있다. String은 문자열 비교를 할 시 동등 연산자가 아닌 equals() 메소드를 사용하자.

4. 논리 연산자

논리곱(&&), 논리합(||), 배타적 논리합(^), 논리부정(!) 연산을 수행한다. 피연산자는 boolean 타입만 사용할 수 있다.

구분 연산자 설명
AND(논리곱) && 또는 & 피연산자 모두 true일 경우에만 연산 결과 true
OR(논리합) || 또는 | 피연산자 중 하나만 true면 연산 결과 true
XOR(배타적논리합) ^ 피연산자가 하나는 true, 하나는 false일 경우만 연산 결과 true
NOT(논리부정) ! 피연산자의 논리값을 바꿈

&보다 &&가, |보단 ||가 더 효율적으로 동작한다. &&와 &는 산출 결과는 같지만 연산 과정이 다르다. &&는 앞의 피연산자가 false면 바로 연산 결과로 false를 내지만, &는 두 피연산자 모두를 평가해 결과를 낸다. 마찬가지로 ||도 앞의 피연산자가 true면 바로 연산 결과를 true로 내지만, |는 모든 피연산자를 평가해 결과를 낸다.

5. instanceof

어떤 객체가 어떤 클래스의 인스턴스임을 확인할 수 있는 연산자다.

boolean result = 좌항(객체) instanceof 우항(타입)

좌항의 객체가 우항의 인스턴스면(우항의 타입으로 객체가 생성되었다면) true를, 다르면 false를 산출한다. instanceof 연산자는 매개값의 타입을 조사할 때 주로 사용된다. 메소드 내에서 강제 타입 변환이 필요할 경우 반드시 매개값이 누구의 객체인지 instanceof로 확인 후 안전하게 강제 타입 변환을 해야 한다. 강제타입변환은 자식 타입이 부모 타입으로 변환되어 있는 상태에서만 가능하기 때문이다.

Parent parent = new Parent();
Child child = (Child) parent; //강제 형변환 불가능

Parent parent2 = new Child();
Child child2 = (Child) parent2; //강제 형변환 가능

public void method(Parent parent){
  if(parent instanceof Child){ //Parent 매개 변수가 참조하는 객체가 Child인지 조사
    Child child = (Child) parent;
  }
}

타입을 확인하지 않고 강제 타입 변환하면 ClassCastException 예외가 발생할 수 있다.

public class A{}
public class B extends A{} //A를 상속받음
public static void main(String[] args){
  A a = new A();
  if(a instanceof B) //false

  A b = new B();
  if(b instanceof B) //true
}

6. assignment(=) operator

대입 연산자라고 하며 오른쪽 피연산자의 값을 좌측 피연산자인 변수에 저장한다. 단순히 오른쪽 피연산자의 값을 좌측 변수에 저장하는 걸 단순 대입 연산자라고 한다. 복합 대입 연산자는 정해진 연산을 실행 후 결과값을 변수에 저장한다.

  • 복합 대입 연산자의 종류
연산식 설명
변수 += 피연산자 변수 = 변수 + 피연산자와 동일
변수 -= 피연산자 변수 = 변수 - 피연산자와 동일
변수 *= 피연산자 변수 = 변수 * 피연산자와 동일
변수 /= 피연산자 변수 = 변수 / 피연산자와 동일
변수 %= 피연산자 변수 = 변수 % 피연산자와 동일
변수 &= 피연산자 변수 = 변수 & 피연산자와 동일
변수 |= 피연산자 변수 = 변수 | 피연산자와 동일
변수 ^= 피연산자 변수 = 변수 ^ 피연산자와 동일
변수 «= 피연산자 변수 = 변수 « 피연산자와 동일
변수 »= 피연산자 변수 = 변수 » 피연산자와 동일
변수 »>= 피연산자 변수 = 변수 »> 피연산자와 동일

대입 연산자는 모든 연산자들 중에 우선순위가 가장 낮아 제일 마지막에 실행된다. 또한 연산의 실행 방향이 오른쪽에서 왼쪽이기 때문에 a = b = c = 5;c = 5 -> b = c -> a = b 순서대로 실행된다.

7. 화살표(->) 연산자

자바 8부터 함수적 프로그래밍을 위해 람다식을 지원하고 있다(함수적 프로그래밍은 병렬 처리와 이벤트 지향 프로그래밍에 적합하기 때문에 최근 각광받고 있다).

람다식은 익명 함수(anonymous function) 생성을 위한 식으로 객체지향 언어 보단 함수지향 언어에 가깝다. 자바에선 람다식을 사용하면 코드가 간결해지고 컬렉션 요소를 필터링하거나 매핑해 원하는 결과를 쉽게 집계할 수 있다.

람다식 -> 매개변수를 가진 코드 블록 -> 익명 구현 객체

예를 들어 Study 인터페이스의 익명 구현 객체를 생성하는 코드를 일반적인 코드와 람다식으로 비교해보겠다.

//일반적 코드
Study study = new Study(){
  public void hard(){
    //내용
  }
}  //new Study~부터 끝 }까지 익명 구현 객체

// 람다식
Study study = () -> {/*내용*/};

함수적 스타일의 람다식을 작성하는 방법은 다음과 같다.

(타입 매개변수, …) -> {실행문; …}

(타입 매개변수, …)는 오른쪽 중괄호{}를 실행하기 위해 필요 값을 제공한다. 화살표 -> 연산자는 매개 변수를 이용해 {}를 실행한다고 이해하면 된다.

//int 매개 변수 a 값을 콘솔 출력하는 람다식
(int a) -> {System.out.println(a);}
//매개 변수 타입은 런타임 시 대입되는 값에 따라 달라질 수 있다. 따라서 보통 생략한다.
(a) -> {System.out.println(a);}
//하나의 매개변수만 있을 시 ()를 생략하고, 하나의 실행문만 있을 시 {}를 생략한다.
a -> System.out.println(a);
// 매개변수가 없다면 매개 변수 자리가 없어지므로 빈 괄호를 반드시 사용해야 한다.
( ) -> {실행문; ...}
//중괄호 {}에 return 문만 있을 경우, 다음과 같이 작성한다.
(x, y) -> x + y; //(x, y) -> {return x+y;} 와 같다.

람다식은 이후 스터디 15주차에 과제가 있는 걸로 아는데 그때 더 자세히 다룰 예정이다. 아직 공부가 더 필요하다ㅠㅠ…

8. 3항 연산자

필요로 하는 피연산자의 수에 따라 단항, 이항, 삼항 연산자로 나뉜다. 부호 연산자와 증가/감소 연산자는 피연산자를 하나만 갖기에 단항연산자로 부른다. 조건 연산자는 조건식, A, B와 같이 세 개의 피연산자가 필요하기에 삼항 연산자라고 한다. 그 외의 연산자는 두 개의 피연산자를 가지므로 이항 연산자다.

삼항 연산자는 ? 앞의 조건식에 따라 : 앞뒤의 피연산자가 선택 되어 조건 연산식이라고도 부른다.

조건식 ? 값 or 연산식(true일때) : 값 or 연산식(false일때)

조건식 결과가 true면 피연산자2 값이 결과값이, false면 피연산자3 값이 결과값이 된다. 삼항 연산자는 if문으로 표현 가능하지만 한 줄로 표현 가능한 경우 삼항 연산자가 효율적이다.

9. 연산자 우선 순위

연산식엔 다양한 연산자가 복합적으로 구성된 경우가 많다. 그러기에 프로그램에선 연산의 연산 방향연산의 우선순위를 정해놓고 있다. 우선순위가 같은 연산일 경우, 연산 방향에 따라 처리순서가 달라진다.

연산자 연산 방향 우선순위
증감(++, --), 부호(+,-), 비트(~), 논리(!) 높음
산술(*, /, %)  
산술(+, -)  
쉬프트(«, », »>)  
비교(<, >, <=, >=, instanceof)  
비교(==, !=)  
논리(&)  
논리(^)  
논리(|)  
논리(&&)  
논리(||)  
조건(?:)  
대입 (=, +=, -=, *=, /=, %=, &=, ^=,|=, «=, »=, »>=) 낮음

우선순위가 있어도 여러 연산자가 섞여있으면 순서가 헷갈리기 때문에 괄호()로 먼저 처리할 연산식을 묶는 게 좋다.

  1. 단항, 이항, 삼항 연산자 순으로 우선순위를 가진다.
  2. 산술, 비교, 논리, 대입 연산자 순으로 우선순위를 가진다.
  3. 단항과 대입 연산자를 제외한 모든 연산의 방향은 왼쪽에서 오른쪽이다(→).
  4. 복잡한 연산식에는 괄호()를 사용해서 우선순위를 정해준다.

10. Java 13 switch 연산자

기존 자바 switch 연산자에선 연산 행위가 같아도 다 적어줬어야 했는데 자바12부턴 케이스를 묶어서 작성할 수 있다. break;도 -> 화살표로 생략할 수 있다.

기존 자바 switch 코드(switch statement)

switch(grade){
  case 1:
    result = "A";
    break;
  case 2:
    result = "A";
    break;
  case 3:
    result = "B";
    break;
  default:
    result = "-";
}

자바 12 switch 코드 (switch operator)

switch(grade){
  case 1, 2 -> "A";
  case 3 -> "B";
  default -> "-";
}

java 13부터 yield 란 것도 생기고 많이 발전된 것 같은데 이 부분은 더 찾아보고 보완할 예정임.

태그:

카테고리:

업데이트:

댓글남기기