본문 바로가기

Java 기본 문법 - 참조 서적 [이것이 자바다 - 한빛미디어]/9. 기본 API 클래스

3. Java 자바 [API] - Object 클래스의 메소드 2 clone(), finalize()

Object 클래스의 메소드들 2


1. 객체 복제 clone()

원본 객체의 필드값과 동일한 값을 가지는 새로운 객체를 생성하는 것.

 

객체를 복제하는 이유 : 원본 객체를 안전하게 보호하기 위해서이다.

신뢰하지 않는 영역으로 원본 객체를 넘겨서 작업할 경우, 원본 객체의 데이터가 훼손될 수 있다.

 

복제에는 얕은 복제깊은 복제가 두 가지가 있다.

 

 

- 얕은 복제 (thin clone)

 

단순히 필드값을 복사해서 객체를 복사하는 것

필드값만 복제하기 때문에 필드가 기본 타입일 경우 값 복사가 일어나고,

필다가 참조 타입일 경우 객체의 번지가 복사된다.

 

얕은 복제

 

Object의 clone() 메소드자신과 동일한 필드값을 가진 얕은 복제된 객체를 리턴한다.

이 메소드로 객체를 복제하려면,

원본 객체는 반드시 java.lang.Cloneable 인터페이스를 구현하고 있어야 한다.

 

메소드 선언이 없음에도 불구하고 Cloneable 인터페이스를 명시적으로 구현하는 이유는

클래스 설계자가 복제를 허용한다는 의도적인 표시를 하기 위해서이다.

클래스 설계자가 복제를 허용하지 않는다면, Cloneable 인터페이스를 구현하지 않으면 된다.

 

Cloneable 인터페이스를 구현하지 않으면 clone() 메소드를 호출할 때

CloneNotSupportException 예외가 발생해서 복제에 실패하게 된다.

 

clone() 메소드는 CloneNotSupportException 예외 처리가 필요한 메소드이기 때문에

try – catch 구문이 필요하다.

 

try {

    object obj = clone();

} catch(CloneNotSupportException e) {  }

 

예) Member.java : Member 클래스가 Cloneable 인터페이스를 구현했기 때문에 getMember() 메소드에서

clone() 메소드로 자신을 복제한 후, 복제된 객체를 외부로 리턴한다.

 

public class Member implements Cloneable {  // 복제 가능!!
    public String id;
    public String name;
    public String passwd;
    public int age;
    public boolean adult;

    public Member(String id, String name, String passwd, int age, boolean adult) {
        this.id = id;
        this.name = name
        this.passwd = passwd;
        this.age = age;
        this.adult = adult;
    }

    public Member getMember() {
        Member cloned = null

        try {   // clone() 메소드의 리턴 타입은 Object 이므로 Member 타입으로 캐스팅(강제 변환)
            cloned = (Member) clone();
        } catch(CloneNotSupportException e) {  }
        return cloned;
    }
}

 

MemberExample.java : 실행 클래스

 

public class MemberExample {
    public static void main(String[] args) {
        // 원본 객체 생성
        Member original = new Member(“blue”, “홍길동”, “12345”, 25, true);

        // 복제 객체를 얻은 후에 패스워드 변경
        Member cloned = original.getMember();
        cloned.passwd = “67890”;    // 복제 객체에서 패스워드 변경

        System.out.println(“[복제 객체의 필드값]”);
        System.out.println(“id : ” + cloned.id);
        System.out.println(“name : ” + cloned.name);
        System.out.println(“password : ” + cloned.passwd);
        System.out.println(“age : ” + cloned.age);
        System.out.println(“adult : ” + cloned.adult);

        System.out.println();

        System.out.println(“[원본 객체의 필드값]”);
        System.out.println(“id : ” + original.id);
        System.out.println(“name : ” + original.name);
        System.out.println(“password : ” + original.passwd);  // 원본 객체의 패스워드는 변함X
        System.out.println(“age : ” + original.age);
        System.out.println(“adult : ” + original.adult);
    }
}     

 

 

 

- 깊은 복제 (deep clone)

 

얕은 복제는 참조 타입 필드는 번지만 복제되기 때문에, 원본 객체의 필드와 복제 객체의 필드는

같은 객체를 참조하게 된다.

 

얕은 복제의 단점

만약 복제 객체에서 참조 객체를 변경하면, 원본 객체도 변경된 객체를 가지게 된다.

 

깊은 복제 : 참조하고 있는 객체도 복제한다.

 

깊은 복제

 

(참조하고 있는 배열 객체도 복제된다.)

 

깊은 복제를 하려면 Object의 clone() 메소드를 재정의해서 참조 객체를 복제하는 코드를 직접 작성해야 한다.

 

Member 클래스에 int[] 배열과 Car 타입의 필드가 있다.

이 필드들은 모두 참조 타입이므로 깊은 복제 대상이 된다. 따라서 Member 클래스는 Object의 clone() 메소드를 재정의해서 int[] 배열과 Car 객체를 복제한다.

 

예) Member.java

 

public class Member implements Cloneable {
    public String name;
    public int age;
    public int[] scores;   // 참조 타입 필드 (깊은 복제 대상)
    public Car car          // 참조 타입 필드 (깊은 복제 대상)

    public Member(String name, int age, int[] scores, Car car) {
        this.name = name;
        this.age = age;
        this.scores = scores;
        this.car = car;
    }

    @Override  // clone() 메소드 재정의
    protected Object clone() throws CloneNotSupportedException {
        // 먼저 얕은 복사를 해서 name, age 필드를 복제한다.
        Member cloned = (Member) super.clone();   // Object 의 clone() 호출
        // scores 를 깊은 복제
        cloned.scores = Arrays.copyOf(this.scores, this scores.length);
        // car 객체 깊은 복제
        cloned.car = new Car(this.car.model);
        // 깊은 복제된 Member 객체 리턴
        return cloned;
    }

    public Member getMember() {
        Member cloned = null;
        try {
            cloned = (Member) clone();
        }catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return cloned;
    }
}

 

Car.java : Car 클래스

 

public class Car {
    public String model;
    public Car(String model) {
        this.model = model;
    }
}

 

Member 클래스의 getMember() 메소드를 호출해서 복제된 Member 객체를 얻은 후,

score 배열 항목값과 car 객체의 모델을 변경한다.

 

하지만, 원본 Member 객체의 scores 배열 항목값과 car 객체의 모델은 변함이 없다.

원본과 복제본이 각각 참조하는 scores 배열과 car 객체는 서로 다르기 때문이다.

 

MemberExample.java : 실행 클래스

 

public class MemberExample {
    public static void main(String[] args) {
        //원본 객체 생성
        Member original = new Member(“홍길동”, 25, new int[] {90, 90}, new Car(“소나타”));

        // 복제 객체를 얻은 후 참조 객체의 값 변경
        Member cloned = original.getMember();  // 깊은 복제 후
        cloned.scores[0] = 100;                 // 참조 객체의 데이터 변경
        cloned.car.model = “그랜저”;

        System.out.println(“[복제 객체의 필드값]”);
        System.out.println(“name : ” + cloned.name);
        System.out.println(“age : ” +  cloned.age);
        System.out.println(“scores : {”);
        for(int i = 0; i < cloned.scores.length; i++) {
            System.out.println(cloned.scores[i]);
            System.out.println((i == (cloned.scores.length – 1)) ? “” : “,”);
        }
        System.out.println(“}”)
        System.out.println(“car : ” + cloned.car.model);

        System.out.println();

        System.out.println(“[원본 객체의 필드값]”);
        System.out.println(“name : ” + original.name);
        System.out.println(“age : ” +  original.age);
        System.out.println(“scores : {”);
        for(int i = 0; i < original.scores.length; i++) {
            System.out.println(original.scores[i]);
            System.out.println((i == (original.scores.length – 1)) ? “” : “,”);
        }
        System.out.println(“}”)
        System.out.println(“car : ” + original.car.model);
    }
}

 

 


2. 객체 소멸자 finalize()

참조하지 않는 배열이나 객체는 쓰레기 수집기(Garbage Collector) 가 힙 영역에서 자동으로 소멸시킨다.

쓰레기 수집기는 객체를 소멸하기 직전에 마지막으로 객체 소멸자 (finalize())를 실행시킨다.

 

소멸자 = Object 의 finalize() 메소드를 말하며, 기본적으로 실행 내용이 없다.

 

만약 객체가 소멸되기 전에 마지막으로 사용했던 자원 (데이터 연결, 파일 등)을 닫고 싶거나,

중요한 데이터를 저장하고 싶다면 Object 의 finalize()를 재정의할 수 있다.

 

아래는 finalize() 메소드를 재정의한 클래스이다. finalize() 메소드가 실행되면 번호를 출력하게 해서

어떤 객체가 소멸되는지 확인할 수 있도록 한다.

 

예) Counter.java : finalize() 메소드 재정의

 

public class Counter {
    private int no;

    public Counter(int no) {
        this.no = no;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(no + “ 번 객체의 finalize() 가 실행됨”);
    }
}

 

FinalizeExample.java : 실행 클래스

 

public class FinalizeExample {
    public static void main(String[] args) {
        Counter counter = null;
        for(int i = 1; i <= 50; i++) {   // 반복적인 객체 생성
            counter = new Counter(i);
            counter = null;  // Counter 객체를 쓰레기로 만듬
            System.gc();     // 쓰레기 수집기 실행 요청
        }
    }
}

 

객체를 쓰레기로 만들었다고 해서 쓰레기 수집기가 실행되지 않기 때문에,

반복해서 객체를 생성하고 쓰레기로 만들었다.

반복할 때마다 System.gc() 를 호출 해서 쓰레기 수집기를 가급적 빨리 실행해달라고 JVM에 요청한다.

 

객체를 순서대로 소멸시키지 않고 무작위로 소멸시키는 것을 볼 수 있다.

그리고 전부 소멸시키는 것이 아니라 메모리의 상태를 보고 일부만 소멸시킨다.

 

위의 예제에서 System.gc()로 쓰레기 수집기 실행 요청을 했지만,

쓰레기 수집기는 메모리가 부족할 때, CPU가 한가할 때, JVM에 의해 자동 실행된다.

따라서 finalize() 메소드가 호출되는 시점은 명확하지 않다.

 

프로그램이 종료될 때 즉시 자원을 해제하거나 즉시 데이터를 최종 저장해야 한다면,

일반 메소드에서 작성하고 프로그램이 종료될 때 명시적으로 메소드를 호출하는 것이 좋다.