ㅁ 자바 가상머신의 메모리 관리방식을 가리켜 '자바 메모리 모델'이라 하는데, 이는 자바를 이해하는데 있어서 매우 중요한 요소이다. 이번 챕터에서는 가상머신의 메모리 관리 방식과 Object 클래스의 설명되지 않은 추가적인 특성들을 살펴본다.
ㅁ 컴퓨터의 메모리 구조
코드 영역 (Code Area)
사용자가 작성한 프로그램 함수들의 코드가 CPU에서 수행할 수 있는 기계어 명령 형태로 변환되어 저장되는 공간
평상시 코드를 타이핑할때 코드창에 렌더링되는 부분이다.
코드 영역은 실행 파일을 수행하는 명령어들이 메모리 공간 쪽으로 제어문, 함수, 상수 등이 지정된다.
데이터 영역 (Data Area)
전역 변수 또는 static 변수 등 프로그램이 사용하는 데이터를 저장하는 공간
전역 변수와 정적 변수를 할당하여 저장하는 영역이다.
이 영역은 실행 중 변수가 변경될 수도 있으므로 Read-Write로 설정되어 있다.
프로그램의 시작을 통해 할당되고, 만일 종료 시 메모리가 소멸된다.
힙 영역 (Heap Area)
프로그래머가 필요할 때마다 사용하는 메모리 영역
사용자 프로그래머가 직접 할당/해제 하는 메모리 영역이다.
힙 영역은 메모리의 낮은 주소부터 높은 주소까지 올라가는 절차 형식으로 할당된다.
스택 영역 (Stack Area)
호출된 함수의 수행을 마치고 복귀할 주소 및 데이터(지역변수, 매개변수, 리턴값 등)를 임시로 저장하는 공간
함수의 호출 시 지역 변수와 매개 변수가 저장되는 되는 임시 메모리 영역이다.
이때 스택 영역은 메모리의 높은 주소부터 낮은 주소(LIFO 후입 선출)까지 내려가는
절차 형식으로 할당되며, 여기서 스택 영역에 저장되는 함수의 호출 정보를 stack frame이라고 부른다.
함수 호출이 완료되면 스택 영역에 함수 정보들은 소멸된다.
ㅁ 학원
(힙 영역)
- 객체들이 동적으로 할당되는 영역입니다
- new 키워드로 생성된 객체와 배열이 저장됩니다.
- 자바에서 new로 생성한 모든 객체들은 기본적으로 힙 영역에 저장된다.
객체, 배열, String 객체
(stack 영역)
- 메소드 호출과 함께 사용되는 메모리 영역입니다.
- 각 스레드마다 별도의 스택이 존재하며, 메소드 호출 시 로컬 변수, 매개변수, 메소드 호출 및 복귀 정보 등을 저장합니다.
- 메소드가 호출될 때 스택 프레임이 생성되고, 메소드 실행이 종료되면 해당 스택 프레임이 제거됩니다.
(static 영역)
- 클래스의 메타데이터, 정적 변수들, 메소드 코드 등이 저장되는 영역입니다.
ㅁ 자바 가상머신은 운영체제 위에서 동작한다.
- 자바 가상머신은 운영체제 위에서 실행되는 하나의 프로그램이다.
- 자바 프로그램은 자바 가상머신 위에서 실행되는 프로그램이다.
- 프로그램의 실행에 필요한 메모리를 가리켜 메인 메모리(main memory)라 하며, 이는 물리적으로 램(RAM)을 의미한다.
그리고 이 메모리의 효율적인 사용을 위해서 Windows나 Linux와 같은 운영체제가 메인 메모리를 관리한다.
- 운영체제는 응용프로그램에게 메모리를 할당해 준다.
- 즉, 자바 가상머신의 실행에 필요한 메모리는 할당해 준다.
- 자바 가상머신은 운영체제가 할당해 주는 메모리 공간을 기반으로 자기 자신도 실행하면서, 자바 응용프로그램의 실행도 돕는다.
ㅁ 자바 가상머신의 메모리 모델
- 운영체제로부터 메모리 공간을 할당받은 자바 가상머신은 메모리 공간을 효율적으로 사용해야 한다.
- 메모리도 일종의 저장공간이므로, 수납장이 여러 개의 수납 공간으로 나뉘어져 있는 것처럼, 자바 가상머신도 메모리 공간을 나눠서 데이터의 특성에 따라 분류해서 저장을 한다.
- 자바 가상머신은 메모리 공간을 메소드 영역, 스택 영역, 힙 영역 크게 3개의 영역으로 나눈다.
- 그리고 각각의 메모리 영역에는 다음의 데이터들을 저장한다.
메소드 영역 : 메소드의 바이트코드, static 변수
스택 영역 : 지역변수, 매개변수
힙 영역 : 인스턴스
ㅁ 메소드 영역
- 소스파일을 컴파일 할 때 생성되는, 자바 가상머신에 의해 실행이 가능한 코드를 가리켜 '자바 바이트코드'라 한다.
이러한 바이트 코드들도 메모리 공간에 저장되어 있어야 실행이 가능하다.
따라서 실행의 흐름을 형성하는 메소드의 바이트코드들은 '메소드 영역'에 저장된다.
- 그리고 static으로 선언되는 클래스 변수들도 이 영역에 할당된다.
- 바이트코드와 static 변수가 메소드 영역에 저장되는 시점이 바로 '클래스가 메모리에 올려지는 시점'이다.
즉 메소드 영역은 클래스 정보를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다.
- 자바프로그램은 main 메소드의 호출에서부터 시작해서 계속된 메소드의 호출로 프로그램의 흐름을 이어간다.
중간에 인스턴스를 생성하기도 하지만, 대부분 메소드 내에서 인스턴스의 생성을 명령한다.
즉, 메소드의 바이트코드는 프로그램의 흐름이며, 컴파일된 바이트코드의 대부분을 의미한다.
- 메소드의 바이트코드는 프로그램의 흐름을 구성하는 바이트코드이다.
그리고 이것이 사실상 컴파일된 바이트코드의 대부분이기 때문에, 전체 바이트코드가 올라간다고 봐도 무리는 없다.
- 메소드의 바이트코드가 메소드 영역에 올라가므로, 당연히 static 메소드도 올라간다.
ㅁ 스택 영역
- 스택은 지역변수와 매개변수가 저장되는 공간이다.이 둘은 메소드 내에서만 유효한 변수들이다.
(참고로 매개변수도 메소드 내에서 선언되는 지역변수의 일종이다. main의 args도 스택에 할당된다.)
- 즉 스택은 프로그램의 실행과정에서 임시로 할당되었다가, 메소드를 빠져나가면 바로 소멸되는 특성의 데이터 저장을 위한 영역이다.
-
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
adder(num1, num2);
}
public static void adder(int n1, int n2) {
int result = n1 + n2;
return result; // 참고로 컴파일 에러 발생.
}
- 스택 영역( U )에 다음과 같이 할당된다.
result
n2
n1 // adder 메소드가 호출된 이후에 추가됨.
num2
num1
args
- 변수 result에 저장되어 있는 값을 반환하면서 메소드를 빠져나가면,
adder 메소드에 의해 할당된 지역변수와 매개변수가 스택에서 전부 소멸한다.
- 결론적으로 지역변수와 매개변수는 선언되는 순간에 스택에 할당되었다가, 자신이 할당된 메소드의 실행이 완료되면 스택에서 소멸된다.
ㅁ 힙 영역
- 인스턴스는 힙 영역에 할당된다. 인스턴스를 스택이 아닌 힙이라는 별도의 영역에 할당하는 이유는 인스턴스의 소멸방법과 소멸시점이 지역변수와는 다르기 때문이다.
- 데이터의 성격이 다르면 별도의 메모리 공간에 저장해야 관리가 용이하다.
-
public static void simpleMethod() {
String str1 = new String("My String");
String str1 = new String("Your String");
}
- String 인스턴스의 생성문이 메소드 내에 존재하므로 str1과 str2는 참조변수이자 지역변수이다.
따라서 스택에 할당이 이뤄진다. 인스턴스는 무조건 힙에 할당되므로, 다음과 같은 관계가 형성된다.
[스택 영역] [힙 영역]
참조변수 인스턴스
str1 ====참조====> "My String"
참조변수 인스턴스
str2 ====참조====> "Your String"
- 이렇게 힙 영역에 생성된 인스턴스의 소멸시기를 정하는 것은 자바 가상머신의 역할이다.
자바 가상머신이 이 인스턴스를 소멸시켜도 되겠다고 판단하면 인스턴스는 자동으로 소멸한다.
- 그래서 자바는 다른 프로그래밍 언어에 비해 메모리 관리에 신경을 덜 써도 된다는 평가를 받는다.
그러나 이것이 메모리 관리가 어떻게 이뤄지는지 몰라도 된다는 뜻은 아니다.
메모리 관리는 자바 가상머신이 대신 해주지만 자바 가상머신의 메모리 관리방식을 이해해야 좋은(메모리 활용도가 높은, 메모리 관리가 효율적인) 코드를 작성할 수 있다.
ㅁ 자바 가상머신의 인스턴스 소멸시기
-
public static void simpleMethod() {
String str1 = new String("My String");
String str1 = new String("Your String");
str1 = null;
str2 = null;
}
- 참조변수 str1과 str2에 null을 대입하고 있다. 이로써 인스턴스 "My String"과 인스턴스 "Your String"은 어떠한 참조변수도 참조하지 않는 상태에 놓이게 된다.
- 어떠한 참조변수로도 참조가 이뤄지지 않는 인스턴스는 존재할 이유가 없다. 프로그램상에서 더 이상 참조가 불가능하기 때문이다. 이렇게 어떠한 형태로건 참조되지 않는 인스턴스가 소멸의 대상이 되며, 이러한 조건이 충족되었을 때 자바 가상머신은 해당 인스턴스를 소멸시킨다.
- 이러한 자바의 인스턴스 소멸기능을 가리켜 '가비지 컬렉션 Garbage Collection'이라 한다.
- 이렇듯 힙 영역은 가비지 컬렉션의 대상이 되는 메모리 공간이므로 이것만 봐도 힙과 스택을 분리해서 운영하는 것이 효율적임을 알 수 있다.
- 참고) 인스턴스가 가비지 컬렉션의 대상이 되었다고 바로 소멸되는 것은 아니다. 빈번한 가비지 컬렉션의 실행은 시스템에 부담이 될 수 있기 때문에 성능에 영향을 미치지 않도록 가비지 컬렉션의 실행 타이밍은 별도의 알고리즘을 기반으로 계산된다.
ㅁ Object 클래스
- Object 클래스는 모든 자바 클래스의 최상위 클래스이다.
ㅁ finalize 메소드
- 인스턴스 소멸 시 반드시 해야 할 일이 있는 경우.
- Object 클래스에는 다음과 같이 finalize라는 이름의 메소드가 정의되어 있다.
protected void finalize() throws Throwable
- 이는 인스턴스가 소멸되기 직전에 자바 가상머신에 의해 자동으로 호출되는 메소드이다.
- 모든 클래스는 Object 클래스를 상속받기에 Object 클래스에 있는 메소드들을 그냥 쓸 수도 있고, 오버로딩, 오버라이딩할 수도 있다.
- ObjectFinalize.java
class MyName {
String objName;
public MyName(String name) { objName = name; }
protected void finalize() throws Throwable { // Object 클래스의 finalize() 메소드 오버라이딩.
super.finalize();
System.out.println(objName + "이 소멸되었습니다.");
}
}
class ObjectFinalize {
public static void main(String[] args) {
MyName obj1 = new MyName("인스턴스1");
MyName obj2 = new MyName("인스턴스2");
obj1 = null;
obj2 = null;
System.out.println("프로그램을 종료합니다.");
}
}
[실행 결과]
프로그램을 종료합니다.
- obj1과 obj2가 참조하는 인스턴스 정보를 null로 지워버렸으므로, 이전에 생성한 인스턴스는 가비지 컬렉션의 대상이 되었다.
- 그러나 두 개의 인스턴스가 소멸되는 과정에서 finalize 메소드가 호출되지 않았다.
그 이유는 가비지 컬렉션이 특정 알고리즘을 통해 계산된 시간에 수행되기 때문에 한번도 실행되지 않을 수 있기 때문이다.
- 따라서 System.gc(); 메소드 호출을 통해서 명시적으로 가비지 컬렉션을 수행시켜야 한다.
System.gc(); 메소드가 호출되면, 자바 가상머신은 가비지 컬렉션을 수행시켜서 참조되지 않는 인스턴스들을 소멸시킨다.
- 그러나 이것만으로는 finalize 메소드 호출을 100% 보장받지 못한다.
가비지 컬렉션이 수행되더라도 상황에 따라 인스턴스의 완전한 소멸은 유보될 수 있기 때문이다.
따라서 완전한 소멸이 유보된 인스턴스들의 finalize 메소드 호출은 System.runFinalization(); 메소드를 호출해야 한다.
- 정리하면, finalize 메소드의 완벽한 호출이 필요한 상황에서는 다음 두 메소드의 연이은 호출이 공식처럼 사용된다.
System.gc();
System.runFinalization();
※ 위의 예제에서 super.finalize();라는 문장을 삽입한 이유는?
-메소드가 오버라이딩되면 오버라이딩이 된 메소드는 호출되지 않음을 알고 있다.
따라서 Object 클래스에 정의된 finalize 메소드는 오버라이딩으로 인해 더 이상 호출되지 않는데,
이는 Object 클래스에 정의된 finalize 메소드에 매우 중요한 코드가 삽입되어 있는 경우 큰 문제로 이어질 수있다.
- 즉, Object 클래스의 finalize 메소드에 반드시 실행되어야 하는 중요한 코드가 삽입되어 있을지도 모르니 이를 호출하는 문장을 삽입하자는 판단이다.
- 결론적으로 말하자면 Objcet 클래스의 finalize 메소드는 텅 비어 있어서 super.finalize(); 문장은 불필요하지만, 이를 넣어둔다고 문제가 되는 것은 아니니 안정성을 위해 삽입해 두는 것도 나쁘지 않다.
ㅁ 인스턴스 비교 : equals 메소드
- == 연산자는 참조변수의 참조 값을 비교한다. 따라서 인스턴스에 저장되어 있는 값 자체를 비교하려면 별도의 방법을 사용해야 한다.
- 일반적으로 인스턴스의 내용 비교를 위한 메소드 정의는 Object 클래스에 정의되어 있는 equals 메소드를 활용한다.
public boolean equals(Object obj)
이 메소드는 == 연산자와 마찬가지로 참조변수의 참조 값을 비교하도록 정의되어 있다. 그런데 == 연산자를 통해 얼마든지 참조 값 비교가 가능하므로, 이 메소드는 내용비교를 하도록 오버라이딩 해도 된다(그런 목적으로 정의된 메소드이다).
-
class IntNumber {
int num;
public IntNumber(int num) { this.num = num; }
public boolean equals(Object obj) {
if( this.num == ( (IntNumber)obj).num )
return true;
else
return false;
}
}
class ObjectEquality2 {
public static void main(String[] args) {
IntNumber num1 = new IntNumber(10);
IntNumber num2 = new IntNumber(12);
if(num1.equals(num2))
System.out.println("num1과 num2는 동일한 정수");
else
System.out.println("num1과 num2는 다른 정수");
}
}
ㅁ 표준 클래스의 equals 메소드가 내용비교를 하도록 이미 오버라이딩 되어있는 경우도 많다.
대표적인 예가 String 클래스이다.
- String 클래스의 equals 메소드는 내용비교를 하도록 이미 오버라이딩 되어있다.
ㅁ 인스턴스의 내용비교가 필요하면 별도의 메소드를 정의하지 말고 equals 메소드를 오버라이딩 하자.
자바 개발자들은 인스턴스의 내용비교가 필요한 상황에서 equals 메소드가 적절히 오버라이딩 되어있을 것을 기대하기 때문에, 이러한 기대를 충족시키는 것이 혼란을 최소화하는 길이다.
ㅁ 인스턴스 복사(복제) : clone 메소드
- Object 클래스에는 인스턴스의 복사를 위한 다음 메소드가 정의되어 있다.
아래의 메소드가 호출되면, 이 메소드를 호출한 인스턴스의 복사본이 생성되고, 그 복사본의 참조 값이 반환된다.
protected Object clone() throws CloneNotSupportedException
- 단, Cloneable 인터페이스를 구현하고 있는 클래스의 인스턴스만이 clone 메소드의 호출이 가능하다.
만약 Cloneable 인터페이스를 구현하지 않는 클래스의 인스턴스에서 clone 메소드가 호출되면 CloneNotSupportedException예외가 발생한다.
- Cloneable 인터페이스는 내용이 없는 텅 빈 인터페이스이다.
즉, 구현해야 할 메소드가 하나도 존재하지 않는 인터페이스이다.
Cloneable 인터페이스를 구현해야만 하게 만든 이유는, 다른 클래스와의 구별을 위한 특별한 표시의 목적이다.
- 인스턴스의 복사는 클래스에 따라서 매우 민감한(또는 허용해서는 안 되는) 작업이 될 수도 있다.
따라서 인스턴스 복사의 허용여부는 클래스를 정의하는 과정에서 결정해야 한다.
인스턴스 복사를 허용할 거면, Cloneable 인터페이스를 구현해서 클래스를 정의하면 된다.
- 그런데 clone() 메소드는 protected로 선언되어 있어서, clone() 메소드를 구현하는 클래스가 아닌, 클래스 외부에서는 호출이 불가능 하다. (인터페이스를 구현함으로 추상메서드를 반드시 구현해야 한다.)
- 다른 클래스에서도 사용하기 위해 public으로 오버라이딩 해야만 한다.
- 메소드 오버라이딩을 통해 접근 범위를 넓히는 것은 가능하나, 좁히는 것은 불가능하다.
- clone() 메서드는
(1) Cloneable 인터페이스를 구현한 클래스에서만 사용할 수 있으며,
(2) 이 메서드를 호출할 때는 CloneNotSupportedException을 처리해야 합니다.
- Cloneable 인터페이스를 구현한다고 해서 자동으로 복제(clone)가 지원되는 것은 아닙니다. 실제로 clone() 메서드는 Object 클래스에 정의되어 있고, 이 메서드는 protected 접근 제한자를 가지고 있습니다. 따라서 복제를 지원하려면 clone() 메서드를 오버라이딩하여 public으로 재정의해야 합니다.
-
class Point implements Cloneable {
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class InstacneCloning {
public static void main(String[] args) {
Point org = new Point();
Point cpy;
try {
cpy = (Point) org.clone();
}
catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
ㅁ Shallow Copy
- clone() 메소드 사용시 주의할 점이, clone() 메소드는 단순히 인스턴스 변수의 값만 복사하기 때문에, 참조타입의 인스턴스 변수가 있는 클래스는 완전한 인스턴스 복제가 이루어지지 않는다.
- Object 클래스에 정의되어 있는 clone 메소드는 인스턴스 변수에 저장되어 있는 값을 복사할 뿐, 인스턴스 변수가 참조하는 대상까지 복사하지는 않는다.
- Shallow copy는 객체의 필드 값들을 복사하지만, 객체가 참조하는 다른 객체들은 원본 객체와 같은 객체를 참조하게 됩니다. 이 경우, 복사된 객체와 원본 객체가 같은 참조를 가지고 있게 되므로 하나의 객체가 변경되면 다른 객체도 영향을 받을 수 있습니다.
- 완전한 인스턴스 복제를 가리켜 Deep Copy라고 하는데, 이러한 형태의 복사가 필요하다면 clone 메소드를 오버라이딩 해야 한다.
(1) 예제1
package com.br.practice.run;
class Person implements Cloneable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public Person clone() {
try {
return (Person) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
Person person1 = new Person("Alice", 30);
// Creating a shallow copy using clone()
Person person2 = person1.clone();
// Modifying person2
person2.setName("Bob");
person2.setAge(25);
// Original person1 remains unchanged
System.out.println("Person 1: {name='" + person1.getName() + "', age=" + person1.getAge() + "}");
System.out.println("Person 2: {name='" + person2.getName() + "', age=" + person2.getAge() + "}");
}
}
[실행 결과]
Person 1: {name='Alice', age=30}
Person 2: {name='Bob', age=25}
- 자바에서 얕은 복사(shallow copy)는 객체의 필드를 복사하지만, 참조 타입의 필드는 같은 객체를 참조합니다. String은 참조 타입이지만 불변성(불변객체)을 가지고 있어 값을 변경할 수 없습니다.
- person1 객체를 생성하고 person2에 얕은 복사를 합니다. 이때 person1과 person2는 같은 String 객체 "Alice"를 참조합니다.
- person2.setName("Bob")을 호출하여 person2의 이름을 "Bob"으로 변경합니다. 이때 setName() 메서드는 새로운 String 객체 "Bob"을 생성하고, person2는 이 새로운 객체를 참조하게 됩니다.
- person1과 person2의 name 필드가 서로 다른 String 객체를 참조하고 있음.
(2) 예제2
class Point {
private int xPos;
private int yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
public void showPosition() {
System.out.printf("%d, %d", xPos, yPos);
}
public void changePos(int x, int y) {
xPos = x;
yPos = y;
}
}
class Rectangle implements Cloneable {
Point upperLeft;
Point lowerRight;
public Rectangle(int x1, int y1, int x2, int y2) {
upperLeft = new Point(x1, y1);
lowerRight = new Point(x2, y2);
}
public void showPosition() {
System.out.print("좌 상단 : ");
upperLeft.showPosition();
System.out.print("\t우 하단 : ");
lowerRight.showPosition();
System.out.println();
}
public void changePos(int x1, int y1, int x2, int y2) {
upperLeft.changePos(x1, y1);
lowerRight.changePos(y2, y2);
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class ShallowCopy {
public static void main(String[] args) {
Rectangle org = new Rectangle(1, 1, 9, 9);
Rectangle cpy;
try {
cpy = (Rectangle) org.clone();
org.changePos(2, 2, 7, 7);
org.showPosition();
cpy.showPosition();
}
catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
[실행 결과]
좌 상단 : 2, 2 우 하단 : 7, 7
좌 상단 : 2, 2 우 하단 : 7, 7
- 일반 필드값이었으면 원본과 복사본이 연동되지 않았다.
- Rectangle 클래스의 인스턴스 변수 upperLeft와 lowerRight의 참조값(번지수)이 복사되었을 뿐, 참조 변수가 가리키는 인스턴스는 복사된 것이 아니다.
- 클래스에서 사용되는 참조 변수는 해당 객체의 주소 값을 가리키는 변수입니다. 따라서 이 변수를 복사할 때는 참조 값(주소 값)이 복사되어 새로운 변수도 같은 객체를 참조하게 됩니다. 이를 얕은 복사(shallow copy)라고 합니다. 새로운 객체를 생성하지 않고 기존 객체의 주소를 복사하여 참조하는 것입니다.
.
ㅁ Deep copy
class Point implements Cloneable {
private int xPos;
private int yPos;
public Point(int x, int y) {
xPos = x;
yPos = y;
}
public void showPosition() {
System.out.printf("%d, %d", xPos, yPos);
}
public void changePos(int x, int y) {
xPos = x;
yPos = y;
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Rectangle implements Cloneable {
Point upperLeft;
Point lowerRight;
public Rectangle(int x1, int y1, int x2, int y2) {
upperLeft = new Point(x1, y1);
lowerRight = new Point(x2, y2);
}
public void showPosition() {
System.out.print("좌 상단 : ");
upperLeft.showPosition();
System.out.print("\t우 하단 : ");
lowerRight.showPosition();
System.out.println();
}
public void changePos(int x1, int y1, int x2, int y2) {
upperLeft.changePos(x1, y1);
lowerRight.changePos(y2, y2);
}
public Object clone() throws CloneNotSupportedException {
Rectangle copy = (Rectangle)super.clone();
copy.upperLeft = (Point)upperLeft.clone(); // 깊은 복사를 위해 upperLeft가 참조하는 대상까지 복사
copy.lowerRight = (Point)lowerRight.clone(); // 깊은 복사를 위해 lowerRight가 참조하는 대상까지 복사
return copy;
}
}
class DeepCopy {
public static void main(String[] args) {
Rectangle org = new Rectangle(1, 1, 9, 9);
Rectangle cpy;
try {
cpy = (Rectangle) org.clone();
org.changePos(2, 2, 7, 7);
org.showPosition();
cpy.showPosition();
}
catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
[실행 결과]
좌 상단 : 2, 2 우 하단 : 7, 7
좌 상단 : 1, 1 우 하단 : 9, 9
- 깊은 복사를 위해서는 clone() 메서드 내에서 객체의 내부 필드들도 복사해주어야 합니다.
-
- 일반적으로 객체를 복사할 때는 기본적으로 얕은 복사(shallow copy)가 이루어집니다. 이는 객체의 필드들을 복사하지만, 필드가 참조 타입일 경우에는 참조하는 객체의 주소(번지)만 복사합니다. 따라서 원본 객체와 복사본 객체가 같은 객체를 참조할 수 있습니다.
- 만약 깊은 복사(deep copy)를 하고 싶다면, 객체를 복사하는 과정에 더하여 참조하는 객체들도 새로 생성하여 복사해야 합니다. 이는 각 참조 필드에 대해 복사 생성자를 호출하거나 clone() 메서드를 사용하여 복사해야 할 수 있습니다. 예를 들어, Rectangle 클래스에서는 Point 객체들을 깊은 복사로 복제해야 하므로, Point 클래스에서도 clone() 메서드를 구현해야 합니다.
따라서 깊은 복사를 하기 위해서는 객체 복사 과정에서 해당 객체가 참조하는 다른 객체들까지 새로 복사해서 참조해야 합니다. 이렇게 하면 원본과 복사본이 독립적인 객체들을 참조하게 됩니다.
ㅁ 인스턴스 변수가 String인 경우의 깊은 복사
class Person implements Cloneable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Object clone() throws CloneNotSupportedException {
Person cpy = (Person) super.clone();
cpy.name = new String(name);
return cpy;
}
}
- 위 예제에서는 깊은 복사가 이뤄지도록 clone 메소드를 오버라이딩 하고 있다.
그러나 자바 프로그래머에게 시킨다면,
class Person implements Cloneable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Object clone() throws CloneNotSupportedException {
Person cpy = (Person) super.clone();
cpy.name = new String(name); // 프로그래머가 삽입하지 않음.
return cpy;
}
}
- 더 간단하게 하면(이게 더 권장됨),
class Person implements Cloneable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Object clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
- String 인스턴스를 깊은 복사의 대상에서 제외시키는 이유는 String 인스턴스는 깊은 복사의 대상에 둘 필요가 없기 때문이다.
- String 인스턴스에 저장된 문자열 정보는 변경이 불가능하다.
따라서 변경해야 하는 상황에 놓이면 변경하고픈 문자열 정보를 담고 있는 String 인스턴스로 대체를 해야 한다.
(String 인스턴스를 새롭게 생성하여 기존의 문자열 정보를 대체해야 한다)
ㅁ 예제
- clone 메서드 호출 시, 아래의 PersonalInfo 클래스가 깊은 복사를 수행하도록 코드를 변경해보자.
class Business implements Cloneable {
private String company;
private String work;
public Business(String company, String work) {
this.company = company;
this.work = work;
}
public void showBusinessInfo() {
System.out.println("회사 : " + company);
System.out.println("업무 : " + work);
}
public void changeWork(String work) {
this.work = work;
}
}
class PersonalInfo implements Cloneable {
private String name;
private int age;
private Business bness;
public PersonalInfo(String name, int age, String Company, String work) {
this.name = name;
this.age = age;
bness = new Business(company, work);
}
public void showPersonalInfo() {
System.out.println("이름 : " + name);
System.out.println("나이 : " + age);
bness.showBusinessInfo();
System.out.println("");
}
public void changeWork(String work) {
bness.changeWork(work);
}
public Object clone() throws CloneNotSupportedException {
PersonalInfo copy = (PersonalInfo) super.clone();
return copy;
}
}
class DeepCopyImpl {
public static void main(String[] args) {
try {
PersonalInfo pInfo = new PersonalInfo( "James", 22, "HiMedia", "encoding" );
PersonalInfo pCopy = (PersonalInfo) pInfo.clone();
pCopy.changeWork( "decoding" );
pInfo.showPersonalInfo();
pCopy.showPersonalInfo();
}
catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
- 변경
class Business implements Cloneable {
private String company;
private String work;
public Business(String company, String work) {
this.company = company;
this.work = work;
}
public void showBusinessInfo() {
System.out.println("회사 : " + company);
System.out.println("업무 : " + work);
}
public void changeWork(String work) {
this.work = work;
}
public Object clone() throws CloneNotSupportedException {
return (Business) super.clone();
}
}
class PersonalInfo implements Cloneable {
private String name;
private int age;
private Business bness;
public PersonalInfo(String name, int age, String Company, String work) {
this.name = name;
this.age = age;
bness = new Business(company, work);
}
public void showPersonalInfo() {
System.out.println("이름 : " + name);
System.out.println("나이 : " + age);
bness.showBusinessInfo();
System.out.println("");
}
public void changeWork(String work) {
bness.changeWork(work);
}
public Object clone() throws CloneNotSupportedException {
PersonalInfo copy = (PersonalInfo) super.clone();
copy.bness = (Business) bness.clone(); // main이 아닌 1차적인 clone 메서드에서, 이렇게 해줘야 함.
return copy;
}
}
class DeepCopyImpl {
public static void main(String[] args) {
try {
PersonalInfo pInfo = new PersonalInfo( "James", 22, "HiMedia", "encoding" );
PersonalInfo pCopy = (PersonalInfo) pInfo.clone();
pCopy.changeWork( "decoding" );
pInfo.showPersonalInfo();
pCopy.showPersonalInfo();
}
catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
- work가 String 형이니 깊은 복사를 안해도 되는 것이 아니다.
changeWork()로 work를 바꾸는 것은 맞으나, 정작 bness 자체가 참조값이 똑같아서(새로운 인스턴스 복사 생성x),
원본과 복사본이 같은 work를 가지게 됨. (깊은 복사를 안하면)
'난 정말 JAVA를 공부한적이 없다구요' 카테고리의 다른 글
23. 쓰레드(Thread)와 동기화 (0) | 2024.07.03 |
---|---|
22. 컬렉션 프레임워크(Collection Framework) (0) | 2024.06.27 |
21. 제네릭(Generics) (0) | 2024.06.25 |
20. 자바의 다양한 기본 클래스 (0) | 2024.06.21 |
18. 예외처리 Exception Handling (0) | 2024.06.19 |