05-1 / 참조 타입과 참조 변수

자바의 타입은 크게 기본 타입(Primitive Type)과 참조 타입(Reference Type)으로 분류된다.

기본 타입은 이 포스팅에서 정리 (https://littlezero48.tistory.com/93) 한 것처럼 정수, 실수, 문자, 논리 리터럴을 저장하는 타입을 말하고

참조 타입이란 객체(object)의 번지를 참조하는 타입으로 배열, 열거, 클래스, 인터페이스를 말한다.

 

 

기본 타입과 참조 타입) 

기본타입 선언 변수와 참조타입 선언 변수의 차이점은 저장되는 값이다. 

기본타입의 변수를 실제 값을 변수에 저장하지만, 참조타입의 변수는 메모리 번지를 변수안에 저장한다.

번지를 통해 객체를 참조한다는 뜻에서 참조 타입이라 명명되었다.

 

다음과 같은 변수들이 있다.

// 기본타입
int a = 100;
double b = 200.0;

// 참조타입
String c = "스트링"
String d = "참조타입"

기본타입 변수와 참조타입 변수가 생성되는 스택에서 이 변수 값들을 보면 

(참고로 숫자는 예시)

a 100
b 200.0
c 100
d 200

이 들어있다. 

a와 b는 분명 내가 넣은 값인데 c와 b는 넣지 않은 이상한 값이 들어있다. 이 값은 무엇일까? 바로 이 값이 변수들의 값을 가지고 있는 String 객체의 주소 즉, 번지들이다.

 

 

 

 


메모리 사용 영역) 

그럼 스택은 뭐고 변수들이 참조한다는 번지는 어디의 번지일까?

이를 알려면 JVM이 운영체제로 부터 할당받아 사용하는 메모리 영역인 Runtime Data Area에 대해서 알아야한다.

출처: https://www.freecodecamp.org/news/jvm-tutorial-java-virtual-machine-architecture-explained-for-beginners/

 

이 중 참조 변수에 대해 알려면 우선 3영역을 알아보자

Method Area (=Class Area =Static Area)
이 영역은 JVM이 시작할 때 생성되고 모든 스레드가 공유하는 영역이다.
JVM이 뭔가를 실행하기 위해서는 먼저 바이트 코드들이 메모리 공간에 저장되어야 한다.
이를 위해 먼저 컴파일된 클래스(~.class) 파일들의 바이트 코드를 클래스 로더로 읽어들여 메소드 영역에 올리는데 이를 클래스 로딩이라고 하며 이후 메인 메소드를 호출하는 것으로 부터 시작한다. 메인 메소드를 호출하면 우리가 항상 메인 메소드에 쓰는 클래스와 static변수가 있는데 이게 바로 이 영역에 올라오게 되는 것이다.

메소드 영역에 바이트 코드들이 올라오게 되면 클래스별로 정적필드(static field), 상수(constant), 메소드 코드, 생성자(constructor) 코드 등으로 분류해서 저장한다.

참조 : (클래스 로딩 절차까지 소개) https://blog.wanzargen.me/16
Heap Area
힙 영역은 객체와 배열이 생성되는 영역이다. 다른 객체의 필드 나 JVM Stack Area의 변수가 참조하는 객체와 배열들이 위치한 곳이다. 
만약 이곳의 객체나 배열 중 참조되지 못하고 덩그러니 남아있는 객체는 쓰레기로 취급되며 Garbage Collector를 실행시켜 자동으로 제거한다. 따라서 개발자가 직접 제거하는 일은 없다.
Stack Area
JVM 스택은
메소드를 호출할때 마다 메소드 상태를 저장하기위한 Frame을 생성(push)하고
메소드가 종료되면 해당 Frame을 제거(pop)하는 동작을 수행한다.

이렇게 스택안에 생성되는 Frame 안에는 로컬 변수 스택이 있는데 여기에 기본타입 변수나 참조타입 변수가 push되거나 pop된다.

스택 영역에 변수가 생성되는 시점은 변수 초기화 때로 최초로 변수에 값이 저장될 때 이며, 로컬 변수가 영향력을 발휘하는 블록내에서만 스택에 존재하고 블록을 벗어나면 pop된다.

기본타입 변수는 이 스택 영역에 직접 값을 가지고 있지만,
참조타입 변수는 이 스택 영역에 힙 영역의 객체 주소를 가진다.

 

 

 

 


참조 변수의 ==, != 연산) 

기본타입 변수에서는 ==는 값이 같다, !=는 값이 다르다 의 단순한 논리로 정리되지만 

참조타입 변수에서는 ==가 동일한 객체인지(= 동일한 힙 번지 값을 가지는 것인지), !=는 동일한 객체가 아닌지 (= 다른 힙번지 값을 사용하는지)를 판별할 때 사용한다.

 

 

 

 

 


null 과 NullPointerException) 

 

* null

참조타입 변수는 힙 영역의 객체를 참조하지 않는 다는 뜻으로 null 값을 갖는다.

하지만 이 null 값 자체도 초기값으로 사용할 수 있기 때문에 null로 초기화된 함수 또한 stack 영역에 push된다.

null값인지 아닌지는 판별하는 것은 == null 과 != null 로 할 수 있다.

 

뿐만 아니라 이전에 다른 객체를 참조한 변수에 null 값을 넣어주어 더 이상 객체를 참조하지 않게 할 수 있다.

+ 참고로 참조를 잃은 객체는 jvm이 gabage collector을 구동시켜 메모리에서 제거한다.

 

 

* NullPointerException

자바는 프로그램 실행 도중에 발생하는 오류를 예외(Exception)이라고 한다.

참조변수에서 가장 많이 발생하는 예외 중 하나가 바로 NullPointerException 인데, 이 예외는 참조타입 변수를 잘못 사용 하면 발생한다. 

int[] intArray = null;
intArray[0] = 10;
// NullPointerException

String str = null;
System.out.println(str.length());
// NullPointerException

 변수 이름만 스택에 올라와 있을 뿐 예외 이름처럼 null은 아무것도 가르키고 있지 않기 때문에 값을 넣을 수도 그것으로 길이를 알수도 없다.

 

 


String 타입) 

String a = "스트링";

String 변수에 문자열을 저장한다고 표현하지만 엄밀히 말해서는 틀린말이다. 

String 객체가 생성되어 문자열을 저장하고 String 변수에는 그 String 객체 번지가 저장되는 것이기 때문 하지만 일반적으로는 String변수에 문자열을 저장한다고 한다.

 

자바는 문자열 리터럴이 동일하다면 String 객체를 공유하도록 되어있다.

String a = "객체";
String b = "객체";

System.out.println(System.identityHashCode(a));  // 객체번지 조회함수
System.out.println(System.identityHashCode(b));

if(a == b){
    System.out.println("일치");
} else {
    System.out.println("불일치");
}
// 1854778591
// 1854778591
// 일치

 

만약 같은 문자열 리터럴이더라도 다른 객체를 공유하게 할 수도 있는데 new 연산자를 사용해 String 객체를 생성하면 된다. 이와 같이 new 연산자는 힙 영역에 새로운 객체를 만들때 사용하는 객체 생성 연산자라고 한다.

만약 둘다 new 연산자로 새로운 객체를 만들었다면 그건 또 각각의 힙 영역에 새로운 객체를 따로 만들었기에 또 다른 객체가 된다.

String a = "객체";
String b = new String("객체");

System.out.println(System.identityHashCode(a));  // 객체번지 조회함수
System.out.println(System.identityHashCode(b));

if(a == b){
    System.out.println("일치");
} else {
    System.out.println("불일치");
}
// 1854778591
// 2054798982
// 불일치

 

만약 String 변수끼리 번지수 비교가 아니라 값 그자체를 비교하고 싶다면 equals() 메소드를 사용해야한다. 

String a = "객체";
String b = new String("객체");

if(a.equals(b)){
    System.out.println("일치");
} else {
    System.out.println("불일치");
}
// 일치

 

 


05-2 / 배열

배열이란?) 

변수는 하나에 한개의 데이터만을 저장할 수 있지만 배열은 여러 데이터를  적은 코드로 쉽게 다룰수있게 한다.

배열은 같은 타입의 데이터를 연속된 공간에 나열하고, 각 데이터에 index를 부여해놓은 자료구조이다.

  index 0 1 2 3  
score[]  = [ 10, 20, 30, 40 ];

 

배열의 각 인덱스는 각 항목 데이터를 읽거나 저장할때 사용하며 인덱스 번호를  []안에 넣어 사용한다.

int[] score = [10,20,30,40]
System.out.println(score[0]);
// 10

 

* 배열의 특징 )

- 같은 타이의 데이터만 저장할 수 있다.

- 한번 생성된 배열은 길이를 늘리거나 줄일 수 없다.

 

 

 


배열 선언) 

배열 선언의 형식은 2가지가 있다.

[]위치는 타입 선언 뒤에 붙을 수도 변수명 뒤에 붙을 수도 있다.

타입[] 변수; 타입 변수[];
int[] intArray;
double[] doubleArray;
String[] strArray;

int intArray[];
double doubleArray[];
String strArray[];

 

배열 변수도 참조 변수에 속하고 배열은 객체이므로 힙 영역에 생성되고 배열 변수에 힙 영역 주소를 참조하게 된다. 배열 객체가 없다면 배열 변수는 null로 초기화 할 수 있다. 하지만 null상태에서 인덱스로 값을 읽거나 저장하려면, 참조하는 객체가 없기에 NullPointerException이 발생한다.

 

 


배열 생성) 

배열 객체를 생성하려면 값 목록을 이용하거나 new 연산자를 이용하는 방법이 있다.

 

* 값 목록으로 배열 생성

아래와 같은 형태로 배열 객체를 생성할 수 있다.

타입[] 변수 = { 값0, 값1, 값2, 값3, .... };

중괄호는 주어진 값을 항목으로 가지는 배열 객체를 힙 영역에 생성하고 배열 객체 번지를 return하고 이 번지값이 변수에 저장됨으로써 참조가 이루어 진다.

 

특정 인덱스의 값을 변경하고 싶다면 = 대입 연산자를 이용하면 된다.

names[1] = "홍길동";

 

배열 객체를 생성할 때 주의해야할 것이 있는데 배열 변수를 이미 선언한 후에는 다른 실행문에서 중괄호를 이용한 배열 생성이 허용되지 않는다.

배열 변수를 미리 선언한 후 갑 목록이 나중에 결정되는 상황이라면 new 연산자를 사용해 값 목록을 지정해주면 된다.

변수 = new 타입[] { 값0, 값1, 값2, 값3 ..... }
String[] names = null;
names = new String[] {"홍길동", "이순신", "강감찬"}

 

이는 메소드의 매개값이 배열일 경우에도 마찬가지 이다. 

매개 변수로 int[] 배열이 선언된 메소드가 있는 경우에도 값 목록으로 배열을 생성함과 동시에 add()의 매개값으로 사용하고자 하면 반드시 new 연산자를 사용해야한다.***

public static void main(String[] args) {

    int[] scores;
    scores = new int[] {83, 90, 87};
    int sum1 = 0;
    for(int i=0; i<3; i++){
        sum1 += scores[i];
    }
    System.out.println("총합: " + sum1);

//  int sum2 = add( {95, 85, 90} ); // 컴파일 에러
    int sum2 = add( new int[] {83, 90, 87});
    System.out.println("총합: "+sum2);
}

public static int add(int[] scores){
    int sum = 0;
    for(int i=0; i<3; i++){
        sum += scores[i];
    }
    return sum;
}

 

* new 연산자로 배열 생성

값의 목록을 갖고있지는 않기만 향후 값들을 저장할 배열을 미리 만들고 싶다면 new 연산자로 다음과 같이 배열 객체를 생성할 수 있다.

타입[] 변수 = new 타입[길이];

길이는 배열이 저장할 수 있는 값의 개수. 이미 배열 변수가 선언된 경우에도 new 연산자로 배열을 생성할 수 있다.

타입[] 변수 = null;
변수 = new 타입[길이];

초기값은 타입마다 다르다.

참조타입 경우는 null이며 기본타입 경우 boolean은 false, char타입은 '\u0000',

byte, short, int 경우는 0, float는 0.0F, double은 0.0, long은 0L 이다. 

String[] arr1 = new String[3];
for(int i=0; i<3; i++){
    System.out.println("arr1["+i+"]: "+arr1[i]);
}
arr1[0] = "1월";
arr1[1] = "2월";
arr1[2] = "3월";
for(int i=0; i<3; i++){
    System.out.println("arr1["+i+"]: "+arr1[i]);
}
// arr1[0]: null
// arr1[1]: null
// arr1[2]: null
// arr1[0]: 1월
// arr1[1]: 2월
// arr1[2]: 3월

 

 


배열 길이) 

배열의 길이란 배열에 저장할 수 있는 전체 항목의 개수를 의미한다. 

필드는 객체 내부의 데이터를 의미하는데 배열의 길이를 얻어려면 다음과 같은 배열 객체의 length 필드를 읽는다

배열변수.length;
int[] intArray = { 10, 20, 30 };
int num = intArray.length;	// 3

length 필드는 읽기 전용 필드이기 때문에 값을 바꿀수 없다. 배열 length는 보통 for문에서 사용해서 배열 전체를 루핑할 때 유용하게 사용한다.

String[] arr1 = { "1월", "2월", "3월" };
for(int i=0; i<arr1.length; i++){
    System.out.println("arr1["+i+"]: "+arr1[i]);
}
// arr1[0]: 1월
// arr1[1]: 2월
// arr1[2]: 3월

 

 


명령 라인 입력) 

프로그램 실행을 위해 main() 메소드가 필요하다는 것을 안다. 하지만 main() 메소드의 매개값인 String[] args가 왜 필요한걸까 

public static void main(String[] args) { ... }

명령 라인(명령 프롬프트)에서 위 코드를 java 명령으로 실행하면 jvm은 길이가 0인 String 배열 args를 먼저 생성하고 man() 메소드를 호출할 때 매개값으로 전달한다. 

main() 메소드는 String[] args 매개 변수를 통해서 명령 라인에서 입력된 데이터의 수(배열의 길이)와 입력된 데이터(배열의 항목 값)을 알 수 있게 된다.

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

        if(args.length != 2){
            System.out.println("값의 수가 부족합니다");
            System.exit(0);
        }

        String strNum1 = args[0];
        String strNum2 = args[1];

        int num1 = Integer.parseInt(strNum1);
        int num2 = Integer.parseInt(strNum2);

        System.out.println(num1 + num2);

    }
}

명령 프롬프트에서 컴파일하고 실행에 값을 넣어 출력한 결과

 

 

--- 다차원 배열부터 다음 포스팅에서 

+ Recent posts