제이제이
article thumbnail

자바 메모리 모델과 Object 클래스 대표 이미지

 

본 내용은 "윤성우의 열혈 Java 프로그래밍"를 참고로 정리한 내용입니다.

 

1. 자바 가상머신의 메모리 모델


자바 가상 머신의 메모리 관리 방식을 가리켜 ‘자바 메모리 모델’이라고 하는데,이는 자바의 전반적인 이해에서 매우 중요한 부분입니다.

가상 머신은 운영체제 위에서 동작합니다.

  • 자바 가상머신운영체제 위에서 실행되는 하나의 프로그램입니다.
  • 그리고 자바 프로그램자바 가상머신 위에서 실행되는 프로그램입니다.

 

❓ 그렇다면 가상머신의 실행에 필요한 메모리는 어떻게 제공되는 것일까요?

  • 프로그램의 실행에 필요한 메모리 공간을 가리켜 ‘메인 메모리(Main Memory)’라 하며 이는 물리적으로 램(RAM)을 의미합니다.
  • RAM의 효율적인 사용을 위해 윈도우, 리눅스와 같은 운영체제가 메모리를 관리합니다.

즉, 운영체제가 응용 프로그램에게 메모리를 할당해주는 역할을 합니다.

ex)

응용 프로그램 A: 메모리를 할당해주세요

운영체제: 네, 4G 바이트를 할당해 드리겠습니다.

 

응용 프로그램 Z: 저도 메모리를 할당해주세요

운영체제: 네, 4G 바이트를 할당해 드리겠습니다.

 

자바 가상머신: 전 좀 특별합니다. 메모리를 좀 특별하게 할당해주세요

운영체제: 제 눈에는 특별하지 않습니다. 마찬가지로 4G 바이트를 드리겠습니다.

자바 가상머신운영체제가 할당해주는 메모리 공간을 기반으로 스스로를 실행하면서 더불어 자바 응용 프로그램의 실행도 돕습니다.

자바 가상 머신의 메모리 관리

자바 가상머신은 운영체제로부터 할당받은 메모리 공간의 효율적인 사용을 고민해야 해야 합니다.

 

그렇다면 효율적인 메모리 공간의 사용방법은 무엇일까요?

→ 먼저 메모리의 특성을 생각해봅시다.

메모리는 저장공간입니다. 이는 서랍 또는 수납장 처럼 성격이 비슷한 물건들끼리 구분하여 저장합니다.

 

💡 이처럼 메모리 공간도 데이터를 특성에 따라 구분해서 저장합니다.

-> 이처럼 분류해서 저장함으로써 찾을 때 쉽고 빠르게 찾을 수 있습니다.

❗ 자바 가상 머신의 메모리 모델

위의 그림처럼 자바 가상머신은 메모리 공간을 크게 3개의 영역으로 나눕니다. 그리고 각각의 메모리 영역에는 다음과 같은 데이터들을 저장합니다.

❗ 자바 가상 머신의 메모리 영역에 저장하는 데이터

이제부터 자바 가상 머신의 메모리 영역을 살펴봅시다.

메소드 영역(Method Area)

  • 소스 파일을 컴파일할 때 생성되는, 자바 가상머신에 의해 실행이 가능한 코드를 가리켜 “바이트 코드(Bytecode)”라고 합니다.
  • 그리고 이 바이트 고트도 메모리 공간에 존재해야 실행이 가능합니다.

이와 관련하여 다음의 코드를 살펴봅시다.

class Boy{
	static int average = 0;
	public void Run(){
		...
		}
}

class MyMain{
	public static void main(String[]args){
		Boy b = new Boy(); //인스턴스 생성
		Boy.average += 5; //클래스 변수에 접금
	}
}

 

위의 Main메소드에서는 Boy인스턴스를 생성하고 또 Boy의 클래스 변수에 접근을 하고 있습니다.

  • 인스턴스 생성 및 클래스 변수의 접근을 위해서는 먼저 해당 클래스의 바이트코드가 메모리 공간에 로딩되어야 하는데(메모리 공간에 올려져야 하는데) 이 때 로딩되는 메모리 공간이 “메소드 영역”입니다.
  • 즉, 메소드 영역특정 클래스의 정보가 메모리 공간에 올려질 때 채워지는 영역입니다.

스택 영역(Stack Area)

  • 스택은 지역변수와 매개변수가 저장되는 공간입니다.

이 둘(지역 변수와 매개변수)는 다음과 같은 공통점이 있습니다.

🚨 “중괄호로 구분되는 지역 내에서만 유효한 변수들입니다”

중괄호 내에 할당된 이후 해당 중괄호를 벗어나면 바로 소멸되는 특성의 데이터 저장을 위한 영역 = 스택

다음의 코드와 그림을 살펴봅시다.

public static void main(String[] args){
	int num1 = 10;
	int num2 = 20; //현재 실행 위치
	adder(num1,num2);
	System.out.println("End Of Program");
}

public static void adder(int n1, int n2){
	int result = n1 + n2;
	return result;
}

🔎 스택의 할당과 해제 1

분석

  • 먼저 main메소드가 호출되고 나서 변수 num1과 num2가 스택에 할당된 상황을 나타냅니다.
  • 위의 코드를 실행하면 다음과 같은 순서로 변수가 선언됩니다.
args -> num1 -> num2

 

그리고 이어서 adder메소드가 호출된 이후의 상황은 다음과 같습니다.

public static void main(String[]args){
	int num1 = 10;
	int num2 = 20;
	adder(num1,num2);//메소드가 호출되고
	System.out.println("End Of Program");
}

public static int adder(int n1, int n2){
	int result = n1 + n2; //현재 실행 위치
	return result;
}

🔎 스택의 할당과 해제 2

설명

adder메소드가 호출이 되면서 매개변수 n1과 n2가 스택에 할당되었고 이어서 변수 result도 할당되었습니다.

  • main메소드는 아직 종료되지 않은 상태이기 때문에 main메소드에서 선언된 변수들도 스택에 함께 쌓여 있습니다.

그 다음 변수 result에 저장되어 있는 값을 반환하면서 메소드를 빠져 나간 이후의 상황을 살펴봅시다.

public static void main(String[] args){
	int num1 = 10;
	int num2 = 20;
	adder(num1,num2); //메소드가 반환되고
	System.out.println("End Of Program"); //현재 실행 위치

}

public static int adder(int n1, int n2){
	int result = n1 + n2;
	return result;
}

🔎 스택의 할당과 해제3

메소드 adder를 빠져나오자 그 안에 할당된 지역변수와 매개변수는 스택에서 전부 소멸됩니다.

🔥 이렇듯 지역변수와 매개변수는 선언되는 순간에 스택에 할당되었다가 자신이 할당된 영역을 벗어나면 소멸이 됩니다.

힙 영역(Heap Area)

  • 인스턴스는 힙 영역에 할당됩니다.

❓ 인스턴스는 스택이 아닌 힙이라는 별도의 영역에 할당되는 이유는 무엇일까요?

❗ 인스턴스의 소별 시점과 소멸 방법이 지역변수와 다르기 때문입니다.

 

이와 관련하여 다음의 코드를 살펴봅시다.

public static void simpleMethod(){
	String str1 = new String("My String");
	String str2 = new String("Your String");
	....
}

분석

String 인스턴스의 생성문은 메소드 내에 존재함으로써 str1과 str2는 참조변수이자 지역변수 입니다.

→ 따라서 이 둘(str1,str2)는 스택에 할당됩니다.

→ 그러나 인스턴스는 무조건 힙에 할당이 되며 메모리 공간에는 다음과 같은 참조 관계가 형성이 됩니다.

🔎 인스턴스와 참조 변수의 메모리 할당

❓ 그렇다면 이렇게 힙에 생성된 인스턴스들은 언제 소멸될까요?

❗ 인스턴스의 소멸 시기를 결정하는 것은 가상머신의 역할 중 하나입니다.

 

가상머신은 다음과 같이 판단하면 인스턴스를 자동으로 소멸되게 됩니다.

“ 이제 이 인스턴스는 소멸해야 되겠군”

→ 따라서 자바는 다른 프로그래밍 언어에 비해 “메모리 관리에 신경을 덜 써도 된다”라는 평가를 받습니다.

→ 하지만 자바 가상 머신의 메모리 관리 방식을 알고 있어야 좋은 코드(메모리를 효율적으로 사용하는 코드)를 작성할 수 있습니다.

자바 가상 머신의 인스턴스 소멸 시기

자바 가상 머신의 인스턴스의 소멸 시기를 알기 위해 다음의 코드를 살펴봅시다.

public static void simpleMethod(){
	String str1 = new String("My String");
	String str2 = new String("Your String");
	...
	str1 = null; //참조 관계의 소멸
	str2 = null; //참조 관계의 소멸

}

위의 코드에서 str1,str2 참조변수에 null을 대입하였습니다.

→ 이로써 str1과 str2가 참조했던 두 String 인스턴스는 어느 참조변수도 참조하지 않는 상태가 되게 됩니다.

🔎 참조되지 않는 두 인스턴스

❓ 그럼 두 인스턴스는 어떻게 될까?

  • 위와 같은 상태로 되었을 때 더 이상 접근할 수 없으므로 두 인스턴스는 존재할 이유가 없습니다.

→ 때문에 이러한 상태(아무런 참조 변수도 참조하지 않는 상태)의 인스턴스는 ‘소멸의 대상이 되어 가비지 컬렉션에 의해 소멸이 됩니다.

정리

힙 영역가상머신에 의한 가비지 컬렉션이 일어나는 메모리 공간이기도 합니다.

+) 가비지 컬렉션

  • 인스턴스가 가비지 컬렉션의 대상이 되었다고 해도 바로 소멸되는 것은 아닙니다.
  • 가비지 컬렉션의 빈번한 실행은 시스템에 부담을 주기 때문에 성능에 영향을 미치지 않도록 가비지 컬렉션의 실행 타이밍은 별도의 알고리즘을 통해 계산되며 이 계산 결과를 기반으로 가비지 컬렉션이 수행됩니다.

2.Object 클래스


❓ Object 클래스란?

 ❗ Objcet 클래스는 모든 자바 클래스의 최상위 클래스입니다.

이와 관련하여 다음의 내용을 살펴봅시다.

인스턴스를 소멸 시 해야 할 일이 있다면 :finalize 메소드

Object 클래스에는 다음의 메소드가 정의되어 있습니다.

  • 아무것도 참조하지 않는 인스턴스가 가비지 컬렉션에 의해 소멸되기 전에 자동으로 호출되는 메소드입니다.
protected void finalize() throws Throwable
🔥 따라서 인스터스 소멸 시 반드시 실행해야 할 코드가 있다면 이 메소드의 오버라이딩을 고려할 수 있습니다.

 

이와 관련하여 다음의 코드를 살펴봅시다.

class Person{
    String name;

    //생성자

    public Person(String name) {
        this.name = name;
    }
    //finalize 메소드 재정의
    @Override
    protected void finalize() throws Throwable{
        super.finalize(); //상위 클래스의 finalize 메소드 호출
        System.out.println("destroyed: " + name);
    }
}

class ObjectFinalize{
    public static void main(String[]args){
        Person p1 = new Person("Yoon");
        Person p2 = new Person("Park");
        p1 = null; //참조대상을 가비지 컬렉션의 대상으로 만듦
        p2 = null; //참조대상을 가비지 컬렉션의 대상으로 만듦

        System.out.println("End of Program!");
    }
}

 

출력 결과

End of Program!

위의 코드를 분석해 봅시다.

분석

위의 코드에서 오버라이딩한 finalize 메소드는 다음과 같습니다.

@Override
    protected void finalize()throws Throwable{
        super.finalize(); // 상위 클래스의 finalize 메소드 호출
        System.out.println("destroyed: " + name);
  }

이렇게 상위 클래스의 메소드를 오버라이딩하는 경우, 오버라이딩 된 상위 클래스의 메소드를 호출하는 것에는 다음과 같은 의미가 있습니다.

🔥 “상위 클래스의 finalize 메소드에 삽입되어 있는 코드들이 실행되도록하자”

 

출력 결과를 살펴보면 두 인스턴스를 가비지 컬렉션의 대상으로 지정함에도 불구하고 finalize 메소드가 호출된 흔적을 살펴볼 수가 없습니다.

❗ 이유

  • 가비지 컬렉션은 빈번히 일어나지 않기 때문입니다.
  • 소멸할 인스턴스가 생겨도 가비지 컬렉션으로 바로 이어지지 않기 때문입니다.
  • 또한 실행 중인 자바 프로그램이 종료가 되면 프로그램을 위해 할당된 메모리 전체가 해체 되기 때문에 finalize메소드의 호출이 생략될 가능성이 있기 때문입니다.

이러한 상황 속 다음 두 메소드의 순차적 호출을 통해서 finalizie 메소드의 호출을 요청할 수 있습니다.

public static void gc(); //가비지 컬렉션의 수행을 요청함(명령이 아닌 요청)

위의 메소드를 호출함으로써 가비지 컬렉션의 수행을 요청할 수 있습니다. 그러나 요청이 있다고 해서 언제나 가비지 컬렉션을 바로 진행하지는 않고 가상 머신이 노력할 뿐입니다.

public static void runFinalization();//소멸이 보류된 인스턴스의 finalize 메소드 호출을 요청함(명령이 아닌 요청)

위의 메소드 호출을 통해 보류된 인스턴스의 소멸까지 요청할 수 있습니다. 그러나 이 역시 요청일 뿐 가산머신에게 독촉하는 수준입니다.

 

참고) 🚨 가비지 컬렉션을 강제로 진행하지 말자!

🔥 가상머신은 매우 합리적인 방법으로 가비지 컬렉션을 수행합니다. 따라서 특별한 상황이 아니면 가비지 컬렉션 동작에 영향을 미치는 메소드 호출을 삼가하는 것이 좋습니다.

인스턴스의 비교: equals 메소드

❗ == 연산자

🔥 ==연산자는 참조변수의 참조 값을 비교합니다.

 

💡  따라서 서로 다른 두 인스턴스의 내용을 비교하려면 별도의 방법을 사용해야합니다.

이와 관련하여 다음의 코드를 살펴봅시다.

class INum{
	private int num;

 public INum(int num){
		this.num = num;
	}

    @Override
    public boolean equals(Object obj){
        if(this.num == ((INum)obj).num)
            return true;
        else
            return false;
    }

public class ObjectEquality{
		public static void main(String[] args) {
		 INum num1 = new INum(10);
		 INum num2 = new INum(12);
		 INum num3 = new INum(10);

        if(num1.equals(num2))
            System.out.println("num1, num2 내용은 동일하다.");
        else
            System.out.println("num1,num2 내용은 다르다.");

        if(num1.equals(num3))
            System.out.println("num1,num3 내용은 동일하다.");
        else
            System.out.println("num1,num3 의 내용은 다르다.");
	}
}

 

출력 결과

num1, num2 내용은 다르다.
num1, num3 내용은 동일하다.

위의 코드를 분석해봅시다.

 

분석

  • INum 클래스는 Object 클래스의 equals 메소드를 다음과 같이 오버라이딩하였습니다.
@Override
    public boolean equals(Object obj){
        if(this.num == ((INum)obj).num)
            return true;
        else
            return false;
    }

→ 클래스 내에 선언된 인스턴스 변수의 내용을 비교하여 그 결과에 따라 true 또는 false를 반환하도록 오버라이딩을 하였습니다.

🔥 이렇듯 두 인스턴스의 내용 비교 결과인 true, false의 반환 조건은 해당 클래스를 정의하는 프로그래머가 결정해야 합니다. → equals 메소드의 오버라이딩을 통해서 반영되어야 합니다.

 

❓ 왜 equals 메소드를 오버라이딩하여 반영해야 하는가?

→ Object 클래스의 equals 메소드는 == 연산자와 마찬가지로 참조변수의 참조 값을 비교하도록 정의되어 있기 때문입니다.

→ 참조 값은 == 연산자를 통해 비교가 가능함으로 equals 메소드는 내용 비교가 이루어지도록 오버라이딩을 하라고 존재하는 메소드입니다.

+) 또한 자바에서 제공하는 표준 클래스의 경우 equals 메소드가 내용을 비교하도록 이미 오버라이딩되어 있는 경우가 많은데 그 중 대표적인 String 클래스에 대해서 살펴봅시다.

class StringEquality{
    public static void main(String[]args){
        String str1 = new String("So Simple");
        String str2 = new String("So Simple");

        //참조 대상을 비교하는 if ~ else문
        if(str1 == str2)
            System.out.println("str1과 str2는 참조 대상이 서로 동일합니다.");
        else
            System.out.println("str1과 str2는 참조 대상이 서로 다르다.");

        //두 인스턴스 내용을 비교하는 if ~ else문
        if(str1.equals(str2))
            System.out.println("str1과 str2는 내용이 동일하다.");
        else
            System.out.println("str1과 str2는 내용이 다르다.");
    }
}

 

출력 결과

str1과 str2는 참조 대상이 서로 다르다.
str1과 str2는 내용이 동일하다.

 

위의 두 가지 코드를 살펴보고 다음과 같은 결론을 내릴 수 있습니다.

두 인스턴스의 내용을 비교를 원한다면 Object 클래스의 equals 메소드를 오버라이딩하자!

↔  단순히 참조변수의 참조 값을 비교하려면 == 연산을 하자

인스턴스의 복사(복제) : clone 메소드

Object 클래스에는 인스턴스의 복사를 위해 다음과 같은 메소드가 정의되어 있습니다.

protected Object clone() throws CloneNotSupportedException
🔥 이 메소드가 호출이 되면 메소드가 속한 인스턴스의 복사본이 생성되고, 만들어진 복사본의 참조값이 반환됩니다.

 

❗ 단, 다음의 인터페이스를 구현한 인스턴스를 대상으로만 위의 메소드를 호출할 수 있습니다.

interface Clonable
-> 이 인터페이스를 구현한 클래스의 인스턴스만 clone 메소드의 호출이 가능합니다.

 

🚨 만약 Clonable 인터페이스를 구현하지 않은 클래스의 인스턴스를 대상으로 clone 메소드를 호출한다면

CloneNotSupportedException 예외가 발생되게 됩니다.

 

다시 한번 Cloneable 인터페이스의 구현의 의미를 살펴봅시다.

 🔥 “이 클래스의 인스턴스는 복사해도 됩니다. 즉, clone 메소드 호출이 가능합니다”

 

따라서 Clonalbe 인터페이스는 “마커 인터페이스(Marker Interface)”입니다.

→ 정의해야 할 메소드가 존재하지는 않으나 복사를 해도 된다는 표식을 알려주는 인터페이스입니다.

→ clone 메소드의 호출이 허용된다는 표식을 의미합니다.

이와 관련하여 다음의 코드를 살펴봅시다.

class  Point implements Cloneable{
    private int xPos;
    private int yPos;

    //생성자

    public Point(int xPos, int yPos) {
        this.xPos = xPos;
        this.yPos = yPos;
    }

    public void showPosition(){
        System.out.printf("[%d, %d]",xPos,yPos);
        System.out.println();
    }

    @Override
    public Object clone() throws CloneNotSupportedException{
        return super.clone();
    }
}

class InstanceCloning{
    public static void main(String[]args){
        Point org = new Point(3,5);
        Point cpy;

        try{
            cpy = (Point) org.clone();
            org.showPosition();
            cpy.showPosition();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
    }
}

 

출력 결과

[3, 5]
[3, 5]

 

분석

위의 코드를 분석해봅시다.

먼저 출력 결과를 통해 참조 변수 cpy가 가리키는 인스턴스의 복사가 정상적으로 이루어졌음을 확인할 수 있습니다. 이를 그림으로 표현하자면 다음과 같습니다.

다음으로 clone 메소드의 오버라이딩된 부분을 살펴봅시다.

  • Object 클래스(부모 클래스)의 clone 메소드를 호출하였습니다.
    단순히 부모 메소드의 clone 메소드를 호출한 것으로 보이나 차이가 있습니다.
@Override
    public Object clone() throws CloneNotSupportedException{
        return super.clone();
    }

 

Object 클래스의 clone 메소드 정의

protected Object clone() throws CloneNotSupportedException

 

두 clone 메소드의 차이점

  1. 먼저 메소드 오버라이딩을 통해 public 선언으로 바꾸었습니다. → 메소드 오버라이딩을 통해 접근 범위를 넓혔습니다.
❗ 메소드 오버라이딩을 통해서 접근 범위를 넓히는 것이 가능합니다

ex) protected로 선언된 메소드를 오버라이딩을 통해서 public으로 변경하는 것은 가능합니다.

단, 거꾸로 접근 범위를 제한하는 형태의 오버라이딩은 불가능합니다.

ex) public으로 선언된 메소드를 오버라이딩을 통해서 protected로 변경은 불가능합니다.

 

Object 클래스에 정의된 clone 메소드의 인스턴스 복사 방식을 알아보기 위해 다음의 코드를 살펴봅시다.

class Point2 implements Cloneable{
    private int xPos;
    private int yPos;

    //생성자

    public Point2(int xPos, int yPos) {
        this.xPos = xPos;
        this.yPos = yPos;
    }

    public void showPosition(){
        System.out.printf("[%x, %d]",xPos,yPos);
        System.out.println();
    }

    public void changePos(int x, int y){
        this.xPos = x;
        this.yPos = y;
    }

    @Override
    public Object clone() throws CloneNotSupportedException{
        return super.clone();
    }
}

class Rectangle implements Cloneable{
    private Point2 upperLeft; //좌측 상단 좌표
    private Point2 lowerRight; //우측 하단 좌표

    public Rectangle(int x1, int y1, int x2, int y2) {
        upperLeft = new Point2(x1,y1);
        lowerRight = new Point2(x2,y2);
    }

    //좌표 정보 수정
    public void changePos(int x1, int y1, int x2, int y2){
        upperLeft.changePos(x1,y1);
        lowerRight.changePos(x2,y2);
    }

    @Override
    public Object clone() throws CloneNotSupportedException{
        return super.clone();
    }

    //직사각형 좌표 정보 출력
    public void showPosition(){
        System.out.print("좌측 상단: ");
        upperLeft.showPosition();

        System.out.print("우측 하단: ");
        lowerRight.showPosition();
        System.out.println();
    }
}

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]

 

분석

위의 코드를 분석해봅시다.

먼저 위의 코드에서는 다음과 같이 인스턴스를 복사한 후에 참조변수 org가 참조하는 인스턴스의 값을 수정하였습니다.

public static void main(String[]args){
		Rectangle org = new Rectangel(1,1,9,9);
		Rectangle cpy;

		try{
			cpy = (Rectangle) org.clone(); //인스턴스 복사
			org.changePos(2,2,7,7); //인스턴스 좌표 정보를 수정함
			...
	
}

따라서 두 인스턴스에 저장된 값이 서로 다른 것이라고 예상할 수 있습니다.

그런데 출력 결과를 살펴보면 이 예상은 잘못되었음을 알려줍니다.

그림을 통해 더 자세히 분석해 보겠습니다.

먼저, org가 참조하는 인스턴스를 대상으로 clone 메소드를 호출하였고, 그 결과 인스턴스의 복사는 제대로 이루어졌습니다. 이를 그림으로 그려보면 다음과 같습니다.

❗ 그런데 Rectangle 인스턴스의 멤버는 다음과 같이 2개의 참조변수로 이루어져 있습니다.

class Rectangle implements Cloneable{
		private Point upperLeft; //좌측 하단 좌표
		private Point lowerRight; //우측 하단 좌표
}

따라서 복사 과정에서 참조변수가 지니는 참조 값이 그대로 새 인스턴스에 복사가 됩니다.

이를 “얕은 복사”라고 하는데 그림으로 보자면 다음과 같습니다.

아마도 upperLeft와 lowerRight가 참조하는 Point 인스턴스가 복사가 이뤄진 다음 형태를 기대했을 것이다. 즉, 다음과 같이 “깊은 복사”가 이루어졌을 것으로 기대하였을 것입니다.

❗ 위와 같은 형태의 복사(깊은 복사)가 이루어지기 위해서는 다음과 같이 clone메소드를 오버라이딩해야 합니다.

@Override
public Object clone() throws CloneNotSupportedException{
		//Object 클래스의 clone 메소드 호출을 통한 복사본 생성
		Rectangle copy = (Rectangle) super.clone();

		//깊은 복사의 형태로 복사본을 완성
		copy.upperLeft = (Point)upperLeft.clone();
		copy.upperRight = (Point)upperRight.clone();

		//완성된 복사본의 참조 값을 반환
		return copy;
}

인스턴스 변수가 String인 경우의 깊은 복사

다음의 클래스도 인스턴스 변수를 멤버로 가지고 있습니다. 따라서 이 클래스가 깊은 복사를 하도록 clone 메소드를 오버라이딩하려고 합니다.

class Person implements Clonable{
	private String name;
	private int age;

	//생성자
	public Person(String name, int age){
		this.name = name;
		this.age = age;
	}
	....
}
❗ 그런데 인스턴스 변수 중 하나가 String입니다.

 

🚨 String형은 cloneable인터페이스를 구현하지 않기 때문에 문제가 됩니다.

따라서 위의 클래스를 깊은 복사를 할때는 clone메소드를 다음과 같이 정의해야 합니다.

class Persion implements Clonable{
	private String name;
	private int age;

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
 }
	@Override
	public Object clone() throws CloneNotSupportedException{
		Person cpy = (Peron)super.clone(); //clone 메소드 호출을 통한 복사본 생성
		cpy.name = new String(name); //깊은 복사의 형태로 복사본을 완성
		return cpy; //완성된 복사본의 참조 값 반환
	}
}

그런데 String 클래스가 Cloneable 인터페이스를 구현하지 않는 이유는 무엇일까요?

🔥 String은 문자열의 수정이 불가능하므로, 깊은 복사의 대상에서 제외해도 됩니다.
  • String 인스턴스의 내용을 이루는 문자열은 인스턴스 생성 시 결정되고, 이렇게 결정이 되면 변경이 불가능합니다.

 

📒 Reference (참고 자료)


  1. 윤성우의 열혈 자바

 

profile

제이제이

@아사비치즈스틱

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!