
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 |