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개 더 사용하고 실행해야 할 명령문도 더 많아졌다.

2020. 11. 20.

2주차 과제: 자바 데이터 타입, 변수 그리고 배열

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

목표

자바의 프리미티브 타입, 변수 그리고 배열을 사용하는 방법을 익힙니다.

학습할 것

  • 프리미티브 타입 종류와 값의 범위 그리고 기본 값
  • 프리미티브 타입과 레퍼런스 타입
  • 리터럴
  • 변수 선언 및 초기화하는 방법
  • 변수의 스코프와 라이프타임
  • 타입 변환, 캐스팅 그리고 타입 프로모션
  • 1차 및 2차 배열 선언하기
  • 타입 추론, var


1. 프리미티브 타입 종류와 값의 범위 그리고 기본 값

byte
8-bit signed two's complement integer (8bit 부호있는 2의 보수 정수)
-128 ~ 127(포함)

  • 바이트 데이터 유형은 메모리 절약이 중요한 대형 배열에 메모리를 절약하는데 유용할 수 있습니다.
  • 또한 제한이 코드를 명확히 하는 데 도움이 되는 곳에 int형 대신 사용할 수 있습니다. 변수의 범위를 제한한다는 사실은 문서화의 한 형태로 제공될 수 있습니다.

short
16-bit signed two's complement integer (16bit 부호있는 2의 보수 정수)
-32,768 ~ 32,767(포함)

  • 바이트와 마찬가지로 동일한 지침이 적용됩니다. 메모리 절약이 실제로 중요한 상황에서 short를 사용하여 큰 배열의 메모리를 절약할 수 있습니다.

int
32-bit signed two's complement integer (32비트 부호있는 2의 보수 정수)
-231 ~ 231-1

  • Java SE 8 이상에서는 int 데이터 타입을 사용해 최솟값 0 ~ 최댓값 232-1 unsigned 32-bit integer (부호 없는 32bit 정수)를 표현할 수 있습니다.
  • Integer 클래스를 사용해 int 데이터 유형을 부호없는 정수로 사용합니다. compareUnsigned, divideUnsigned 등과 같은 static methods가 Integer 클래스에 추가되어 unsigned integer에 대한 산술 연산을 지원합니다.

long
64-bit two's complement integer (62bit 2의 보수 정수)
-263 ~ 263-1

  • Java SE 8 이상에서는 long 데이터 타입을 사용해 최솟값 0 ~ 최대값 264-1 unsigned 64-bit long (부호 없는 64bit long)을 표현할 수 있습니다.
  • int에서 제공하는 것보다 더 넓은 값 범위가 필요한 경우 이 데이터 유형을 사용하십시오.
  • Long 클래스에는 unsigned long에 대한 산술 연산을 지원하는 compareUnsigned, divideUnsigned 등의 method도 포함되어 있습니다.

float
single-precision 32-bit IEEE 754 floating point (단정밀도 32bit IEEE 754 부동소수점)

  • 값의 범위는 이 주제의 범위를 벗어나지만, Java 언어 명세서에 부동 소수점 유형, 형식, 값이 명시돼 있습니다.
  • byte나 short의 권장 사항과 마찬가지로 부동 소수점 숫자의 큰 배열에 메모리를 절약해야 하는 경우 double 대신 float를 사용하십시오.
  • 이 데이터 타입은 통화와 같은 정확한 값에 사용해서는 안 됩니다. 이를 위해 대신 java.math.BigDecimal 클래스 사용해야 합니다.
  • 숫자나 문자열은 Java 플랫폼에서 제공하는 BigDecimal이나 다른 유용한 클래스들이 처리를 도와줍니다.

double
double-precision 64-bit IEEE 754 floating point (배정밀도 64bit IEEE 754 부동소수점)

  • 값의 범위는 이 주제의 범위를 벗어나지만, Java 언어 명세서에 부동 소수점 유형, 형식, 값이 명시돼 있습니다.
  • 기본적으로 10진수 입니다.
  • 위에서 언급했듯이 이 데이터 유형은 통화와 같은 정확한 값에 사용해서는 안 됩니다.

boolean
boolean 데이터 유형은 true와 false 두가지 값만 있습니다.

  • 참/거짓 조건을 추적하는 간단한 flag 데이터 타입으로 사용합니다.
  • 이 데이터 타입은 1bit의 정보를 표현하지만 "크기"는 정확하게 정의된 것이 아닙니다.

char
single 16-bit Unicode character (싱글 16bit 유니코드 문자)
'\u0000' (또는 0) ~ '\uffff' (또는 65,535 포함)

기본값

필드(field)가 선언될 때 항상 값을 할당할 필요는 없습니다. 선언되었지만 초기화되지 않은 필드는 컴파일러에 의해 적절한 기본값으로 설정됩니다. 일반적으로는 데이터 타입에 따라 기본값이 0 또는 null입니다. 그러나 기본값에 의존하는 것은 일반적으로 잘못된 프로그래밍 스타일로 간주합니다.

기본값은 다음과 같습니다.


2. 프리미티브 타입과 레퍼런스 타입

Primitive type (기본형 타입)

  • 메모리에 기본형 타입이 보유한 실제 데이터를 저장합니다.
  • 기본형 값이 동일한 유형의 다른 변수에 지정되면 사본이 만들어집니다.
  • 기본형이 메소드에 전달되면 기본형의 복사본만 전달됩니다. 호출된 메소드는 오리지널 기본형 값에 엑세스할 수 없음으로 변경할 수 없습니다. 호출된 메소드는 복사된 값을 변경할 수 있습니다.

Reference type (참조형 타입)

  • 사용자가 정의한 무제한 참조 유형입니다.
  • 메모리에 데이터에 대한 참조를 저장합니다.
  • 참조 유형이 다른 참조 유형에 할당되면 둘 다 동일한 객체를 가리킵니다.
  • 객체가 메소드에 전달되면 호출된 메소드는 전달된 객체의 내용을 변경할 수 있지만 객체의 주소는 변경할 수 없습니다.


3. 리터럴

기본형 타입 변수를 초기화할 때 new 키워드를 사용하지 않는다는 것을 보셨을 겁니다.
기본형 타입은 언어에 내장된 특수 데이터 유형입니다. 클래스에서 생성된 객체가 아닙니다.
리터럴은 고정된 값의 소스 코드 표현입니다. 계산 없이 코드에서 직접 표현됩니다.
아래와 같이 기본형 타입 변수에 리터럴을 할당할 수 있습니다.

boolean result = true;
char capitalC = 'C';
byte b = 100;
short s = 10000;
int i = 100000;


4. 변수 선언 및 초기화하는 방법

int num = 0;
//타입 변수이름 = 값;

//example
int myNum = 5;
float myFloatNum = 5.99f;
char myLetter = 'D';
boolean myBool = true;
String myText = "Hello";


5. 변수의 스코프와 라이프타임

public class MyStudyClass {
  int x = 0; // 인스턴스 변수
  static int result = 0; // 클래스 변수
  
  void method1() {
    int x=0; // 지역 변수
  }
}

인스턴스 변수
클래스 내부에 선언되었지만, 메소드 및 블록 외부에 선언된 변수
스코프: 정적 메소드를 제외한 클래스 전체
라이프타임: 클래스 객체가 메모리에 남아있는 동안

클래스 변수
클래스 내부, 모든 블록 외부에서 선언되고 static으로 선언된 변수
스코프: 클래스 전체
라이프타임: 프로그램이 끝날 때까지

지역 변수
인스턴스 또는 클래스 변수가 아닌 다른 모든 변수
스코프: 선언된 블록 내부 라이프타임: 컨트롤이 선언된 블록을 떠날 때까지


6. 타입 변환, 캐스팅 그리고 타입 프로모션

확대(widening) 캐스팅 (자동적으로) - 작은 타입을 큰 타입으로 변환
byte → short → char → int → long → float → double

public class Main {
  public static void main(String[] args) {
    int myInt = 9;
    double myDouble = myInt; // 자동 캐스팅

    System.out.println(myInt);      // 출력 9
    System.out.println(myDouble);   // 출력 9.0
  }
}

축소(narrowing) 캐스팅 (수동적으로) - 큰 타입을 작은 타입으로 변환
double → float → long → int → char → short → byte

public class Main {
  public static void main(String[] args) {
    double myDouble = 9.78;
    int myInt = (int) myDouble; // 수동 캐스팅

    System.out.println(myDouble);   // 출력 9.78
    System.out.println(myInt);      // 출력 9
  }
}

타입 프로모션은 자동으로 작은 범위의 타입을 큰 범위의 타입으로 승격(promotion)한 후 연산합니다.

class Test {			
  public static void main (String [] args) {
    int i = (int)12.5f;  // i 출력 12 (narrowing)
    float f = i; // f 출력 12.0 (widening)
    f = f * i; // i의 타입이 flot로 승격 후 folat*float로 연산
  }
}


7. 1차 및 2차 배열 선언하기

class ArrayDemo {
  public static void main(String[] args) {
    // integer 배열 선언
    int[] anArray;

    // 10개의 integer 배열 선언
    anArray = new int[10];
    
    // 이 양식은 관례상 권장하지 않습니다.
    float anArrayOfFloats [];
    
    // 2차원 배열 선언
    String[][] names = {
        {"Mr. ", "Mrs. ", "Ms. "},
        {"Smith", "Jones"}
    };
    // Mr. Smith
    System.out.println(names[0][0] + names[1][0]);
    // Ms. Jones
    System.out.println(names[0][2] + names[1][1]);
    }
}


8. 타입 추론, var

var a = 1;            // Legal
var b = 2, c = 3.0;   // Illegal: multiple declarators
var d[] = new int[4]; // Illegal: extra bracket pairs
var e;                // Illegal: no initializer
var f = { 6 };        // Illegal: array initializer
var g = (g = 7);      // Illegal: self reference in initializer

var a = 1;                // a has type 'int'
var b = java.util.List.of(1, 2);  // b has type List
var c = "x".getClass();   // c has type Class
var d = new Object() {};  // d has the type of the anonymous class
var e = (CharSequence & Comparable) "x";
                          // e has type CharSequence & Comparable
var f = () -> "hello";    // Illegal: lambda not in an assignment context
var g = null;             // Illegal: null type

왜 Java에 var가 있어야 하나요?

로컬 변수는 Java의 핵심 요소입니다. 로컬 변수를 사용해 메소드는 중간 값을 저렴하게 저장하여 중요한 결과를 계산합니다. 필드와 달리 로컬 변수는 동일한 블록에서 선언, 초기화하고 사용합니다. 로컬 변수의 이름과 초기화는 종종 타입보다 독자의 이해를 위해 더 중요합니다. 일반적으로 이름과 초기화는 타입만큼 많은 정보를 제공합니다.

Person person = new Person()

로컬 변수 선언에서 var의 역할은 이름과 초기화를 강조하도록 타입 자리를 대신하는 것입니다.

var person = new Person()

컴파일러는 초기화시 로컬 변수의 타입을 유추합니다.

이것은 특히 이럴때 가치가 있습니다.
타입이 와일드 카드로 매개 변수화 된 경우
또는 초기화에서 타입이 언급 된 경우.
(This is especially worthwhile if the type is parameterized with wildcards, or if the type is mentioned in the initializer.)
※모르는 부분이 많고 이해가 잘 안 돼서 다시 들여다봐야 할 부분

var를 사용하면 가독성을 떨어 뜨리지 않고 코드를 더 간결하게 만들 수 있으며 경우에 따라 중복성을 제거하여 가독성을 향상시킬 수 있습니다.

출처: http://openjdk.java.net/projects/amber/LVTIFAQ.html

2020. 11. 17.

1주차: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.


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

목표

자바 소스 파일(.java)을 JVM으로 실행하는 과정 이해하기.

학습할 것

  • JVM이란 무엇인가
  • 컴파일 하는 방법
  • 실행하는 방법
  • 바이트코드란 무엇인가
  • JIT 컴파일러란 무엇이며 어떻게 동작하는지
  • JVM 구성 요소
  • JDK와 JRE의 차이


1. JVM이란 무엇인가

JVM(Java Virtual Machine)은 Java와 자바 바이트 코드로 컴파일이 가능한 다른 언어로 쓰인 프로그램이 컴퓨터에서 실행이 가능하도록 해주는 가상 머신 입니다.

Java는 "한 번 작성하면 어디서나 실행"이라는 개념으로 다양한 플랫폼에서 실행되도록 설계되었습니다. 특정 플랫폼 용으로 컴파일되고 실행되는 C++ 같은 언어와 달리 Java 소스 코드는 먼저 .class 파일 인 바이트 코드로 컴파일 됩니다. 컴파일 된 클래스 파일은 가상 머신에서 해석됩니다.

먼저 자바 소스 코드 (.java 파일)를 바이트 코드 (.class 파일)로 컴파일합니다. 바이트 코드는 자바와 기계어 사이의 중간 언어입니다. 다른 OS나 플랫폼에서 코드를 수정하지 않고도 JVM에서 동일한 바이트 코드를 실행할 수 있습니다.


2. 컴파일 하는 방법

public class FirstJavaProgram {
  public static void main(String[] args){
    System.out.println("This is my first program in java");
  }//End of main
}//End of FirstJavaProgram Class

출처: https://beginnersbook.com/2013/05/first-java-program/

Step 1: 윈도우의 메모장이나 Mac의 'TextEdit'같은 프로그램을 열어 위의 코드를 작성합니다.

Step 2: 우리가 만든 프로그램의 'public class' 이름이 'FirstJavaProgram'이기 때문에 파일 이름을 'FirstJavaProgram.java'로 저장합니다.

Step 3: 윈도우의 'command prompt (cmd)', Mac OS의 Terminal을 열어 아래의 명령어를 작성하고 엔터를 입력합니다.

javac FirstJavaProgram.java

"javac is not recognized as an internal or external command, operable program or batch file" 와 같은 애러 메세지가 보인다면 Path(경로) 설정을 해야 합니다.

윈도우 - cmd에서 java가 설치돼 있는 경로의 bin 폴더 위치를 복사해 아래와 같이 작성합니다.

set path=C:\Program Files\Java\jdk1.8.0_121\bin

Note: jdk 버전이 다를 수 있습니다. 제가 쓰는 자바 버전은 1.8.0_121으로 똑같이 작성했습니다.

Mac OS X - 터미널에서 다음과 같이 작성합니다.

export JAVA_HOME=/Library/Java/Home

터미널에서 다음 명령을 입력해 경로를 확인합니다.

echo $JAVA_HOME

PATH 시스템 변수를 설정하거나 변경하려면 어떻게 해야 합니까?


3. 실행하는 방법

컴파일 후 .class(바이트 코드) 파일을 다음 명령어로 실행합니다.

java FirstJavaProgram


4. 바이트 코드란 무엇인가

자바 바이트 코드는 JVM(Java Virtual Machine)의 명령어 세트(instruction set)입니다.

Java bytecode instruction listings

Java 프로그래머는 Java 바이트 코드를 전혀 인식하거나 이해할 필요가 없습니다. 그러나 IBM developerWorks 저널에서 제안한 것처럼 "바이트 코드와 Java 컴파일러에 의해 생성될 수 있는 바이트 코드를 이해하면 어셈블리에 대한 지식이 C 또는 C ++ 프로그래머에게 도움이 되는 것과 동일한 방식으로 Java 프로그래머에게 도움이 됩니다."
출처: https://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/


5. JIT 컴파일러란 무엇이며 어떻게 동작하는지

JIT (Just-In-Time) 컴파일러는 런타임시 Java 애플리케이션의 성능을 향상시키는 Java Runtime Environment의 구성 요소입니다. JIT 컴파일러는 시작시 수천개의 메서드를 호출 할 수 있으므로 호출되는 모든 메서드를 컴파일하지는 않습니다. 대신 메서드가 호출 된 횟수를 기록합니다. 개수가 사전 정의된 호출 임계값에 도달하면 JIT 컴파일이 작동합니다. JIT에 의해 메소드가 컴파일되면 VM은 해석(interpreting)하는 대신 컴파일 된 메소드를 호출합니다.

나중에 읽어볼 것
Chapter 4. Working with the JIT Compiler
The JIT compiler


6. JVM 구성 요소

JVM은 크게 세 가지 주요 영역으로 구성됩니다.

  • 클래스 로더 하위 시스템
  • 런타임 데이터 영역
  • 실행 엔진

1. 클래스 로더 서브시스템(ClassLoader Subsystem)

  1. 로딩(Loading)
    • 클래스(Class)를 사용하려고 하면 자바 클래스 로더가 해당 클래스를 메모리에 로드
    • 첫번째 클래스 실행은 일반적으로 정적 main() 메서드를 선언하고 사용
  2. 연결(Linking)
    • 검증(Verify) – 바이트 코드 검증기는 생성된 바이트 코드가 적절한지 여부를 확인하고 확인이 실패하면 확인 오류가 발생.
    • 준비(Prepare) – 모든 정적 변수에 대해 메모리가 할당(allocated)되고 기본값으로 지정(assigned)됨.
    • 해결(Resolve) – 모든 기호 메모리 참조(symbolic memory references)가 Method Area의 원래 참조(original references)로 대체.
  3. 초기화(Initialization)
    • ClassLoading의 마지막 단계입니다. 여기서 모든 정적 변수(static variables)는 원래 값(original values)으로 할당(assigned)되고 정적 블록(static block)이 실행됩니다.

2. 런타임 데이터 영역(Runtime Data Area)

런타임 데이터 영역은 5개의 주요 구성 요소로 나뉩니다.

  1. 메서드 영역(Method Area) – 정적 변수를 포함하여 모든 클래스 수준 데이터가 여기에 저장됩니다. JVM당 하나의 메소드 영역만 있으며 공유자원입니다.
  2. 힙 영역(Heap Area) – 모든 개체와 해당 인스턴스 변수 및 배열이 여기에 저장됩니다. JVM 당 하나의 힙 영역도 있습니다. 메서드 및 힙 영역은 여러 스레드에 대한 메모리를 공유하므로 저장된 데이터는 스레드로부터 안전(thread-safe)하지 않습니다.
  3. 스택 영역(Stack Area) – 모든 스레드는 각자 런타임 스택이 생성됩니다. 모든 메서드 호출에 스택 프레임이라는 불리는 스택 메모리가 만들어집니다. 모든 지역 변수는 스택 메모리에 생성됩니다. 스택 영역은 공유 리소스가 아니므로 스레드로부터 안전합니다. 스택 프레임은 세 가지 하위 항목으로 나뉩니다.
    • 로컬 변수 배열(Local Variable Array) – 얼마나 많은 로컬 변수가 메소드와 연관돼 있는지와 해당 값이 여기에 저장됩니다.
    • 피연산자 스택(Operand stack) – 중간 작업이 필요한 경우 피연산자 스택은 작업을 수행하는 런타임 작업 영역으로 작동합니다.
    • 프레임 데이터(Frame data) – 메소드와 연관된 기호가 여기에 저장됩니다. 예외가 발생하면 캐치 블록(catch block) 정보를 프레임 데이터에 유지합니다.
  4. PC 레지스터(PC Registers) – 각 스레드에는 별도의 PC 레지스터가 있으며, 명령어가 실행되면 현재 실행중인 명령어의 주소를 유지하기 위해 PC 레지스터가 다음 명령(instruction)으로 업데이트됩니다.
  5. 네이티브 메서드 스택(Native Method stacks) – 네이티브 메서드 스택은 네이티브 메서드 정보를 보유합니다. 모든 스레드에 대해 별도의 네이티브 메서드 스택이 생성됩니다.

3. 실행 엔진(Execution Engine)

런타임 데이터 영역(Runtime Data Area)에 할당 된 바이트 코드는 실행 엔진(Execution Engine)에 의해 실행됩니다. 실행 엔진은 바이트 코드를 조각 조각으로 읽고 실행합니다.

  1. 인터프리터(Interpreter) – 인터프리터는 바이트 코드를 더 빨리 해석하지만 느리게 실행됩니다. 인터프리터의 단점은 하나의 메서드가 여러 번 호출 될 때마다 새로운 해석(interpretation)이 필요하다는 것입니다.
  2. JIT 컴파일러 – JIT 컴파일러는 인터프리터의 단점을 무력화합니다. 실행 엔진은 바이트 코드를 변환하는 데 인터프리터를 사용하지만 반복된 코드를 찾으면 전체 바이트 코드를 컴파일하고 네이티브 코드로 변경하는 JIT 컴파일러를 사용합니다. 이 네이티브 코드는 반복되는 메서드 호출에 직접 사용되어 시스템 성능을 향상시킵니다.
    1. 중간 코드 생성기(Intermediate Code Generator)
    2. 코드 최적화(Code Optimizer)
    3. 타겟 코드 생성기(Target Code Generator) – 기계 코드 또는 네이티브 코드 생성 담당
    4. 프로파일러(Profiler) - 여러번 사용되는 메소드 핫스팟을 찾는 역할
  3. 가비지 수집기(Garbage Collector): 참조되지 않은 개체를 수집하고 제거합니다. 가비지 수집은 System.gc()를 호출하여 실행할 수 있지만, 실행이 보장되지는 않습니다. JVM의 가비지 수집기는 생성된 개체(objects)를 수집합니다.

JNI(Java Native Interface): JNI는 Native Method Libraries와 상호 작용하며 실행 엔진에 필요한 Native Libraries를 제공합니다.

Native Method Libraries: 실행 엔진(Execution Engine)에 필요한 네이티브 라이브러리 모음입니다.


7. JDK와 JRE의 차이

JRE(Java Runtime Environment)는 자바 프로그램 실행 환경을 위한 설치 패키지입니다. 자바 프로그램 실행에 필요한 파일들이 포함돼 있지만 개발하지 못합니다.

JDK(Java Development Kit)는 자바 개발에 모든 기능을 갖춘 SDK(Software Development Kit)입니다. JRE에 있는 모든 기능뿐만 아니라 컴파일러(javac)와 개발 도구가 포함돼 있습니다.


https://github.com/whiteship/live-study/issues/1#issuecomment-727216519
1주차 추가 과제 - javac 옵션 조사

-g
지역 변수를 포함한 모든 디버깅 정보를 생성합니다. 기본적으로 행 번호와 소스 파일 정보만 생성됩니다.

-g:none
디버깅 정보를 생성하지 않습니다.

-g:[lines, vars, source],[lines, vars, source],[lines, vars, source]
쉼표로 구분된 키워드 목록 중 지정된 종류의 디버깅 정보만 생성합니다. 유효한 키워드는 다음과 같습니다.

    lines : 줄 번호 디버깅 정보

    vars : 지역 변수 디버깅 정보

    source : 소스 파일 디버깅 정보

-nowarn
경고 메시지를 비활성화합니다. 이 옵션은 -Xlint:none 옵션과 동일하게 작동합니다.

-verbose
컴파일러가 수행하는 작업에 대한 메시지를 출력합니다. 메시지에는 로드된 클래스 및 컴파일된 소스 파일에 대한 정보가 포함됩니다.

-deprecation
더 이상 사용되지 않는(deprecated) 멤버 또는 클래스의 사용 또는 상속(override)에 대한 설명을 표시합니다. -deprecation 옵션이 없으면 javac는 소스 파일의 요약을 표시합니다. -deprecation 옵션은 -Xlint:deprecation의 줄임 표현입니다.

-classpath <path>, --class-path <path>, -cp <path>
사용자의 클래스 파일 및 주석(annotation) 프로세서를 찾을 위치를 지정합니다. 이 클래스 경로는 사용자의 CLASSPATH 환경 변수를 재정의(overrides)합니다.

    --class-path, -classpath 또는 -cp 지정되지 않은 경우, 클래스 경로는 현재 디렉토리입니다.

    -sourcepath 옵션이 지정되어 있지 않은 경우, 사용자 클래스 패스는 소스 파일을 검색합니다.

    -processorpath 옵션이 지정되어 있지 않은 경우, 클래스 패스는 주석(annotation) 프로세서를 검색합니다.

-source-path <path>, -sourcepath <path>
입력 받을 소스 파일의 위치를 지정합니다. 클래스나 인터페이스의 정의를 찾기 위해 사용할 소스 코드 경로입니다. 사용자 class path와 마찬가지로 소스 경로 항목은 Oracle Solaris에서는 콜론(:)으로, Windows에서는 세미콜론(;)으로 구분합니다. Jar archives, Zip archives, 폴더일 수 있습니다. 패키지에 사용되는 경우 디렉토리나 아카이브(archives) 내의 로컬 경로 이름이 패키지 이름을 반영해야 합니다.

-bootclasspath <path>, --boot-class-path <path>
부트스트랩(bootstrap) 클래스 파일의 위치를 재정의합니다.

-extdirs <dirs>
설치된 확장 파일의 경로를 재정의합니다. 디렉토리 변수는 콜론으로 구분된 디렉토리 목록입니다. 지정된 특정 경로의 JAR 파일을 검색해 클래스 파일을 찾습니다. 발견한 모든 JAR 파일은 클래스 경로의 일부가 됩니다.

-endorseddirs <dirs>
승인된 표준 경로의 위치를 재정의합니다.

-proc: {none,only}, {none,only}
주석 처리(annotation processing)와 컴파일이 수행되는지 여부를 제어합니다. -proc:none 주석 처리없이 컴파일이 수행됨을 의미합니다. -proc:only 후속 컴파일 없이 주석 처리만 수행됨을 의미합니다.

-processor <class1>[,<class2>,>class3>...]
실행할 주석 프로세서의 이름입니다. 기본 검색 프로세스를 무시합니다.

-processorpath <path>, -processor-path <path>
주석 프로세서를 찾을 위치를 지정합니다. 이 옵션을 사용하지 않으면 클래스 경로에서 프로세서를 검색합니다.

-parameters
메소드 매개변수의 리플렉션(reflection)을 위한 메타 데이터를 생성합니다. 생성된 클래스 파일안의 생성자와 메소드의 형식 매개변수(formal parameter) 이름을 저장해 리플렉션 API의 java.lang.reflect.Executable.getParameters 메소드가 가져올 수 있게 합니다.

-d <directory>
클래스 파일의 대상 디렉토리를 설정합니다. 클래스가 패키지의 일부인 경우 javac는 패키지 이름을 반영하는 하위 디렉토리에 클래스 파일을 넣고 필요에 따라 디렉토리를 만듭니다. -d 옵션이 지정되지 않은 경우 javac는 소스파일과 동일한 디렉토리에 저장합니다.

-s <directory>
생성된 소스 파일을 배치할 디렉토리를 지정합니다.

-h <directory>
생성된 기본 헤더 파일을 배치할 위치를 지정합니다.

-implicit:{[none, class],[none, class]}
암시적으로 참조된(implicitly referenced) 파일의 클래스 파일을 생성할지 여부를 지정합니다.

   -implicit:class 자동으로 클래스 파일을 생성합니다.

   -implicit:none 클래스 파일 생성을 금지합니다.

이 옵션을 지정하지 않으면 기본적으로 클래스 파일을 생성합니다. 이 경우 컴파일러는 주석 프로세서를 수행할 때 클래스 파일이 생성되면 경고를 발생합니다. -implicit 옵션이 명확하게 설정된 경우에는 경고가 발생하지 않습니다.

-encoding <encoding>
EUC-KR, UTF-8 같은 소스 파일에서 사용하는 문자 인코딩을 지정합니다. -endocing 옵션을 지정하지 않으면 플랫폼 기본으로 사용됩니다.

-source <release>
허용되는 소스 코드의 버전을 지정합니다.

Note: JDK 9부터 javac는 5보다 작거나 같은 값(버전)을 지원하지 않습니다. 5보다 작거나 같은 값을 사용하는 경우 javac -source 6이 지정된 것처럼 작동합니다.

-target <release>
특성 VM 버전의 클래스 파일을 생성합니다.

-profile <profile>
사용된 API가 지정된 프로필에서 사용 가능한지 확인합니다.

-version
버전 정보를 출력합니다.

-help
표준 옵션의 요약을 출력합니다.

-Akey[=value]
주석 프로세서에 전달할 옵션을 지정합니다. javac에서 직접 해석(interpreted)되지 않지만, 개별 프로세서에서 사용할 수 있습니다. 키값은 점(.)으로 구분된 하나 이상의 식별자입니다.

-X
추가 옵션에 대한 도움말을 출력합니다.

-J<flag>
옵션을 런타임 시스템에 전달합니다. 여기서 옵션은 javacommand에 설명된 옵션 중 하나입니다. 예를 들어 '-J-Xms48m'은 시작 메모리를 48MB로 설정합니다.

-Werror
경고가 발생하면 컴파일을 종료합니다.

@<filename>
파일에서 옵션 및 파일 이름을 읽습니다. javac 명령어의 단순화를 위해 javac 명령어에 대한 인수를 포함하는 하나 이상의 파일을 지정할 수 있습니다.(-J 옵션 제외한) 이를 통해 모든 운영 체제에서 다양한 길이의 javac 명령어를 만들 수 있습니다.

출처: https://docs.oracle.com/javase/9/tools/javac.htm