Live Study 3주차 - 연산자


산술 연산자

수학 연산을 처리하는 연산자이다.

  • + : 더하기 연산자
  • - : 빼기 연산자
  • * : 곱하기 연산자
  • / : 나누기 연산자
  • % : 나머지 연산자
1
2
3
4
5
6
7
8
9
public class StudyHalle {
public static void main(String[] args){
int num1 = 1+1; // 2
float num2 = 1.25f-0.5f; // 0.75
int num2 = 11*3; // 33
float num3 = 0.1f/0.2f // 0.5
int num4 = 10%3; // 1
}
}

이외에 증감 연산자(++, --)도 있다. 다만 연산자의 위치에 따라 증감이 이뤄지는 시기가 달라진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StudyHalle03 {
public static void main(String[] args) {
afterSensitization(1, 5);
// beforeSensitization(1, 5);
}

public static void afterSensitization(int param, int count){
for (int i = 1; i <= count; i++) {
System.out.println(i+" 증감 : "+(param++));
System.out.println(i+" 증감 후 : "+param+"\n===============");
}
}

public static void beforeSensitization(int param, int count){
for (int i = 0; i < count; i++) {
System.out.println(i+" 증감 : "+(++param));
System.out.println(i+" 증감 후 : "+param+"\n===============");
}
}
}

초기값(param)과 반복 횟수(count)를 파라미터로 받는 메서드 2개를 만들었다. 둘다 for문을 통해 반복 횟수만큼 초기값을 증감하여 콘솔에 출력하는 메서드인데, 값을 증감하는 방식이 다르다.

  • afterSensitization() : param++;
  • beforeSensitization() : ++param;

먼저 afterSensitization()를 콘솔에 출력했을때의 결과이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1 증감 : 1
1 증감 후 : 2
===============
2 증감 : 2
2 증감 후 : 3
===============
3 증감 : 3
3 증감 후 : 4
===============
4 증감 : 4
4 증감 후 : 5
===============
5 증감 : 5
5 증감 후 : 6
===============

초기값이 1인데 증감연산자를 통해 증감(param++)을 실행하자마자 출력했을 때의 값도 1이었다. 이후에 다시 값을 출력했을때 비로소 증감이 이뤄진걸 알 수 있다.

이번엔 beforeSensitization()을 실행해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0 증감 : 2
0 증감 후 : 2
===============
1 증감 : 3
1 증감 후 : 3
===============
2 증감 : 4
2 증감 후 : 4
===============
3 증감 : 5
3 증감 후 : 5
===============
4 증감 : 6
4 증감 후 : 6
===============

아까와는 다른 결과이다. 초기값 1을 받아서 증감(++param)을 실행했을때, 곧바로 적용되어 출력되었다.

변수 또는 리터럴 뒤에 증감연산자가 올 경우는 해당 라인에서 즉시 값이 바뀌는게 아니라 그 다음 실행문부터 증감이 적용되며, 변수 또는 리터럴 앞에 증감연산자가 올 경우는 해당 라인에서 즉시 값이 바뀌는 것이다.


비트 연산자

비트 연산은 개발할때 해본적이 없어서 좀 생소했다.

일단 비트는 0과 1만 표현할 수 있기 때문에 비트 연산 역시 0과 1로 이뤄진다.

& (AND : 비트 논리곱)

x & y : x,y 모두 1일 때에만 1을 반환

0101 & 0111 을 검토해보면, 각각의 자릿수를 비교해보면 두번째와 4번째 비트만 모두 1이므로 결과는 0101이 된다.

인텔리제이에서 실제로 비트 연산이 이뤄지는지 확인해보았다. IDE에서 바이너리 코드를 보고 싶다면, Integer 객체의 toBinaryString() 메서드를 이용하면 볼수 있다.

1
2
3
int a = 5;   // 0101
int b = 7; // 0111
System.out.println("a & b : "+(a&b)); // a & b : 5

| (OR : 비트 논리합)

x | y : x,y 중 하나라도 1이면 1을 반환

1010 | 0100 을 검토해보면, 둘중 하나라도 1인 비트는 첫번째, 두번째, 세번째 비트이므로 결과는 1110 이 된다.

1
2
3
int c = 10;  // 1010
int d = 4; // 0100
System.out.println("c | d : "+(c|d)); // c | d : 14

^ (XOR : 비트 베타적 논리합)

x ^ y : x, y가 서로다를때 1을 반환

1
2
3
int e = 10;  // 0000 1010
int f = 20; // 0001 0100
System.out.println("e ^ f : "+(e^f)); // e ^ f : 30

~ (NOT 연산자)

~ : 비트를 1이면 0, 0이면 1로 반환

1
2
3
int num = 10;  // 0000 1010
// ~num : 1 0101
System.out.println("~num : "+(~num)); // ~num : -11

<< (Left shift 연산자)

x << y : x의 비트를 왼쪽으로 y만큼 이동(빈자리는 0으로 채움)

0011 << 0010 을 검토해보면, 0011을 왼쪽으로 0010 만큼 이동해야 한다. 0010 은 십진수로 2이므로 0011 의 비트를 왼쪽으로 2만큼 이동하면, 1100이 된다.

1
2
3
4
int g = 3;  // 0011
int h = 2; // 0010
// 0011 << 0010 = 1100
System.out.println("g << h : "+(g<<h)); // g << h : 12

>> (Right shift 연산자)

x >> y : x의 비트를 y만큼 오른쪽으로 이동(빈자리는 a)

-8 >> 2 를 검토해보자. 음의 십진수 -8의 비트는 다음과 같다.

1
2
3
int i = -8;
System.out.println(i+" : "+Integer.toBinaryString(i));
// -8 : 1 1000

이 수를 오른쪽으로 비트를 2만큼 이동하면, 1 1110 이므로 -2가 된다.

비트로 십진수 음수를 표현하는 방법
출처 : 마유의 전자 이야기 - 음수 십진수를 음수 이진수로 표현 하는 방법

1
2
3
4
5
int i = -8;  // 1 1000
int j = 2; // 0010
// 오른쪽으로 2만큼 비트 이동하면
// 1 1110
System.out.println("i >> j : "+(i>>j)); // i >> j : -2

>>> (Unsigned right shift 연산자)

  • x >>> y : >> (Right shift 연산자)와 기본적으로 같지만, 오른쪽으로 밀려나면서 생기는 비트 왼쪽의 공백은 최상위 부호 비트가 아니라 0으로 채워진다. 따라서 양수이건 음수이건 항상 양수로 반환된다.
1
2
3
4
5
6
int i = -8;  // 1 1000
int j = 2; // 0010

// Unsigned right shift는 항상 양수를 반환하므로
// 1073741822
System.out.println("i >>> j : "+(i>>>j)); // i >>> j : 1073741822

출처


관계 연산자

  • x > y : x가 y보다 크면 true, x가 y와 같거나 작으면 false 반환
  • x < y : x가 y보다 작으면 true, x가 y와 같거나 크면 false 반환
  • x >= y : x가 y보다 크거나 같으면 true, x가 y보다 작으면 false 반환
  • x <= y : x가 y보다 작거나 같으면 true, x가 y보다 크면 false 반환
  • x == y : x와 y가 같으면 true, 다르면 false 반환
  • x != y : x와 y가 다르면 true, 같으면 false 반환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Arithmetic {
public static void main(String[] args) {
int x = 10;
int y = 20;
int z = 20;

System.out.println("x : "+x);
System.out.println("y : "+y);
System.out.println("z : "+z);
System.out.println("x>y : "+(x>y));
System.out.println("x<y : "+(x<y));
System.out.println("x>=y : "+(x>=y));
System.out.println("x<=y : "+(x<=y));
System.out.println("y==z : "+(y==z));
System.out.println("y!=z : "+(y!=z));
}
}
1
2
3
4
5
6
7
8
9
x : 10
y : 20
z : 20
x>y : false
x<y : true
x>=y : false
x<=y : true
y==z : true
y!=z : false

여기서 하나 짚고넘어가야할 점이 있다. 비교연산자(==)를 이용하여 문자열 비교가 가능하지만, new 키워드로 생성자를 이용하여 String 객체를 생성한 경우라면, 원했던 비교가 이뤄지지 않을수 있다. 글이 길어져서 아래 링크로 대체한다.


논리 연산자

  • && : 좌항 우항 모두 true일때만 true를 반환, 그렇지 않으면 false 반환.
  • || : 좌항 우항 중 하나의 항이라도 true이면 true를 반환, 둘다 false 일때만 false를 반환.
  • ! : true면 false, false면 true를 반환.
1
2
3
4
5
6
7
8
9
10
public class Arithmetic {
public static void main(String[] args) {
boolean ex01 = true;
boolean ex02 = false;

System.out.println("ex01 && ex02 : "+(ex01&&ex02));
System.out.println("ex01 || ex02 : "+(ex01||ex02));
System.out.println("!ex02 : "+(!ex02));
}
}
1
2
3
ex01 && ex02 : false
ex01 || ex02 : true
!ex02 : true

instance of

instance of는 런타임시에 객체의 타입을 검사하는 연산자이다.
아래의 예제코드를 확인해보자.

StudyHalle03 클래스의 main()을 실행하면 다음과 같은 결과를 출력한다.

1
2
3
4
5
6
7
true

true
true
true

true

첫번째 출력문은 Arithmetic 클래스의 인스턴스 arithmetic의 객체 타입이 Arithmetic인지를 instanceof 연산자를 통해 검사한 결과이다. 당연히 true를 반환한다.

두번째 출력문은 ex01 의 인스턴스 변수 num의 객체 타입이 Integer인지 검사했다. Example01.java를 보면 당연히 true인걸 알 수 있다.

네번째 출력문에서는 ex01.numInteger이지만, Integer의 상위 객체가 Object이므로 이역시도 true를 반환한다.

다섯번째 출력문에서는 인스턴스 ex02 의 타입이 Example01 타입인지 검사했다. ex02 인스턴스의 객체 타입은 Example02이지만, Example02 클래스는 Example01 클래스를 상속받으므로 인스턴스 ex02의 객체 타입은 Example01이면서 동시에 Example02 이기도하다. 이 역시 true를 반환한다.


assignment(=) operator

  • = : 좌항에 변수가 오면 우항엔 리터럴, 좌항에 인스턴스가 오면 우항엔 객체가 오는 대입 연산자.
1
2
int num = 10;
Example ex = new Example();
  • += : 좌항의 변수에 우항의 리터럴을 + 한후, 다시 좌항의 변수에 대입하는 연산자
1
2
3
int num = 10;
num += 20;
System.out.println(num); // 30
  • -= : 좌항의 변수에 우항의 리터럴을 - 한후, 다시 좌항의 변수에 대입하는 연산자
1
2
3
int num = 30;
num -= 20;
System.out.println(num); // 10
  • *= : 좌항의 변수에 우항의 리터럴을 * 한후, 다시 좌항의 변수에 대입하는 연산자
1
2
3
int num = 5;
num *= 3;
System.out.println(num); // 15
  • /= : 좌항의 변수에 우항의 리터럴을 / 한후, 다시 좌항의 변수에 대입하는 연산자
1
2
3
int num = 15;
num /= 3;
System.out.println(num); // 5
  • %=: 좌항의 변수에 우항의 리터럴을 % 연산 후, 다시 좌항의 변수에 나머지 값을 대입하는 연산자
1
2
3
int num = 20;
num %= 6;
System.out.println(num); // 2

3항 연산자

변수를 선언 또는 리터럴을 대입할때 조건 분기처리할 수 있는 연산자이다.

1
int var = (condition) ? (true) : (false);

? 을 기준으로 좌항에 조건식이 들어가며, 우항은 다시 :을 기준으로 나눈다. 조건식의 결과에 따라 true이면, : 의 좌항, false이면, : 우항으로 대입된다.

아래 예제 코드는 Date 클래스를 통해 불러온 현재 시간을 통해 String형 변수에 넣는 값을 분기처리하여 대입하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.text.SimpleDateFormat;
import java.util.Date;

public class StudyHalle03 {
public static void main(String[] args) {
Date date = new Date();
SimpleDateFormat time = new SimpleDateFormat("HH시 mm분");
SimpleDateFormat hour = new SimpleDateFormat("HH");
int getHour = Integer.parseInt(hour.format(date));

String 오전일까_오후일까 = (getHour>=12) ? "오후" : "오후";
System.out.println("현재 시각 : "+time.format(date));
System.out.println(오전일까_오후일까);
}
}

연산자 우선 순위

우선순위 연산자
1 ++, –, ~, ! (증감/부정 연산자)
2 *, /, %
3 +, -
4 <<, >>, >>> (비트 단위 시프트 연산자)
5 <, <=, >, >=
6 ==, !=
7 & (비트 단위 논리 연산자)
8 ^
9 |
10 &&
11 ||
12 ? : (삼항 연산자)
13 ==, +=, -=, *=, /=, %=, <<=, >>=, &=, ^=, ~= (대입 연산자)

출처 : 남궁성 - Java의 정석


화살표(->) 연산자

자바에서 화살표 연산자(->)는 람다식(Lambda Expression)을 의미한다. 람다는 JDK 1.8부터 도입되었는데, 자바라는 언어를 객체지향 언어인 동시에 함수형 언어로서도 동작하게 해준 개념이다.

람다는 메서드를 하나의 식으로 표현하는 표현식이다. 메서드이면서 이름도 없고, 반환값도 없다. 클래스에 종속되지도 않는다. 이상의 설명은 아래 포스팅으로 대체하고, 이 포스팅에선 사용법만 간단히 설명하겠다.
DevAndy - [Java8] 람다와 함수형 인터페이스

람다식을 알아보기 위해 예제코드를 통해 들여다보자.

람다에선 타입을 명시하지 않아도 되는데, 컴파일러가 타입 추론이 가능케 하기 위해서는 함수형 인터페이스 를 사용해야 한다. 인터페이스는 반드시 메서드가 하나만 작성되어 있어야 하며, 인터페이스에 함수형 인터페이스임을 명시하는 @FuntionalInterface도 필요하다.

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Question {
int maxNum(int a, int b);
}
class Answer {
public void calculate(Question param){
int num = param.maxNum(3, 4);
System.out.println("max num is "+num);
}
}
1
2
3
4
5
6
7
8
9
10
11
public class StudyHalle03 {
public static void main(String[] args) {
Answer answer = new Answer();
answer.calculate(new Question() {
@Override
public int maxNum(int a, int b) {
return (a>b)?a:b;
}
});
}
}

위의 코드에서 프로그램 메인 메서드를 람다식으로 바꾸면 다음과 같다.

1
2
3
4
5
6
7
8
public class StudyHalle03 {
public static void main(String[] args) {
Answer answer = new Answer();
answer.calculate((a,b)->{
return (a>b)?a:b;
});
}
}

변경사항을 확인해보자.

maxNum() 의 메서드명과 파라미터의 데이터 타입을 삭제했다. 함수형 인터페이스이기 때문에 메서드가 하나만 존재하여 컴파일러가 타입 추론이 가능하기 때문에 데이터 타입을 삭제했다.

메서드명 역시 함수형 인터페이스에선 메서드가 하나만 존재하기 때문에 굳이 이름을 명시하지 않아도 어떤 메서드를 사용하는지 컴파일러가 추론할 수 있다.

이처럼 함수형 인터페이스를 사용하여 메서드를 보다 간단하게 작성한걸 람다식이라고 한다.

출처