본문 바로가기
네이버클라우드/JAVA 웹 프로그래밍

JAVA 7일차 (2023-05-31) 자바 기초 DAY5

by prometedor 2023. 5. 31.

레퍼런스와 인스턴스

ㄴ 레퍼런스는 객체를 가리키는 변수이며, 실제로 객체가 생성되고 메모리에 할당된 후에 해당 객체를 참조하는 역할을 수행함

ㄴ 인스턴스는 클래스로부터 생성된 객체 자체를 의미하며, 클래스의 구조와 동작을 갖춘 실체임

ㄴ Method 의 로컬변수는 JVM Stack 에 저장되며, new 명령으로 만드는 변수는 모두 Heap 메모리에 저장됨

ㄴ Heap 에 생성되는 new 명령으로 만든 변수는 초기에 모든 메모리 비트를 0으로 셋팅함 

ㄴ no2 에 no 를 할당하면 둘의 주소는 같아지므로 no2[2] 에 31을 할당할 시 no[2] 의 값도 31 을 가리킴

ㄴ Java 에서는 C언어에서의 *(no2 + 2) = 200; 처럼 포인터를 이용할 수 없음

package bitcamp.myapp;

public class Test {
  public static void main(String[] args) {
    int[] no;

    no = new int[5];

    int[] no2;
    
    no2 = no;

    no2[2] = 31;

    System.out.println(no[2]);
  }
}

ㄴ int[] no; // no 라는 이름을 가진 int 타입의 배열 변수 선언

ㄴ no = new int[5]; // no 라는 배열을 크기 5로 초기화

ㄴ int[] no2; // no2 라는 이름을 가진 int 타입의 배열 변수 선언

ㄴ no2 = no; // no2 변수에 no 배열을 할당 --> 주소 값을 할당(레퍼런스)

ㄴ no2[2] = 31; // no2 배열의 인덱스 2에 31을 할당 --> no 배열의 인덱스 2의 값을 확인해보면 31을 출력함

 

package bitcamp.myapp;

public class Test {
  public static void main(String[] args) {
    int[] no = new int[5];
  }
}
package bitcamp.myapp;

public class Test {
  public static void main(String[] args) {
    int[] no;
    no = new int[5];
  }
}

 

// 배열 - 배열 레퍼런스와 null
public class Exam0551 {
  public static void main(String[] args) {

    int[] arr1;
    arr1 = new int[5];

    // 배열 레퍼런스를 초기화시키고 싶다면 null로 설정.
    arr1 = null; // 0으로 설정. 즉 레퍼런스가 아무것도 가리키지 않음을 의미.

    // 레퍼런스가 배열 인스턴스를 가리키지 않은 상태에서 사용하려 하면?
    System.out.println(arr1[0]); // NullPointerException 실행 오류!
  }
}

 

ㄴ no 라는 이름의 배열을 5 크기로 선언 (주소 : 200)

ㄴ no 라는 이름의 배열을 3 크기로 선언 (주소 : 300)

=> no 배열은 주소 200을 잃어버리고 300으로 변경됨

ㄴ 5 크기로 선언됐던 배열은 200인 인스턴스 주소를 잃어버려 200으로 접근할 수 없어짐 --> 이 값이 Garbage 값이 됨

ㄴ 이렇게 사용할 수 없는 변수들(사용 불가 메모리)은 Garbage 값으로, 메모리만 차지하여 메모리 낭비로 이어지므로 제거되어야 함

 

## 가비지(garbage)
- 주소를 잃어 버려 사용할 수 없는 메모리
- 특정 조건이 되면 가비지 수집기(garbage collector)에 의해 메모리 해제됨
 메모리 해제? 다른 용도로 사용할 수 있도록 표시한다는 의미
 
## 가비지 수집 조건 = 가비지 컬렉터가 동작할 때
- 메모리가 부족할 때
 - 운영체제로부터 메모리를 추가로 받기 전에 먼저 기존에 사용하던 메모리에서 가비지를 제거
- CPU가 한가할 때
 - 24시간 365일 내내 실행하는 서버 프로그램인 경우, 실행 중간에 CPU가 한가할 때 가비지를 제거
- 주의!
 - 프로그램(JVM)을 종료하면 JVM 사용한 메모리를 운영체제가 모두 회수
 
## 가비지를 강제로 지우도록 명령하는 방법?
- 자바는 없음!
- C 언어 => free(메모리주소);
- C++ 언어 => delete 객체주소;
- 요즘 언어의 트랜드는 사용하지 않는 메모리를 개발자가 직접 해제하는 것이 아니라 VM이 해제하는 것
 예) JavaScript, C#, Python, PHP, Go, Java 등
- 요즘 언어의 트랜드는 VM으로 실행하는 것임
 왜? 직접 기계어로 전환되면 메모리를 관리를 자동으로 수행할 수 없음
 
## 가비지 컬렉터를 강제로 실행하는 방법?
- 없다!
- 단 원래 계획보다 가능한 빨리 실행하라고 독촉하는 방법은 있음
 System.gc() 메서드 호출
- 그런데 바로 실행할 지 나중에 실행할 지 그 시점을 보장하지는 않음

 

 

ㄴ int[] no = new int[5];  --> no 라는 5크기의 배열을 선언 (주소 : 200 => 1개)

ㄴ int[] no2 = no;  -->no2 라는 배열을 no 의 주소값을 갖도록 선언 (주소 : 200 => 2개)

ㄴ no = new int[3];  -->no 배열을 3크기의 배열로 새로 선언 (주소 : 200 => 1개, 주소 : 300 => 1개)

=> no 배열은 주소 200을 잃어버리고 300 이라는 새로운 주소로 할당됨

ㄴ no2 = no;  --> no2 배열은 no 주소 값을 할당 받음 (주소 : 200 => 0개, 주소 : 300 => 2개)

=> 주소 200은 사용 불가능한 주소이므로 Garbage 값이 됨

 

 

변수의 종류

인스턴스 변수 (non-static 변수)

ㄴ 클래스의 인스턴스(객체)에 속하는 변수

ㄴ 인스턴스 변수에서는 변수를 필드(field)라고도 부름

ㄴ 필드(field) 는 클래스에 직접적으로 선언된 변수

클래스 변수 (static 변수)

ㄴ 클래스 자체에 속하는 변수로서, 해당 클래스의 모든 인스턴스들에 의해 공유되는 변수

ㄴ 클래스 변수에서는 변수를 필드(field)라고도 부름

ㄴ 필드(field) 는 클래스에 직접적으로 선언된 변수

Local 변수

ㄴ 지역 범위 내에서 선언되고 사용되는 변수

ㄴ 해당 블록 내에서만 유효하며, 블록을 벗어나면 사용할 수 없음

ㄴ Local 변수에서는 변수를 필드(field)라고 부를 수 없음

 

//# 변수의 종류
public class Exam0710 {

  int a; // 인스턴스 변수

  static int b; // 클래스 변수 == 스태틱 변수


  public static void main(String[] args/*로컬변수=파라미터*/) {

    int c; // 로컬 변수

  }
}

 

public class Exam0720 {
  public static void main(String[] args) {

    //1) 인스턴스 변수를 사용하는 방법
    MyClass obj1 = new MyClass(); // new 명령이 실행될 때 변수가 준비되미
    System.out.println(obj1.a); // 인스턴스를 가리키는 레퍼런스를 통해 변수를 사용할 수 있음

    //2) 클래스 변수를 사용하는 방법
    System.out.println(MyClass.b); // 클래스를 사용(로딩)하는 시점에 준비됨


    //3) 로컬 변수를 사용하는 방법
    MyClass.m1(100); // 메서드를 실행할 때 변수가 준비됨 (메서드를 실행하는 동안만)

    // 메서드 호출이 끝나면, 그 메서드의 로컬 변수를 사용할 방법이 없음
    //    v1 = 300; // 오류!
    // 메서드를 실행하는 동안, 그 메서드 안에서만 사용할 수 있음
  }
}

 

인스턴스 변수(instance variable)
- new 명령을 사용하여 인스턴스를 생성할 때 준비되는 변수

클래스 변수(class variable = static variable)
- 클래스가 로딩될 때 준비되는 변수

로컬 변수(local variable)
- 블록을 실행할 때 준비되는 변수

파라미터(parameter)
- 메서드의 아규먼트를 받는 로컬 변수
  예) 위의 코드에서 main()의 args 로컬 변수

 

argument 와 parameter

ㄴ argument 는 넘겨주는 값을 의미함

ㄴ parameter 는 argument 를 받는 변수를 의미함

ㄴ argument 와 parameter 는 실무에서 혼용하여 쓰는 경우가 많음

 

 

 

 

 

 

Wrapper 클래스

ㄴ primitive 타입을 더 정교하게 다룰 수 있는 클래스 (function 묶음/method 묶음)

ㄴ 기본 데이터(primitive) 타입의 값을 객체로 포장하고, 추가적인 메소드와 기능을 제공

ㄴ 클래스명.기능(); 으로 사용가능

 

 

리터럴 연산 결과와 값 할당

ㄴ byte 타입의 변수 b 선언 시 b 는 1byte 크기를 할당받음

ㄴ 4 + 5 를 할 경우 정수와 정수의 덧셈이므로 int 연산을 함 => 결과값도 int

=> 리터럴과 리터럴을 연산할 경우 연산 결과도 리터럴임

리터럴과 변수의 연산 결과는 리터럴이 아님

 

자바의 정수 연산은 기본이 int

ㄴ byte 와 byte 연산을 하여도 int 결과값이 도출됨

 

# 산술 연산자 : 기본 연산 단위

public class Exam0130 {
  public static void main(String[] args) {

    byte b;
    b = 4; // OK!
    b = 5; // OK!
    // - 리터럴 4, 5은 4바이트 정수 값
    // - 정수 리터럴은 기본이 4바이트 크기이지만,
    // - byte 변수에 저장할 수 있다면 허락함!

    b = 4 + 5; // OK!
    // - 리터럴끼리 산술 연산한 결과도 리터럴로 간주
    // - 그 결과 값이 변수의 범위 내의 값이면 허락함

    System.out.println(b);
    // - 이유? 리터럴 값은 컴파일 단계에서 그 값이 얼마인지 확인할 수 있기 때문
    // - 변수의 경우는 컴파일 단계에서 값을 확인할 수 없음

    byte x = 4, y = 5, z;
    z = x; // OK!
    z = y; // OK!

    // z = x + y; // 컴파일 오류!

    // "자바의 정수 연산은 최소 단위가 4바이트"
    // "그래서 byte나 short의 연산 단위가 기본으로 4바이트"

    // - 자바는 정수 변수에 대해 산술 연산을 수행할 때,
    // 그 변수의 값이 4바이트 보다 작다면(byte나 short 라면),
    // 4바이트로 만들어 연산을 수행
    // - 즉 임시 4바이트 정수 메모리를 만든 다음에 그 메모리에 값을 담은 후에 연산을 수행
    // - 따라서 x + y는 바로 실행하지 않고 임의의 4바이트 정수 메모리를 만든 다음에
    // 그 메모리에 x와 y 값을 넣고 연산을 수행
    // - 연산 결과도 당연히 4바이트
    // 그래서 4바이트 값을 1바이트 메모리에 넣지 못하기 때문에 컴파일 오류가 발생하는 것

    // short도 마찬가지
    short s1 = 4;
    short s2 = 5;
    short s3;
    s3 = s1; // OK!
    s3 = s2; // OK!
    // s3 = s1 + s2; // 컴파일 오류!

    int s4 = s1 + s2;
    System.out.println(s4);
    // 이유?
    // - byte 경우와 마찬가지고 short 메모리의 값을 직접 연산할 수 없음
    // - 임시 4바이트 메모리를 만든 다음에 그 메모리에 값을 저장한 후 연산을 수행
    // - 당연히 그 연산 결과를 담은 메모리도 4바이트이기 때문에
    // short(2byte) 메모리에 저장할 수 없는 것

  }
}

// 결론
// - 숫자의 크기에 상관없이 작은 숫자를 다루더라도 정수를 다룰 때는 그냥 int를 사용하자
// - byte는 보통 파일의 데이터를 읽어 들일 때 사용

ㄴ 변수와 변수 계산을 못 하는 이유 => 메모리 이슈

 

형변환 (type conversion = type casting)

명시적 형변환

ㄴ int 형 정수 나누기 int 형 정수의 결과 값을 float 변수에 저장할 때 각각을 모두 float 형으로 명시적 형변환을 하여(각각 5.0, 2.0) 임시 float 변수에 담은 후 나누기 연산을 함

=> 5 / 2 --> 5.0 / 2.0 = 2.5

 

명시적 형변환
- 큰 메모리의 값을 작은 메모리로 변환 할 때
- 부동소수점을 정수로 변환 할 때
- 문법
변수 = (바꾸고자하는타입) 변수 또는 값;

 

암시적 형변환

같은 type 끼리만 연산 가능!

ㄴ int 형 정수 나누기 float 형 정수의 결과 값을 float 변수에 저장할 때 int 형 정수 값을 float 형으로 암시적 형변환을 하여 (5 -> 5.0) 임시 float 변수에 담은 후 나누기 연산을 함

=> 암시적 형변환은 (float) 을 직접 추가하지 않아도 프로그래밍 언어에서 자동으로 타입을 변환하는 것

 

# 산술 연산자 : 연산의 결과 타입

public class Exam0141 {
  public static void main(String[] args) {

    // 연산을 수행한 후 생성된 결과도 피연산자와 같은 타입

    int i = 5;
    int j = 2;
    float r = i / j; // int와 int의 연산 결과는 항상 int
    // 따라서 r 변수에 넣기 전에 이미 결과는 정수 2가 됨
    // 정수 2를 float 변수에 넣으면 출력할 때 2.0이 됨
    System.out.println(r);

    // 해결책
    // - 변수에 들어 있는 값을 다른 타입으로 바꾸기
    // "형변환(type conversion=type casting)"하기
    r = (float) i / (float) j; // float / float = float
    // i / j의 값은 2.5가 되고 r에 저장되는 것은 2.5
    System.out.println(r);

    // 물론 두 개의 정수 값 중 한 개만 float으로 형변환해도 됨
    // why?
    // => 연산을 수행할 때 나머지 변수가 암시적 형변환이 이루어지기 때문
    r = i / (float) j;
    System.out.println(r);
  }
}

 

- int와 int의 연산 결과는 int
- float과 float의 연산 결과는 float
- 즉 연산 결과는 피연산자의 타입과 같음
- 그래서 두 값을 계산했을 때 결과 메모리를 초과할 경우 값이 짤릴 수 있음
주의하자
- 코드를 작성할 때 피연산자의 계산 결과가 피연산자의 메모리 크기를 벗어날 가능성이 있다면,
  처음부터 피연산자의 값을 더 큰 메모리에 담아서 연산을 수행하자
  
  
형변환(type casting=type conversion)?
- 변수나 리터럴을 다른 타입의 값을 바꾸는 것
- 주의
원래 변수의 타입을 바꾸는 것이 아님
내부적으로는 변수에 들어 있는 값을 꺼내 지정된 타입의 임시 메모리를 만들어 저장함


결론
1) 자바의 최소 연산 단위는 int
따라서 int 보다 작은 크기의 메모리 값을 다룰 때는 내부적으로 int로 자동 형변환을 수행한 다음에 연산을 수행
내부적으로 자동 형변환하는 것을 "암시적 형변환(implicit type conversion)"이라 부름
 => byte + byte = int
 => short + short = int
 => byte + short = int

2) 연산 결과는 항상 피연산자의 타입과 같음
 => int + int = int
 => long + long = long
 => float + float = float
 => double + double = double

3) 다른 타입과 연산을 수행할 때는
내부적으로 같은 타입으로 맞춘 다음에 실행

 

암시적 형변화나 규칙 (implicit type casting)

ㄴ long -> float : 값이 잘려도 long 을 float 으로 변경

 

# 관계 연산자 : 부동소수점 비교 
package com.eomcs.lang.ex05;

public class Exam0220 {
  public static void main(String[] args) {
    double d1 = 987.6543;
    double d2 = 1.111111;
    System.out.println((d1 + d2) == 988.765411);
    // 결과는 false
    // 이유
    // - 부동소수점 값을 IEEE 754 명세에 따라 2진수로 바꿔 메모리에 담을 때
    // 정규화(소수점 이하의 수를 2진수로 바꾸는) 과정에서 정수로 딱 떨어지지 않는 경우가 있음
    // 즉 극한의 미세 소수점이 붙을 수 있음
    // - CPU나 OS, JVM의 문제가 아님
    // - IEEE 754 명세에 따라 부동소수점을 처리하는 모든 컴퓨터에서 발생하는 문제
    // - 이런 부동소수점을 계산할 때 기대하는 값과 다른 값이 나올 수 있음
    // - 또한 연산한 결과를 메모리에 담을 때도 정규화 과정에서 극한의 미세 소수점이 붙을 수 있음
    System.out.println(d1);
    System.out.println(d2);
    System.out.println(d1 + d2);
    // 987.6543 + 1.111111 = 988.7654110000001
    // => 결과 뒤에 극소수의 값이 붙음
    // => 그래서 부동 소수점의 비교를 대충 다루지 않아야 함
    // 0 10000001000 1110110111010011110000000001101000110110111000101111 (987.6543)
    // 0 01111111111 0001110001110001110001010011111100111001110100011011 (1.111111)
    //
    // 1.1110110111010011110000000001101000110110111000101111
    // 0.0000000010001110001110001110001010011111100111001110
    // ---------------------------------------------------------
    // 1.1110111001100001111110001111110011010110011111111101
    //
    // 0 10000001000 1110111001100001111110001111110011010110011111111101
    // 0 10000001000 1110111001100001111110001111110011010110011111111101

    double x = 234.765411;
    double y = 754.0;
    System.out.println((x + y) == 988.765411);

    System.out.println(x);
    System.out.println(y);
    System.out.println(x + y);
    // d1 + d2와 달리 x + y의 계산 결과는 뒤에 극소수의 값이 붙지 않음 => 극한의 값이 무조건 붙는 것은 아님
    // 234.765411 + 754.0 = 988.765411
    //
    // 0 10000000110 1101010110000111111000111111001101011001111111110101
    // (234.765411)
    // 0 10000001000 0111100100000000000000000000000000000000000000000000 (754.0)
    //
    // 0.0111010101100001111110001111110011010110011111111101
    // 1.0111100100000000000000000000000000000000000000000000
    // ------------------------------------------------------
    // 1.1110111001100001111110001111110011010110011111111101
    //
    // 0 10000001000 1110111001100001111110001111110011010110011111111101
    //

    // IEEE 754의 변환 공식에 따라 발생되는 이런 문제를 실무 프로그래밍 할 때 해결하는 방법
    System.out.println((d1 + d2) == (x + y)); // false

    // 소수점 뒤에 붙은 극소수의 값을 무시하면 됨
    // => JVM이 자동으로 처리하지 않음
    // => 다음과 같이 개발자가 직접 처리해야 함
    double EPSILON = 0.00001;
    System.out.println(Math.abs((d1 + d2) - (x + y)) < EPSILON);
  }
}

 

** Math.abs(양수   음수)

ㄴ abs : 절대값

=> 양수 -> 양수 / 음수 -> 양수

 

# 산술 연산자 : 데이터 타입과 연산자

public class Exam0140 {
  public static void main(String[] args) {
    // 모든 종류의 데이터에 대해 산술 연산자를 사용할 수 있는 것은 아님
    // 데이터 타입에 따라 제공되는 연산자가 다름

    System.out.println(5.75 % 0.24); // OK!
    // System.out.println(true % false); // boolean 타입에 대해서는 산술 연산자를 사용할 수 없음
    // System.out.println(true + true); // 컴파일 오류

    System.out.println("Hello," + "world!"); // OK! '+' 연산자는 문자열 연결 용도로 사용됨
    // System.out.println("Hello," - "o,"); // 컴파일 오류
    // System.out.println("Hello," * 5); // 컴파일 오류

    System.out.println(true && true); // 양쪽 모두 boolean 타입이어야 && 연산 가능
    // System.out.println(10 && 10); // 컴파일 오류 => 정수 값을 논리적으로 연산하려면 조건문이나 비트 연산자 등을 사용해야 함
  }
}

 

# 산술 연산자 : 연산의 결과 타입

public class Exam0143 {
  public static void main(String[] args) {

    float f1 = 987.6543f;
    float f2 = 1.111111f;
    System.out.println(f1);
    System.out.println(f2);

    float r1 = f1 + f2;
    // f1과 f2에 들어 있는 값이 유효자릿수라 하더라도
    // 연산 결과가 유효자릿수가 아니라면 값이 잘리거나 반올림 됨
    // => float과 float의 연산 결과는 float이기 때문
    // 987.6543
    // + 1.111111
    // ---------------
    // 988.765411 <=== float의 유효자릿수인 7자리를 넘어감
    // 988.7654 <=== 유효자릿수를 넘어가는 수는 짤림

    System.out.println(r1);
    // 기대값: 987.6543 + 1.111111 = 988.765411
    // 결과값: 988.7654
    // 결과가 옳지 않게 나온 이유
    // => float과 float의 연산 결과는 float이므로 메모리 크기를 넘어가는 뒤의 11은 짤림

    // 그럼 결과를 담을 변수의 크기를 늘리면 되는가?
    double r2 = f1 + f2;
    System.out.println(r2);
    // 기대값: 988.765411
    // 결과값: 988.765380859375
    // 기대한 결과가 나오지 않은 이유?
    // => float과 float의 연산 결과는 float임
    // => double 변수에 저장하기 전에 이미 float 값이 되면서 일부 값이 왜곡됨

    // r1 변수와 달리 뒤에 이상한 숫자가 많이 붙는 이유
    // => IEEE 754의 이진수 변환 문제 때문
    // => 4바이트 float 부동소수점을 8바이트 double 부동소수점 변수에 저장할 때 왜곡된 값이 들어 갈 수 있음
    // => float을 double 변수에 넣을 때 왜곡이 발생하기 때문에
    // 가능한 double 변수로 값을 바꾼 다음에 연산을 수행하자
    // 더 좋은 것은 처음부터 double 변수를 사용하자

    // 다음과 같이 처음부터 double 변수를 사용하자
    double d1 = 987.6543; // => 처음부터 double 로 정의하하자
    double d2 = 1.111111;
    double r5 = d1 + d2; // = 988.765411
    System.out.println(r5);
    // 그럼에도 실제 출력해보면 맨 뒤에 극한의 작은 수가 붙음
    // 이유 => IEEE 754 이진수 변환 문제임
    // 맨 뒤에 붙은 극한의 작은 수는 그냥 잘라 버림
  }
}

 

# 산술 연산자 : 암시적 형변환과 연산 우선순위

public class Exam0160 {
  public static void main(String[] args) {
    float r1 = 5 / 2 + 3.1f;
    // 계산 순서:
    // r1 = int(5) / int(2) + float(3.1);
    // r1 = int(2) + float(3.1);
    // r1 = float(2.0) + float(3.1)
    // r1 = float(5.1)
    //
    // => 연산 우선 순위에 따라 계산하는 순간에 암시적 형변환이 이루어짐
    // => 모든 값을 최종 결과 타입으로 바꾸고 계산하지는 않음
    System.out.println(r1);

    float r2 = 3.1f + 5 / 2; // --> 암시적 형변환은 연산자 우선으로 실행
    // 계산 순서
    // r2 = float(3.1) + int(5) / int(2)
    // r2 = float(3.1) + int(2)
    // r2 = float(3.1) + float(2.0)
    // r2 = float(5.1)
    System.out.println(r2);
  }
}

 

 

증감 연산자 : 후위(post-fix) 증가 연산자

# 증감 연산자 : 후위(post-fix) 증가 연산자

public class Exam0610 {

  static void f1(int x, int y) {
    int b = 5;
    // int temp = b;
    // b += 1;
    // System.out.println(temp);
    System.out.println(b++);
  }

  static void f2(int x, int y) {
    int b = 5;

    // b += 1;
    // System.out.println(temp);
    System.out.println(++b);
  }

  public static void main(String[] args) {
    int i = 2;

    // 증감 연산자가 없다면, 기존 변수의 값을 1증가시키기 위해 다음과 같이 코딩해야 함
    // i = i + 1;

    // 증감 연산자를 사용하면 다음과 같이 간략하게 작성할 수 있음
    i++; // i => 3
    // 현재 위치에 i 메모리에 들어 있는 값(2)을 꺼내 놓음
    // 그런 다음에 i 메모리의 값을 1 증가시킴
    // 결론:
    // ==> i++ 문장은 컴파일러가 i = i + 1 문장으로 바꿈
    // ==> 즉 i = i + 1 문장을 축약한 문법에 불과함

    i++; // i => 4

    System.out.println(i); // 4

    System.out.println(i++); // 4
    // 위의 코드는 컴파일 할 때 다음의 코드로 바뀜

    // int temp = i; //<-- 임시 변수를 만들어 현재 i 값을 저장
    // i = i + 1;
    // System.out.println(temp);

    System.out.println(i); // 5

  }
}

 

JVM Bytecode Viewer 를 이용하여 컴파일된 .class 파일로 증감 연산자 실행 순서 확인

ㄴ Extensions > bytecode viewer 설치

 

ㄴ 컴파일 한 클래스 파일(.class) 우클릭 > Show Bytecode (Verbose) 선택

 

Exam0610.java 파일

package com.eomcs.lang.ex05;

//# 증감 연산자 : 후위(post-fix) 증가 연산자
//
public class Exam0610 {

  static void f1(int x, int y) {
    int b = 5;
    // int temp = b;
    // b += 1;
    // System.out.println(temp);
    System.out.println(b++);
  }

  public static void main(String[] args) {
    int i = 2;

    // 증감 연산자가 없다면, 기존 변수의 값을 1증가시키기 위해 다음과 같이 코딩해야 함
    // i = i + 1;

    // 증감 연산자를 사용하면 다음과 같이 간략하게 작성할 수 있음
    i++; // i => 3
    // 현재 위치에 i 메모리에 들어 있는 값(2)을 꺼내 놓음
    // 그런 다음에 i 메모리의 값을 1 증가시킴
    // 결론:
    // ==> i++ 문장은 컴파일러가 i = i + 1 문장으로 바꿈
    // ==> 즉 i = i + 1 문장을 축약한 문법에 불과함

    i++; // i => 4

    System.out.println(i); // 4

    System.out.println(i++); // 4
    // 위의 코드는 컴파일 할 때 다음의 코드로 바뀜
    //
    // int temp = i; //<-- 임시 변수를 만들어 현재 i 값을 저장함
    // i = i + 1;
    // System.out.println(temp);

    System.out.println(i); // 5

  }
}

 

 

Exam0610.class 파일에서 f1 메서드 부분 확인

Classfile /Users/yangsoyul/git/bitcamp-study/java-lang/app/bin/main/com/eomcs/lang/ex05/Exam0610.class
  Last modified 2023. 6. 2.; size 523 bytes
  SHA-256 checksum 3a82a6f9d9accc208b754d613da10460894d58bcd23fc7d985582eca6deb2dd7
  Compiled from "Exam0610.java"
public class com.eomcs.lang.ex05.Exam0610
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #19                         // com/eomcs/lang/ex05/Exam0610
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Class              #10            // java/lang/System
   #9 = NameAndType        #11:#12        // out:Ljava/io/PrintStream;
  #10 = Utf8               java/lang/System
  #11 = Utf8               out
  #12 = Utf8               Ljava/io/PrintStream;
  #13 = Methodref          #14.#15        // java/io/PrintStream.println:(I)V
  #14 = Class              #16            // java/io/PrintStream
  #15 = NameAndType        #17:#18        // println:(I)V
  #16 = Utf8               java/io/PrintStream
  #17 = Utf8               println
  #18 = Utf8               (I)V
  #19 = Class              #20            // com/eomcs/lang/ex05/Exam0610
  #20 = Utf8               com/eomcs/lang/ex05/Exam0610
  #21 = Utf8               Code
  #22 = Utf8               LineNumberTable
  #23 = Utf8               f1
  #24 = Utf8               (II)V
  #25 = Utf8               main
  #26 = Utf8               ([Ljava/lang/String;)V
  #27 = Utf8               SourceFile
  #28 = Utf8               Exam0610.java
{
  public com.eomcs.lang.ex05.Exam0610();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

  static void f1(int, int);
    descriptor: (II)V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iconst_5	// --> 스택에 상수 5를 넣음
         1: istore_2	// --> 스택의 값을 로컬 변수 2에 저장(istore_2 : 로컬 변수 2에 값을 저장)
         2: getstatic     #7	// Field java/lang/System.out:Ljava/io/PrintStream;
				// --> System.out의 PrintStream 객체를 가져옴(getstatic : 정적 필드에 접근하는 명령어) 
         			// --> java/lang/System.out은 표준 출력을 나타내는 정적 필드.
	 5: iload_2		// --> 로컬 변수 2의 값을 스택으로 가져옴(iload_2 : 로컬 변수 2의 값을 스택으로 가져옴)
         6: iinc          2, 1		// 로컬 변수 2의 값을 1만큼 증가시킴(iinc : 정수형 로컬 변수의 값을 증가시키는 명령어)
         9: invokevirtual #13	// Method java/io/PrintStream.println:(I)V
				// --> 스택의 값을 PrintStream 객체의 println 메소드의 인자로 전달하여 값을 출력
                        	// --> invokevirtual : 인스턴스 메소드를 호출하는 명령어 
                        	// --> println:(I)V : PrintStream 클래스의 println 메소드를 호출하는 표기법
                        	// --> println 메소드는 int 값을 출력
	12: return	// --> 메소드 실행을 종료하고 반환

      LineNumberTable:
        line 8: 0
        line 12: 2
        line 13: 12

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_2
         1: istore_1
         2: iinc          1, 1
         5: iinc          1, 1
         8: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_1
        12: invokevirtual #13                 // Method java/io/PrintStream.println:(I)V
        15: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_1
        19: iinc          1, 1
        22: invokevirtual #13                 // Method java/io/PrintStream.println:(I)V
        25: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_1
        29: invokevirtual #13                 // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 24: 0
        line 31: 2
        line 38: 5
        line 40: 8
        line 42: 15
        line 49: 25
        line 51: 32
}
SourceFile: "Exam0610.java"

ㄴ "변수 1"은 main 메소드 내에서 선언된 로컬 변수 i를 가리킴

=> 따라서 istore_1은 i 변수의 값을 스택 프레임의 인덱스 1에 저장하는 역할을 함 

 

 	 0: iconst_5	// --> 스택에 상수 5를 넣음
         1: istore_2	// --> 스택의 값을 로컬 변수 2에 저장(istore_2 : 로컬 변수 2에 값을 저장)
         2: getstatic     #7	// Field java/lang/System.out:Ljava/io/PrintStream;
				// --> System.out의 PrintStream 객체를 가져옴(getstatic : 정적 필드에 접근하는 명령어) 
         			// --> java/lang/System.out은 표준 출력을 나타내는 정적 필드.
	 5: iload_2		// --> 로컬 변수 2의 값을 스택으로 가져옴(iload_2 : 로컬 변수 2의 값을 스택으로 가져옴)
         6: iinc          2, 1		// 로컬 변수 2의 값을 1만큼 증가시킴(iinc : 정수형 로컬 변수의 값을 증가시키는 명령어)
         9: invokevirtual #13	// Method java/io/PrintStream.println:(I)V
				// --> 스택의 값을 PrintStream 객체의 println 메소드의 인자로 전달하여 값을 출력
                        	// --> invokevirtual : 인스턴스 메소드를 호출하는 명령어 
                        	// --> println:(I)V : PrintStream 클래스의 println 메소드를 호출하는 표기법
                        	// --> println 메소드는 int 값을 출력
	12: return	// --> 메소드 실행을 종료하고 반환

 

 

ㄴ 스택에 5를 넣고, istore_2 (변수 2 => 변수 b)를 이용해 로컬 변수 2에 스택 값을 저장하고, System.out 의 PrintStream 객체를 가져옴

ㄴ 로컬 변수 2의 값을 스택으로 가져옴

ㄴ 로컬 변수  2의 값을 1만큼 증가시킴

ㄴ 스택의 값을 printStream 객체의 println 메소드의 인자로 전달하여 값을 출력

 

int b = 5;

System.out.println(b++);

ㄴ 이 코드는 아래와 같은 순서로 실행됨을 알 수 있음

 

int b = 5;
    
int temp = b;
b += 1;
System.out.println(temp);

ㄴ 변수 b에 5를 두고, 임시 변수 temp에 b값을 저장해둔 후 b를 1 증가시킴

ㄴ temp에 저장한 원래 b 값인 5를 출력함

 

후위 연산자와 전위 연산자의 내부 실행 차이

static void f1(int x, int y) {
    int b = 5;
    // int temp = b;
    // b += 1;
    // System.out.println(temp);
    System.out.println(b++);
  }
  
static void f2(int x, int y) {
    int b = 5;

    // b += 1;
    // System.out.println(temp);
    System.out.println(++b);
}