기본형 vs 참조형1 - 시작

변수의 데이터 타입

  • 기본형(Primitive Type): 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입
    ex) int, long, double, boolean
    - 해당 값을 바로 사용할 수 있다.
    - 들어있는 값을 그대로 계산에 사용할 수 있다.
    - 기본형은 개발자가 새로 정의할 수 없다.
    - 소문자로 시작한다. (int, long, double, boolean)
  • 참조형(Reference Type): 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입. 참조형은 객체 또는 배열에 사용된다.
    ex) Student student1, int[ ] students
    - 객체는 . (dot)을 통해서, 배열은 [ ]를 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있다.

    - 들어있는 값인 참조값은 계산에 그대로 사용할 수 없다.
    cf. . (dot) 을 통해 객체의 기본형 멤버 변수에 접근한 경우에는 연산을 할 수 있다.
    - 개발자는 참조형인 클래스를 직접 정의할 수 있다.
    - 클래스는 대문자로 시작한다. 클래스는 모두 참조형이다.

cf) String

String은 사실 클래스다. 따라서 String은 참조형이다. 그런데 기본형처럼 문자 값을 바로 대입할 수 있는데, 문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공하는 것이다.

 


기본형 vs 참조형2 - 변수 대입

자바는 항상 변수의 값을 복사해서 대입한다.
기본형: 변수에 들어 있는 실제 값을 복사해서 대입
참조형: 변수에 들어 있는 참조값을 복사해서 대입

 


기본형과 변수 대입

package ref;

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라고 했을 때 변수에 들어있는 값을 복사해서 전달하기 때문에 각각 본인의 값만 변경된다.


참조형과 변수 대입

package ref;

public class Data {
    int value;
}
package ref;

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 변수는 Data 클래스를 통해서 만들었기 때문에 참조형이다. 이 변수는 Data형 객체의 참조값을 저장한다.

Data 객체를 생성하고, 참조값을 dataA에 저장한다. 그리고 객체의 value 변수에 값 10을 저장했다.

 

 

변수 dataA에는 참조값 x001이 들어 있다. 여기서는 변수 dataA에 들어 있는 참조값 x001을 복사해서 변수 dataB에 대입한다. 이제 dataA와 dataB에 들어 있는 참조값이 같다. 따라서 둘 다 같은 x001 Data 인스턴스를 가리킨다.

 

dataA.value = 20 코드를 실행하면 dataA가 가리키는 x001 인스턴스의 value 값을 10에서 20으로 변경한다. 그런데 dataA 와 dataB는 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataB.value는 둘 다 같은 값인 20을 출력한다.

 

dataB.value = 30 코드를 실행하면 dataB가 가리키는 x001 인스턴스의 value 값을 20에서 30으로 변경한다. 그런데 dataA 와 dataB는 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataB.value는 같은 값인 30을 출력한다.

 

[ 핵심 ]

Data dataB = dataA 라고 했을 때 변수에 들어있는 값을 복사해서 사용한다. 그런데 그 값이 참조값이다. 따라서 dataA와 dataB는 같은 참조값을 가지게 되고, 두 변수는 같은 객체 인스턴스를 참조하게 된다.

 


기본형 vs 참조형3 - 메서드 호출

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

 

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

 

기본형과 메서드 호출

package ref;

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

실행 결과

 

1. 메서드 호출

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

int x = a

자바에서 변수에 값을 대입할 때, 항상 값을 복사해서 대입한다. 따라서 변수 a, x 각각 숫자 10을 가지고 있다.

 

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

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

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

 

3. 메서드 종료

메서드 종료 후 값을 확인해보면 a는 10이 출력된다. 참고로 메서드가 종료되면 매개변수 x는 제거된다.

 

 

참조형과 메서드 호출

package ref;

public class Data {
    int value;
}
package ref;

public class MethodChange2 {
    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);
    }
    static void changeReference(Data dataX) {
        dataX.value = 20;
    }
}

실행 결과

 

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

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

1. 메서드 호출

메서드를 호출할 때 변수 dataA의 값을 매개변수 dataX에 전달한다. 이 코드는 다음과 같이 해석할 수 있다.

Data dataX = dataA

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

따라서 변수 dataA, dataX 둘 다 같은 참조값인 x001을 가지게 된다.

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

 

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

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

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

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

 

3. 메서드 종료

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

 

기본형과 참조형의 메서드 호출 정리
  • 기본형(Primitive Type): 메서드로 기본형 데이터를 전달하면 해당 값이 복사되어 전달된다.
    이 경우, 메서드 내부에서 매개변수(파라미터)의 값을 변경해도 호출자의 변수 값에는 영향이 없다.
    ※ A가 B에게 1이라는 숫자가 써 있는 종이를 넘겨준다. A와 B는 각각 1이라는 종이를 가지게 된다. 그런데 B가 1을 2로 바꾼다고 해서 A가 2를 갖게 되는 것이 아니다. A는 1, B는 2의 종이를 각각 갖게 된다.
  • 참조형(Reference Type): 메서드로 참조형 데이터를 전달하면 참조값이 복사되어 전달된다.
    이 경우, 메서드 내부에서 매개변수(파라미터)로 전달된 객체의 멤버 변수를 변경하면 호출자의 객체도 변경된다.
    ※ A가 B에게 어떤 건물의 주소가 써 있는 종이를 넘겨준다. A와 B는 각각 같은 주소의 종이를 가지게 된다. 그 건물에는 1이라는 변수가 있다. A가 그 건물에 가서 1을 2로 바꿔 적으면, B도 숫자 2를 보게 된다. 마찬가지로 B가 그 건물에 가서 2를 3으로 바꿔 적으면, A도 숫자 3을 보게 된다.

 


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

package class1;

public class ClassStart3 {
    public static void main(String[] args) {
        Student student1 =new Student();
        student1.name = "학생1";
        student1.age = 15;
        student1.grade = 90;

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

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

  1. name, age, grade를 할당하는 부분
  2. 학생 정보를 출력하는 부분

이런 중복은 메서드를 통해 제거할 수 있다.

 


메서드에 객체 전달

package ref;

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

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

    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);
    }
}
  1. initStudent 메서드: 전달한 학생 객체의 필드에 값을 설정
  2. printStudent 메서드: 전달한 학생 객체의 필드 값을 읽어서 출력

 

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

 

코드를 변경해보자.

package ref;

public class Method2 {
    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(); //x001 생성
        student.name = name; //x001의 name = "학생1"
        student.age = age; //x001의 age = 15
        student.grade = grade; //x001의 grade = 90
        return student; //x001값을 위로 반환
    }

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

 

createStudent( ) 라는 메서드를 만들고 객체를 생성하는 부분도 이 메서드 안에 함께 포함했다. 이제 이 메서드 하나로 객체의 생성과 초기값 설정을 모두 처리한다.

그런데 메서드 안에서 객체를 만들었기 때문에 만들어진 객체를 메서드 밖에서 사용할 수 있게 돌려주어야 한다. 그래야 메서드 밖에서도 이 객체를 사용할 수 있다. 메서드는 호출 결과를 반환(return)할 수 있다. 메서드의 반환 기능을 사용해서 만들어진 객체의 참조값을 메서드 밖으로 반환하면 된다.

 

[createStudent( ) 메서드 분석]

메서드 내부에서 인스턴스 생성 → 참조값을 메서드 외부로 반환

이 참조값은 student1이 보관하게 되었다.

 

진행과정

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

 

createStudent( )는 생성한 Student 인스턴스의 참조값을 반환한다. 이렇게 반환된 참조값을 student1 변수에 저장했다.

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

 


변수와 초기화

변수의 종류
  • 멤버 변수(필드): 클래스에 선언
  • 지역변수: 메서드에 선언 (매개변수도 지역변수의 한 종류이다.) - 특정 지역에서만 사용되는 변수로, 해당 메서드가 끝나면 제거됨

 

변수의 값 초기화
  • 멤버 변수(필드): 자동 초기화
    - 인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화된다.
    - int(숫자) = 0, boolean = false, 참조형 = null
    - 개발자가 초기값을 직접 지정할 수 있다.
  • 지역 변수: 수동 초기화
    - 지역 변수는 항상 직접 초기화해야 한다.

 

[멤버 변수의 초기화]

package ref;

public class InitData {
    int value1; //초기화 하지 않음
    int value2 = 10; //10으로 초기화
}
package ref;

public class InitMain {
    public static void main(String[] args) {
        InitData data = new InitData();
        System.out.println("value1 = " + data.value1);
        System.out.println("value2 = " + data.value2);
    }
}

실행 결과

멤버 변수는 자동으로 초기화되기 때문에 value1값에 초기값을 지정하지 않았지만 숫자라서 0으로 초기화된다.

value2는 10으로 초기값을 지정해두었기 때문에 10으로 초기화된다.

 

 


null

택배를 보낼 때 제품은 준비가 되었지만 보낼 주소지가 아직 결정되지 않아서 주소지가 결정될 때까지는 주소지를 비워두어야 할 수 있다.

참조형 변수에는 항상 객체가 있는 위치를 가리키는 참조값이 들어간다. 그런데 아직 가리키는 대상이 없거나, 가리키는 대상을 나중에 입력하고 싶다면 어떻게 해야할까?

참조형 변수에서 아직 가리키는 대상이 없다면 null이라는 특별한 값을 넣어둘 수 있다. null은 값이 존재하지 않는, 없다는 뜻이다.

 

null 값 할당

package ref;

public class Data {
    int value;
}
package ref;

public class NullMain1 {
    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);
    }
}

실행 결과

  1. Data data = null;
    Data 타입을 받을 수 있는 참조형 변수 data를 만들고, 여기에 null 값을 할당했다. 따라서 data 변수는 아직 가리키는 객체가 없다.
  2. data = new Data( );
    이후에 새로운 객체(인스턴스)를 생성해서 그 참조값을 data 변수에 할당했다. 이제 data 변수는 참조할 객체가 존재하며, 그 객체의 참조값은 1d81eb93이다.
  3. data = null;
    다시 data에 null 값을 할당했다. 이렇게 하면 data 변수는 앞서 만든 참조값이 1d81eb93인 객체(인스턴스)를 더 이상 참조하지 않는다.
  4. data = new Data( );
    다시 data에 새로운 객체(인스턴스)를 생성하고 그 참조값을 data 변수에 할당했다. data 변수는 이전에 참조했던 참조값이 아닌 새로운 참조값으로 Data 객체(인스턴스)를 참조한다. 새로운 참조값은 7291c18f이다.

 

GC(garbage collection)

코딩을 하다 보면 유효하지 않은 메모리인 가비지(garbage)가 발생하게 되는데, 자바의 GC(가비지 컬렉션)는 이러한 가비지를 자동으로 메모리에서 제거해준다.

 

data = new Data( ); 코드로 참조값이 1d81eb93인 객체를 가리키게 되었는데, data = null; 값을 부여함으로 인해 참조값이 1d81eb93인 객체는 더 이상 아무도 참조하지 않게 된다. 이렇게 아무도 참조하지 않게 되면 1d81eb93이라는 참조값을 다시 구할 방법이 없다. 따라서 해당 인스턴스에 다시 접근할 방법이 없다. 아무도 참조하지 않는 인스턴스는 사용되지 않고 메모리 용량만 차지할 뿐이다.

C와 같은 과거 프로그래밍 언어는 개발자가 직접 명령어를 사용해서 인스턴스를 메모리에서 제거해야 했지만, 자바는 이러한 과정을 자동으로 처리해준다. 아무도 참조하지 않는 인스턴스가 있으면 JVM의 GC(가비지 컬렉션)가 더 이상 사용하지 않는 인스턴스라 판단하고 해당 인스턴스를 자동으로 메모리에서 제거해준다.

 

 


NullPointerException

택배를 보낼 때 주소지 없이 택배를 발송하면 어떤 문제가 발생할까?

참조값 없이 객체를 찾아가면 NullPointerException이라는 예외가 발생한다.

객체를 참조할 때 . (dot)을 사용하는데, 참조값이 null이라면 찾아갈 수 있는 객체(인스턴스)가 없다. NullPointerException은 이처럼 null에 . (dot)을 찍었을 때 발생한다.

null.~~

 

[예제]

package ref;

public class Data {
    int value;
}
package ref;

public class NullMain2 {
    public static void main(String[] args) {
        Data data = null;
        data.value = 10; //NullPointerException 예외 발생
        System.out.println("data = " + data.value);
    }
}

실행 결과: 컴파일 오류(NullPointerException)

data 변수에 null 값이 들어 있는데 data.value = 10이라고 하면 NullPointerException 오류가 발생한다.

data.value = 10
null.value = 10 //data에는 null 값이 들어있다.

 

 

멤버 변수와 null

1. Data 클래스

package ref;

public class Data {
    int value; //0
}

 

2. BigData 클래스

package ref;

public class BigData {
    Data data; //null
    int count; //0
}

 

3. 메인 메서드

package ref;

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

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

실행 결과

 

[예외 발생 과정]

bigData.data.value
x001.data.value //bigData는 x001 참조값을 가진다.
null.value //x001.data는 null 값을 가진다.
NullPointerException //null 값에 .(dot)을 찍으면 예외가 발생한다.

 

[문제 해결]

Data 인스턴스를 만들고 BigData.data 멤버 변수에 참조값을 할당하면 된다.

package ref;

public class NullMain4 {
    public static void main(String[] args) {
        BigData bigData = new BigData();
        bigData.data = new Data(); // 클래스가 Data인 인스턴스 생성 → bigData.data에 참조값 부여됨
        System.out.println("bigData.count=" + bigData.count);
        System.out.println("bigData.data=" + bigData.data);

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

실행 결과

 

실행 과정

bigData.data.value
x001.data.value //bigData는 x001 참조값을 가진다.
x002.value //x001.data는 x002 값을 가진다.
0 // 최종 결과

+ Recent posts