[Java 자바] 11. 기본 API 클래스 ② Object 클래스
11-3. Object 클래스
- 클래스 선언시 extends 키워드로 다른 클래스를 상속하지 않으면 암시적으로 java.lang.Object 클래스를 상속
- 최상위 부모 클래스로 필드가 없고 11개의 메소드로만 구성되어 있음
11-3-1. 객체 비교 (equals())
public boolean equals(Object obj) {...}
- 매개 타입이 Object이기 때문에 모든 객체를 매개값으로 대입 가능
- 두 객체를 비교하여 논리적으로 동등하면 true, 그렇지 않으면 false 리턴
(논리적으로 동등하다 == 객체가 저장하고 있는 데이터가 동일하다는 것을 의미)
- Object의 equals() 메소드는 직접 사용되지 않고 하위 클래스에서 재정의하여 논리적으로 동등비교할 때 사용
(ex. String객체에선 equals()를 번지 비교가 아닌 문자열 비교가 가능하도록 오버라이딩하여 사용)
[ equals() 메소드 재정의 예시 ]
객체 간 동일 필드를 가지고 있다면 논리적으로 동등하다고 가정하고 싶을 때
① 매개값(비교 객체)이 기준 객체와 동일한 타입의 객체인지 먼저 확인 필요(instanceof)
② 비교 객체가 동일 타입이라면 기준 객체 타입으로 강제 타입 변환하여 필드값이 동일한지 검사
// Member.java
public class Member {
public String id;
public Member(String id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Member) {
Member member = (Member) obj;
if(id.equals(member.id)) {
return true;
}
}
return false;
}
}
//MemberTest.java
public class MemberTest {
public static void main(String[] args) {
Member obj1 = new Member("blue");
Member obj2 = new Member("blue");
Member obj3 = new Member("red");
if(obj1.equals(obj2)) {
System.out.println("동등합니다.");
} else {
System.out.println("동등하지 않습니다.");
}
if(obj1.equals(obj3)) {
System.out.println("동등합니다.");
} else {
System.out.println("동등하지 않습니다.");
}
}
}
// 출력값
// 동등합니다.
// 동등하지 않습니다.
11-3-2. 객체 해시코드 (hashCode())
- 객체 해시코드: 객체를 식별할 하나의 정수값
- Object의 hashCode() 메소드는 객체의 메모리 번지를 이용해 해시코드를 만들어 리턴하기 때문에 객체마다 다른 값을 가짐
- 논리적으로 동등한 객체는 hashcode 규약에 따라 반드시 같은 해시코드 값을 가져야 함
- 따라서, equals() 재정의를 통해 서로 다르게 생성한 객체를 같다고 정의할 경우, hashcode()도 이에 맞게 재정의 필요
…
If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
…
- HashSet, HashMap, Hashtable은 동등비교시 아래와 같은 방법으로 두 객체를 비교하기 때문에, 재정의하지 않으면 오류 발생
[ 해시코드를 재정의하지 않고 equals만 재정의 했을 때 ]
- number 필드가 같더라도, hashCode()에서 리턴하는 해시코드가 다르기 때문에 다른 식별키로 인식됨
- 즉, 해시맵에 값을 넣을 때 사용된 해시값과 조회할 때 사용되는 해시값이 다르기 때문에 null 값이 출력됨
//Key.java
public class Key {
public int number;
public Key(int number) {
this.number = number;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Key) {
Key compareKey = (Key) obj;
if(this.number == compareKey.number) {
return true;
}
}
return false;
}
}
//KeyTest.java
public class KeyTest {
public static void main(String[] args) {
HashMap<Key, String> hashMap = new HashMap<Key, String>();
hashMap.put(new Key(1), "Erin");
String value = hashMap.get(new Key(1));
System.out.println(value); // null
}
}
[ 해시코드까지 재정의 ]
- 리턴하는 해시코드의 값을 number로 바꿔줌으로써, 같은 number 값을 가진 객체는 동일한 해시코드 값을 갖도록 변경
//Key.java
public class Key {
...
@Override
public int hashCode() {
return number;
}
}
11-3-3. 객체 문자 정보 (toString())
- 객체의 문자 정보(객체를 문자열로 표현한 값)를 리턴
- 기본적으로 Object 클래스의 toString() 메소드는 "클래스명@16진수해시코드"로 구성된 문자 정보를 리턴
- Object 하위 클래스는 toString() 메소드를 재정의하여 간결하고 유익한 정보를 리턴하도록 되어 있음
ex. Date 클래스는 toString() 메소드를 재정의하여 현재 시스템의 날짜와 시간 정보 리턴
String 클래스는 저장하고 있는 문자열을 리턴
- 사용자가 만드는 클래스 역시 toString() 메소드를 재정의하여 유용한 정보를 리턴하게 할 수 있음
- System.out.println() 메소드는 매개값이 기본 타입일 경우 해당 값을 그대로 출력, 객체일 경우 객체의 toString() 메소드를 호출해서 리턴값을 받아 출력하도록 되어 있음
11-3-4. 객체 복제 (clone())
protected native Object clone() throws CloneNotSupportedException;
- 객체 복제: 원본 객체의 필드값과 동일한 값을 가지는 새로운 객체를 생성하는 것
- 원본 객체를 안전하게 보호하기 위해 복제된 객체를 만들어 신뢰하지 않은 영역으로 넘겨 작업을 진행할 수 있음
1) 얕은 복제 (thin clone, shallow copy)
- 단순 필드값을 복사해서 객체를 복제하는 것을 의미
- 필드가 기본 타입일 경우 값 복사가 일어나고 참조 타입일 경우 객체의 번지가 복사되어 같은 객체를 참조하게 됨
- 이 경우, 복제 객체를 변경하면 원본 객체 역시 영향을 받게 됨 (기본 타입일 경우에는 원본 값에 영향 없음)
- Object의 clone() 메소드는 자신과 동일한 필드값을 가진 얕은 복제된 객체를 리턴
❗️ clone() 메소드로 객체를 복사하려면 원본 객체는 반드시 Cloneable 인터페이스를 구현하고 있어야 함
- 클래스 설계자가 복제를 허용한다는 의도적 표시
- 해당 인터페이스가 없는 클래스 복사 시도시에 CloneNotSupportedException 예외 발생
- clone()은 Clonable 인터페이스의 추상메소드로 작동하기 때문에 사용하는 곳에서 오버라이딩 필요
//Member.java
public class Member implements Cloneable{
public String id;
public String password;
public Member(String id, String password) {
this.id = id;
this.password = password;
}
@Override
public Member clone() {
try {
return (Member) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
//MemberTest.java
public class MemberTest {
public static void main(String[] args) {
Member member1 = new Member("Erin", 1234);
Member cloned = member1.clone();
cloned.password = 2345;
System.out.println(member1.password); // 1234
System.out.println(cloned.password); // 2345
}
}
2) 깊은 복제 (deep clone, deep copy)
- 참조하고 있는 객체도 복제
- 깊은 복제를 위해서는 Object의 clone() 메소드를 재정의하여 참조 객체를 복제하는 코드를 직접 작성해야 함
package object3_clone;
import java.util.Arrays;
public class Member implements Cloneable{
public String id;
public int password;
public int[] scores;
public Car car;
public Member(String id, int password, int[] scores, Car car) {
this.id = id;
this.password = password;
this.scores = scores;
this.car = car;
}
@Override
public Member clone() {
try {
// 기본 필드 얕은 복제
Member cloned = (Member) super.clone();
// 배열 깊은 복제
cloned.scores = Arrays.copyOf(this.scores, this.scores.length);
// 객체 깊은 복제
cloned.car = new Car();
return cloned;
} catch (CloneNotSupportedException e) {
return null;
}
}
}
11-3-5. 객체 소멸자 (finalize())
- GC는 참조하지 않는 배열이나 객체를 힙 영역에서 자동으로 삭제
- 객체 소멸 직전에 마지막으로 객체의 소멸자(finalize())를 실행시킴
- 객체가 소멸되기 전에 마지막으로 사용했던 자원(데이터 연결, 파일 등)을 닫고 싶거나 중요 데이터를 저장하고 싶으면
Object의 finalize()를 재정의하여 사용 가능
- finalize()의 경우, JVM에 의해 자동 실행되는 메소드이기 때문에 호출 시점이 명확하지 않음
- 따라서, 일반 메소드로 명시적으로 호출하는 것이 좋은 방법