프로그래밍 공부
[자바] #05. Object 클래스에 정의되어 있는 주요 메소드 (equals, clone) 본문
📢 equals(Object obj): 객체 비교
Object 클래스의 기본 equals()는 두 객체의 메모리 주소(참조값)를 비교한다.
public boolean equals(Object obj) {
return (this == obj); // 기본적으로 같은 객체인지 주소 비교
}
즉, 기본적으로 == 연산자와 동일하게 동작하는 것이다.
equals() 오버라이딩
객체 간에 내용(값)이 같은 경우에 true를 반환하도록 equals()를 재정의해보자.
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
Person person = (Person)obj;
if (age == person.age && name.equals(person.name))
return true;
else
return false;
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("홍길동", 30);
Person p2 = new Person("홍길동", 30);
Person p3 = new Person("김철수", 25);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false
}
}
❓ equals(Person per)로 선언하고 굳이 다운캐스팅을 안 할수는 없는 걸까?
public boolean equals(Person per) {
if(age == per.age && name.equals(per.name))
return true;
else
return false;
}
// 이렇게는 안 되는 걸까?
🚨 안 된다!
자바의 equals()는 항상 Object 타입을 인자로 받아야 한다.
그래야 Object의 equals()를 올바르게 오버라이딩할 수 있는 것이다.
만약에 equals(Person per)로 해 버리면 매개변수가 다르기 때문에 오버라이딩하는 게 아니라 아예 새로운 메소드를 만드는 것이 돼버린다.
✅ Object 타입을 받으면 모든 객체에서 equals()를 호출할 수 있고,
✅ 자바 컬렉션(HashMap, HashSet)에서도 정상 동작한다.
📢 clone(): 객체 복사
clone()을 사용하려면 Cloneable 인터페이스를 구현해야 한다.
class Person implements Cloneable {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 'Object' 클래스의 clone() 호출 (얕은 복사)
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("홍길동", 30);
Person p2 = (Person) p1.clone();
System.out.println(p1.equals(p2)); // false
}
}
✅ clone()을 사용하면 새로운 객체를 만들 수 있다.
✅ Cloneable 인터페이스를 구현하지 않으면 CloneNotSupportedException 예외 발생!
✅ 기본적으로 "얕은 복사(Shallow Copy)"가 수행된다.
❓ 얕은 복사와 깊은 복사?
👀 얕은 복사 (Shallow Copy)
기본형 변수(int, double 등)는 값이 복사되지만,
참조형 변수(String, Address)는 주소값이 복사되어 원본과 같은 객체를 참조한다.
class Address {
String city;
public Address(String city) {
this.city = city;
}
}
class Person implements Cloneable {
String name;
Address address; // 참조 타입
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); // 얕은 복사
}
@Override
public String toString() {
return "Person{name='" + name + "', address=" + address.city + "}";
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("홍길동", new Address("서울"));
Person p2 = (Person) p1.clone(); // 얕은 복사 수행
System.out.println(p1);
System.out.println(p2);
p2.address.city = "부산"; // 원본 객체도 영향을 받음
System.out.println(p1); // 주소가 변경됨!
System.out.println(p2);
}
}
💡 실행 결과
Person{name='홍길동', address=서울}
Person{name='홍길동', address=서울}
Person{name='홍길동', address=부산} // 원본 객체도 변경됨!
Person{name='홍길동', address=부산}
✅ 객체(Address)의 참조값이 복사되었으므로, 하나의 객체를 공유하게 된다.
✅ 즉, 복사본(p2)의 값을 변경하면 원본(p1)에도 영향을 미친다.
✅ 이 문제를 해결하려면 깊은 복사가 필요하다.
👀 깊은 복사 (Deep Copy)
참조형 필드도 새로운 객체로 복사하여 독립적인 복사본을 생성한다.
class Address implements Cloneable {
String city;
public Address(String city) {
this.city = city;
}
@Override
public Object clone() throws CloneNotSupportedException {
return new Address(this.city); // 깊은 복사: 새로운 객체 생성
}
}
class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
public Object clone() throws CloneNotSupportedException {
Person copy = (Person)super.clone();
copy.address = (Address)address.clone(); // 깊은 복사 수행
return copy;
}
@Override
public String toString() {
return "Person{name='" + name + "', address=" + address.city + "}";
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("홍길동", new Address("서울"));
Person p2 = (Person)p1.clone(); // 깊은 복사 수행
System.out.println(p1);
System.out.println(p2);
p2.address.city = "부산"; // 원본 객체에는 영향 없음!
System.out.println(p1); // 원본 유지됨
System.out.println(p2);
}
}
💡 실행 결과
Person{name='홍길동', address=서울}
Person{name='홍길동', address=서울}
Person{name='홍길동', address=서울} // 원본 유지됨!
Person{name='홍길동', address=부산}
✅ 객체의 참조값을 복사하지 않고, 새로운 객체를 만들어 독립적으로 유지한다.
✅ 이제 복사본(p2)의 값이 변경되어도 원본(p1)은 영향을 받지 않는다.
✅ 깊은 복사는 객체 내부에 clone()을 재귀적으로 호출해야 한다.
❓ 질문 1. clone() 메소드 내에서 super.clone()은 무엇을 의미하는 것인가?
자바에서 clone() 메서드를 오버라이딩할 때 super.clone()을 호출하는 이유는,
부모 클래스(Object)에서 제공하는 clone() 메서드를 사용하여 객체를 복제하기 위해서이다.
protected native Object clone() throws CloneNotSupportedException;
native 키워드가 붙어 있기 때문에, clone()은 JVM 내부에서 실행되는 네이티브 코드(C++로 구현됨)를 사용하여 객체를 복제한다.
즉 super.clone()을 호출하면, 현재 객체의 메모리 구조를 그대로 복사한 새 객체를 생성하여 반환한다.
서로 다른 새로운 객체가 생성되지만, 필드의 값은 동일하다.
기본형(Primitive Type) 데이터는 그대로 복사되고, 참조형(Reference Type) 데이터는 메모리 주소만 복사된다.
근데 참조형 필드(address 객체)를 직접 복제할 때도 그냥 얕은 복사가 진행되는 clone()을 쓰는 것 아닌가?
그런데 어떻게 address 객체의 메모리 주소가 달라지는 것인가?
-> 그래서 위 예제에서 Address 클래스도 clone()을 오버라이딩해서 새로운 객체를 생성하도록 해 놓았다.
❓ 질문 2. 그냥 new로 새 객체를 생성하면 되는 거 아냐? 왜 clone()을 써야 해?
✔ clone()은 객체의 모든 필드를 자동으로 복사해주기 때문에,
new를 사용한 복사 생성자보다 더 빠르고 간편하게 객체를 복제할 수 있다.
✔ 하지만, clone()은 구현이 복잡할 수 있기 때문에, 상황에 따라 new를 사용하는 복사 생성자 방식도 고려해야 한다.
// 복사 생성자 방식
class Person {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = new Address(address.city); // 깊은 복사
}
}
❓ 추가 질문)
(Person) p1.clone()은 p1을 Person으로 형 변환한 후 clone()을 호출하는 게 되지 않을까?
즉, (Person)p1.clone()가 아니라 (Person)(p1.clone())로 써야 하는 거 아닌가?
-> ✅ 정답은 (Person) p1.clone()는 사실 p1.clone()을 먼저 실행한 후,
반환된 값을 Person으로 형 변환하는 것이다!
📌 실행 순서
- p1.clone() 실행
- clone()은 Object 타입을 반환함. (Object의 clone() 정의를 따름)
- 반환된 Object 타입을 (Person)으로 형 변환
- p1.clone()의 결과는 Object 타입이므로, 이를 Person 타입으로 캐스팅.
즉, 괄호 위치와 상관없이 항상 p1.clone()이 먼저 실행되고, 그 결과를 Person 타입으로 형 변환하는 것.
아니 그럼 왜 항상 p1.clone()이 먼저 실행되는 것인가?
-> ✅ 이는 자바의 연산자 우선순위와 형 변환(캐스팅) 규칙 때문이다.
메서드 호출 연산자 .는 형 변환 연산자 ()보다 우선순위가 높다!
그래서 p1.clone()가 먼저 실행되고 clone()의 반환 타입은 Object, 그 결과를 Person 타입으로 변환한다.
'자바' 카테고리의 다른 글
| [자바] #07. 제네릭 (Generics) (0) | 2025.02.15 |
|---|---|
| [자바] #06. 배열의 정렬 sort (0) | 2025.02.15 |
| [자바] #04. 자바에서의 메모리 구조 - 메소드 영역과 힙 영역 (0) | 2025.02.14 |
| [자바] #03. 예외 클래스의 구분, try-with-resources 구문 (0) | 2025.02.14 |
| [자바] #02. 디폴트 메소드(Default Method) (0) | 2025.02.14 |