포스트

19. Object 클래스 메서드 심화

Object 클래스와 equls에 대해서 이해해보자

19. Object 클래스 메서드 심화

object 클래스

1
2
3
4
class A { - }
// 변환
class A extends Object{}
// 컴파일 A.class
  • Object (super class = parent class)
    • A (sub class = child class)

A →링크(포함X)→ Object(= 모든 클래스의 최상위 클래스)
Object 클래스의 코드를 제 것처럼 사용함, 포함이 아님 > A가 Object 클래스의 메소드를 포함하고 있지않음

instance of

1
2
3
4
5
// Object ← Vehicle ← Car ← Truck
// t(200) -> (200)[Object의 인스턴스 변수 | Vehich의 인스턴스 변수 | Car의 인스턴스 변수 | Truck의 인스턴스 변수]
Truck t = new Truck();

t instanceof Truck // t가 가리키는 인스턴스는 Truck 설계도에 따라 만들었는가?(= t는 Truck의 인스턴스인가?)

Object.toString( - )

toString → return : FQCN@16진수 해시값(= hashCode()의 리턴값을 사용)

  • FQCN : Fully-Qualified Class Name (= 클래스의 완전한 이름 : 패키지명 + 클래스명)

toString은 최상위 클래스인 Object의 메서드이기 때문에 모든 클래스가 사용할 수 있다.

객체의 toString은 식별자로 반환되기 때문에 내부에 저장된 값을 알 수가 없다.

1
2
3
My obj = new My();
System.out.println(obj.toString()); // 패키지명 + 클래스명 @ 해시값 : com.eomcs.basic.ex01.Exam0120$My@7ad041f3
System.out.println(obj);            // println은 객체에 대해 toString()을 호출 한 후 리턴값을 출력하기 때문에 위와 동일하다.
1
2
3
4
5
6
My obj2 = new My();
My obj3 = new My();

// 아래 객체 두 개는 다른 해시 값을 부여받기 때문에 같은 조건을 가진 My라도 다른 객체이다. 
System.out.println(obj2.toString()); // com.eomcs.basic.ex01.Exam0120$My@251a69d7
System.out.println(obj3.toString()); // com.eomcs.basic.ex01.Exam0120$My@7344699f

이걸 toString을 오버라이딩 후 바꾸면 원하는 내부 값을 쉽게 확인 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
    public String toString() {
      return "My [name=" + name + ", age=" + age + "]";
    }
  
  public static void main(String[] args) {
    My obj1 = new My();

    obj1.name = "홍길동";
    obj1.age = 20;
    
    // -> 아까 위에서도 설명했듯 println은 해당 객체의 toString을 불러서 리턴값을 반환하기 때문에 toString을 붙이지않아도 문제 없다.
    System.out.println(obj1); // My [name=홍길동, age=20]
  }

equals

  • equals인스턴스가 동일한지를 비교한다.

아래처럼 new 객체를 생성했다면 두 개의 다른 인스턴스를 비교하기 때문에 같은 내용물을 가졌어도 false가 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class My {
    String name;
    int age;
  }

	public static void main(String[] args) {
    My obj1 = new My();
    obj1.name = "홍길동";
    obj1.age = 20;

    My obj2 = new My();
    obj2.name = "홍길동";
    obj2.age = 20;

    System.out.println(obj1 == obj2);       // false
    System.out.println(obj1.equals(obj2));  // false

  }

equals 오버라이딩

그렇다면 내용물을 비교하기 위해선 어떻게 해야할까. 직접 equals를 오버라이딩 해서 사용할 수 있다.

이때, 따라야할 규칙이 있는데…

  • equals 메서드 오버라이딩 시 따라야할 규칙
    1. 반사성(Reflexive) : 객체는 자기자신과 동일해야함
    2. 대칭성(Symmetric) : 두 객체가 서로를 동일하다고 판단하면 반대도 동일해야함
    3. 추이성(Transitive) : 객체1과 객체2가 같고 객체2와 객체3이 같다면 객체1과 객체3도 동일해야함
    4. 일관성(Consistent) : 두 객체가 동일하다고 판단되면 상태가 변하지않는 한 계속 동일해야함. x.equals(y)가 반복 호출 시에도 동일해야함
    5. null-아님 : 모든 객체는 null일 때 무조건 false를 반환해야함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public boolean equals(Object obj) {
	// 같은 객체를 참조하면 true를 반환함
	if (this == obj) { 
		return true;
	}
	// 비교하는 객체가 null이면 false를 반환함
	if (obj == null) {
		return false;
	}
	// 클래스 타입이 다르면 false를 반환함 (My.equals(Test))
	if (getClass() != obj.getClass()) {
		return false;
	}
	// 받아온 Object 객체를 해당하는 클래스에 맞게 형변환 후 내부 데이터 동일 확인
	My other = (My) obj;
	return age == other.age && Objects.equals(email, other.email) && gender == other.gender
	&& Objects.equals(name, other.name) && Objects.equals(tel, other.tel)
	&& working == other.working;
	}
}

String과 StringBuffer의 equals 차이

1
2
3
4
5
6
7
8
9
10
11
String s1 = new String("Hello");
String s2 = new String("Hello");
    
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true

StringBuffer sb1 = new StringBuffer("Hello");
StringBuffer sb2 = new StringBuffer("Hello");
    
System.out.println(sb1 == sb2); // false
System.out.println(sb1.equals(sb2)); // false
  • String은 String 클래스에서 equals를 오버라이딩하여 재정의하여 사용한다.
    ㅣ그렇기에 비교 시 같은 값을 가지면 true를 반환 해준다.
  • StringBuffer는 StringBuffer 클래스에서 equals를 오버라이딩하여 재정의한 클래스가 아니기 때문에 같은 값을 가져도 equals는 다른 인스턴스로 판단한다.

hashCode()

  • 해당 객체의 해시 값을 반환해준다.(해시 값은 특정 수학 공식에 따라 값을 계산한다)
  • 해시값은 객체가 동일한지 확인하기 위한 식별자로 객체의 주소와는 아무 상관없다.
  • 객체의 내용이 동일하다고 해도 new~ 로 만든 객체는 모두 다른 객체이다. 즉, 해시코드도 모두 다르다

주의. 해시값과 주소.
해시 값과 메모리 주소는 다르다. 인스턴스가 같은지를 검사하기 위한 임의적인 부여된 식별자라고 봐야한다.
hashCode( )를 오버라이딩 해서 재정의하지 않을 경우 무조건 인스턴스마다 새 해시값이 부여된다.

hashCode() 오버라이딩

  • 해시 코드를 사용할 때에는 값을 주고 동일하면 같은 해시코드를 가질 수 있게 만들기 위함인데…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 이런 식으로 만들어지는 객체마다 같은 해시코드를 부여하게 되면 내부 값이 같은지 다른지 해시코드로 확인 할 수 없게된다...
@Override
    public int hashCode() {
      // 무조건 모든 Score 인스턴스가 같은 해시코드를 갖게 하자!
      return 1000;
}

Score s1 = new Score("홍길동", 100, 100, 100);
Score s2 = new Score("홍길동", 100, 100, 100);
Score s3 = new Score("임꺽정", 90, 80, 70);

System.out.println(s1.hashCode()); // 1000
System.out.println(s2.hashCode()); // 1000
System.out.println(s3.hashCode()); // 1000

단순한 방법으로는 java.lang.String 클래스에 이미 오버라이딩 된 hashCode()를 활용하는 방법이있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 왜? String 클래스에 있는 hashCode()는 문자열이 같은 경우
// 같은 해시 값을 리턴하도록 이미 오버라이딩 되어 있기 때문이다.
@Override
    public int hashCode() {
      // 모든 값을 문자열로 만들어 붙인다.
      String value = String.format("%s,%d,%d,%d,%d,%.1f", this.name, this.kor, this.eng, this.math, this.sum, this.aver);

      // String 클래스에 있는 hashCode()를 사용한다.
      // 데이터가 같으면 문자열이 같을 것이고, 문자열이 같으면 해시코드의 리턴 값도 같을 것이다.
      return value.hashCode();
    }
    
Score s1 = new Score("홍길동", 100, 100, 100);
Score s2 = new Score("홍길동", 100, 100, 100);
Score s3 = new Score("임꺽정", 90, 80, 70);

System.out.println(s1.hashCode()); // d8146d0b
System.out.println(s2.hashCode()); // d8146d0b
System.out.println(s3.hashCode()); // bbe6309b

hashCode()와 equals()의 활용

hashSet

  • 고유한 요소의 집합을 저장하기 위해 사용됨
  • 중복 요소를 허용하지 않는다.
  • 저장된 요소의 순서를 보장하지않는다. (삽입 순서와 요소의 순서는 다를 수 있다.)

Hash Set add

  • 이때 값이 같은지를 판단의 척도가 hashCode와 equals이다.
    그래서 hashCode와 equals는 한쌍으로 오버라이딩 되어야한다.
  1. hashCode와 equals를 강제로 모두 동일하게 부여

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
     static class Student {
         String name;
         int age;
         boolean working;
        
         public Student(String name, int age, boolean working) {
           this.name = name;
           this.age = age;
           this.working = working;
         }
        
          @Override
          public int hashCode() {
          return 100;
          }
            
          @Override
          public boolean equals(Object obj) {
          return true;
          }
       }
    
  2. 각각의 값을 부여해 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
    
     static class Student {
         String name;
         int age;
         boolean working;
        
         public Student(String name, int age, boolean working) {
           this.name = name;
           this.age = age;
           this.working = working;
         }
        
         @Override
         public int hashCode() {
           return Objects.hash(age, name, working);
         }
        
         @Override
         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;
           return age == other.age && Objects.equals(name, other.name) && working == other.working;
         }
       }
    
  3. 최종

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
     public static void main(String[] args) {
         Student s1 = new Student("홍길동", 20, false);
         Student s2 = new Student("홍길동", 20, false);
         Student s3 = new Student("임꺽정", 21, true);
            
         HashSet<Student> set = new HashSet<Student>();
         set.add(s1);
         set.add(s2);
         set.add(s3);
         set.add(s4);
        
         // 해시셋에 보관된 객체를 꺼낸다.
         Object[] list = set.toArray();
         for (Object obj : list) {
           Student student = (Student) obj;
           System.out.printf("%s, %d, %s\n", student.name, student.age, student.working ? "재직중" : "실업중");
         }
            
         // 1안으로 출력 시 hashCode와 equals를 강제로 모두 동일하게 부여하기 때문에 hashSet에서 hashCode와 equals를 객체마다 돌려 확인 시 동일한 값으로 인식한다.
         // 이는 결국 중복을 허용하지 않는 hashSet에게 필터가 되어 [홍길동, 20, 실업중]이라는 하나의 결과만 저장된다.
         // 2안으로 출력 시 각각의 값을 부여해 hashCode를 짜고 객체가 동일한지를 확인하는 작업을 거치기 때문에
         // 동일한 요소를 가지고 있는 s1, s2는 하나만, 다른 요소인 임꺽정이 다음에 추가되게 된다. [홍길동, 20, 실업중], [임꺽정, 21, 재직중]
    

hashMap

  • 키-값 쌍의 맵을 저장하기 위해 사용됨
  • 키와 값을 각각의 엔트리로 저장함. 각 엔트리는 키와 값으로 이루어짐
  • 키는 중복될 수 없지만 값은 중복을 허용함. 키가 중복 될 시 값이 덮어 씌워짐
  • 저장된 키-값 쌍의 순서를 보장하지않는다. 삽입 순서와 다를 수 있다.

hash map put

  • 이때 값이 같은지를 판단의 척도가 hashCode와 equals이다.
    그래서 hashMap 또한 hashCode와 equals는 한쌍으로 오버라이딩 되어야한다.
  1. 오버라이딩 하지않고 객체를 새로 만들어 꺼내보려고 해보자…

    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
    
     static class MyKey {
         String contents;
            
         public MyKey(String contents) {
           this.contents = contents;
         }
        
         @Override
         public String toString() {
           return "MyKey [contents=" + contents + "]";
         }
       }
        
     // HashMap
     // => 값을 저장할 때 key 객체의 해시코드를 이용하여 저장할 위치(인덱스)를 계산한다.
     // => 따라서 해시코드가 같다면 같은 key로 간주한다.
     HashMap<MyKey,Student> map = new HashMap<>();
        
     MyKey k3 = new MyKey("haha");
        
     map.put(k3, new Student("유관순", 17, true));
        
     System.out.println(map.get(k3));   // 값을 저장할 때 사용한 key 객체로 값을 찾아 꺼낸다.
     MyKey k6 = new MyKey("haha");      // 다른 key 객체를 사용하여 값을 꺼내보자.
     System.out.println(map.get(k6));   // 해당 키로 저장된 엔트리가 없어 null이 나온다
        
     System.out.println(k3 == k6);      //인스턴스는 당연히 다르고 해시 값도 다르다.
     System.out.println(k3.hashCode());
     System.out.println(k6.hashCode())
    
    • HashMap은 key의 해시코드를 사용해 저장할 위치를 계산함.
    • 위에서 같은 동일한 “haha” key 객체를 가진 k6객체를 만들어 유관순 정보를 뽑아내려했지만…
      • 내용만 같을 뿐 key 객체는 엄연히 새로 만들어진 객체이다.
        해시코드와 equals 모두 false로 나오기 때문에 다른 key로 간주한다.
  2. 2안

    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
    
     static class MyKey2 {
         String contents;
        
         public MyKey2(String contents) {
           this.contents = contents;
         }
        
         @Override
         public String toString() {
           return "MyKey2 [contents=" + contents + "]";
         }
        
         @Override
         public int hashCode() {
           return Objects.hash(contents);
         }
        
         @Override
         public boolean equals(Object obj) {
           if (this == obj) {
             return true;
           }
           if (obj == null) {
             return false;
           }
           if (getClass() != obj.getClass()) {
             return false;
           }
           MyKey2 other = (MyKey2) obj;
           return Objects.equals(contents, other.contents);
         }
       }
        
       public static void main(String[] args) {
         HashMap<MyKey2, Student> map = new HashMap<>();
        
         MyKey2 k3 = new MyKey2("haha");
        
         map.put(k3, new Student("유관순", 17, true));
        
         System.out.println(map.get(k3));   // 값을 저장할 때 사용한 key 객체로 값을 찾아 꺼낸다.
         MyKey2 k6 = new MyKey2("haha");    // 다른 key 객체를 사용하여 값을 꺼내보자.
         System.out.println(map.get(k6));   // hashCode와 equals가 true로 유관순 조회가 가능하다.
            
         System.out.println(k3 == k6);      // 인스턴스는 다르지만...
         System.out.println(k3.hashCode()); // hash code는 같다.
         System.out.println(k6.hashCode()); // hash code는 같다.
    
  3. 최종

    1. Integer 형-

      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
      
       Integer k3 = new Integer(103);
                  
       // 위에서 준비한 key 객체를 가지고 Student 객체를 보관한다.
       map.put(k3, new Student("유관순", 17, true));
              
       // => key 값으로 int를 넘겨준다면,
       // 컴파일러가 컴파일 할 때 auto-boxing을 수행하여 Integer 객체를 만든다.
       // 그리고 그 객체를 넘겨주는 것이다.
       map.put(104 /* Integer.valueOf(104) */, new Student("안중근", 24, true));
              
       // 값을 저장할 때 사용한 key로 다시 값을 꺼내보자!
       System.out.println(map.get(k3));
              
       // k2와 같은 정수값을 가지는 key를 새로 생성한다.
       Integer k6 = new Integer(102);
              
       // k2와 같은 값을 갖는 k6로 값을 꺼내보자!
       System.out.println(map.get(k6));
              
       // 다음과 같이 k2와 k6는 분명히 다른 객체이다.
       System.out.println(k3 == k6);
              
       // 그러나 k2와 k6는 같은 해시코드를 갖는다.
       System.out.println(k3.hashCode()); // hash code는 같다.
       System.out.println(k6.hashCode()); // hash code는 같다.
      
    2. String 형 - String 클래스에서 자체 오버라이딩한 hashCode와 equals를 이용해 내부 값만을 비교한다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
       HashMap<String,Student> map = new HashMap<>();
                  
       String k3 = new String("haha");
                  
       // String을 key로 사용해보자! 
       map.put(k3, new Student("유관순", 17, true));
              
       // k3 key로 저장한 값을 다시 k3 key로 꺼내보자!
       System.out.println(map.get(k3));
                  
       // k3랑 같은 문자열을 갖는 String key를 생성한다.
       String k6 = new String("haha");
              
       // k3랑 k6는 서로 다른 인스턴스이다.
       System.out.println(k3 == k6); 
                  
       // 그러나 문자열은 같다.
       // String은 같은 문자열일 경우 같은 해시코드를 리턴하도록 메서드를 오버라이딩 하였다.
       System.out.println(k3.hashCode()); // hash code는 같다.
       System.out.println(k6.hashCode()); // hash code는 같다.
      

getClass

  • 클래스의 정보를 가져온다. (배열, 객체 상관없이)
1
2
3
4
5
6
7
8
9
10
11
12
// 레퍼런스를 통해서 인스턴스의 클래스 정보를 알아낼 수 있다.
Class classInfo = obj1.getClass();

// 클래스 정보로부터 다양한 값을 꺼낼 수 있다.
System.out.println(classInfo.getName()); // 패키지명 + 바깥 클래스명 + 클래스명
System.out.println(classInfo.getSimpleName()); // 클래스명

// Primitive Type은 Object의 서브 클래스가 아니기 때문에 getClass()를 호출할 수 없다.
// 대신 static 변수인 class 를 사용하여 Class 정보를 리턴 받을 수 있다.
Class classInfo = byte.class;
System.out.println(classInfo.getName());   // byte
System.out.println(short.class.getName()); // short

clone

  • 인스턴스를 복제할 때 사용하는 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 인스턴스 복제
// 방법1:
// => 직접 복제한다. 즉 새 객체를 만들어 기존 객체의 값을 저장한다.
Score s2 = new Score(s1.name, s1.kor, s1.eng, s1.math);

// s1과 s2는 서로 다른 인스턴스이다.
System.out.println(s1 == s2);

// 방법2:
// Object에서 상속 받은 clone()을 호출한다.
//    Score s3 = s1.clone(); // 컴파일 오류!
try {
	Exam0170 test = new Exam0170();
	test.clone(); // OK!
} catch (Exception e) { - }
  • clone은 Object의 protected 단위의 메서드이기 때문에 오버라이딩 받을 때 주의해야한다.

    clone.png

clone( ) 오버라이딩 규칙

오버라이딩 규칙1 오버라이딩 규칙2

super 키워드

super 키워드

Shallow copy와 Deep copy

shallow copy

  • shallow copy : 클래스가 가지고 있는 내부 객체는 복제하지 않는다.
    • 최상위 클래스는 복제해서 독립적인 객체를 보유할 수 있다
    • 하지만 내부 객체는 레퍼런스만 복제를 하기에 동일한 객체를 가진다.
  • deep copy: 클래스가 가지고 있는 내부 객체도 복제해서 가져온다.
    • 최상위 클래스와 내부 객체 모두 복제해서 독립적인 객체를 가진다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.