제 12장 모든 클래스의 부모 클래스는 Object
모든 자바 클래스의 부모인 java.lang.object 클래스
public class InheritanceObject {
public static void main(String[] args) {
InheritanceObject object = new InheritanceObject();
System.out.println(object.toString());
}
}
위 코드를 보면 extends
예약어를 사용하여 상속을 받고 있지 않다. 하지만 자바에서는 기본적으로 Object
라는 것을 상속하고 있다.
그렇다면 위의 클래스가 Object
클래스를 상속하고 있는지 어떻게 알 수 있을까?
- 가장 쉬운 방법 :
object
안에 있는 메소드를 사용하는 것
위 코드를 보면 InheritanceObejct
클래스에는 main() 메소드외에는 선언되어 있지 않다.
하지만 object.toString()
이라는 메소드가 호출 되었다. 이것이 object
클래스 안에 메소드를 사용하므로써 object
를 상속하고 있다고 증명할 수 있다.
모든 클래스는 Object
클래스를 상속을 받는 것일까?
가장 큰 이유는 Object
클래스에 있는 메소드들을 통해서 클래스의 기본적인 행동을 정의할 수 있기 때문이다.
Object클래스에는 어떤 메소드들이 있을까?
Object
클래스에 선언되어 있는 메소드는 객체를 처리하기 위한 메소드와 쓰레드를 위한 메소드로 나뉜다.
- 객체를 처리하기 위한 메소드
메소드 | 설명 |
---|---|
protected Object clone( ) | 객체의 복사본을 만들어 리턴한다. |
public boolean equals(Object obj) | 현재 객체와 매개 변수로 넘겨받은 객체가 같은지 확인한다. 같으면 true를 다르면 false를 리턴한다. |
protected void finalize( ) | 현재 객체가 더 이상 쓸모가 없어졌을 때 가비지 컬렉터(garbage collector)에 의해서 이 메소드가 호출된다. |
public Class<?> getClass( ) | 현재 객체의 Class 클래스의 객체를 리턴한다. |
public int hashCode( ) | 객체에 대한 해시 코드(hash code)값을 리턴한다. 해시 코드라는 것은 “16진수로 제공되는 객체의 메모리 주소”를 말한다. |
public String toString( ) | 객체를 문자열로 표현하는 값을 리턴한다. |
- 쓰레드 처리를 위한 메소드
메소드 | 설명 |
---|---|
public void notify( ) | 이 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨운다. |
public void notifyAll( ) | 이 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨운다. |
public void wait( ) | 다른 쓰레드가 현재 객체에 대한 notify( ) 메소드나 notifyAll( )메소드를 호출할 때까지 현재 쓰레드가 대기하고 있도록 한다. |
public void wait(Long timeout) | wait() 메소드와 동일한 기능을 제공하며, 매개 변수에 지정한 시간만큼만 대기한다. 즉, 매개 변수 시간을 넘어 섰을 때에는 현재 쓰레드는 다시 깨어 난다. 여기서의 시간은 밀리초로 1/1,000초 단위다. 만약 1초간 기다리게 할 경우에는 1000을 매개 변수로 넘겨 주면 된다. |
public void wait(long timeout, int nanos) | wait() 메소드와 동일한 기능을 제공한다. 하지만 wait(timeout)에서 밀리초 단위의 대기 시간을 기다린다면, 이 메소드는 보다 자세한 밀리초 + 나노초(1/100,000,000초) 만큼만 대기한다. 뒤에 있는 나노초의 값은 0 ~ 999,999 사이의 값만 지정할 수 있다. |
Object 클래스에서 가장 많이 쓰이는 toString() 메소드
객체를 처리하기 위한 메서드
- toString()
- equals()
- hashCode()
- getClass()
- clone()
- finalize()
toString( )메소드
Object 클래스의 메소드 중 가장 많이 사용된다. 해당 클래스가 어떤 객체인지를 쉽게 나타낼 수 있는 메소드이다.
이 메소드가 자동으로 호출되는 경우는 다음과 같다.
- Systme.out.println() 메소드
- 객체 대하여 더하기 연산을 하는 경우
public class ToString {
public static void main(String[] args) {
ToString thisObject = new ToString();
thisObject.toStringMethod(thisObject);
}
public void toStringMethod(Object obj){
System.out.println(obj); // 객체 그대로 출력
System.out.println(obj.toString()); //toString() 메소드 호출
System.out.println("plus " + obj); // 객체의 더하기 연산 수행
}
}
결과 :
main.Chapter12.inheritance.ToString@1cd072a9
main.Chapter12.inheritance.ToString@1cd072a9
plus main.Chapter12.inheritance.ToString@1cd072a9
object
클래스에 구현되어 있는 toString() 메소드
getClass()의 결광[ getName() 메소드를 부르면 현재 클래스의 패키지 이름
과 클래스 이름
이 나온다.
at
(@, 보통 골뱅이라 부른다)가 붙는다. 앞의 결과와 뒤의 결과를 구분하기 위한 구분자이다.
그리고 마지막 hashCode()
메소드에서는 int 타입의 값을 리턴해준다.
그 값을 Integer
라는 클래스에서 제공하는 toHexString()
이라는 메소드를 활용하여 16진수로 변환하는 작업이 수행된다.
위와 같이 분석해야하는 이유는 Overriding
을 적용할 때 접근제어자, 리턴타입, 메소드이름, 매개 변수 타입과 개수들이 모두 동일해야 한다.
public class ToString {
public static void main(String[] args) {
ToString thisObject = new ToString();
thisObject.toStringMethod(thisObject);
}
public String toString(){
return "ToString class";
}
}
toString()
메소드의 선언부를 보면 접근 제어자는 pubilc이고, 리턴 타입은 String인 것을 알 수 있다. 이렇게 toString() 메소드를 변경한 후에 ToString클래스를 컴파일하고 다시 실행해보자.
결과 :
ToString class
ToString class
plus ToString class
패키지를 포함한 클래스 이름과 골뱅이, 그리고 hashCode()
메소드를 수행한 결과가 나오지 않고, 방금 수정한 toString() 메소드의 내용대로 나온 것을 볼 수 있다.
toString() 메소드는 언제 Overriding해야 할까?
모든 클래스의 toString()을 Overriding할 필요는 없다.
public class MemberDTO {
public String name;
public String phone;
public String email;
public MemberDTO(){
//아무 정보도 모를 때
}
public MemberDTO(String name) {
//이름만 알고 있을 때
this.name = name;
}
public MemberDTO(String name, String phone) {
//이름과 전화번호만 알고 있을 때
this.name = name;
this.phone = phone;
}
public MemberDTO(String name, String phone, String email) {
//모든 정보를 알고 있을 때
this.name = name;
this.phone = phone;
this.email = email;
}
}
만약 toString() 메소드가 Overriding 되어 있지 않다면, 이 MemberDTO에 선언된 값들을 확인할 때 어떻게 해야할까 ?
MemberDTO dto = new MemberDTO("Dante", "01011112222", "abc@naver.com");
System.out.println("Name=" + dto.name + "phone=" + dto.phone+ "eMail=" + dto.email);
이 MemberDTO
를 사용해야하는 부분이 많은데, 사용할 때마다 이와 같이 출력을 해봐야 각각의 값이 어떻게 되어 있는지를 확인할 수 있을 것이다. 이런 경우 toString()
을 Overriding
해 놓으면 된다.
public class MemberDTO{
//중간 생략
public String toString(){
return "Name ="+name+" phone="+phone + " eMail=" + email;
}
}
객체는 ==만으로 같은지 확인이 안된다. 그래서 equals()를 사용한다.
import main.Chapter08.MemberDTO;
public class Equals {
public static void main(String[] args) {
Equals thsiObject = new Equals();
thsiObject.equalMehtod();
}
public void equalMehtod(){
MemberDTO obj1 = new MemberDTO("Dante");
MemberDTO obj2 = new MemberDTO("Dante");
if(obj1 == obj2){
System.out.println("obj1 and obj2 is same");
}else{
System.out.println("obj1 and obj2 is different");
}
}
}
MemberDTO는 name, phone, email이라는 세 개의 속성을 갖고 있다.
==
는 값을 비교하는 것이다.
obj1
의 출력 결과 : Name =Dante phone=null eMail=nullobj2
의 출력 결과 : Name =Dante phone=null eMail=null
결과 :
obj1 and obj2 is different
different
가 나온 이유는 두 객체는 각각의 생성자를 사용하여 만들었기 때문에 주소값이 다르다.
그런데
그 안에 있는 속성값들은 name “Dante”, phone과 email은 모두 null이므로 동일하다. 그래서 이와 같이 참조 자료형은 equals()
라는 메소드를 사용하여 두 객체를 비교해야 한다.
또한 Object
클래스에 선언되어 있는 equals()
메소드를 Overriding
해놔야지 제대로 된 비교가 가능하다.
public void equalsMethod2(){
MemberDTO obj1 = new MemberDTO("Dante");
MemberDTO obj2 = new MemberDTO("Dante");
if(obj1.equals(obj2)){
System.out.println("obj1 and obj2 is same");
}else{
System.out.println("obj1 and obj2 is different");
}
}
결과 :
obj1 and obj2 is different
왜 different? equals() 메소드로 비교를 하긴 했지만, 비교 대상 객체인 MemberDTO 클래스에서는 아직 equals() 메소드를 Overriding
하지 않았기 때문에 이와 같은 결과가 나온 것이다.
Overriding
을 하지 않으면 equals()
메소드 에서는 hashCode() 값을 비교한다.
hashCode()
값은 해당 객체의 주소값을 리턴한다.
따라서 클래스의 인스턴스 변수값들이 같다고 하더라도, 서로 다른 생성자로 객체를 생성했으면 해시 코드가 다르니 두 객체는 다르다는 결과가 나온 것이다.
- MemberDTO 클래스에 equals() 메소드를 Overriding 한다.
public class MemberDTO {
public boolean equals(Object obj) {
if(this == obj) return true; // 주소가 같으므로 당연히 true
if(obj == null) return false; // obj가 null이므로 당연히 false
if(getClass() != obj.getClass()) return false; // 클래스의 종류가 다르므로 false
MemberDTO other = (MemberDTO) obj; // 같은 클래스이므로 형 변환 실행
// 이제부터는 각 인스턴스 변수가 같은지 비교하는 작업 수행
if(name == null){ // name이 null일 때
if(other.name != null) return false; // 비교 대상의 name이 null이 아니면 false
}else if(!name.equals(other.name)) return false;// 두 개의 email 값이 다르면 false
// email
if (email == null) { //email이 null일 때
if(other.email != null) return false;
}else if(!email.equals(other.name)) return false;
if (phone == null) { //phone이 null일 때
if(other.phone != null) return false;
}else if(!phone.equals(other.phone)) return false;
// 모든 난관을 거쳐서 false를 리턴하지 않은 객체는 같은 값을 가지는 객체로 생각해서 true를 리턴한다.
return true;
}
}
결과 :
obj1 and obj2 is same
equals()
메소드를 Overriding
할 때에는 “필수조건”
다섯 가지가 있다.
- 재귀(reflexive) : null 이 아닌 x라는 객체의 x.equals(x) 결과는 항상 true 여야만 한다.
- 대칭(symmetric) : null이 아닌 x와 y객체가 있을 때 y.equals(x)가 true를 리턴했다면, xequals(y)도 반드시 true를 리턴해야만 한다.
- 타동적(transitive) : null 아닌 x,y,z가 있을 때 x.equals(y)가 true를 리턴해야만 한다.
- 일관(consistent) : null이 아닌 x와 y가 있을 때 객체가 변경되지 않은 상황에서는 몇 번을 호출하더라도 x.equals(y)의 결과는 항상 true 이거나 항상 false여야만 한다.
- null과의 비교 : null이 아닌 x라는 객체의 x.equals(null) 결과는 항상 false여야만 한다.
이 기준은 Java API문서에 적혀져있다.
한 가지 유념해야 하는 것이 있다. equals() 메소드를 Overriding할 때에는 hashCode() 메소드도 같이 Overriding해야만 한다는 것이다.
이유는 equals() 메소드를 Overriding해서 객체가 서로 같다고 이야기할 수 있지만, 그 값이 같다고 해서 그 객체의 주소 값이 같지는 않기 때문이다. 즉, eqauls() 메서드의 결과가 true인데도 불구하고, hashCode() 메소드의 값은 다르게 된다.
같은 hashCode() 메소드 결과를 갖도록 하려면 hashCode() 메소드도 object 클래스에서 제공하는 그대로 사용하면 안 된다.
public class MemberDTO{
//중간 생략
public int hashCode(){
final int prime = 31;
int result = 1;
result = prime * result + ((email == null) ? 0 : email.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((phone == null) ? 0 : phone.hashCode());
return result;
}
}
equals()
메소드를 반드시 Overriding
해야 하는 것은 아니다.
필요할 때만 Overriding
하면 된다. DTO
를 만들 경우에는 객체 비교를 위해서 반드시 필요한 것이지만, 그렇지 않은 메소드만 있는 기능 위주의 클래스를 만들 때에는 힘들게 equals()
메소드를 Overriding
할 필요가 없다.
객체의 고유값을 나타내는 hashCode()
hashCode() 메소드는 기본적으로 객체의 메모리 주소를 16진수로 리턴한다.
어떤 두 개의 객체가 서로 동일하다면, hashCode()값은 무조건 동일해야만 한다.
equals() 메소드를override하면, hashCode() 메소드도override해서 동일한 결과가 나오도록 만들어야만 한다.
hashCode() 메소드를 구현할 때 지켜야할 약속
자바 API문서에서는 이 메소드를 Overriding할 때에는 다음과 같은 조건을 따라야 한다고 명시되어있음
- 자바 애플리케이션이 수행되는 동안에 어떤 객체에 대해서 이 메소드가 호출될 때에는 항상 동일한 int 값을 리턴해 주어야 한다. 하지만, 자바를 실행할 때마다 같은 값이어야 할 필요는 전혀 없다.
- 어떤 두 개의 객체에 대하여 equals() 메소드를 사용하여 비교한 결과가 true일 경우에, 두 객체의 hashCode() 메소드를 호출하면 동일한 int 값을 리턴해야만 한다.
- 두 객체를 equals() 메소드를 사용하여 비교한 결과 false를 리턴했다고 해서, hashCode() 메소드를 호출한 int 값이 무조건 달라야 할 필요는 없다. 하지만 이 경우에 서로 다른 int 값을 제공하면 hashtable의 성능을 향상시키는데 도움이 된다.
위와 같은 제약들 때문에 직접 equals() 메소드나 hashCode() 메소드를 작성하는 것은 별로 권장하지 않는다.
각종 툴(이클립스, 인텔리 제이, STS 등) 자동으로 생성해주는 기능을 제공하므로 그 기능 사용을 권장한다.