[Java] 15. Object 클래스

Java / / 2019. 3. 30. 16:36

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

상속이라는 것에 대해서 앞에서 배웠습니다. 그런데 중요한 것 하나는 바로 모든 클래스의 부모 클래스가 있다는 사실입니다. 자바에서는 기본적으로 아무런  상속을 받지 않으면 java.lang.Object 클래스를 확장합니다. 이렇게 말하면 자바는 이중 상속이 안 된다고 했는데 어떻게 된 거냐고 생각할 수 있습니다. 자바는 한 번에 이중 상속을 받을 수는 없지만, 여러 단계로 상속을 받을 수는 있습니다. 그러면 왜 모든 클래스는 Object 클래스의 상속을 받을까요? 가장 큰 이유는 Object 클래스에 있는 메소드들을 통해서 클래스의 기본적인 행동을 정의할 수 있기 때문입니다. 예를 들면 "사람"은 두발로 걷고, 생각을 한다를 들 수 있습니다. 이와 마찬가지로 클래스 라면 이 정도의 메소드는 정의되어 있어야 하고, 처리해 주어야 한다는 것을 정의하는 작업이 필요하기 때문에 Object 클래스를 상속받았다고 생각하면 됩니다.

2. Object 클래스에서 제공하는 메소드들의 종류는?

Object 클래스에 선언되어 있는 메소드는 객체를 처리하기 위한 메소드와 쓰레드를 위한 메소드가 있습니다. 자바에서 메소드를 표현할 때에는 보통 여기에 정리된 것과 같이 "접근제어자 리턴타입 메소드이름(리턴타입)순으로 나열합니다. 이 순서는 메소드 선언문과 동일합니다.

 

메소드 설명
protected Object clone() 객체의 본사본을 만들어 리턴한다.
protected boolean equals(Object obj) 현재 객체와 매개 변수로 넘겨받은 객체가 같은지 확인한다. 같으면 true를 다르면 false를 반환한다.
protected void finalize() 현재 객체가 더 이상 쓸모가 없어졌을 때 가비지 컬렉터에 의해서 이 메소드가 호출된다.
public Class<?> getClass() 현재 객체의 Class 클래스의 객체를 리턴한다. 
public int hashCode() 객체에 대한 해시코드 값을 리턴한다. 해시 코드라는 것은 "16진수로 제공되는 객체의 메모리 주소"를 말한다.
public String toString() 객체를 문자열로 표현하는 값을 리턴한다.

 

다음은 쓰레드 처리를 위한 메소드 입니다.

 

메소드 설명
public void notify() 이 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨운다.
public void notifyAll() 이 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨운다.
public void wait() 다른 쓰레드가 현재 객체에 대한 notify() 메소드나 notifyAll() 메소드를 호출할 때까지 현재 쓰레드가 대기하고 있도록 한다.
public void wait(long timeout) wait() 메소드와 동일한 기능을 제공하며, 매개 변수에 지정한 시간만큼만 대기한다. 즉, 매개 변수 시간을 넘어 섰을 떄에는 현재 쓰레드는 다시 꺠어 난다. 여기서의 시간은 밀리초로 1/1000 초 단위다. 만약 1초간 기다리게 할 경우에는 1000을 매개 변수로 넘겨주면 된다.
public void wait(long timeout, int nanos) wait() 메소드와 동일한 기능을 제공한다. 하지만, wait(timeout)에서 밀리초 단위의 대기시간을 기다린다면, 이 메소드는 보다 자세한 밀리초 + 나노초(1/1,000,000,000초) 만큼만 대기한다. 뒤에 있는 나노초의 값은 0~999,999 사이의 값만 지정할 수 있다.

 

3. Object클래스에서 가장 많이 쓰이는 toString()메소드

toString()메소드는 Object 클래스 중에서 가장 많이 사용되는 메소드입니다. 해당 클래스가 어떤 객체인지를 쉽게 나타낼 수 있는 중요한 메소드입니다. 이 메소드가 자동으로 호출되는 경우는 다음과 같습니다. 


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


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

다음과 같은 toStringMethod()메소드를 InheritanceObject 클래스에 추가해봅시다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

package c.inheritance;

 

public class InheritanceObject {

    

    public void toStringMethod(InheritanceObject obj) {

        System.out.println(obj);

        System.out.println(obj.toString());

        System.out.println("plus "+obj);

    }

    

    public static void main(String[] args) {

        InheritanceObject obj = new InheritanceObject();

        obj.toStringMethod(obj);

    }

}

 

 

 

실행 결과 1

메소드의 첫 번째 줄은 객체를 그대로 출력했고, 그다음 줄에서는 toString() 메소드를 불렀습니다. 두번째 줄의 toString() 메소드는 여러분들이 InheritanceObject 클래스에 전혀 선언되어 있지 않습니다. InheritanceObject 클래스는 Object 클래스의 상속을 자동으로 받고, toString() 메소드는 Object 클래스에 선언되어 있기 때문에 toString() 메소드를 호출하여도 전혀 문제가 되지 않습니다. 그리고, 마지막 줄에서의 객체의 더하기 연산을 수행했습니다. 참조 자료형의 더하기 연산은 String만 가능합니다. 다시 말하면, String을 제외한 참조 자료형에 더하기 연산을 수행하면, 자동적으로 toString() 메소드가 호출되어 객체의 위치에는 String 값이 놓이게 됩니다.

이 예제를 더 깔끔하게 만들어 보겠습니다. this라는 것을 사용하여 다음과 같이 더 깔끔한 메소드를 만들 수 있습니다. this는 자기 자신을 의미하므로, 굳이 매개 변수를 넘겨줄 필요가 없습니다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

package c.inheritance;

 

public class InheritanceObject {

    

    public void toStringMethod() {

        System.out.println(this);

        System.out.println(this.toString());

        System.out.println("plus "+this);

    }

    

    public static void main(String[] args) {

        InheritanceObject obj = new InheritanceObject();

        obj.toStringMethod();

    }

}

 

그런데 이렇게 toString()으로 출력된 결과가 무엇인지에 대해서 의문이 생길 수 있습니다. 이 결과를 출력하는 코드는 다음과 같습니다. 

 

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

 

Object 클래스에 있는 getClass()의 결과에 getName() 메소드를 부르면 현재 클래스의 패키지 이름과 클래스 이름이 나옵니다.  여기서는 c.inheritance.InheritanceObject가 이 부분에 속합니다. 그 다음에는 at(@, 보통 골뱅이라 부름)가 붙습니다. 그냥 앞의 결과와 뒤의 결과를 구분하기 위한 구분자이므로, 별로 신경 쓰지 않아도 됩니다. 그리고 가장 마지막 부분에는 객체의 해시 코드 값을 출력합니다. hashCode() 메소드에서는 int 값을 리턴해줍니다. 그 값을 Integer라는 클래스에서 제공하는 toHexString() 이라는 메소드를 활용하여 16진수로 변환하는 작업이 수행됩니다.

그런데 이렇게 클래스 이름과 해시 코드 값을 보는 이유가 무엇일까요? 여러분들이 클래스를 만들 때 toString() 메소드는 그냥 사용하면 되는 것이 아니라, 직접 구현해야만 합니다. 만들어져 있긴 하지만 앞절에서 배운 Overriding을 적용해야만 합니다. Overriding을 제대로 구현하려면, 접근 제어자, 리턴 타입, 메소드 이름, 매개 변수 타입과 개수들이 모두 동일해야 합니다. InheritanceObject 클래스의 toString() 클래스를 다음과 같이 Overriding 합시다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

package c.inheritance;

 

public class InheritanceObject {

    

    public void toStringMethod() {

        System.out.println(this);

        System.out.println(this.toString());

        System.out.println("plus "+this);

    }

    

    public String toString() {

        return "InheritanceObject Class";

    }

    

    public static void main(String[] args) {

        InheritanceObject obj = new InheritanceObject();

        obj.toStringMethod();

    }

}

 

 

실행 결과 2

패키지를 포함한 클래스 이름과 @, 그리고 hashCode() 메소드를 수행한 결과가 나오지 않고, 방금 수정한 toString() 메소드의 내용대로 나온 것을 확인할 수 있습니다. 그렇다면 언제 toString()을 Overriding해야할까요? 모든 클래스의 toString()을 Overriding할 필요는 없습니다. 하지만, DTO를 사용할 때에는 아무리 개발 일정이 촉박하더라도 이 toString() 메소드를 Overriding해 놓는 것이 좋습니다. 그래야 내용 확인이 쉽기 때문입니다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

package c.inheritance;

 

public class MemberDTO {

    

    public String name;

    public String phone;

    public String email;

    

    public String toString() {

        return "Name="+name+"Phone="+phone+" eMail="+email;

    }

    

}

 

만약 toString() 메소드가 Overriding 되어 있지 않다면, 이 MemberDTO에 선언된 값을 확인할 때 어떻게 해야 할까요?
아마도 다음과 같이 확인해야 할 것입니다.

 

 - MemberDTO dto = new MemberDTO("Sangmin", "010XXXXYYYY", "javatuning@gmail.com");
   System.out.println("Name="+dto.name+" Phone="+dto.phone+" eMail="+email);

 

이 MemberDTO를 사용해야 하는 부분이 많은데, 사용할 때마다 이와 같이 출력을 해보는 것은 비효율 적이기 때문에 toString()을 Overriding 해놓으면 됩니다.

 

4. 객체는 == 만으로 같은지 확인이 안 되므로, equals()를 사용합니다.

참조 자료형에서는 ==과 !=으로 같은지 다른지를 비교하면 안 됩니다. 이 연산자들은 기본 자료형에서만 사용할 수 있습니다. "값"을 비교하는 것이 아니라 "주소값"을 비교합니다.  다음의 예를 봅시다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

package c.inheritance;

 

public class InheritanceObject {

    

    public void toStringMethod() {

        System.out.println(this);

        System.out.println(this.toString());

        System.out.println("plus "+this);

    }

    

    public String toString() {

        return "InheritanceObject Class";

    }

    

    public void equalMethod() {

        MemberDTO obj1 = new MemberDTO("Sangmin");

        MemberDTO obj2 = new MemberDTO("Sangmin");

        

        if(obj1 == obj2) {

            System.out.println("obj1 and obj2 is same");

        } else {

            System.out.println("obj1 and obj2 is different");

        }

        

    }

    

    public static void main(String[] args) {

        InheritanceObject obj = new InheritanceObject();

        obj.equalMethod();

    }

}

 

이 예제에서는 생성자에 name에 들어갈 이름을 모두 "Sangmin"이라고 넘겨주었습니다. 두 객체는 자바 세상에서는 다르지만, 현실 세계에서는 동일한 값을 갖고 있으면(이름, 전화번호, 이메일이 같다면) 같다고 볼 수 있습니다. 물론 동명이인이 없다고 가정했을 때입니다. 어떤 결과가 나오나요?

 

실행 결과 3

 

왜냐하면 두 객체는 각각의 생성자를 사용하여 만들었기 때문에 주소 값이 다릅니다. 그런데 객체 안에 있는 속성 값들은 모두 동일합니다. 그래서, 이와 같이 참조 자료형은 equals()라는 메소드를 사용하여 두 객체를 비교해야 합니다. 그리고 Object 클래스에 선언되어 있는 equals() 메소드를 Overriding해 놓아야지만 제대로 된 비교가 가능합니다. equalMethod() 메소드의 비교 부분을 주석처리하고 equals() 메소드를 사용해서 비교해 봅시다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

package c.inheritance;

 

public class InheritanceObject {

    

    public void toStringMethod() {

        System.out.println(this);

        System.out.println(this.toString());

        System.out.println("plus "+this);

    }

    

    public String toString() {

        return "InheritanceObject Class";

    }

    

    public void equalMethod() {

        MemberDTO obj1 = new MemberDTO("Sangmin");

        MemberDTO obj2 = new MemberDTO("Sangmin");

        

        /*

        if(obj1 == obj2) {

            System.out.println("obj1 and obj2 is same");

        } else {

            System.out.println("obj1 and obj2 is different");

        }

        */

        

        if(obj1.equals(obj2)) {

            System.out.println("obj1 and obj2 is same");

        } else {

            System.out.println("obj1 and obj2 is different");

        }            

    }

    

    public static void main(String[] args) {

        InheritanceObject obj = new InheritanceObject();

        obj.equalMethod();

    }

}

 

 

결과가 원하는 대로 모든 속성이 동일하니 같다고 나올까요? 

 

실행 결과 4

 

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

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

package c.inheritance;

 

public class MemberDTO {

    

    public String name;

    public String phone;

    public String email;

    

    public MemberDTO(String name) {

        this.name = name;

    }

    

    public String toString() {

        return "Name="+name+"Phone="+phone+" eMail="+email;

    }

    

    public boolean equals(Object obj) {

        if(this == obj) return true//주소가 같으므로 true

        if(this == nullreturn false//obj가 null이므로 false

        if(getClass() != obj.getClass()) return false//클래스의 종류가 다르므로 false

        

        MemberDTO other = (MemberDTO) obj; //같은 클래스이므로 형변환

        

        //각 인스턴스 변수가 같은지 비교하는 작업 수행

        if(name == null) {

            if(other.name != nullreturn false;

        } else if(!name.equals(other.name))return false;

        

        if(email == null) {

            if(other.email != nullreturn false;

        } else if(!email.equals(other.email))return false;

        

        if(phone == null) {

            if(other.phone != nullreturn false;

        } else if(!phone.equals(other.phone))return false;

        

        return true;

    }

}

 

 

제대로 된 equals() 메소드를 처음 본 분들은 객체를 비교할 때 마다 만들라는 것이라고 생각할 수 있습니다. 이 equals() 메소드는 이클립스에서 자동으로 생성한 것입니다. equals() 메소드를 Overriding 할 때에는 다음 다섯 가지의 조건을 만족시켜야만 합니다.

 

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


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


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


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


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

 

이 기준은 자바 API 문서에 정해져 있는 것입니다.

 

한 가지 유념해야 하는 것이 있습니다. equals() 메소드를 Overriding할 때에는 hashCode() 메소드도 같이 Overriding 해야만 한다는 것입니다. 왜냐하면, equals() 메소드를 Overriding해서객체가 서로 같다고 이야기 할 수 있겠지만, 그 값이 같다고 해서 객체의 주소값이 같지는 않기 때문입니다. 같은 hashCode() 메소드 결과를 갖도록 하려면 hashCode() 메소드도 Object 클래스에서 제공하는 그대로 사용하면 안 됩니다. 이클립스에서 자동으로 생성한 MemberDTO 클래스 hashCode() 메소드는 다음과 같습니다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

package c.inheritance;

 

public class MemberDTO {

    

    public String name;

    public String phone;

    public String email;

    

    public MemberDTO(String name) {

        this.name = name;

    }

    

    public String toString() {

        return "Name="+name+"Phone="+phone+" eMail="+email;

    }

    

    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;

    }

    

    public boolean equals(Object obj) {

        if(this == obj) return true//주소가 같으므로 true

        if(this == nullreturn false//obj가 null이므로 false

        if(getClass() != obj.getClass()) return false//클래스의 종류가 다르므로 false

        

        MemberDTO other = (MemberDTO) obj; //같은 클래스이므로 형변환

        

        //각 인스턴스 변수가 같은지 비교하는 작업 수행

        if(name == null) {

            if(other.name != nullreturn false;

        } else if(!name.equals(other.name))return false;

        

        if(email == null) {

            if(other.email != nullreturn false;

        } else if(!email.equals(other.email))return false;

        

        if(phone == null) {

            if(other.phone != nullreturn false;

        } else if(!phone.equals(other.phone))return false;

        

        return true;

    }

}

 

다시 말하지만, 여러분들이 반드시 equals() 메소드를 Overriding 해야 하는 것은 아닙니다. 이 예에서와 같이 DTO를 만들 경우에는 객체 비교를 위해서 반드시 필요한 것이지만, 그렇지 않은 메소드만 있는 기능 위주의 클래스를 만들 때에는 힘들게 equals() 메소드를 Overriding할 필요는 없습니다.

 

5. 객체의 고유값을 나타내는 hashCode()

hashCode() 메소드는 기본적으로 객체의 메모리 주소를 16진수로 리턴합니다. 만약 어떤 두 개의 객체가 서로 동일하다면, hashCode() 값은 무조건 동일해야만 합니다. 따라서, equals() 메소드를 override 하면, hashCode() 메소드도 override 해서 동일한 결과가 나오도록 해야만 합니다. 

 

즉,  Object의 hashCode() 메소드는 객체의 메모리 번지를 이용해서 해시코드를 만들어 리턴하기 때문에 객체 마다 다른 값을 가지고 있습니다. 그래서 이 hash값을 같게 해주어야 멤버변수가 같은지 비교를 할 수 있습니다. 객체의 값을 동등성 비교시 hashCode()를 오버라이딩할 필요성이 있는데, 컬렉션 프레임워크에서 HashSet, HashMap, HashTable은 다음과 같은 방법으로 두 객체가 동등한지 비교합니다.

 



만약 hashCode() 메소드를 구현할 때에는 지켜야 하는 약속이 있습니다. 

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


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


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

이러한 제약들 때문에 직접 equals() 메소드나 hashCode() 메소드를 작성하는 것은 별로 권장하지 않습니다. 요즘 나오는 툴에서는 이 두개의 메소드를 자동으로 생성해주는 기능을 제공하고 있으므로 이 기능을 사용할 것을 권장합니다. 

 

이클립스 hashCode(), equals() 메소드 자동 생성

 

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기