공부/Java

자바 기본형과 참조형

Stair 2024. 8. 29. 16:23
반응형

자바에서 참조형을 제대로 이해하는 것은 정말 중요하다고 한다.

어제 배운 기본형과 참조형에 관하여 막연하게 이해하고만 있기 때문에 오늘 복습을 통해 제대로 이해하고 정리하려 한다.

 

변수의 데이터 타입은 가장 크게 보면 기본형과 참조형으로 분류할 수 있다. 사용하는 값을 변수에 직접 넣을 수 있는 기본형, 그리고 이전에 만들어 봤던 Student student1과 같이 객체가 저장된 메모리 위치를 가르키는 참조값을 넣을 수 있는 참조형으로 분류할 수 있다.

 

기본형(Primitive Type) : int, long, boolean, double 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터타입을 기본형이라고 한다.

참조형(Reference Type) : Student student1, int[] students 와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 참조형이라고 한다. 참조형은 객체 또는 배열에 사용된다.

 

기본형은 10, 20과 같이 실제 사용하는 값을 변수에 담는다.

참조형은 실제 사용하는 값을 변수에 담는 것이 아니라 이름 그대로 실제 객체의 위치*참조, 주소 를 저장한다. 참조형에는 객체와 배열이 있다.

객체는 "."을 통해 메모리 상에 생성된 객체를 찾아가야 사용할 수 있다.

배열은 []을 통해 메모리 상에 생성된 배열을 찾아가야 사용할 수 있다.

 

그렇기 때문에 참조형은 참조값을 그대로 사용할 수 없다. 참조값은 객체를 가르키고 있는 주소이기 때문이다.

주소지에 가야 실체가 있다.

 

+ 기본형은 모두 소문자로 시작한다 ex)int, boolean ,double ,long 등 모두 소문자

   참조형 및 클래스는 대문자이다. 클래스는 모두 참조형이다

 

 

참고-String : 자바에서 String은 특별하다. String도 사실 클래스이기때문에 참조형이다. 문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공한다.

 

 

"대원칙 : 자바는 항상 변수의 값을 복사해서 대입한다."

 

자바에서 변수에 값을 대입하는 것은 변수에 들어있는 값을 복사해서 대입하는 것이다.

기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입한다. 기본형이면 변수에 들어있는 실제 값을 복사해서 대입하고, 참조형이면 변수에 들어 있는 참조값을 복사해서 대입한다.

 

public static void main(String[] args) {
    int a = 10;
    int b = a;

    System.out.println("a = " + a);
    System.out.println("b = " + b);

    a = 20;
    System.out.println("변경 a = 20");
    System.out.println("a = " + a);
    System.out.println("b = " + b);

    b = 30;
    System.out.println("변경 b = 30");
    System.out.println("a = " + a);
    System.out.println("b = " + b);
}

 

위의 코드를 보자

int a = 10; a를 10으로 초기화해주었고

int b = a; 로 b에 a의 값을 대입해주었다.

 

위에서 말했듯이 자바에서 대입은 값을 복사해서 붙혀넣는 것이다.

 

따라서

a = 10;

b = 20; 이 된다.

 

a = 20;으로 a에 20을 대입하여도 b는 여전히 10이고

b = 30;으로 b에 30을 대입하여도 a는 20이다.

a 와 b는 독립적으로 수행된다.

 

 

반면 참조형은 다른 결과가 나타나게 된다.

public static void main(String[] args) {

    Data dataA = new Data();
    dataA.value = 10;
    Data dataB = dataA;

    System.out.println("dataA 참조값 = " + dataA);
    System.out.println("dataB 참조값 = " + dataB);
    System.out.println("dataA.value = " + dataA.value);
    System.out.println("dataB.value = " + dataB.value);

    dataA.value = 20;
    System.out.println("dataA.value = " + dataA.value);
    System.out.println("dataB.value = " + dataB.value);

    dataB.value = 30;
    System.out.println("dataA.value = " + dataA.value);
    System.out.println("dataB.value = " + dataB.value);

}

 

Data dataA = new Data(); 에서 new Data()객체 생성 후 객체에 대한 참조값(주소)를 dataA에 대입하는 것이기에(x001로 가정한다)

dataA.value = 10;은 x001.value = 10;을 대입한 것이고

Data dataB는 dataB에 Data의 인스턴스인 dataA의 참조값을 대입하였기에 dataB 또한 x001이다.

 

현재 dataA = x001, dataB = x001

 

참조값도 동일하기 때문에 dataA.value의 값이나 dataB.value 은 같은 곳을 가리키고 있으므로 서로 영향을 미친다.

dataA.value == x001.value

dataB.value == x001.value

 

 

 

기본형과 참조형은 메서드 호출에 따라서 달라진다.

 

"대원칙 : 자바는 항상 변수의 값을 복사해서 대입한다"

메서드 호출도 마찬가지이다. 메서드를 호출할 때 사용하는 매개변수(파라미터)도 결국 변수일 뿐이다. 메서드를 호출할 때 매개변수에 값을 전달하는 것도 앞서 설명한 내용과 같이 값을 복사해서 전달한다.

 

public static void main(String[] args) {
    int a = 10;
    System.out.println("메서드 호출 전 a = " + a);
    changePrimitive(a);
    System.out.println("메서드 호출 후 a = " + a);
}

public static void changePrimitive(int x){
    x = 20;
}

 

실행 결과는 호출 전이나 호출 후나 a = 10이다.

 

changePrimitive(a)를 넣어도 이 a는 10을 복사해서 대입한 것이기 때문에 a가 변하지 않는다.

 

참조형 메서드는 다음과 같다.

 

 

public static void main(String[] args) {
    Data dataA = new Data();
    dataA.value = 10;
    System.out.println("메서드 호출 전 dataA.value = " + dataA.value);

    changeReference(dataA);
    System.out.println("메서드 호출 후 dataA.value = " + dataA.value);
}

public static void changeReference(Data data){
    data.value = 20;
}

 

위와 같은 참조형 메서드는 메서드를 호출하면 DataA.value의 값이 

 

10에서 20으로 변화한다.

Data dataA = new Data();에서 참조값이 x001이라 가정하였을 때

 

changeReference(Data data); 는 참조값 x001이 대입되었고

x001.value 값을 20으로 변경시켜주었기 때문에

메서드가 종료 되어도 dataA.value의 값은 20이 되기 때문이다.

 

 

 

자바에서 메서드의 매개변수는 항상 값에 의해 전달된다. 그러나 이 값이 실제 값이냐, 참조 값이냐에 따라 동작이 달라진다.

기본형 : 메서드로 기본형 데이터를 전달하면, 해당 값이 복사되어 전달된다. 이 경우, 메서드 내부에서 파라미터의 값을 변경해도, 호출자의 변수 값에는 영향이 없다.

참조형 : 메서드로 참조형 데이터를 전달하면, 참조값이 복사되어 전달된다. 이 경우, 메서드 내부에서 "파라미터로 전달된 객체의 멤버변수를 변경하면 호출자의 객체도 변경된다."

 

다음은 Student 클래스와 그에 따른 예제이다.

public class Student {
    String name;
    int age;
    int grade;
}

 

Student 클래스에는 name과 age, grade가 선언되어있다.

 

public static void main(String[] args) {


    Student student1;
    student1 = new Student();
    student1.name = "학생1";
    student1.age = 15;
    student1.grade = 90;

    Student student2;
    student2 = new Student();
    student2.name = "학생2";
    student2.age = 16;
    student2.grade = 80;

    Student[] students = new Student[2];
    students[0] = student1;
    students[1] = student2;

    for (int i = 0; i<students.length; i++){
        System.out.println("이름: " + students[i].name + "나이: " + students[i].age
                + " 성적: " + students[i].grade);
    }

}

 

이전에는 객체를 하나씩 만들고 변수를 하나하나 대입해주었다면

이번에는 printStudent라는 메소드를 만들어서 main문을 더 간결하게 작성할 수 있다.

 

 

public static void main(String[] args) {

    Student student1 = new Student();
    initStudent(student1,"학생1", 15, 90);

    Student student2 = new Student();
    initStudent(student2,"학생2", 16, 80);

    printStudent(student1);
    printStudent(student2);


}



static void initStudent(Student student, String name, int age, int grade){
    student.name = name;
    student.age = age;
    student.grade = grade;
}

static void printStudent(Student student){
    System.out.println("이름: " + student.name + " 나이: "
            + student.age + " 성적: " + student.grade);
}

 

 

initStudent(Student student, String name, int age, int grade)는 sout을 해주는 메서드로 Student 타입의 참조값을 받고, 다른 매개변수인 name, age, grade를 받은 후 참조값이 가리키는 변수에 대입시켜 주는 메서드이다.

 

printStudent(Student student)는 출력을 해주는 메서드로 Student 타입의 참조값을 받아서 그 참조값에 해당하는 name, age, grade를 print 해주는 메서드이다.

 

 

Student student1 = new Student();
initStudent(student1,"학생1", 15, 90);

Student student2 = new Student();
initStudent(student2,"학생2", 16, 80);

 

또한 위와 같은 main 코드의 내용을 더 간소화 할 수 있다.

 

createStudent 라는 메소드를 만들어서 간소화 시킨 코드는 다음과 같다.

 

public static void main(String[] args) {

    Student student1 = createStudent("학생1", 15, 90);

    Student student2 = createStudent("학생2", 16, 80);

    printStudent(student1);
    printStudent(student2);


}

static Student createStudent(String name, int age, int grade){
    Student student = new Student();
    student.name = name;
    student.age = age;
    student.grade = grade;
    return student;

}


static void printStudent(Student student){
    System.out.println("이름: " + student.name + " 나이: "
            + student.age + " 성적: " + student.grade);
}

 

static Student createStudent(String name, int age, int grade)를 보면 Student 타입의 메소드 안에서 객체를 선언하는

Student student = new Student(); 가 있다. //x001이라고 가정한다.

이 메소드 안에 참조값을 이용하여 값을 전부 대입 해준 뒤, return을 통하여 x001을 반환한다.

Student 타입이기에 Student 타입 student1을 만들어 메서드를 받고(참조값 x001) 기존에 작성되었던 printStudent를 통해 print를 한다.

 

 

 

 

변수의 종류는 멤버 변수, 지역변수가 있다.

멤버 변수 : 클래스에 선언

지역 변수 : 메서드에 선언, 매개변수도 지역 변수의 한 종류이다.

 

지역 변수는 이름 그대로 특정 지역에서만 사용되는 변수이다. 메서드가 끝나면 제거된다.

 

멤버 변수 -> 자동 초기화

인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화된다.

 

지역 변수 -> 수동 초기화

지역 변수는 항상 직접 초기화해야한다.

public class InitData {
    int value1;
    int value2 = 10;
}

 

public static void main(String[] args) {
    InitData data = new InitData();

    System.out.println("data.value1 = " + data.value1);
    System.out.println("data.value2 = " + data.value2);
}

 

InitData의 인스턴스 data를 만들고 value 값을 찍어보면 value1은 초기화 해주지 않았음에도 0이라는 결과값을 볼 수 있다. 즉 인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화 된다.

 

 

 

nll 

참조형 변수에는 항상 객체가 있는 위치를 가리키는 참조값이 들어간다. 그런데 아직 가리키는 대상이 없거나, 가리키는 대상을 나중에 입력하고 싶다면 null 이라는 값을 넣어둘 수 있다. null은 값이 존재하지 않는, 없다는 뜻이다.

 

public class Data {
    int value;
}

Data라는 클래스를 만들고

public static void main(String[] args) {
    Data data = null;
    System.out.println("1.data = " + data);

    data = new Data();
    System.out.println("2. data = " + data);

    data = null;
    System.out.println("3. data = " + data);

    data = new Data();
    System.out.println("4. data = " + data);

}

 data에 null 값을 할당하면 null이 표시된다.

 

메모리를 null로 해제하고 다시 할당하면 주소가 바뀌게 된다. 해당 인스턴스에 다시 접근할 방법이 없기 때문이다.

객체는 해당 객체를 참조하는 곳이 있으면,JVM이 종료할때까지 계속 생존한다.

 

GC-더 이상 사용하지 않는 인스턴스를 자동으로 메모리에서 제거해준다.

 

 

만약 참조값 없이 객체를 찾아가면 NullPointerException이란 예외가 발생한다. 이름 그대로 Null을 가리키면(Pointer) 발생하는 예외(Exception)이다. null은 없다는 뜻이므로 결국 주소가 없는 곳을 찾아갈 때 발생하는 예외이다.

 

public static void main(String[] args) {
    Data data = null;
    data.value = 10; //null.value가 된다
    System.out.println("data = " + data.value);;
}

 

위와 같은 코드는 null에다가 . 을 찍어 value를 대입하는데 이러한 경우 NullPointerException이 발생하게 된다.

 

public class Data {
    int value;
}

 

public class BigData {
    Data data; //참조형은 초기값이 null
    int count; //int 초기값이 0
}

 

public static void main(String[] args) {
    BigData bigData = new BigData();
    System.out.println("bigData.count = " + bigData.count);
    System.out.println("bigData.data = " + bigData.data);

    System.out.println("bigData.data.value = " + bigData.data.value);
}

 

 

다음과 같은 상황에서 실수가 자주 나온다.

bigData의 멤버 변수들은 따로 초기화 되지 않은 상태이기에

Data data는 null로

int count 는 0으로 자동 초기화 되었다.

 

main에서 data와 count를 찍어보면 당연히 null과 0이 나오겠지만

bigData.data.value는 bigData.data가 null이기 때문에 null.value가 되어 참조할 곳이 없으므로 NullPointerException예외가 발생한다.

 

public static void main(String[] args) {
    BigData bigData = new BigData();
    bigData.data = new Data();
    System.out.println("bigData.count = " + bigData.count);
    System.out.println("bigData.data = " + bigData.data);


    System.out.println("bigData.data.value = " + bigData.data.value);
}

 

 

bigdata.data 가 참조할 Data(); 참조값을 주어 NullPointerException 에러를 해결한다.

 

NullPointerException이 발생하면 null 값에 .을 찍었다고 생각하면 문제를 쉽게 찾을 수 있다.

반응형

'공부 > Java' 카테고리의 다른 글

자바 생성자  (1) 2024.08.30
자바 객체 지향 프로그래밍  (0) 2024.08.30
자바 클래스와 데이터  (1) 2024.08.29
자바 객체 지향 프로그래밍과 절차 지향 프로그래밍  (0) 2024.08.29
자바 Null  (0) 2024.08.28