도도한 개발자

[Java] #12. Object. 모든 클래스의 부모 클래스 본문

Backend/Java

[Java] #12. Object. 모든 클래스의 부모 클래스

Kiara Kim 2022. 3. 15. 21:54

* 모든 자바 클래스의 부모인 java.lang.Object

 

자바에선 기본적으로 이무런 상속을 받지 않으면 java.lang.Object 클래스를 확장한다. 확장하는지 안하는지를 확인하고자 하면 그 클래스에 있는 메소드를 사용하면 된다. 

 

public class C {
	public static void main(String[] args) {
    	C c = new C();
        System.out.println(object.toString());	// @
    }
}

 

C 클래스에는 main() 메소드 외에는 선언되어 잇는 메소드가 없다. 그런데 toString() 메소드가 호출된 것을 볼 수 있는데 컴파일이나 실행에도 전혀 문제가 없다. 자바는 한번에 이중 상속을 받을 수는 없지만 여러 단계로 상속을 받을 수는 있다. 부모 클래스 A 클래스를 자식 클래스 B 클래스가 상속하면 부모 클래스의 부모 클래스인 Object 클래스의 메소드를 B 클래스에서도 사용할 수 있는 것이다. 

 

이렇게 모든 클래스가 Object 클래스의 상속을 받는 이유는 이 클래스의 메소드들을 통해 클래스의 기본적인 행동을 정의할 수 있기 때문이다. 

 

 

* Object 클래스의 메소드

 

Object 클래스에 선언되어 있는 메소드는 객체를 처리하기 위한 메소드와 쓰레드를 위한 메소드로 나뉜다.

 

** 객체 처리 메소드

 

· protected Object clone() : 객체의 복사본을 만들어 리턴한다.

· public boolean equals(Object obj) : 현재 객체와 매개 변수로 넘겨받은 객체가 같은지 확인한다. 같으면 true를, 다르면 false를 리턴한다.

· protected void finalize() : 현재 객체가 더 이상 쓸모가 없어졌을 때 가비지 컬렉터에 의해서 이 메소드가 호출된다. 

· 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초 단위이다.

· public void wait(long timeout, int nanos) : wait() 메소드와 동일한 기능을 제공한다. 밀리초보다 자세한 밀리초 + 나노초(1/1,000,000,000초) 만큼만 대기한다.

 

 

* toString() 메소드

 

이 메소드는 Object 클래스의 메소드 중에서 가장 많이 사용된다. 해당 클래스가 어떤 객체인지를 쉽게 나타낼 수 있는 중요한 메소드다. 이 메소드가 호출되는 경우는 다음과 같다.

 

· System.out.println() 메소드에 매개 변수로 들어가는 경우

· 객체에 대하여 더하기 연산을 하는 경우

 

public class ToString {

	public static void main(String[] args) {
		ToString thisObj = new ToString();
		thisObj.toStringMethod(thisObj);
	}
	
	public void toStringMethod(Object obj) {
		System.out.println(obj);		// @1
		System.out.println(obj.toString());	// @2
		System.out.println("plus" + obj);	// @3
	}
}

 

@1은 객체를 그대로 출력했고, @2는 toString() 메소드를 호출했다. toString() 메소드는 ToString 클래스에 선언되지 않았지만 Object 클래스의 상속을 자동으로 받았기 때문이다. 그리고 @3은 객체의 더하기 연산을 수행했다. 그런데 참조 자료형의 더하기 연산은 String만 가능한데 어떻게 된 일일까? 클래스를 실행해보면 다음과 같다.

 

chapter12.ToString@123772c4
chapter12.ToString@123772c4
plus chapter12.ToString@123772c4

 

앞의 chapter12는 패키지 이름이니 패스하고, 객체를 그냥 출력하는 것과 객체의 toString() 메소드를 호출하는 것을 동일하다는 것을 볼 수 있다. 그렇다면 마지막줄의 더하기 연산을 한 결과는 어떤가? plus 뒤에 toString() 한 것과 동일한 결과가 출력되었다. 다시 말해, String을 제외한 참조 자료형에 더하기 연산을 수행하면, 자동으로 toString() 메소드가 호출되어 객체의 위치에는 String 값이 놓이게 된다.

 

위의 예제를 보다 깔끔하게 만드는 방법은 없을까? 자신의 객체에 대한 참조를 할 때는 8장의 참조 자료형에서 this를 사용한다고 했다. 그럼 다음과 같이 표현할 수 있다.

 

public void toStringMethod2() {
	System.out.println(this);
	System.out.println(toString());
	System.out.println("plus " + this);
}

 

this는 자기 자긴을 의미하므로 굳이 매개 변수를 넘겨줄 필요가 없는 것이다. 근데 toString()으로 출력된 결과는 뭘까? 실제 Object 클래스에 구현되어 있는 toString() 메소드는 다음과 같다.

 

getClass().getName() + '@' + Integer.toHexString(hashCode())

 

Object 클래스에 있는 getClass()의 결과에 getName()메소드를 부르면 현재 클래스의 패키지 이름과 클래스 이름이 나온다. 그 다음에 at(@)가 붙는다. 앞의 결과와 뒤 결과를 구분하기 위한 구분자이다. 그리고 마지막 부분에는 객체의 해시 코드 값을 출력한다. hashCode() 메소드에서는 int 타입의 값을 리턴해준다. 그 값을 Integer 라는 클래스에서 제공하는 toHexString()이라는 메소드를 활용하여 16진수로 변환하는 작업이 수행된다.

 

도대체 이런 작업을 왜 알아야 할까? Overriding을 제대로 구현하기 위해서이다. 그러려면 접근 제어자, 리턴 타입, 메소드 이름, 매개 변수 타입과 개수들이 모두 동일해야 한다.

 

public class ToString {

	public static void main(String[] args) {
		ToString thisObj = new ToString();
		thisObj.toStringMethod(thisObj);
	}
	// 중간 생략
	public String toString() {
		return "ToString class";
	}
}

 

toString() 메소드의 선언부를 보면 접근 제어자는 public이고, 리턴 타입은 String인 것을 알 수 있다. 이 클래스를 컴파일하고 실행하보면 다음과 같다.

 

ToString class
ToString class
plus ToString class

 

패키지를 포함한 클래스 이름과 골뱅이, 그리고 hashCode() 메소드를 수행한 결과가 나오지 않고, 방금 수정한 toString() 메소드의 내용대로 나온 것을 볼 수 있다. 그러면 이런 Overriding은 언제 해야 할까? DTO를 사용할 때 이렇게 toString() 메소드를 Overriding 해 놓는 것이 좋다. 그래야 내용을 확인하기 쉽기 때문이다. 

 

public class infoDTO {
	public String name, phone, email;
	// 이하 생략
}

 

만약 toString() 메소드가 Overriding되어 있지 않다면 이 infoDTO에 선언된 값들을 어떻게 확인할 수 있을까?

 

infoDTO dto = new infoDTO("Kiara", "01012341234", "abc@java.com");
System.out.println("Name = " + dto.name + " Phone = " + dto.phone + " Email = " + dto.email);

 

이 infoDTO를 사용해야 하는 부분이 많은데 사용할 때마다 이렇게 출력을 해서 확인해봐야 한다.

 

public class infoDTO {
	public String toString() {
		// 중간 생략
		return "Name = " + name + " Phone = " + phone + " Email = " + email;
	}
}

 

이럴 때 toString()을 Overriding 해놓으면 다음과 같이 간단하게 사용할 수 있다.

 

infoDTO dto = new infoDTO("Kiara", "01012341234", "abc@java.com");
System.out.println(dto);

 

 

* equals() 메소드

 

두 연산자가 같은지 다른지 확인하기 위해 ==또는 !=연산자를 사용했다. 그러나 객체는 ==만으로 같은지 다른지 확인아 안된다. 이 연산자들은 기본 자료형에서만 사용할 수 있으며 참조 자료형은 사용할 수 없다. 비교는 되겠지만 그건 값을 비교하는 것이 아닌 '주소값'을 비교하는 것이라 의미가 없다. 

 

예를 살펴보기 위해 위에서 사용한 infoDTO 클래스를 사용해보겠다.

 

public class infoDTO {
	public String name, phone, email;
	// 이하 생략
}

 

그리고 Equals라는 클래스를 만든다.

 

public class Equals {

	public static void main(String[] args) {
		Equals thisObj = new Equals();
		thisObj.equalMethod();
	}
	
	public void equalMethod() {
		infoDTO obj1 = new infoDTO("Kiara");
		infoDTO obj2 = new infoDTO("Kiara");
		if(obj1.equals(obj2)) {
			System.out.println("obj1 and obj2 is same");
		} else {
			System.out.println("obj1 and obj2 is different");
		}
	}
}

 

이와 같은 참조 자료형은 equals()라는 메소드를 사용하여 두 객체를 비교해야 한다. 그리고, Object 클래스에 선언되어 있는 equals()메소드를 Overriding해 놓아야지만 제대로 된 비교가 가능하다.  obj1과 obj2 안에 있는 속성값들은 name은 "Kiara", phone과 email은 모두 null이므로 동일하다 그러나 결과는 다음과 같다.

 

obj1 and obj2 is different

 

equals() 메소드로 비교를 하긴 했지만, 비교 대상 객체인 infoDTO 클래스에서는 아직 equals() 메소드를 Overriding하지 않았기 때문이 이와 같은 결과가 나왔다. 메소드를 Overriding하지 않으면 equals() 메소드에서는 hashCode() 값을 비교한다. hashCode() 값은 해당 객체의 '주소값'을 리턴하기 때문에 클래스의 인스턴스 변수닶들이 같다고 하더라도, 서로 다른 생성자로 객체를 생성했으면 해시 코드가 달라 두 객체는 다르다고 나온 것이다.

 

이제 infoDTO 클래스에 equals() 메소드를 Overriding하자. 

 

public class infoDTO {
	// 중간 생략
	public boolean equals(Object obj) {
		if (this == obj) return true;				// 주소가 같으므로
		if (obj == null) return false;				// obj가 null이므로
		if (getClass() != obj.getClass()) return false;		// 클래스의 종류가 다르므로
		
		infoDTO other = (infoDTO) obj; 				// 같은 클래스이므로 형 변환
		
		if (name == null) {					// name이 null일 때
			if(other.name != null) return false;		// 비교 대상의 name이 null이 아니면
		} else if (!name.equals(other.name)) return false;	// 두 개의 email 값이 다르면
		
		if (phone == null) {
			if(other.phone != null) return false;
		} else if (!phone.equals(other.phone)) return false;

		if (email == null) {
			if(other.email != null) return false;
		} else if (!email.equals(other.email)) return false;
		
        // 여기까지 false를 리턴하지 않은 객체는 같은 값을 가지는 객체로 생각해 true 리턴
		return true;
	}
}

 

제대로 된 equals() 메소드는 사실 이클립스에서 자동으로 생성한 것이다. equals() 메소드를 Overriding 할 땐 반드시 다음 다섯 가지 조건을 만족시켜야 한다.

 

· 재귀(reflaxive) : null이 아닌 x라는 객체의 x.equals(x) 결과는 항상 true여야 한다.

· 대칭(symmetric) : null이 아닌 x와 y 객체가 있을 때, y.equals(x)가 true를 리턴했다면, x.equals(y)도 반드시 true를 리턴해야만 한다.

· 타동적(transitive) : null이 아닌 x,y,z가 있을 때, x.equals(y)가 true를 리턴하고, y.equals(z)가 true를 리턴하면, x.equals(z)는 반드시 true를 리턴해야 한다.

· 일관(consistent) : null이 아닌 x와 y가 있을 때 객체가 변경되지 않은 상황에서는 몇 번을 호출하더라도, x.equals(y)의 결과는 항상 true이거나 항상 false여야만 한다.

· null과의 비교 : null이 아닌 x라는 객체의 x.equals(null) 결과는 항상 false여야만 한다.

 

그런데 한가지, equals() 메소드를 Overriding할 때엔 hashCode()메소드도 같이 Overriding해야 한다. equals() 메소드를 Overriding해서 객체가 서로 같다고 말할 수 있지만 그 객체의 주소 값이 같진 않기 때문이다. 따라서 같은 hashCode() 메소드 결과를 갖도록 하기 위해선 hashCode() 메소드도 Object 클래스에서 제공하는 그대로 사용하면 안된다.

 

public class infoDTO {
	// 중간 생략
	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;
	}
}

 

 

* hashCode() 메소드

 

hashCode() 메소드는 기본적으로 객체의 메모리 주소를 16진수로 리턴한다. 만약 두 객체가 서로 동일하다면, hashCode() 값은 무조건 동일해야한다. 요즘 나오는 개발 툴에선 equals() 메소드나 hashCode()메소드를 자동으로 생성해주는 기능을 제공하므로 직접 작성할 일은 없지만 hashCode() 메소드를 구현할 때에는 지켜야 하는 약속이 있다.

 

· 자바 애플리케이션이 수행되는 동안에 어떤 객체에 대해서 이 메소드가 호출될 때에는 항상 동일한 int값을 리턴해 주어야 한다. 하지만, 자바를 실행할 때마다 같은 값이어야 할 필요는 전혀 없다.

· 어떤 두개의 객체에 대하여 equals() 메소드를 사용하여 비교한 결과가 true일 경우에, 두 객체의 hashCode() 메소드를 호출하면 동일한 int 값을 리턴해야만 한다.

· 두 객체를 equals() 메소드를 사용하여 비교한 경과 false를 리턴했다고 해서, hashCode() 메소드를 호출한 int 값이 무조건 달라야 할 필요는 없다. 하지만, 이경우에 서로 다른 int 값을 제공하면 hashtable의 성능을 향상시키는 데 도움이 된다.

 

 

* 실습

 

public class Student {
	String name, address, phone, email;
	
	public Student(String name) {
		this.name = name;
	}
	
	public Student(String name, String address, String phone, String email) {
		this.name = name;
		this.address = address;
		this.phone = phone;
		this.email = email;
	}
	
	public String toString() {
		return name + " " + address + " " + phone + " " + email;
	}
	
	public boolean equals(Object obj) {
		if (this == obj) return true;
		if (obj == null) return false;
		if (getClass() != obj.getClass()) return false;
		
		Student other = (Student) obj;
		
		if (name == null) {
			if(other.name != null) return false;
		} else if (!name.equals(other.name)) return false;
		
		if (address == null) {
			if(other.address != null) return false;
		} else if (!address.equals(other.address)) return false;
		
		if (phone == null) {
			if(other.phone != null) return false;
		} else if (!phone.equals(other.phone)) return false;

		if (email == null) {
			if(other.email != null) return false;
		} else if (!email.equals(other.email)) return false;
		
		return true;
	}
}
public class ManageStudent {

	public static void main(String[] args) {
		ManageStudent manage = new ManageStudent();
		manage.checkEquals();
	}
    
	public void checkEquals() {
		Student a = new Student("Kiara", "Seoul", "010123241234", "abc@java.com");
		Student b = new Student("Kiara", "Seoul", "010123241234", "abc@java.com");
		
		if(a.equals(b)) 
			System.out.println("Equal");
		else
			System.out.println("Not Equal");
	}
}

 

 

* 정리

 

클래스가 어떻게 선언되어 있는지 확인할 수 있는 명령어(실행파일)의 이름은 무엇인가요?
Object 클래스는 java.lang 패키지에 선언되어 있다.

Object 클래스에 선언되어 있는 모든 메소드를 Overriding해야 하나요?
클래스 파일만 갖고 클래스가 어떻게 선언되어 있는지 확인하려면 javap 명령을 사용하면 된다. 

Object 클래스의 clone() 메소드의 용도는 무엇인가요?
Object 클래스에 선언되어 있는 메소드 중에서 필요한 메소드만 Overriding하여 사용하면 된다.

System.out.println() 메소드를 사용하여 클래스를 출력했을 때 "최종적으로" 호출되는 Object 클래스에 있는 메소드는 무엇인가요?
참조 자료형을 System.out.println() 메소드에서 출력하면 toString() 메소드가 호출된 결과가 제공된다.

객체의 주소를 비교하는 것이 아닌, 값을 비교하려면 Object 클래스에 선언되어 있는 어떤 메소드를 overrding해야 하나요? 
참조 자료형의 비교는 equals() 메소드를 사용해야 확실히 비교가 가능하다.
만약 직접 구현한 클래스의 비교를 정확하게 하려면, 이 equals() 메소드를 Overriding하는 것이 좋다.

Object 클래스에 선언되어 있는 hashCode()라는 메소드는 어떤 타입의 값을 리턴 하나요?

hashCode() 메소드는 int 타입의 결과를 리턴한다. 

 

 

그럼 이만.

'Backend > Java' 카테고리의 다른 글

[Java] #14. 예외 (Exception)  (0) 2022.03.16
[Java] #13. 인터페이스와 추상클래스, enum  (0) 2022.03.16
[Java] #11. API  (0) 2022.03.15
[Java] #10. 상속  (0) 2022.03.14
[Java] #09. 패키지와 접근 제어자  (0) 2022.03.14