반응형

Object 클래스는 모든 클래스가 직간접적으로 상속을 하는 클래스이므로 모든 클래스들의 조상이라고 할 수 있습니다.

Object 클래스 내부의 메소드 중에서도 아래의 메소드들을 순서대로 정리해보겠습니다.

1. finalize

2. equals

3. clone


1. finalize 메소드

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

이 메소드는 아무도 참조하지 않는 인스턴스가 가비지 컬렉션에 의해 소멸되기 전에 자동으로 호출되는 메소드입니다.

    protected void finalize() throws Throwable { }

 

따라서 인스턴스 소멸 시 반드시 실행해야 할 코드가 있다면 이 메소드의 오버라이딩을 고려해볼 수 있습니다.

아래는 간단하게 작성한 예제입니다.

public class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Instance Destroy : " + name);
    }
}

public class FinalizeMethod {
    public static void main(String[] args) {
        Person person1 = new Person("Park");
        Person person2 = new Person("Han");

        person1 = null;
        person2 = null;

        System.gc();
        System.runFinalization();
        
        System.out.println("Main Method End");
    }
}​

 

위 예제 코드에서는 인스턴스의 소멸을 강제하기 위해 GC를 강제로 진행했지만,

평소 JVM은 매우 합리적인 방법으로 GC를 수행하기 때문에 특별한 상횡이 아니라면 강제하지 않는 것이 좋습니다.

결과 출력값은 다음과 같습니다.

Instance Destroy : Park
Instance Destroy : Han
Main Method End

2. equals 메소드

equals 메소드는 간단하게  두 개의 값을 비교할 때 사용합니다.

따라서 비슷한 역할을 가진 == 연산자와 많은 비교를 하곤 합니다.

 

이 둘은 양 쪽의 값을 비교한 후 그 결과를 boolean type 으로 반환한다는 공통점을 가집니다.

하지만 가장 눈에 띄는 차이점으로는 equals() 는 메소드이고, ==은 연산자라는 점이 있습니다.

우선 자세한 내용은 아래의 코드들을 통해 알아보도록 합시다.

 

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

    public boolean equals(Object obj) {
        return (this == obj);
    }

 

equals 메소드는 양 쪽의 내용을 비교해야하는 상황일 때 오버라이딩을 고려해볼 수 있습니다.

아래는 간단하게 작성한 예제입니다.

public class INum {
    private int num;

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

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

public class ObjectEquality {
    public static void main(String[] args) {
        INum num1 = new INum(10);
        INum num2 = new INum(20);
        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 메소드를 다음과 같이 클래스 내에 선언된 인스턴스 변수의 내용을 비교하여,

그 결과에 따라 true 또는 false의 boolean 값을 반환하도록 오버라이딩 했습니다.

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

 

이렇듯 두 인스턴스의 비교 조건은 해당 클래스를 정의하는 개발자가 결정해야 합니다.

그리고 그 결정 사항을 equals 메소드를 equals 메소드의 오버라이딩을 통해 반영해야 합니다.

Object 메소드는 참조변수의 참조값을 비교하도록 정의되어 있습니다.

그런데 == 연산을 통해서도 참조값은 비교가 가능합니다.

따라서 equals 메소드의 호출을 통해 참조값을 비교할 필요는 없습니다.

즉 equals 메소드는 내용 비교가 이뤄지도록 오버라이딩 하라고 존재하는 메소드입니다.

자바에서 제공하는 표준 클래스의 경우 equals 메소드가 이미 내용 비교를 하도록 오버라이딩 된 경우가 많습니다.

그 대표적인 예가 String 클래스인데 이와 관련하여 다음 코드를 보겠습니다.

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

        if (str1 == str2) {
            System.out.println("str1 = str2");
        } else {
            System.out.println("str1 != str2");
        }

        if (str1.equals(str2)) {
            System.out.println("str1 = str2");
        } else {
            System.out.println("str1 != str2");
        }
    }
}

 

결과 출력은 다음과 같습니다.

str1 != str2
str1 = str2

 

분명 일반적인 equals 메소드일 경우는 str1 과 str2는 동일하지 않다고 출력할 것입니다.

하지만 결과는 그렇지 않습니다.

그 이유는 String 내부에 오버라이딩 된 equals 메소드는 내용을 비교하도록 정의되어있기 때문입니다.

 public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

 

결론입니다.

두 인스턴스의 내용 비교를 원한다면 Object 클래스의 equals 메소드를 오버라이딩하여 사용합니다.

자바 개발자들이 원한 설계가 바로 그것입니다.

만약 단순한 참조변수의 참조값을 비교하려면 == 연산을 사용합니다.


3. clone 메소드

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

protected native Object clone() throws CloneNotSupportedException;

 

이 메소드가 호출되면, 호출된 메소드가 속한 인스턴스의 복사본이 생성되고,

이렇게 만들어진 복사본의 참조값이 반환됩니다.

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

 

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

CloneNotSupportedException 예외가 발생합니다.

Cloneable 인터페이스의 구현은 "이 클래스의 인스턴스는 복사해도 됩니다" 와 같은 마커 인터페이스 역할을 합니다.

즉, 정의해야 할 메소드가 존재하지 않는, 복사를 해도 된다는 표식의 인터페이스입니다.

 

인스턴스의 복사는 클래스에 따라 허용해서는 안되는 작업이 될 수 있습니다.

따라서 인스턴스 복사의 허용 여부를 클래스를 정의하는 과정에서 고민하고 결정한 후 Cloneable 인터페이스를 구현해서 clone 메소드의 호출이 가능하도록 하면 됩니다.

아래의 코드로 확인해보겠습니다.

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

public class InstanceCloning {
    public static void main(String[] args) throws CloneNotSupportedException {
        Point original = new Point(3, 5);
        Point copy;

        copy = (Point) original.clone();
        original.showPosition();
        copy.showPosition();
    }
}

 

결과 출력은 다음과 같습니다.

[3, 5]
[3, 5]

 

실행 결과는 인스턴스의 복사가 정상적으로 이뤄졌음을 보여줍니다.

이건 관련성이 없는 문제이지만, 위 코드에서는 clone 메소드를 다음과 같이 오버라이딩 했습니다.

잘보면 그저 상위 클래스의 clone 메소드를 호출한 것이 전부이기 때문에, 오버라이딩이 무의미해 보입니다.

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

그러나 중요한 차이가 있습니다.

앞서 Object 에 정의된 clone 메소드는 접근제어지시자가 protected 로 되어있었습니다.

즉 오버라이딩의 목적은 protected -> public 으로 접근 범위를 넓힌 것입니다.

하지만 반대의 경우인 public -> protected 로 접근 범위를 제한하는 형태의 오버라이딩은 불가능합니다.

 

마지막으로 얕은 복사와 깊은 복사라는 개념을 아래의 코드로 확인해보겠습니다.

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

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

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

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

    public Rectangle(int x1, int y1, int x2, int y2) {
        upperLeft = new Point(x1, y1);
        lowerRight = new Point(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();
    }
}

public class ShallowCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        Rectangle original = new Rectangle(1, 1, 9, 9);
        Rectangle copy;

        copy = (Rectangle) original.clone();
        original.changePos(2, 2, 7, 7);
        original.showPosition();
        copy.showPosition();
        
        System.out.println(original);
        System.out.println(copy);
        System.out.println(original.getUpperLeft());
        System.out.println(original.getLowerRight());
        System.out.println(copy.getUpperLeft());
        System.out.println(copy.getLowerRight());
    }
}

 

결과 기대값은 아래와 같습니다.

좌측 상단 좌표 : [2, 2]
우측 하단 좌표 : [7, 7]

좌측 상단 좌표 : [1, 1]
우측 하단 좌표 : [9, 9]

 

실제 결과 출력값은 아래와 같습니다.

좌측 상단 좌표 : [2, 2]
우측 하단 좌표 : [7, 7]

좌측 상단 좌표 : [2, 2]
우측 하단 좌표 : [7, 7]

 

예상했던 결과와 상이합니다.

인스턴스가 정상적으로 복사된 것은 맞지만,

Rectangle 클래스 내부의 Point 형 참조변수인 upperLeft 와 lowerRight 의 참조값이 그대로 새 인스턴스에 복사가 된 것입니다.

	System.out.println(original);
        System.out.println(copy);
        System.out.println(original.getUpperLeft());
        System.out.println(original.getLowerRight());
        System.out.println(copy.getUpperLeft());
        System.out.println(copy.getLowerRight());

출력 결과값은 다음과 같습니다.

복사본의 내부 참조변수의 참조값이 원본의 참조변수의 참조값과 동일하단 것을 알 수 있습니다.

그리고 이런 현상을 '얕은 복사'라고 합니다.

또한 원래 기대하던 완전한 복사를 '깊은 복사'라고 생각하면 됩니다.

objectClass.Rectangle@60e53b93
objectClass.Rectangle@5e2de80c
objectClass.Point@1d44bcfa
objectClass.Point@266474c2
objectClass.Point@1d44bcfa
objectClass.Point@266474c2

 

깊은 복사를 하기 위해 Rectangle 에 오버라이딩 된 clone 메소드를 아래의 코드와 같이 수정합니다.

    @Override
    public Object clone() throws CloneNotSupportedException {
        Rectangle copy = (Rectangle) super.clone();

        copy.upperLeft = (Point) upperLeft.clone();
        copy.lowerRight = (Point) lowerRight.clone();

        return copy;
    }

 

출력 결과값입니다.

처음 기대한 값 그대로 결과값으로 확인할 수 있습니다.

그렇다면 String 도 참조형인데 방금과 같은 깊은 복사를 적용해야 하나? 라는 의문점이 생길 수도 있습니다.

다행히도 String 인스턴스의 내용을 이루는 문자열은 인스턴스 생성 시 결정이 되고,

변경이 불가능하기 때문에 얕은 복사를 해도 문제 없습니다.

좌측 상단 좌표 : [2, 2]
우측 하단 좌표 : [7, 7]

좌측 상단 좌표 : [1, 1]
우측 하단 좌표 : [9, 9]

objectClass.Rectangle@60e53b93
objectClass.Rectangle@5e2de80c
objectClass.Point@1d44bcfa
objectClass.Point@266474c2
objectClass.Point@6f94fa3e
objectClass.Point@5e481248

 

이상입니다.

오늘도 좋은 하루 보내세요.

감사합니다.

반응형

'JAVA > 기본 개념' 카테고리의 다른 글

#10 열거형(Enum)  (0) 2022.08.31
#5 추상 클래스 (Abstract Class)  (0) 2022.08.24
#4 예외처리(Exception Handling)  (0) 2022.08.22
#3 인터페이스  (0) 2022.08.22
#1 자바 개발 핵심 원칙  (0) 2022.07.11

+ Recent posts