본문 바로가기

Java

[Java] 기본형 VS 참조형

자바를 배우기로 선택했다면 참조형을 제대로 이해하는 것은 정말 중요합니다.

 

변수의 데이터 타입을 가장 크게 보면 기본형참조형으로 분류할 수 있습니다.

사용하는 값을 변수에 직접 넣을 수 있는 기본형, 그리고 이전에 본 Student student1과 같이 객체가 저장된 메모리의 위치를 가르키는 참조값을 넣을 수 있는 참조형으로 분류할 수 있습니다.

  • 기본형(Primitive Type): int, long, double, boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입을 기본형이라고 한다.
  • 참조형(Reference Type): Student student1, int[ ] students와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는    데이터 타입을 참조형이라 한다. 참조형은 객체 또는 배열에 사용된다.

기본형 변수에는 직접 사용할 수 있는 값이 들어있지만 참조형 변수에는 위치(참조값)가 들어있습니다.

참조형 변수를 통해서 뭔가 하려면 결국 참조값을 통해 해당 위치로 이동해야 합니다.

 

기본형 VS 참조형

기본

  • 기본형은 숫자 10, 20과 같이 실제 사용하는 값을 변수에 담을 수 있다. 그래서 해당 값을 바로 사용할 수 있다.
  • 참조형은 실제 사용하는 값을 변수에 담는 것이 아니다. 이름 그대로 실제 객체의 위치(참조, 주소)를 저장한다.             참조형에는 객체와 배열이 있다.
  • 객체는 .(점)을 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있다.
  • 배열은 [ ]을 통해서 메모리 상에 생성된 배열을 찾아가야 사용할 수 있다.

계산

  • 기본형은 들어있는 값을 그대로 계산에 사용할 수 있다.
  • ex) 더하기, 빼기, 곱하기 (숫자 같은 것들은 바로 계산할 수 있음)
  • 참조형은 들어있는 참조값을 그대로 사용할 수 없다. 주소지만 가지고는 할 수 있는게 없다. 주소지에 가야 실체가 있다!
  • ex)  참조값만 가지고는 더하고, 뺄수 없음

기본형은 연산이 가능하지만, 참조형은 연산이 불가능하다.

기본형은 변수에 실제 사용하는 값이 담겨있기 때문에 +, - 같은 연산이 가능하다.

int a = 10, b = 25;
int sum = a + b;

 

참조형은 변수에 객체의 위치인 참조값이 들어있다. 참조값은 계산에 사용할 수 없다. 따라서 오류가 발생한다.

Student s1 = new Student();
Student s2 = new Student();
s1 + s2 // 오류 발생

 

물론 .(점)을 통해서 기본형 멤버 변수에 접근한 경우에는 연산을 할 수 있다.

Student s1 = new Student();
s1.grade = 100;
Student s2 = new Student();
s2.grade = 200;

int sum = s1.grade + s2.grade; // 연산 가능

 

기본형을 제외한 나머지는 모두 참조형입니다.

  • 기본형은 소문자로 시작한다. int, long, double, boolean 모두 소문자로 시작한다.
  • 기본형은 자바가 기본적으로 제공하는 데이터 타입이다. 이러한 기본형은 개발자가 새로 정의할 수 없다. 개발자는      참조형인 클래스만 직접 정의할 수 있다.
  • 클래스는 대문자로 시작한다.
  • 클래스는 모두 참조형이다.
String
자바에서 String은 특별하다. String은 사실은 클래스다. 따라서 참조형이다. 그런데 기본형처럼 문자 값을 바로        대입할 수 있다. 문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공한다. 

 

 

변수 대입

 

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

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

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

 

기본형 대입

int a = 10;
int b = a;

 

참조형 대입

Student s1 = new Student();
Student s1 = x001 // 주소값
Studnet s2 = s1;

 

기본형은 변수에 값을 대입하더라도 실제 사용하는 값이 변수에 바로 들어있기 때문에 해당 값만 복사해서 대입한다고

생각하면 쉽게 이해할 수 있습니다. 그런데 참조형의 경우 실제 사용하는 객체가 아니라 객체의 위치를 가르키는 참조값만 복사됩니다. 쉽게 이야기해서 실제 건물이 복사 되는 것이 아니라 건물의 위치인 주소만 복사되는 것입니다.

따라서 같은 건물을 찾아갈 수 있는 방법이 하나 늘어날 뿐입니다.

 

public class VarChange1 {

    public static void main(String[] args) {

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

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

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

    }
}

 

핵심은 int b = a 라고 했을 때 변수에 들어있는 값을 복사해서 전달한다는 점입니다.

따라서 a = 20, b = 30이라고 했을 때 각각 본인의 값만 변경되는 것을 확인할 수 있습니다.

 

참조형과 변수 대입

다음 코드를 보고 어떤 결과가 나올지 예측해봅시다.

public class Data {

    int value; // 멤버 변수(필드)
}
public class VarChange2 {

    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 변경
        dataA.value = 20;
        System.out.println("변경 dataA.value = 20");
        System.out.println("dataA.value = " + dataA.value);
        System.out.println("dataB.value = " + dataB.value);

        // dataB 변경
        dataB.value = 30;
        System.out.println("변경 dataB.value = 30");
        System.out.println("dataA.value = " + dataA.value);
        System.out.println("dataB.value = " + dataB.value);
    }
}

 

여러분들이 예상한대로 값이 나왔나요?

dataA 참조값 = ref.Data@2f4d3709
dataB 참조값 = ref.Data@2f4d3709
dataA.value = 10
dataB.value = 10
변경 dataA.value = 20
dataA.value = 20
dataB.value = 20
변경 dataB.value = 30
dataA.value = 30
dataB.value = 30

 

변수의 대입은 변수에 들어있는 값을 복사해서 대입합니다.

변수 dataA에는 참조값 ref.Data@2f4d3709가 들어있습니다.

여기서 변수 dataA에 들어있는 참조값 ref.Data@2f4d3709을 복사해서 변수 dataB에 대입합니다.

참고로 변수 dataA가 가르키는 인스턴스를 복사하는 것이 아닙니다!

변수에 들어있는 참조값만 복사해서 전달하는 것 입니다.

이제 dataAdataB에 들어있는 참조값은 같습니다.

따라서 둘다 같은 Data 인스턴스를 가르키게 됩니다.

 

dataAdataB는 같은 인스턴스를 참조하기 때문에  dataA.valuedataB.value는 둘다 같은 값인 20을 출력합니다.

 

메서드 호출

지금까지는 기본형과 참조형의 변수 대입을 알아보았습니다.

이번에는 기본형과 참조형이 메서드 호출에 따라서 어떻게 달라지는지 알아보겠습니다.

 

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

복사해서 대입하고, 참조형이면 변수에 들어 있는 참조값을 복사해서 대입합니다.

 

메서드 호출도 마찬가지 이겠죠?

메서드를 호출할 때 사용하는 매개변수(파라미터)도 결국 변수일 뿐입니다.

따라서 메서드를 호출할 때 매개변수에 값을 전달하는 것도 값을 복사해서 전달합니다.

 

기본형과 메서드 호출

public class MethodChange1 {

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

static void changePrimitive(int x) {
        x = 20;
    }
}
메서드 호출 전: a = 10
메서드 호출 후: a = 10

 

 

1. 메서드 호출

메서드를 호출할 때 매개변수 x에 변수 a의 값을 전달합니다.

이 코드는 다음과 같이 해석할 수 있습니다.

int x = a

 

자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다.

a, x 각각 숫자 10을 가지고 있다.

 

 

2. 메서드 안에서 값을 변경

메서드 안에서 x = 20 으로 새로운 값을 대입합니다.

결과적으로 x의 값만 20으로 변경되고, a의 값은 10으로 유지됩니다.

 

 

3. 메서드 종료

메서드 종료 후 값을 확인해보면 a는 10이 출력되는 것을 확인할 수 있습니다.

참고로 메서드가 종료되면 매개변수 x는 제거됩니다.

 

참조형과 메서드 호출

이번에는 참조형 변수의 메서드 호출에 대해 알아봅시다.

다음 메서드 호출 코드를 보고 결과를 예측해봅시다.

public class Data {
    int value; // 멤버 변수(필드)
}
public class MethodChange2 {

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

   public static void changeReference(Data dataX) {
        dataX.value = 20;
    }
}
메서드 호출 전: dataA = 10
메서드 호출 후: dataA = 20

 

Data 인스턴스를 생성하고, 참조값을 dataA 변수에 담고 value에 숫자 10을 할당한 상태

1. 메서드 호출

 

메서드를 호출할 때 매개변수 dataX에 변수 dataA의 값을 전달한다.

이 코드는 다음과 같이 해석할 수 있습니다.

Data dataX = dataA

 

자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다.

변수 dataA는 참조값 x001을 가지고 있으므로 참조값을 복사해서 전달했습니다.

이제 dataX를 통해서도 x001에 있는 Data 인스턴스에 접근할 수 있습니다.

 

 

2. 메서드 안에서 값을 변경

 

메서드 안에서 dataX.value = 20으로 새로운 값을 대입합니다.

참조값을 통해 x001 인스턴스에 접근하고 그 안에 있는 value의 값을 20으로 변경했습니다.

dataA, dataX 모두 같은 x001 인스턴스를 참조하기 때문에 dataA.valuedataB.value는 둘다 20 이라는 값을 가집니다.

 

 

3. 메서드 종료

 

 

메서드 종료 후 dataA.value의 값을 확인해보면 20으로 변경된 것을 확인할 수 있습니다.

 

 

 

기본형과 참조형의 메서드 호출

 

자바에서 메서드의 파라미터는 항상 값에 의해 전달됩다.

그러나 이 값이 실제 값이나, 참조(메모리주소)값이냐에 따라 동작이 달라지게 됩니다.

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

 

참조형과 메서드 호출 - 활용

아래 코드에는 중복되는 부분이 2가지 있습니다.

  • name, age, grade 에 값을 할당
  • 학생 정보를 출력
public class Student {

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

    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;

        System.out.println("이름: " + student1.name + " 나이:" + student1.age + " 성적: " + student1.grade);
        System.out.println("이름: " + student2.name + " 나이:" + student2.age + " 성적: " + student2.grade);
    }
}

 

이런 중복은 메서드를 통해 손쉽게 제거할 수 있습니다.

 

 

메서드에 객체 전달

public class Method1 {

    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);

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

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

 

참조형은 메서드를 호출할 때 참조값을 전달합니다.

따라서 메서드 내부에서 전달된 참조값을 통해 객체의 값을 반환하거나, 값을 읽어서 사용할 수 있습니다.

  • initStudent(Student student, ... ): 전달한 학생의 객체의 필드에 값을 설정한다.
  • printStudent(Student student, ... ): 전달한 학생 객체의 필드 값을 읽어서 출력한다.

initStudent( ) 메서드 호출 분석

public static void initStudent(Student student, String name, int age, int grade){
        student.name = name;
        student.age = age;
        student.grade = grade;
    }
  • student1 이 참조하는 Student 인스턴스에 값을 편리하게 할당하고 싶어서 initStudent( ) 메서드를 만들었다.
  • 이 메서드를 호출하면서 student1을 전달한다. 그러면 student1의 참조값이 매개변수 student에 전달된다. 이 참조값을 통해 initStudent( ) 메서드 안에서 student1이 참조하는 것과 동일한 x001 Student 인스턴스에 접근하고 값을 변경할 수 있다.

 

메서드에서 객체 반환

 

다음 코드에도 중복이 있습니다.

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

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

 

바로 객체를 생성하고, 초기값을 설정하는 부분입니다.

 

public class Method2 {

    public static void main(String[] args) {

        Student student1 = createStudent("학생1", 15, 90); 
        Student student2 = createStudent("학생2", 16, 80);

        printStudent(student1); // x001
        printStudent(student2); // x002
    }
    public static Student createStudent(String name, int age, int grade){
        Student student = new Student();
        student.name = name;
        student.age = age;
        student.grade = grade;
        return student;
    }

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

 

  • createStudent( )라는 메서드를 만들고 객체를 생성하는 부분도 이 메서드 안에 함께 포함했다.
  • 이제 이 메서드 하나로 객체의 생성과 초기값을 모두 처리한다.
  • 그런데 메서드 안에서 객체를 생성했기 때문에 만들어진 객체를 메서드 밖에서 사용할 수 있게 돌려주어야 한다.
  • 그래야 메서드 밖에서 이 객체를 사용할 수 있다. 메서드 호출 결과를 반환(return)할 수 있다.
  • 메서드의 반환 기능을 사용해서 만들어진 객체의 참조값을 메서드 밖으로 반환하면 된다.

 

creatStudent( ) 메서드 호출 분석

Student student1 = createStudent("학생1", 15, 90);
public static Student createStudent(String name, int age, int grade){
        Student student = new Student();
        student.name = name;
        student.age = age;
        student.grade = grade;
        return student;
    }

 

메서드 내부에서 인스턴스를 생성한 후에 참조값을 메서드 외부로 반환했습니다.

이 참조값만 있으면 해당 인스턴스에 접근할 수 있습니다.

 

진행과정

Student student1 = createStudent("학생1", 15, 90); // 메서드 호출 후 결과 반환
Student student1 = student; // 참조형인 student를 반환
Student student1 = x001; // student의 참조값 대입
student1 = x001;

 

createStudent( )는 생성한 Student 인스턴스의 참조값을 반환합니다.

이렇게 반환된 참조값을 student1 변수에 저장했습니다.

앞으로는 student1을 통해 Student 인스턴스를 사용할 수 있습니다.

 

참조값을 확인해보고 싶다면 출력해보면 된다.

public class Method2 {

    public static void main(String[] args) {

        Student student1 = createStudent("학생1", 15, 90);
        System.out.println("student1 = " + student1);
        Student student2 = createStudent("학생2", 16, 80);
        System.out.println("student2 = " + student2);

        printStudent(student1); // x001
        printStudent(student2); // x002
    }
    public static Student createStudent(String name, int age, int grade){
        Student student = new Student();
        System.out.println("student = " + student);
        student.name = name;
        student.age = age;
        student.grade = grade;
        return student;
    }

    public static void printStudent(Student student) { // x001, x002
        System.out.println("이름: " + student.name + " 나이:" + student.age + " 성적: " + student.grade);
    }
}
student = ref.Student@2f4d3709
student1 = ref.Student@2f4d3709
student = ref.Student@7291c18f
student2 = ref.Student@7291c18f

 

createStudent( )로 생성한 Student 인스턴스 참조값이 서로 다른 것을 확인할 수 있습니다.