2020. 11. 27.

3주차 과제: 연산자

https://github.com/whiteship/live-study/issues/3

목표

자바가 제공하는 다양한 연산자를 학습하세요.

학습할 것

  • 산술 연산자
  • 비트 연산자
  • 관계 연산자
  • 논리 연산자
  • instanceof
  • assignment(=) operator
  • 화살표(->) 연산자
  • 3항 연산자
  • 연산자 운선 순위
  • (optional) Java 13. switch 연산자


1. 산술 연산자

public static void main(String[] args) {
    int a = 1;
    int b = 2;
    int c = a + b;
}
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return

출처: https://dzone.com/articles/introduction-to-java-bytecode

iconst_1: 정수 상수(integer constant) 1을 피연산자 스택(operand stack)으로 푸시

istore_1: 최상위 피연산자(int 값)를 pop하고 인덱스 1 지역 변수 a에 저장

iconst_2: 정수 상수 2를 피연산자 스택으로 푸시

istore_2: 최상위 피연산자 int 값을 pop하고 인덱스 2 지역 변수 b에 저장

iload_1: 인덱스 1의 지역 변수에서 int 값을 로드하고 피연산자 스택으로 푸시

iload_2: 인덱스 1의 지역 변수에서 int 값을 로드하고 피연산자 스택으로 푸시

iadd: 피연산자 스택에서 상위 두 개의 int 값을 가져와 추가한 다음 결과를 다시 피연산자 스택으로 푸시
istore_3: 최상위 피연산자 int값을 pop하고 인덱스 3 로컬 변수 c에 저장

return: void 메서드에서 반환


public class Main {
  public static void main(String[] args) {
    int a = 1;
    int b = 2;
    int c = a - b;
  }
}

javap -c Main.class 실행 결과

0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: isub		// int 뺄셈
7: istore_3
8: return


곱셈을 해보자

public static void main(String[] args) {
  int a = 1;
  int b = 2;
  int c = a * b;
}
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: imul		// 두 정수 곱셈
7: istore_3
8: return


나눗셈

public static void main(String[] args) {
  int a = 3;
  int b = 4;
  int c = a / b;
}
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: idiv		// 두 정수 나눗셈
7: istore_3
8: return


나머지 연산자도 써보자

public static void main(String[] args) {
  int a = 3;
  int b = 4;
  int c = a % b;
}
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: irem		// 논리적 정수 나머지(logical int remainder)
7: istore_3
8: return


2. 비트 연산자

public static void main(String[] args) {
  int a = 2;
  int c = a << 3; // 출력 16
  c = c >> 4;     // 출력 1
  c = c >>> 1;    // 출력 0
}
 0: iconst_2
 1: istore_1
 2: iload_1
 3: iconst_3
 4: ishl      // 정수를 왼쪽으로 비트 이동(빈자리는 0으로)
 5: istore_2
 6: iload_2
 7: iconst_4
 8: ishr      // 정수를 오른쪽으로 비트 이동(빈자리는 정수 최상위 부호비트와 같은 값으로)
 9: istore_2
10: iload_2
11: iconst_1
12: iushr     // 정수를 오른쪽으로 비트 이동(빈자리는 0으로)
13: istore_2
14: return

예시의 2개 변수가 너무 많은 거 같아 줄여보니 컴퓨터님이 계산하실 게 1줄이라도 더 줄어들어 흐뭇하다. 갑은 컴퓨터님 을은 나.

public static void main(String[] args) {
  int a = 2;
  a = a | 4;  // 110 출력 6
  a = a & 5;  // 5(101) 출력 4(100)
  a = a ^ 3;  // 3(011) 출력 7(111)
  a = ~a;     // 출력 -8
}
 0: iconst_2
 1: istore_1
 2: iload_1
 3: iconst_4
 4: ior         // or 연산
 5: istore_1
 6: iload_1
 7: iconst_5
 8: iand        // and 연산
 9: istore_1
10: iload_1
11: iconst_3
12: ixor        // xor 연산
13: istore_1
14: iload_1
15: iconst_m1   // 정수를 스택에 -1 정수값으로 로드
16: ixor        // xor 연산
17: istore_1
18: return


3. 관계 연산자

public static void main(String[] args) {
  int a = 2;
  int b = 3;
  boolean chk = a > b;
}

컴파일 후 javap -c Main.class로 확인

 0: iconst_2
 1: istore_1
 2: iconst_3
 3: istore_2
 4: iload_1
 5: iload_2
 6: if_icmple    13
 9: iconst_1
10: goto         14
13: iconst_0
14: istore_3
15: return

6번의 조건이 실패하면 다음 9번의 라인으로, 성공하면 13번의 라인으로 이동
goto는 14번으로 이동

7번 8번은 어디갔지?
찾아보니 'if_icmple', 'goto' 두 명령어가 다음 2개의 브랜치바이트(branchbyte)를 사용해 생략하고 보여준 거 같다.

기호는 '>' 이지만 코드는 'value1 ≤ value2'를 확인하는 'if_icmple"가 입력되었다.

나머지 관계 연산자( <, >=, <=, ==, != )도 순서대로 if_icmpge (value1 ≥ value2), if_icmplt (value1 < value2), if_icmpgt (value1 > value2), if_icmpne (value1 ≠ value2), if_icmpeq (value1 = value2) 바이트코드가 생성되었다.


4. 논리 연산자

public static void main(String[] args) {
  int a = 3;
  int b = 5;
  boolean chk = (b > a) && (a > b);
}

컴파일 후 javap -c -v Main.class
이번에는 -v(verbose) 명령어도 추가해 보았다.

stack=2, locals=4, args_size=1
 0: iconst_3
 1: istore_1
 2: iconst_5
 3: istore_2
 4: iload_2
 5: iload_1
 6: if_icmple     18
 9: iload_1
10: iload_2
11: if_icmple     18
14: iconst_1
15: goto          19
18: iconst_0
19: istore_3
20: return

위 코드에서 &&(and)를 ||(or)로 변경

int a = 3;
int b = 5;
boolean chk = (b > a) || (a > b);
stack=2, locals=4, args_size=1
 0: iconst_3
 1: istore_1
 2: iconst_5
 3: istore_2
 4: iload_2
 5: iload_1
 6: if_icmpgt     14
 9: iload_1
10: iload_2
11: if_icmple     18
14: iconst_1
15: goto          19
18: iconst_0
19: istore_3
20: return

6번 라인의 'if_icmple'(≤)가 'if_icmpgt'(>)로 변하고 성공 시 옮겨갈 라인 번호가 바뀌었다.

OR 연산은 두 개 중 하나의 조건만 맞아도 성공이기에 빠르게 14번 라인으로 가고자 하고 AND 연산은 반대 조건을 확인 후 실패(사실은 진실) 시 뒤에 오는 조건을 또 확인


int a = 5;
int b = 6;
boolean chk = !(a > b);
stack=2, locals=4, args_size=1
 0: iconst_5
 1: istore_1
 2: bipush        6
 4: istore_2
 5: iload_1
 6: iload_2
 7: if_icmpgt     14
10: iconst_1
11: goto          15
14: iconst_0
15: istore_3
16: return

!(부정) 연산자는 청개구리 마냥 if_icmpgt (value1 > value2)


5. instanceof

public static void main(String[] args) {
  String s = "hello";
  boolean chk = s instanceof java.lang.String;
}
stack=1, locals=3, args_size=1
0: ldc           #2                  // String hello
2: astore_1
3: aload_1
4: instanceof    #3                  // class java/lang/String
7: istore_2
8: return

ldc - 상수 풀(constant pool)에서 #index 상수를 스택으로 push
astore_1 - 지역 변수 1에 레퍼런스 저장
aload_1 - 스택위의 지역 변수 1번의 레퍼런스 로드

instanceof - 상수 풀의 참조(reference) 인덱스를 확인해 같은 타입인지 확인.
unsigned indexbyte1, indexbyte2를 사용 (indexbyte1 << 8 | indexbyte2)

자바 바이트코드 위키를 참고하면 ldc 항목에 constant pool (String, int, float, Class, java.lang.invoke.MethodType, java.lang.invoke.MethodHandle, or a dynamically-computed constant)에 대한 설명이 적혀있다. 이를 참조하고 비교해 같은 타입인지를 확인하는 거 같다.

풀이라고 하니까 수영장에서 헤엄치고 있는 오브젝트들이 상상된다.


6. assignment(=) operator

public static void main(String[] args) {
  int a = 2;
  a += a;    // 4 출력
}
stack=2, locals=2, args_size=1
0: iconst_2
1: istore_1
2: iload_1
3: iload_1
4: iadd
5: istore_1
6: return

4번라인에서 2번 3번에서 로드한 a 변수를 더하고 저장

int a = 2;
a =+ a; // 2 출력
stack=1, locals=2, args_size=1
0: iconst_2
1: istore_1
2: iload_1
3: istore_1
4: return

=+ 연산자로 바꾼 후 확인해 보니 a에 a값을 할당만 한다.
- 기호로 바꾸면 어떻게 되나 궁금하다.

public static void main(String[] args) {
  int a = 2;
  a =- a;    // -2 출력
}
stack=1, locals=2, args_size=1
0: iconst_2
1: istore_1
2: iload_1
3: ineg		// negate int
4: istore_1
5: return

이번에는 -2 값으로 출력된다. (코드에서 출력은 바이트 코드가 길어져서 출력을 확인만 하고 생략 후 컴파일했다.)

public static void main(String[] args) {
  int a = 8;
  a /= 2;	// 출력 4
}
stack=2, locals=2, args_size=1
0: bipush        8
2: istore_1
3: iload_1
4: iconst_2
5: idiv
6: istore_1
7: return

int a 값을 높여보니 bipush 새로운 바이트 코드가 생성됐다.
표에서 찾아보니 바이트를 정수 값으로 스택에 푸시하는 명령어

이후 모든 대입 연산자 (%=, &=, |=, ^=, <<=, >>=, >>>=)를 넣고 바이트 코드를 확인해 보니 위에서 확인한 산술, 비트, 논리연산자에서 나온 바이트 코드와 일치해 생략


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

public static void main(String[] args) {
  Runnable r2 = () -> System.out.println("Howdy, world!");
  r2.run();
}

출처: https://www.oracle.com/technical-resources/articles/java/architect-lambdas-part1.html

stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1                  // Method java/lang/Object."":()V
4: return

invokespecial: 객체 objectref에서 인스턴스 매소드를 호출하고 결과를 스택에 넣습니다.

오라클 람다 설명 문서에서 예시로 바이트 코드로 보기 편한 코드를 가져와 확인해 보았습니다. 설명에는 '그러나 내부적으로 Java 8 버전은 Runnable 인터페이스 상속과 익명 클래스 생성하는 것 이상의 작업을 수행하고 있습니다. 이 중 일부는 Java 7에 도입된 동적 바이트 코드 호출과 관련이 있습니다. 여기서 세부 디테일을 설명하지 않겠지만 단순한 익명 클래스 인스턴스 그 이상이란 걸 알아야 합니다'

Java8 버전부터 지원.
(@FunctionalInterface) 함수가 하나만 존재하는 Interface를 선언하고 간결하게 사용하고 구현

자바스크립트에서는 종종 써봤지만, 자바에서는 인터페이스 사용 같은 제약? 사항을 두고 사용해 정확한 적재요소에서 사용이 가능하도록 도움을 주는 느낌을 받았다. 조금이라도 사용을 해보던가 예시 작성을 통해 익숙해지는 작업이 많이 필요할 것으로 예상


8. 3항 연산자

public static void main(String[] args) {
  int x = 7;
  int y = 2;
  int a = (x < y) ? 30 : 50;	// 출력: 50;
}
stack=2, locals=4, args_size=1
 0: bipush        7
 2: istore_1
 3: iconst_2
 4: istore_2
 5: iload_1
 6: iload_2
 7: if_icmpge     15	// value1 ≥ value2
10: bipush        30
12: goto          17
15: bipush        50
17: istore_3
18: return

7번 비교를 통해 나오는 값을 스택에 정수 형태로 저장
그러면 if 구문으로 이걸 똑같이 구현하면 바이트코드가 많이 달라질까?

public static void main(String[] args) {
  int x = 7;
  int y = 2;
  // int a = 0;
        
  if (x < y) {
    a = 30;
  } else {
    a = 50;
  }
}
stack=2, locals=4, args_size=1
 0: bipush        7
 2: istore_1
 3: iconst_2
 4: istore_2
 5: iload_1
 6: iload_2
 7: if_icmpge     16
10: bipush        30
12: istore_3
13: goto          19
16: bipush        50
18: istore_3
19: return

코드에서 드라마틱한 변화가 보이지는 않는거 같다.


9. 연산자 우선 순위

  • 최우선연산자 ( ., [], () )
  • 단항연산자 ( ++,--,!,~,+/- : 부정, bit변환>부호>증감)
  • 산술연산자 ( *,/,%,+,-,shift) < 시프트연산자 ( >>,<<,>>> ) >
  • 비교연산자 ( >,<,>=,<=,==,!= )
  • 비트연산자 ( &,|,,~ )
  • 논리연산자 (&& , || , !)
  • 삼항연산자 (조건식) ? :
  • 대입연산자 =,*=,/=,%=,+=,-=

우선 순위는 어떻게 처리를 할까?

public static void main(String[] args) {
  int a = 5;
  int b = 10;
  int c = 5;
  a = (a - b * c);
}
stack=3, locals=4, args_size=1
 0: iconst_5
 1: istore_1
 2: bipush        10
 4: istore_2
 5: iconst_5
 6: istore_3
 7: iload_1
 8: iload_2
 9: iload_3
10: imul
11: isub
12: istore_1
13: return

3 정수를 차례차례 저장하고 한 번에 다 불러서 내부적으로 우선순위를 확인 후 차례대로 계산 후 값을 저장하는 것으로 보인다.

다른 연산도 해봤지만, 우선순위 계산하는 것으로 보여 나머지 내용 생략


10. (optional) Java 13. switch 연산자

스위치 연산자는 최적화에 효율적이라는 말을 들은 적이 있는데 그 호기심을 직접 볼 기회가 온 거 같다.

public static void main(String[] args) {
  int a = 5;
  switch (a) {
    case 1:
      a = 4;
      break;
    case 5:
      a = 3;
      break;
    default:
      a = 7;
  }
}
stack=1, locals=2, args_size=1
 0: iconst_5
 1: istore_1
 2: iload_1
 3: lookupswitch  { // 2
     1: 28
     5: 33
     default: 38
 }
28: iconst_4
29: istore_1
30: goto          41
33: iconst_3
34: istore_1
35: goto          41
38: bipush        7
40: istore_1
41: return

lookupswitch는 키를 사용하여 테이블에서 대상 주소를 조회하고 해당 주소의 명령어에서 계속 실행

이번에는 이 구문을 if로 변환해서 확인

public static void main(String[] args) {
  int a = 5;
  if (a == 4) {
    a = 4;
  } else if (a == 5) {
    a = 3;
  } else {
    a = 7;
  }
}
stack=2, locals=2, args_size=1
 0: iconst_5
 1: istore_1
 2: iload_1
 3: iconst_4
 4: if_icmpne     12
 7: iconst_4
 8: istore_1
 9: goto          25
12: iload_1
13: iconst_5
14: if_icmpne     22
17: iconst_3
18: istore_1
19: goto          25
22: bipush        7
24: istore_1
25: return

이번에는 정말 확실한 차이를 보여준다.
stack 공간도 1개 더 사용하고 실행해야 할 명령문도 더 많아졌다.

댓글 없음:

댓글 쓰기