[자바 스터디] 3주차. 연산자
28 Nov 2020
Reading time ~12 minutes
- 1. 산술 연산자
- 2. 비트 연산자
- 3. 관계 연산자
- 4. 논리 연산자
- 5. instanceof
- 6. assignment(=) operator
- 7. 화살표(->)연산자
- 8. 3항 연산자
- 9. 연산자 우선 순위
1. 산술 연산자
- + (Addition)
사칙연산의 덧셈연산과 같은 역할을 합니다.int a = 5; int b = 2; System.out.println(a+b);
7
※ 주의사항
문자열과 함께 사용될 경우에는 문자열과 문자열을 붙여주는 역할을 합니다. 따라서 문자열과 같이 사용할 경우 괄호를 사용하여 우선 덧셈연산을 먼저 하도록 해주어야 합니다.int a = 5; int b = 2; System.out.println("a + b : " + a+b); System.out.println("a + b : " + (a+b));
a + b : 52 a + b : 7
- - (Subtraction)
사칙연산의 뺄셈연산과 같은 역할을 합니다. 문자열과 함께 사용될 경우에는 괄호를 반드시 붙여주어야 합니다.int a = 5; int b = 2; System.out.println("a - b : " + (a-b));
a - b : 3
- * (Multiplication)
사칙연산의 곱셈연산과 같은 역할을 합니다.int a = 5; int b = 2; System.out.println("a * b : " + a*b);
a * b : 10
- / (Division)
사칙연산의 나눗셈 연산에서 몫을 구하는 역할을 합니다.int a = 5; int b = 2; System.out.println("a / b : " + a/b);
a / b : 2
- % (Modulo)
사칙연산의 나눗셈 연산에서 나머지를 구하는 역할을 합니다.int a = 5; int b = 2; System.out.println("a % b : " + a%b);
a % b : 1
2. 비트 연산자
- & (AND)
AND 연산자 &는 두 수의 각 비트들을 비교해서 두 비트 모두 1이여야만 해당 비트를 1이 되도록 결과를 만들어 주는 역할을 합니다.int binaryFive = 0b0101; int binarySix = 0b0110; int fiveAndSix = 0b0100; System.out.println(binaryFive); System.out.println(binarySix); System.out.println(binaryFive&binarySix); System.out.println(fiveAndSix);
5 // binaryFive 6 // binarySix 4 // binaryFive&binarySix 4 // fiveAndSix
- | (OR)
OR 연산자 |는 두 수의 각 비트들을 비교해서 1이 하나라도 있으면 해당 비트를 1이 되도록 결과를 만들어 주는 역할을 합니다.int binaryFive = 0b0101; int binarySix = 0b0110; int fiveOrSix = 0b0111; System.out.println(binaryFive); System.out.println(binarySix); System.out.println(binaryFive|binarySix); System.out.println(fiveOrSix);
5 // binaryFive 6 // binarySix 7 // binaryFive|binarySix 7 // fiveOrSix
- ^ (XOR)
XOR 연산자 ^는 두 수의 각 비트들을 비교해서 해당 비트를 같으면 0 다르면 1이 되는 결과를 만들어 주는 역할을 합니다.int binaryFive = 0b0101; int binarySix = 0b0110; int fiveXorSix = 0b0011; System.out.println(binaryFive); System.out.println(binarySix); System.out.println(binaryFive^binarySix); System.out.println(fiveXorSix);
5 // binaryFive 6 // binarySix 3 // binaryFive^binarySix 3 // fiveXorSix
- ~ (NOT)
NOT 연산자 ~는 위의 결과로 알 수 있듯이 모든 비트의 0과 1을 뒤집어 주는 역할을 합니다.int binaryOne = 0b0000_0000_0000_0000_0000_0000_0000_0001; int invertedOne = 0b1111_1111_1111_1111_1111_1111_1111_1110; System.out.println(binaryOne); System.out.println(~binaryOne); System.out.println(invertedOne);
1 // binaryOne -2 // ~binaryOne -2 // invertedOne
- << (Left Shift)
비트들을 정해진 수 만큼 왼쪽으로 이동하는 역할을 합니다.
이때 아래 그림과 같이 왼쪽으로 이동하고 나서 오른쪽에 남은 자리에는 0이 채워지게 됩니다.
https://en.wikipedia.org/wiki/Arithmetic_shift 크기가 32비트로 이루어진 int 타입 변수에 시프트 비트 수가 32비트 이상일 경우 내부적으로 컴파일러가 그 수와 숫자 31을\((=11111_2\)) & 연산을 해서 하위 5비트만을 사용하여 시프트 됩니다.
연산의 결과값은 우항의 시프트 비트 수를 \(k\)라고 하면 부호비트의 값이 변하기 전까지는 좌항의 변수값에서 \(2^k\)를 곱한 결과가 됩니다.
int binaryThree = 0b0000_0000_0000_0000_0000_0000_0000_0011;
int leftShift1 = 0b0000_0000_0000_0000_0000_0000_0000_0110;
int leftShift2 = 0b0000_0000_0000_0000_0000_0000_0000_1100;
int leftShift30 = 0b1100_0000_0000_0000_0000_0000_0000_0000;
int leftShift31 = 0b1000_0000_0000_0000_0000_0000_0000_0000;
System.out.println("binaryThree\t : " + binaryThree);
System.out.println("binaryThree<<1\t : " + (binaryThree<<1));
System.out.println("leftShift1\t : " + leftShift1);
System.out.println("binaryThree<<2\t : " + (binaryThree<<2));
System.out.println("leftShift2\t : " + leftShift2);
System.out.println("binaryThree<<30\t : " + (binaryThree<<30));
System.out.println("leftShift30\t : " + leftShift30);
System.out.println("binaryThree<<31\t : " + (binaryThree<<31));
System.out.println("leftShift31\t : " + leftShift31);
System.out.println("binaryThree<<32\t : " + (binaryThree<<32));
System.out.println("binaryThree<<(32&31)\t : " + (binaryThree<<(32&31)));
binaryThree : 3
binaryThree<<1 : 6
leftShift1 : 6
binaryThree<<2 : 12
leftShift2 : 12
binaryThree<<30 : -1073741824
leftShift30 : -1073741824
binaryThree<<31 : -2147483648
leftShift31 : -2147483648
binaryThree<<32 : 3
binaryThree<<(32&31): 3
- >> (Signed Right Shift) : 비트들을 정해진 수 만큼 오른쪽으로 이동하는 역할을 합니다.
이때 아래 그림과 같이 오른쪽으로 이동하고 나서 왼쪽에 남은 자리에는 부호비트를 채우는 방식으로 동작합니다. Left Shift와 마찬가지로 int 타입 변수의 경우 시프트 비트 수가 32비트 이상일 경우 그 수의 하위 5비트만 사용합니다.
연산 결과는 좌항의 변수값의 부호에 따라 달라집니다.양의 정수일 경우
연산의 결과값은 우항의 시프트 비트 수를 \(k\)라고 하면 좌항의 변수값을 \(2^k\)로 나눈 몫이 됩니다.
int binaryFive = 0b0000_0000_0000_0000_0000_0000_0000_0101; int rightShift1 = 0b0000_0000_0000_0000_0000_0000_0000_0010; int rightShift3 = 0b0000_0000_0000_0000_0000_0000_0000_0000; System.out.println("binaryFive\t : " + binaryFive); System.out.println("binaryFive>>1\t : " + (binaryFive>>1)); System.out.println("rightShift1\t : " + rightShift1); System.out.println("binaryFive>>3\t : "+ (binaryFive>>3)); System.out.println("rightShift3\t : " + rightShift3);
binaryFive : 5 binaryFive>>1 : 2 rightShift1 : 2 binaryFive>>3 : 0 rightShift3 : 0
시프트 비트 수가 커질수록 결과값이 0에 수렴합니다.
음의 정수일 경우
결과값은 좌항의 변수값을 \(2^k\)로 나눈 몫과 유사합니다. 하지만 2로 더 이상 나눠질 수 없을 때 부터는 값이 조금 달라집니다. 예를 들어 -5을 1칸 시프트하면 -3이 됩니다.
또한 시프트 비트 수가 커질수록 결과값이 0이 아닌 -1로 수렴합니다.int binaryMinusFive = 0b1111_1111_1111_1111_1111_1111_1111_1011; int rightShift1_MinusFive = 0b1111_1111_1111_1111_1111_1111_1111_1101; int rightShift2_MinusFive = 0b1111_1111_1111_1111_1111_1111_1111_1110; int rightShift3_MinusFive = 0b1111_1111_1111_1111_1111_1111_1111_1111; System.out.println("binaryMinusFive\t : " + binaryMinusFive); System.out.println("binaryMinusFive>>1\t : " + (binaryMinusFive>>1)); System.out.println("rightShift1_MinusFive\t : " + rightShift1_MinusFive); System.out.println("binaryMinusFive>>2\t : " + (binaryMinusFive>>2)); System.out.println("rightShift2_MinusFive\t : " + rightShift2_MinusFive); System.out.println("binaryMinusFive>>3\t : " + (binaryMinusFive>>3)); System.out.println("rightShift3_MinusFive\t : " + rightShift3_MinusFive);
binaryMinusFive : -5 binaryMinusFive>>1 : -3 rightShift1_MinusFive : -3 binaryMinusFive>>2 : -2 rightShift2_MinusFive : -2 binaryMinusFive>>3 : -1 rightShift3_MinusFive : -1
- >>> (Unsigned Right Shift) : 비트들을 정해진 수 만큼 오른쪽으로 이동하는 역할을 합니다.
이때 아래 그림과 같이 오른쪽으로 이동하고 나서 왼쪽에 남은 자리에는 무조건 0을 채우는 방식으로 동작합니다. Left Shift와 마찬가지로 int 타입 변수의 경우 시프트 비트 수가 32비트 이상일 경우 그 수의 하위 5비트만 사용합니다.
연산 결과는 좌항의 변수값의 부호에 따라 달라집니다.양의 정수일 경우
연산의 결과값은 우항의 시프트 비트 수를 \(k\)라고 하면 좌항의 변수값을 \(2^k\)로 나눈 몫이 됩니다.
int binaryFive = 0b0000_0000_0000_0000_0000_0000_0000_0101; int rightShift1 = 0b0000_0000_0000_0000_0000_0000_0000_0010; int rightShift3 = 0b0000_0000_0000_0000_0000_0000_0000_0000; System.out.println("binaryFive\t : " + binaryFive); System.out.println("binaryFive>>1\t : " + (binaryFive>>1)); System.out.println("rightShift1\t : " + rightShift1); System.out.println("binaryFive>>3\t : "+ (binaryFive>>3)); System.out.println("rightShift3\t : " + rightShift3);
binaryFive : 5 binaryFive>>1 : 2 rightShift1 : 2 binaryFive>>3 : 0 rightShift3 : 0
시프트 비트 수가 커질수록 결과값이 0에 수렴합니다.
음의 정수일 경우
처음 1비트를 시프트 하면서 Sign Bit가 1에서 0으로 바뀌게 되어 전혀 다른 결과 값이 나오게 됩니다. 2비트 이상 시프트할 경우 1비트 시프트 한 결과에서 결과값은 좌항의 변수값을 \(2^{k-1}\)로 나눈 몫이 됩니다.
int binaryMinusFive = 0b1111_1111_1111_1111_1111_1111_1111_1011; int unsignedRightShift1_MinusFive = 0b0111_1111_1111_1111_1111_1111_1111_1101; int unsignedRightShift2_MinusFive = 0b0011_1111_1111_1111_1111_1111_1111_1110; int unsignedRightShift3_MinusFive = 0b0001_1111_1111_1111_1111_1111_1111_1111; System.out.println("binaryMinusFive\t\t\t : " + binaryMinusFive); System.out.println("binaryMinusFive>>>1\t\t : " + (binaryMinusFive>>>1)); System.out.println("unsignedRightShift1_MinusFive\t : " + unsignedRightShift1_MinusFive); System.out.println("binaryMinusFive>>>2\t\t : " + (binaryMinusFive>>>2)); System.out.println("unsignedRightShift2_MinusFive\t : " + unsignedRightShift2_MinusFive); System.out.println("binaryMinusFive>>>3\t\t : " + (binaryMinusFive>>>3)); System.out.println("unsignedRightShift3_MinusFive\t : " + unsignedRightShift3_MinusFive);
binaryMinusFive : -5 binaryMinusFive>>>1 : 2147483645 unsignedRightShift1_MinusFive : 2147483645 binaryMinusFive>>>2 : 1073741822 unsignedRightShift2_MinusFive : 1073741822 binaryMinusFive>>>3 : 536870911 unsignedRightShift3_MinusFive : 536870911
3. 관계 연산자
- ==
두 값이 같으면 true, 다르면 false를 반환한다.1) Primitive 타입
Primitive 타입 변수의 == 연산의 경우 값을 비교하여 결과를 반환해 줍니다.
int one = 1; int ONE = 1; int hundred = 100; System.out.println("one == ONE\t : " + (one == ONE)); System.out.println("one == hundred\t : " + (one == hundred));
one == ONE : true one == hundred : false
2) Reference 타입
Reference 타입 변수의 == 연산의 경우 주소값을 비교하여 결과를 반환해 줍니다.
(1) 리터럴일 때
아래 코드의 hello1과 hello2 두 변수의 == 연산 결과인 true에서 알 수 있듯이 같은 주소 값을 가지는 것을 알 수 있습니다.
String 변수에 동일한 리터럴을 대입하게 되면 String Constant Pool에 저장이 되어서 같은 주소 값을 갖게 됩니다.String hello1 = "Hello"; String hello2 = "Hello"; System.out.println("hello1 == hello2\t : " + (hello1 == hello2)); System.out.println("hello1.equals(hello2)\t : " + (hello1.equals(hello2)));
hello1 == hello2 : true hello1.equals(hello2) : true
(2) 생성자로 생성했을 때
hello3과 hello4 두 변수와 같이 new 키워드와 생성자를 사용하여 객체를 생성하게 되면 서로 다른 주소값을 가지게 되어 == 연산의 결과가 false가 출력이 됩니다.
이럴 경우 주소 값이 아닌 해시코드 값(객체의 내용물)을 비교해주는 equals 메소드를 사용하여 비교해주어야 합니다.String hello2 = "Hello"; String hello3 = new String("Hello"); String hello4 = new String("Hello"); System.out.println("hello2 == hello3\t : " + (hello2 == hello3)); System.out.println("hello2.equals(hello3)\t : " + (hello2.equals(hello3))); System.out.println("hello3 == hello4\t : " + (hello3 == hello4)); System.out.println("hello3.equals(hello4)\t : " + (hello3.equals(hello4)));
hello2 == hello3 : false hello2.equals(hello3) : true hello3 == hello4 : false hello3.equals(hello4) : true
- !=
== 연산자와는 반대로 다르면 true, 같으면 false를 반환합니다.int one = 1; int ONE = 1; int hundred = 100; System.out.println("one != ONE\t : " + (one != ONE)); System.out.println("one != hundred\t : " + (one != hundred));
one != ONE : false one != hundred : true
- >
좌항의 값이 우항의 값보다 크면 true, 작으면 false를 반환합니다.int one = 1; int ten = 10; int TEN = 10; int hundred = 100; System.out.println("ten > one\t : " + (ten > one)); System.out.println("ten > TEN\t : " + (ten > TEN)); System.out.println("ten > hundred\t : " + (ten > hundred));
ten > one : true ten > TEN : false ten > hundred : false
- <
좌항의 값이 우항의 값보다 작으면 true, 크면 false를 반환합니다.int one = 1; int ten = 10; int TEN = 10; int hundred = 100; System.out.println("ten < one\t : " + (ten < one)); System.out.println("ten < TEN\t : " + (ten < TEN)); System.out.println("ten < hundred\t : " + (ten < hundred));
ten < one : false ten < TEN : false ten < hundred : true
- >=
좌항의 값이 우항의 값보다 크거나 같으면 true, 작거나 같으면 false를 반환합니다.int one = 1; int ten = 10; int TEN = 10; int hundred = 100; System.out.println("ten >= one\t : " + (ten >= one)); System.out.println("ten >= TEN\t : " + (ten >= TEN)); System.out.println("ten >= hundred\t : " + (ten >= hundred));
ten >= one : true ten >= TEN : true ten >= hundred : false
- <=
좌항의 값이 우항의 값보다 작거나 같으면 true, 크거나 같으면 false를 반환합니다.int one = 1; int ten = 10; int TEN = 10; int hundred = 100; System.out.println("ten >= one\t : " + (ten >= one)); System.out.println("ten >= TEN\t : " + (ten >= TEN)); System.out.println("ten >= hundred\t : " + (ten >= hundred));
ten >= one : true ten >= TEN : true ten >= hundred : false
4. 논리 연산자
- &&
피연산자 양쪽 모두가 true이면 true를 그게 아니라면 false를 반환합니다.boolean True = true; boolean False = false; System.out.println("True && True\t : " + (True && True)); System.out.println("True && False\t : " + (True && False)); System.out.println("False && True\t : " + (False && True)); System.out.println("False && False\t : " + (False && False));
True && True : true True && False : false False && True : false False && False : false
- ||
피연산자 중 어느 한 쪽이라도 true이면 true를 양쪽 모두가 false 이면 false를 반환합니다.boolean True = true; boolean False = false; System.out.println("True || True\t : " + (True || True)); System.out.println("True || False\t : " + (True || False)); System.out.println("False || True\t : " + (False || True)); System.out.println("False || False\t : " + (False || False));
True || True : true True || False : true False || True : true False || False : false
5. instanceof
객체 타입을 확인하는 연산자로 형변환 가능여부를 확인하는데 주로 사용합니다. 상속 관계에서 부모타입은 자식 타입으로 형변환이 가능합니다.
1번이라고 주석 처리된 문장은 최상위 클래스인 Object 타입은 Parent으로 형변환이 가능하다는 뜻으로 해석할 수 있습니다. 2번이라고 주석 처리된 문장은 자식 클래스인 Child 타입은 Parent으로 형변환이 불가능하다는 뜻으로 해석할 수 있습니다.
public static void main(String[] args) {
Parent parent = new Parent();
System.out.println("parent instanceof Object : " + (parent instanceof Object)); // 1번
System.out.println("parent instanceof Child : " + (parent instanceof Child)); // 2번
}
static class Parent {
}
static class Child extends Parent {
}
parent instanceof Object : true
parent instanceof Child : false
6. assignment(=) operator
1) Primitive 타입
아래와 같이 y라는 int 타입의 변수에 10이라는 값을 할당할 때 주로 사용하는 연산자입니다. 복사한 값이 변경되어도 깊은 복사가 이루어지기 때문에 원래의 값에는 변화가 없습니다.
int original = 10;
int copy = original;
System.out.println("original : " + original);
System.out.println("copy\t : " + copy);
copy = 20;
System.out.println("original : " + original);
System.out.println("copy\t : " + copy);
original : 10
copy : 10
original : 10
copy : 20
아래 주석 처리된 코드를 줄여서 쓰기 위해 연산자 바로 뒤에 연이어 사용되기도 합니다.
int sum = 0;
for(int i = 1; i < 10; i++){
// sum = sum + i;
sum += i;
}
2) Reference 타입
복사한 값이 변경되면 원래의 값에도 변화가 있는 얕은 복사가 이루어집니다.
public class ReferenceCopyTest {
public static void main(String[] args) {
Pare original = new Pare(0,0);
Pare copy = original;
System.out.println("original.x, original.y : "+ original.x + "," + original.y);
System.out.println("copy.x, copy.y : " + copy.x + "," + copy.y);
copy.x=10;
copy.y=20;
System.out.println("original.x, original.y : "+ original.x + "," + original.y);
System.out.println("copy.x, copy.y : " + copy.x + "," + copy.y);
}
static class Pare {
int x, y;
Pare(int x,int y){
this.x = x;
this.y = y;
}
}
}
original.x, original.y : 0,0
copy.x, copy.y : 0,0
original.x, original.y : 10,20
copy.x, copy.y : 10,20
7. 화살표(->)연산자
화살표 연산자는 인터페이스의 추상메소드 구현을 간단하게 할 수 있게 도와주는 역할을 합니다. 인터페이스를 클래스를 만들지 않고 바로 사용하기 위한 방법으로 익명 객체로 추상메소드를 구현할 수 있습니다. 이를 위해 아래와 같은 주석 처리된 코드가 필요한데 이를 간편하게 줄여줍니다. 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해지는 장점이 있습니다.
// AddInterface.java
public interface AddInterface{
public int add(int a,int b);
}
// AddInterface.java
public interface AddInterface{
public int add(int a,int b);
}
public class ArrowFunctionTest{
public static void main(String[] args) {
// AddInterface addInterface = new AddInterface() {
// @Override
// public int add(int a, int b) {
// return a + b;
// }
// };
AddInterface addInterface = (a, b) -> {return a + b;};
System.out.println(addInterface.add(1, 2));
}
}
8. 3항 연산자
아래와 주석 처리된 if else문과 같은 역할을 하는 프로그램을 3항 연산자를 사용해서 더 짧고 직관적인 코드를 짤 수 있습니다.
int a = 2;
int b = 1;
int max;
// if(a > b) max = a;
// else max = b;
max = a > b ? a : b;
System.out.println("max : " + max);
max : 2
9. 연산자 우선 순위
자바의 연산자 우선순위는 아래 그림과 같습니다. https://noritersand.github.io/java/java-%EC%97%B0%EC%82%B0%EC%9E%90-operator/