자바 기본형과 참조형 - 1

계리·2024년 2월 2일
0

기본형 vs 참조형1 - 시작

변수의 타입에는 크게 두 가지로 기본형과 참조형으로 나뉘어진다. 사용하는 값을 직접 변수에 보관하는 변수를 기본형, 사용하는 값의 위치 정보(메모리의 위치를 가리키는 참조 값 또는 주소 값)를 보관하고 있는 변수를 참조형

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

기본형 vs 참조형 - 기본

  • 기본형은 int a = 10, int b = 20 처럼 선언했을 경우 정수형(숫자) 변수 a에 실제 값 10을, b에 실제 값 20을 담고 이 값들을 바로 사용할 수 있다.
  • 참조형은 실제 사용하는 값을 가지고 있는 것이 아니라 실제 값을 가지고 있는 참조 값 또는 주소 값을 변수에 담고 있다.(앞서 계속 반복적으로 얘기하고 있는데 그만큼 중요하기 때문이다.) 이러한 참조형은 객체, 배열 등이 있다.
    • 객체는 .(점, dot)을 이용해서 객체가 가지고 있는 실제 값에 접근을 한다.
    • 배열은 []을 통해 메모리 상에 생성된 배열을 찾아가서 사용한다.

기본형 vs 참조형 - 계산

  • 기본형은 변수에 가지고 있는 값을 사칙연산을 통해 계산할 수 있다.
  • 참조형은 참조 값 또는 주소 값을 가지고 있는 것이기 때문에 사칙연산을 통해 계산할 수 없다. 참조 값을 가지고 실제 객체에 접근하여 객체가 가지고 있는 값을 가지고 사칙연산을 해야한다.

기본형 사칙연산 가능

int a = 10, b = 20;
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 = 90;
int sum = s1.grade + s2.grade; //연산 가능

위와 같이 객체 .(점, dot)을 통해 접근해서 사칙연산을 할 수 있다.


간략하게 보기

기본형을 제외한 나머지는 참조형이다.

  • 기본형은 소문자로 시작한다. int, long, double, boolean 모두 소문자로 시작한다.
    • 기본형은 자바가 기본으로 제공하는 데이터 타입이다. 그래서 이러한 데이터 타입은 개발자가 새로 정의할 수 없다. 개발자는 참조형인 클래스만 직접 정의할 수 있다.
  • 클래스는 대문자로 시작한다. Student
    • 클래스는 모두 참조형이다.

참고

자바에서 String 타입은 특별하다. String 타입도 클래스로 정의되어 있다. 그런데 String 타입은 기본형처럼 문자 값을 바로 대입할 수 있다. 왜냐하면 문자는 매우 많이 다루기 때문에 자바에서 특별하게 편의 기능으로 제공한다.


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

대원칙 : 자바는 항상 변수의 값을 복사해서 대입한다.
자바에서 값을 대입하는 것은 변수가 보관하고 있는 값을 복사해서 대입한다.
기본형, 참조형 모두 똑같이 보관하고 있는 값을 복사해서 대입한다. 기본형은 변수가 실제로 보관하고 있는 값, 참조형은 참조 값을 보관하고 있어 참조 값을 복사해서 대입한다.


계속해서 얘기하는 두 가지이다.

  • 자바는 항상 변수의 값을 복사해서 대입한다.
  • 참조형은 변수가 보관하고 있는 참조 값 또는 주소 값을 가지고 있다.

이 두가지는 꼭 기억하고 있으면 소스분석 시 흐름을 따라갈 수 있게 된다.

기본형 대입

int a = 10;
int b = a;

참조형 대입

Student s1 = new Student();
Student s2 = s1;

기본형변수가 보관하고 있는 값을 복사해서 대입한다고 생각하면 쉽게 이해할 수있다. 참조형은 실제 객체를 가리키고 있는 주소 값을 가지고 있기 때문에 주소 값을 복사해서 대입한다.

예를 들면 집이 있으면 집을 가져다 복사 해주는게 아니라 집 주소를 복사해서 준 것이다. 그러면 집을 찾아갈 수 있는 경로가 더 늘어난 것 뿐이다.


기본형과 변수 대입

쉬운 내용이지만 참조형과 비교를 위해 코드를 통해서 정리를 해보자.

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

실행결과
a = 10
b = 10
변경 a = 20
a = 20
b = 10
변경 b = 30
a = 20
b = 30


실행결과

a = 10
b = 10

정수형 변수(기본형) a에 10을 대입하고 b에 a를 대입했다. a가 가지고 있는 실제 값을 b에 대입 했기 때문에 b도 10으로 출력한다.


실행결과

a = 10
b = 20

이후 a에 20을 새로 대입해서 a는 20으로 변경되었다. a와 b 변수는 기본형이므로 실제 값을 가지고 있기 때문에 변수 b에는 영향 없이 변수 a만 20으로 출력이 됐다.


실행결과

a = 20
b = 30

그 다음 변수 b에 30을 대입해서 변경이 되었다. 이것 또한 변수 a에는 영향 없이 b만 변경 됐다.


참조형과 변수 대입

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 변경
		dataA.value = 30;
		System.out.println("변경 dataA.value = 30");
		System.out.println("dataA.value 참조 값 = " + dataA.value);
		System.out.println("dataB.value 참조 값 = " + dataB.value);
	}

}

실행결과
dataA 참조 값 = ref.Data@4232c52b
dataB 참조 값 = ref.Data@4232c52b
dataA.value 참조 값 = 10
dataB.value 참조 값 = 10
변경 dataA.value = 20
dataA.value 참조 값 = 20
dataB.value 참조 값 = 20
변경 dataA.value = 30
dataA.value 참조 값 = 30
dataB.value 참조 값 = 30

그림을 통해 차근차근 알아보자.

dataA 변수는 Data 클래스를 통해 생성했기 때문에 참조형이다. Data형 객체를 생성하고 dataA 변수는 참조 값 또는 주소 값을 보관한다. dataA.value 변수(멤버 변수)에 값 10을 보관한다.


실행 코드

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 = ref.Data@x001
dataB = ref.Data@x001
dataA.value = 10
dataB.value = 10

dataA 변수에는 x001 주소 값을 저장하고 있다. 이 주소 값을 dataB에 복사하여 대입한 것이다. 참고로 객체의 주소 값을 복사해서 대입한거지 객체의 실제 사용하는 값을 복사해서 대입한 것이 아니다. 그래서 dataA, dataB 변수 둘다 같은 주소(x001)를 가리키고 있는 것이다.


실행 코드
dataA.value = 20;
System.out.println("dataA = " + dataA.value);
System.out.println("dataB = " + dataB.value);

출력 결과
dataA.value = 20
dataB.value = 20

dataA.value = 20 코드를 실행하면 dataA가 가리키는 X001.value의 값이 20으로 변경된다. 그러면 같은 주소의 value를 가리키고 있던 dataB도 20을 접근하게 되서 dataA.value, dataB.value 둘다 20으로 출력이 된다.


실행 코드
dataB.value = 30;
System.out.println("dataA.value = " + dataA.value);
System.out.println("dataB.value = " + dataB.value);

출력 결과
dataA.value = 30
dataB.value = 30

dataB.value = 30 코드를 실행하면 dataB가 가리키고 있는 x001.value의 값이 30으로 변경된다. 그러면 같은 곳을 가리키고 있는 dataA도 30을 접근하게 되서 dataA.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;
	}

}

실행결과
메서드 호출 전 : a = 10
메서드 호출 후 : a = 10

int a 변수가 저장하고 있는 값을 매개변수 int x에 대입했는데 int x = a 와 같은 상황이다.


메서드 안에서 int x = 20으로 대입해서 x 변수의 값이 20으로 변경됐다.


메서드 종료 후 a 변수 값을 출력하면 10이 출력되는 것을 확인할 수 있다. 결과적으로 x 변수에만 20으로 변경되고 a 변수는 10이 유지가 됐다. 참고로 메서드가 종료되면 매개변수 x는 제거된다.


참조형과 메서드 호출

참조형 예시를 위한 클래스이다.

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);
		changePrimitive(dataA);
		System.out.println("메서드 호출 후 : dataA.value = " + dataA.value);

	}
	
	static void changePrimitive(Data dataA) {
		dataA.value = 20;
	}

}

실행 결과
메서드 호출 전 : dataA.value = 10
메서드 호출 후 : dataA.value = 20

Data 인스턴스를 생성하고 dataA 변수에 참조 값(또는 주소 값)을 저장하고 value에 값 10을 저장한다.


메서드 호출 시 매개변수 dataX에 dataA 변수의 값을 전달한다. 그러면 이렇게 해석할 수도 있다.
Data dataX = dataA

이것도 별반 다를게 없다. dataX 변수에 dataA가 저장하고 있는 값을 복사해서 전달한다. dataA가 저장하고 있던 값이 참조 값(또는 주소 값)이기 때문에 참조 값을 복사해서 전달하고 그러면 dataX도 같은 참조 값을 가리키게 된다.


dataX.value 값을 20으로 변경했다. 그러면 dataX가 가지고 있던 참조 값 x001.value 변수에20을 대입하여 변경하게 된다. 그러면 처음에 x001을 먼저 가리키고 있던 dataA.value의 값도 같이 20을 출력하게 될 것이다.


메서드 종료 후 dataA.value의 값을 출력해본 결과 20이 나온다는 것을 확인할 수 있다.

실행 결과
메서드 호출 전 : dataA.value = 10
메서드 호출 후 : dataA.value = 20

자바에서 메서드의 매개변수(파라미터)는 항상 매개인자한테 값을 전달 받는다. 매개인자가 기본형인지 참조형(메모리주소)인지에 따라 동작이 달라진다.

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

참조형과 메서드 호출 (활용)

package class1;

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

}

위에 코드를 보면 중복되는 코드들이 보일 것이다.

  • 객체 생성 시 값 설정하는 코드(name, age, grade 값 할당)
  • 학생 정보 출력하는 코드

아래 코드에서 보면 메서드를 통해 중복된 코드를 줄일 수 있다.

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 student1) {
      System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
    }
}

주의!

Method1 클래스에서 Student 클래스 사용할 때 상단에 import class1.Student; 이 선언되어 있으면 안된다. 나중에 오류가 생길 수 있는 부분이고, 이 부분은 나중에 뒷 부분에 접근제어자에 대해서 실습할 때 강의할 것으로 보인다.

여기서도 앞서 얘기한 것처럼 참조형은 메서드를 호출할 때 전달해주는 값이 참조 값을 전달한다.(또 얘기하지만 그만큼 중요하기 때문에 계속해서 강조한다.) 그래서 참조 값을 통해 객체의 실제 값을 설정, 변경, 읽어오는 것들을 할 수 있다.

먼저 initStudent(), printStudent() 메서드를 통해 코드가 줄어든 것을 확인할 수 있다.

간략하게 함수 설명을하자면

  • initStudent() : 전달한 값으로 객체의 필드에 값을 설정한다.
  • printStudent() : 전달한 값으로 객체의 설정된 값들을 읽어서 출력한다.

  • student1이 참조하는 Student 인스턴스에 편리하게 값을 설정하기 위해 initStudent() 메서드를 만들었다.
  • student1 변수에 저장되어 있는 참조 값을 매개변수(파라미터) student 변수에 전달한다. 그러면 전달받은 참조 값을 통해 initStudent() 메서드 안에서 student1이 참조하는 것과 동일한 x001을 참조하여 값을 설정, 수정, 읽기를 할 수 있다.

메서드에서 객체 변환

코드가 줄어들긴 했지만 아직 중복된 부분이 있다.

		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);
		System.out.println("student1 = " + student1);
		Student student2 = createStudent("학생2", 16, 80);
		System.out.println("student2 = " + student2);
		
		printStudent(student1);
		printStudent(student2);
	}

	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;
	}
	
	static void printStudent(Student student) {
		System.out.println("이름 : " + student.name + "나이 : " + student.age + "성적 : " + student.grade);
	}
}

createStudent() 메서드 안에 객체를 생성하는 부분도 포함시켰다. 앞선 코드에서는 객체 생성하는 코드도 반복하고 있었지만 createStudent() 메서드만 호출해서 객체 생성과 설정을 동시에 할 수 있게 된다. 그리고 객체를 생성하고 설정해줬으면 밖으로 반환을 해줘야 한다. return student 코드로 반환해야 메서드 밖에서도 참조 값을 사용할 수 있다.

이 부분에 대해서는 나중에 배울 생성자를 통해 다시 접할 것으로 보인다. 생성자도 createStudent() 메서드의 형태와 다를게 없기 때문이다.


여기서도 별반 다를게 없다. 메서드 밖에서 동작하던 코드가 메서드 안에서 객체 생성 후 참조 값을 반환해준다는 것만 다를 뿐 원리는 똑같다.(참조형은 참조 값을 저장하고 복사해서 대입한다. 아니면 전달한다.)

메서드 호출 후 반환하는 진행과정

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

createStudent() 메서드를 통해 생성한 Student 인스턴스의 참조 값을 반환 후 student1 변수에 대입한다. 그러면 student1 변수도 createStudent() 메서드 안에서 생성한 인스턴스의 참조 값을 통해 접근할 수 있다.


참고

  • 김영한의 실전 자바 기본편
profile
gyery

0개의 댓글