[ Java 기초 ] 연산자

황승환·2021년 12월 27일
0

Java 기초

목록 보기
3/6
post-thumbnail

Goal

자바가 제공하는 다양한 연산자 학습하기

Study

위의 그림에서 보이는 다양한 연산자들에 대해 알아보자.

산술 연산자

산술 연산자는 수학적인 계산에 사용되는 연산이다.

public class Arithmetic {
	public static void main (String[] args) {
		int result=0;
        // 덧셈 +
        result = 1 + 2;
        System.out.println(result); // 3 출력
        // 뺄셈 -
        result = 4 - 2;
        System.out.println(result); // 2 출력
        // 곱셈 *
        result = 2 * 3;
        System.out.println(result); // 6 출력
        // 나눗셈(몫) /
        result = 5 / 2;
        System.out.println(result); // 2 출력
        // 나눗셈(나머지) %
        result = 5 % 2;
        System.out.println(result); // 1 출력
    }
}

자바에는 위의 코드와 같이 +, -, *, /, % 이렇게 5가지의 산술 연산자가 존재한다.

+의 경우에는 숫자와 숫자를 더할 때 사용되지만, 문자열과 문자열의 결합에도 사용된다.

class Concat {
	public static void main (String[] args) {
    	String first = "My name";
        String second = "is";
        String third = "Seunghwan.";
        String result = first + second + third;
        System.out.println(result); // My name is Seunghwan 출력
    }
}

비트 연산자

비트 연산자는 데이터를 비트 단위로 연산한다. 비트 단위로 연산하기 때문에 0과 1로 표현이 가능한 정수 타입이나 정수형으로 캐스팅이 가능한 자료형에만 비트 연산이 가능하다. 비트 연산자는 비트 이동 연산자, 비트 논리 연산자로 나뉜다.

비트 이동 연산자

비트 이동 연산자는 <<, >>, >>> 이렇게 3가지가 존재한다.

  • x<<y
    정수 x의 각 비트를 y만큼 왼쪽으로 이동시킨다. (빈자리는 0으로 채워진다.)
  • x>>y
    정수 x의 각 비트를 y만큼 오른쪽으로 이동시킨다. (빈자리는 정수 x의 최상위 부호 비트와 같은 값으로 채워진다.)
  • x>>>y
    정수 x의 각 비트를 y만큼 오른쪽으로 이동시킨다. (빈자리는 0으로 채워진다.)
class Shift {
	public static void main (String[] args) {
  		int result=0;
  		// << 연산 왼쪽으로 이동
  		result = 2 << 3;
  		System.out.println(result); // 16 출력
  		// >> 연산 오른쪽으로 이동. 최상위 부호 비트와 동일한 값으로 채워짐
  		result = 16 >> 3;
  		System.out.println(result); // 2 출력
  		// >> 연산 오른쪽으로 이동. 최상위 부호 비트와 동일한 값으로 채워짐
  		result = -16 >> 3;
  		System.out.println(result); // -2 출력
  		// >>> 연산 오른쪽으로 이동. 0으로 채워짐
  		result = -16 >>> 3;
  		System.out.println(result); // 536870910 출력 (0으로 채워지기 때문에 무조건 양수로 출력됨)
  	}
}

위의 코드의 비트의 이동을 눈으로 보기 쉽게 나타내보았다.

  • 2 << 3
    00000000 00000000 00000000 00000010 = 2
000 00000000 00000000 00000000 00010??? = 16

2 << 3은 2를 32비트로 분해한 뒤 왼쪽으로 3비트만큼 이동하는 연산이다. 비트를 왼쪽으로 3비트 이동할 때 가장 왼쪽의 3비트는 32비트 밖으로 밀려서 버려지게 되고 가장 오른쪽 3비트는 0으로 채워지게 된다. 이를 계산하면 16이라는 결과가 도출된다.

  • 16 >> 3
00000000 00000000 00000000 00010000 = 16
00000000 00000000 00000000 00000010 (000) = 2

16 >> 3은 16을 32비트로 분해한 뒤 오른쪽으로 3비트만큼 이동하는 연산이다. 비트를 오른쪽으로 3비트 이동할 때 가장 오른쪽의 3비트는 밀려서 버려지고 가장 왼쪽 3비트는 최상위 부호 비트와 동일한 값으로 채워지게 된다. 그러므로 2라는 결과가 도출된다.

  • -16 >> 3
11111111 11111111 11111111 11110000 = -16
11111111 11111111 11111111 11111110 (000) = -2

-16 >> 3은 -16을 32비트로 분해한 뒤 오른쪽으로 3비트만큼 이동하는 연산이다. 비트를 오른쪽으로 3비트 이동할 때 가장 오른쪽의 3비트는 밀려서 버려지고 가장 왼쪽 3비트는 최상위 부호 비트와 동일한 값으로 채워지게 된다. 그러므로 -2라는 결과가 도출된다.

  • -16 >>> 3
11111111 11111111 11111111 11110000 = -16
00011111 11111111 11111111 11111110 (000) = 536870910

>>> 연산은 자바에만 있는 연산으로 >> 연산과 기본 원리는 같다. -16 >>> 3은 -16을 32비트로 분해한 뒤 오른쪽으로 3비트만큼 이동하는 연산이다. 비트를 오른쪽으로 3비트 이동할 때 가장 오른쪽의 3비트는 밀려서 버려지고 가장 왼쪽 3비트는 0으로 채워진다. 빈값이 모두 0으로 채워지기 때문에 무조건 양수로 출력된다. 결과는 536870910이다.

비트 논리 연산자

비트 논리 연산자는 &, |, ^, ~ 이렇게 4가지가 존재한다.

  • &
    AND 논리 연산을 수행한다. 두 비트 모두 1일 경우에만 연산 결과가 1이다.
  • |
    OR 논리 연산을 수행한다. 두 비트 중 하나라도 1일 경우 연산 결과가 1이다.
  • ^
    XOR 논리 연산을 수행한다. 두 비트 중 하나는 1이고 다른 하나가 0일 경우에만 연산 결과가 1이다.
  • ~
    NOT 논리 연산을 수행한다. 비트를 반전시킨다 (보수)
class Logical {
	public static void main (String[] args) {
  		int result = 0;
  		// & 연산
  		result = 15 & 25;
  		System.out.println(result); // 9 출력
  		// | 연산
  		result = 15 | 25;
  		System.out.println(result); // 31 출력
  		// ^ 연산
  		result = 15 ^ 25;
  		System.out.println(result); // 22 출력
  		// ~ 연산
  		result = ~25;
  		System.out.println(result); // -26 출력
  	}
}

위의 코드를 눈으로 보기 쉽게 나타내보았다.

  • 15 & 25
00001111 (15)
    &
00011001 (25)
    =
00001001 (9)

각 자리에 해당하는 두 비트의 수가 1일 경우에만 1을 도출하기 때문에 9라는 결과가 나왔다.

  • 15 | 25
00001111 (15)
    |
00011001 (25)
    =
00011111 (31)

각 자리에 해당하는 두 비트 중 하나라도 1이 있다면 1을 도출하기 때문에 31이라는 결과가 나왔다.

  • 15 ^ 25
00001111 (15)
    ^
00011001 (25)
    =
00010110 (22)

각 자리에 해당하는 두 비트 중 하나만 1일 경우 1을 도출하기 때문에 22라는 결과가 나왔다.

  • ~25
00011001 (25)
    ~
11100110 (-26)

이진수로 표현된 값을 반전시켜주어 표현하므로 -26이라는 결과가 나왔다.

관계 연산자

비교 연산자라고도 하며 수학 시간에 배웠던 부등호를 생각하면 된다. 관계 연산자의 결과는 true 또는 false값인 boolean 자료형으로 반환된다.

관계 연산자는 >, <, >=, <=, ==, != 이렇게 6가지가 존재한다.

  • >
    왼쪽 항이 크면 true, 아니면 false를 반환한다.
  • <
    왼쪽 항이 작으면 true, 아니면 false를 반환한다.
  • >=
    왼쪽 항이 오른쪽 항보다 크거나 같으면 true, 아니면 false를 반환한다.
  • <=
    왼쪽 항이 오른쪽 항보다 작거나 같으면 true, 아니면 false를 반환한다.
  • ==
    두 항의 값이 같으면 true, 아니면 false를 반환한다.
  • !=
    두 항이 다르면 true, 아니면 false를 반환한다.

논리 연산자

논리 연산자는 &&, ||, ! 이렇게 3가지 존재한다. 관계 연산자와 같이 사용되는 경우가 많다. 논리 연산자도 관계 연산자와 마찬가지로 true 또는 false를 반환한다.

  • && (논리 곱)
    두 항이 모두 true인 경우에만 true를 반환하고 아닐 경우에는 false를 반환한다.
  • || (논리 합)
    두 항 중 하나의 항이라도 true인 경우 true를 반환하고 아닐 경우에는 false를 반환한다.
  • ! (부정)
    단항 연산자로 true인 경우 false를, false인 경우 true를 반환한다.

단락 회로 평가 (Short Circuit Evaluation)

단락 회로 평가는 논리 연산자를 사용할 때 고려해야 할 사항으로 두 항 중 앞의 항에서 결과 값이 정해지는 경우 뒤의 항이 대한 평가를 생략하는 것을 말한다.

단락 회로 평가에 의해서 뒤의 항이 평가되지 않아 의도치 않은 결과가 도출될 수 있으므로 유의해야한다.

instanceOf

instanceOf 연산자는 객체가 어떤 클래스인지, 어떤 클래스를 상속 받았는지 확인하기 위해 사용되는 연산자이다.

object instanceOf type

이러한 형식으로 사용된다. object가 type이거나 type을 상속 받은 클래스이면 true를 반환하고 그렇지 않으면 false를 반환한다.

public class ArrayList<E> implements List {
}
public List {
}

위와 같은 구조로 ArrayList와 List 클래스가 존재할 경우 다음과 같이 instanceOf를 사용할 수 있다.

ArrayList list = new ArrayList();
System.out.println(list instanceOf ArrayList); // true 출력
System.out.println(list instanceOf List); // true 출력
System.out.println(list instanceOf Set); // false 출력

Object에 대한 instanceOf

모든 클래스는 Object를 상속하기 때문에 object instanceOf Object는 항상 true를 반환한다.

ArrayList list = new ArrayList();
System.out.println(list instanceOf Object); // true 출력

null 객체에 대한 instanceOf

object가 null이라면 instanceOf는 항상 false를 반환한다.

ArrayList list = null;
System.out.println(list instanceOf Object); // false 출력
System.out.println(list instanceOf ArrayList); // false 출력
System.out.println(list instanceOf List); // false 출력
System.out.println(list instanceOf Set); // false 출력

대입 연산자 (Assignment Operator)

대입 연산자는 변수에 값을 대입할 때 사용하는 이항 연산자로 피연산자들의 결합 방향은 오른쪽에서 왼쪽이다. 자바에서는 대입 연산자와 다른 연산자를 결합하여 만든 다양한 복합 대입 연산자를 제공한다.

  • =
    왼쪽의 피연산자에 오른쪽의 피연산자를 대입한다.
  • +=
    왼쪽의 피연산자에 오른쪽의 피연산자를 더한 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
  • -=
    왼쪽의 피연산자에서 오른쪽의 피연산자를 뺀 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
  • *=
    왼쪽의 피연산자에 오른쪽의 피연산자를 곱한 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
  • /=
    왼쪽의 피연산자를 오른쪽의 피연산자로 나눈 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
  • %=
    왼쪽의 피연산자를 오른쪽의 피연산자로 나눈 후, 그 나머지를 왼쪽의 피연산자에 대입한다.
  • &=
    왼쪽의 피연산자를 오른쪽의 피연산자와 비트 AND 연산한 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
  • |=
    왼쪽의 피연산자를 오른쪽의 피연산자와 비트 OR 연산한 후, 그 결괏값을 왼쪽 피연산자에 대입한다.
  • ^=
    왼쪽의 피연산자를 오른쪽의 피연산자와 비트 XOR 연산한 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
  • <<=
    왼쪽의 피연산자를 오른쪽의 피연산자만큼 왼쪽 시프트한 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
  • >>=
    왼쪽의 피연산자를 오른쪽의 피연산자만큼 부호를 유지하며 오른쪽 시프트한 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
  • >>>=
    왼쪽의 피연산자를 오른쪽의 피연산자만큼 부호에 상관없이 오른쪽 시프트한 후, 그 결괏값을 왼쪽의 피연산자에 대입한다.
public class Assignment {
    public static void main (String[] args) {
    	int a = 3;
    	int b = 3;
    	int c = 3;
    	a=a-1;
    	b-=1;
    	c=-1;
    	System.out.println(a); // 2 출력
    	System.out.println(b); // 2 출력
    	System.out.println(c); // -1 출력
    }
}

화살표(->) 연산자

화살표 연산자는 Java 8 버전부터 추가된 것으로 람다 표현식과 함께 사용된다.

람다 표현식(lambda expression)

람다 표현식은 간단히 말해 메소드를 하나의 식으로 표현한 것이다.

int min(int x, int y) {
    return x < y ? x : y;
}

위의 간단한 메소드는 다음과 같은 람다 표현식으로 표현 가능하다.

(x, y) -> x < y ? x : y;

위와 같이 람다 표현식으로 표현하면 클래스를 작성하고 객체를 생성하지 않아도 메소드를 사용할 수 있다. 즉 람다 표현식은 자바에서 익명 클래스와 같다고 할 수 있다.

new Object() {
	int min(int x, int y) {
		return x < y ? x : y;
    }
}

람다 표현식은 메소드의 매개변수로 전달될 수 있고 메소드의 결괏값으로 반환될 수도 있다. 따라서 람다 표현식을 사용하면 기존의 불필요한 코드를 줄여주고 작성된 코드의 가독성을 높여준다.

람다 표현식 작성

자바에서는 화살표(->) 기호를 사용하여 람다 표현식을 작성할 수 있다.
(매개변수 목록) -> { 함수 로직 }

자바에서 람다 표현식을 작성할 때 유의해야 할 사항들이 있다.

  • 매개변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있다.
  • 매개변수가 하나인 경우에는 괄호()를 생략할 수 있다.
  • 함수의 몸체가 하나의 명령문만으로 이루어진 경우에는 중괄호{}를 생략할 수 있다. 이때 세미콜론;은 붙지 않는다.
  • 함수의 몸체가 하나의 return문으로만 이루어진 경우에는 중괄호{}를 생략할 수 있다.
  • return문 대신 표현식을 사용할 수 있으며 이때 반환값은 표현식의 결괏값이 된다. 이때 세미콜론;은 붙지 않는다.

전통적인 스레드 생성과 람다 표현식을 사용한 스레드 생성을 비교해보았다.

new Thread(new Runnable() {
    public void run() {
    	System.out.println("Traditional");
    }
}).start();

new Thread())->{
    System.out.println("Lambda");
}).start();

위와 같이 람다 표현식을 사용하면 불필요한 코드를 줄일 수 있으며, 코드의 가독성이 좋아진다.

함수형 인터페이스 (functional interface)

람다 표현식을 사용할 때는 람다 표현식을 저장하기 위한 참조 변수의 타입을 결정해야한다.
참조변수타입 참조변수이름 = 람다표현식
위의 형식으로 람다 표현식을 하나의 변수에 대입할 때 사용하는 참조 변수의 타입을 함수형 인터페이스라고 한다.

함수형 인터페이스는 추상 클래스와 달리 단 하나의 추상 메소드만을 가져야 한다. 또한 다음과 같은 어노테이션을 사용하여 함수형 인터페이스임을 명시할 수 있다.

@FunctionalInterface

위와 같은 어노테이션을 인터페이스의 선언 앞에 붙이면, 컴파일러는 해당 인터페이스를 함수형 인터페이스로 인식한다. 자바 컴파일러는 이렇게 명시된 함수형 인터페이스에 두 개 이상의 메소드가 선언되면 오류를 발생시킨다.

@FunctionalInterface
interface Calc { // 함수형 인터페이스 선언
    public int min(int x, int y);
}
public class Lambda {
    public static void main (String[] args) {
    	Calc minNum = (x, y) -> x < y ? x : y; // 추상 메소드 구현
	System.out.println(minNum.min(3,4)); // 함수형 인터페이스 사용. 3 출력
    }
}

3항 연산자

3항 연산자는 말 그대로 3개의 연산자를 필요로 하는 연산자를 의미한다.
조건식(피연산자1) ? 값or연산식(피연산자2) : 값or연산식(피연산자3);
피연산자1이 true일 경우 피연산자2로 반환되고, false일 경우 피연산자3으로 반환된다. 이때 반환값에는 값 뿐만 아니라 수식, 함수 호출 등 여러가지 형태의 명령문이 올 수 있다.

int a = 0;
if(1 > 2){ // if 문
    a = 3;
}
else {
    a = 1;
}
System.out.println(a); // 1 출력
    
int a = (1 > 2) ? 3 : 1; // 3항 연산자 
System.out.println(a); // 1 출력

위의 코드를 보면 if문을 사용할 때와 3항 연산자를 사용했을 때의 차이를 분명하게 확인할 수 있다. 같은 기능을 수행하지만 if문을 사용했을 때 3항 연산자를 사용했을 때 보다 코드의 길이가 훨씬 길어지는 것을 볼 수 있다. 이는 동일한 로직을 표현하지만 코드의 가독성을 높일 수 있는 3항 연산자의 장점이다.

하지만 코드가 짧다고 해서 if문에 비해 속도가 빠르지 않고 축약된 형식이기 때문에 잘못 사용할 경우 코드의 가독성을 해치기도 한다. 특히 한 줄에 조건식과 결과값들이 모두 모여있기 때문에 줄 단위 디버깅시 상당히 불편하다. 따라서 가독성을 해치지 않으면서 코드가 간결해지는 경우에만 3항 연산자를 사용해야 한다. 대부분의 경우에는 if문을 사용하여 여러줄로 작성하는 것이 좋다.

연산자 우선순위

기본적으로 연산자에는 우선순위가 존재하며 괄호의 우선순위가 가장 높다. 그 뒤를 이어 산술 > 비교 > 논리 > 대입의 순서이고, 단항 > 이항 > 삼항의 순서이다. 연산자의 연산 진행방향은 왼쪽에서 오른쪽으로 수행되고 단항 연산자와 대입 연산자의 경우에는 오른쪽에서 왼쪽으로 수행된다.

  1. (), [] (괄호 / 대괄호)
  2. !, ~, ++, -- (부정 / 증감 연산자)
  3. *, /, % (곱셈 / 나눗셈 연산자)
  4. +, - (덧셈 / 뺄셈 연산자)
  5. <<, >>, >>> (비트 단위의 시프트 연산자)
  6. <, <=, >, >= (관계 연산자)
  7. ==, !=
  8. & (비트단위의 논리 연산자)
  9. ^
  10. |
  11. && (논리곱 연산자)
  12. || (논리합 연산자)
  13. ?: (조건 연산자)
  14. =, +=, -=, *=, /=, %=, <<=, >>=, >>>=, &=, ^=, ~= (대입 / 할당 연산자)

Java 13. switch 연산자

Java 13 이전의 switch문은 다음과 같이 동작했다.

public class Before13 {
    public static void main (String[] args) {
    	String day = "Fri"
    	switch (day) {
    		case "Mon":
    		case "Fri":
    		case "Sun":
    			System.out.println(6);
    			break;
    		case "NOT":
    			System.out.println("Not");
    			break;
    		default:
    			System.out.println("Default");
    			break;
    	}
    }
}

Java는 이전 자바의 switch문의 문제점은 다음과 같이 제시했다.

  • 불필요하게 장황하다.
  • Error 발생시 디버깅이 어렵다.
  • Missing Break

Java 12 버전에서는 위의 문제점을 다음과 같이 lambda 연산을 사용하여 해결하려했다.

public class After12 {
    public static void main (String[] args) {
    	String day = "Fri"
    	switch (day) {
    		case "Mon", "Tue", "Sun" -> System.out.println(6);
    		case "Not" -> System.out.println("Not");
    		default -> System.out.println("Default");
    	}
    }
}

위와 같은 방식은 우선 ->와 :를 혼용해서 사용할 수 없다. 그리고 multi case Label을 허용하게 되어서 case문의 작성이 확연하게 줄었다.

Java 13 버전부터는 yield라는 산출값을 반환가능하다. 예전에는 break를 사용했지만 yield를 사용하여 변수에 값을 넣을 수 있게 되었다.

public class After13 {
    public static void main (String[] args) {
    	String day = "Fri"
    	int value = switch (day) {
    		case "Mon":
    			System.out.println("Monday");
    			yield 1;
    		case "Wed":
    			System.out.println("Wednesday");
    			yield 2;
    		default:
    			System.out.println("Default");
    			yield -1;
    	};
    	System.out.println(value);
    }
}
profile
꾸준함을 꿈꾸는 SW 전공 학부생의 개발 일기

0개의 댓글