본문 바로가기
자바

[Java] Object 클래스

by 책 읽는 개발자_테드 2020. 12. 26.
반응형

Object 클래스

 

유튜브에서 백기선님이 진행하는 온라인 스터디를 진행 중입니다. 아래는 스터디의 링크입니다.

 

www.youtube.com/watch?v=rPYhY5kFD5k

github.com/whiteship/live-study

 

 

클래스를 선언할 때 extends 키워드로 다른 클래스를 상속하지 않으면 암시적으로 java.lang.Object 클래스를 상속하게 된다. 따라서 자바의 모든 클래스는 Object 클래스의 자식이거나 자손 클래스다.

 

Object 클래스는 필드가 없고, 메소드들로만 구성되어 있다. 이 메소드들은 모든 클래스가 Object를 상속하기 때문에 모든 클래스에서 사용이 가능하다. Object는 다음과 같은 메소드들로 구성된다.

 

✏️equals(), 객체 비교

 

매개변수로 객체의 참조변수를 받아서 비교하여 그 결과를 boolean 값으로 알려 주는 역할을 한다. 아래 코드는 Object 클래스에 정의된 equals 메서드의 내용이다.

 

public boolean equals(Object obj){
	return (this == obj);
}

 

위 코드에서 알 수 있듯이 두 객체의 같고 다름을 참조변수의 값(주소값)으로 판단한다. 즉, 두 개의 참조변수가 같은 객체를 참조하고 있는지 판단한다. 때문에 서로 다른 두 객체를 equals 메서드로 비교하면 항상 false를 결과로 얻는다.

 

public class Main {

   public static void main(String[] args){
       Value v1 = new Value(10);
       Value v2 = new Value(10);

       if(v1.equals(v2))
           System.out.println("v1 == v2");
       else
           System.out.println("v1 != v2");

       v2 = v1;

       if(v1.equals(v2))
           System.out.println("v1 == v2");
       else
           System.out.println("v1 != v2");

   }

}

class Value{

   int value;
   
   Value(int value){
       this.value = value;
   }

}

 

🔔결과

 

equals메서드로 참조변수에 저장된 주소값이 아닌 인스턴스가 가지고 있는 value값을 비교하도록 하기 위해서는 equals 메서드를 오버라이딩하여 주소가 아닌 개체에 저장된 내용을 비교하도록 변경하면 된다. 

 

public class Main {

   public static void main(String[] args){

       Value v1 = new Value(10);
       Value v2 = new Value(10);

       if(v1.equals(v2))
           System.out.println("v1 == v2");
       else
           System.out.println("v1 != v2");
   }
}

class Value{

   int value;

   Value(int value){
       this.value = value;
   }

   @Override
   public boolean equals(Object obj){
       
       if(obj instanceof Value)    // obj가 Value 타입으로 형변환 가능한지 확인
           return value == ((Value)obj).value;
       else
           return false;
   }
}

 

🔔결과

 String 클래스 역시 Object클래스의 equals 메서드를 그대로 사용하는 것이 아니라 이처럼 오버라이딩을 통해 String 인스턴스가 갖는 문자열 값을 비교한다. 때문에 같은 내용의 문자열을 갖는 두 String 인스턴스에 equals 메서드를 사용하면 항상 true값을 얻는다.

 

✏️hashCode(), 객체 해시코드

 

hashCode() 메서드는 해싱기법에 사용되는 해시함수를 구현할 것이다. 해싱은 데이터관리기법 중 하나이며, 데이터를 저장하고 검색하는 데 유용한다.

 

해시함수는 찾고자하는 값을 입력하면, 그 값이 저장된 위치를 알려주는 해시코드를 반환한다. 위에서 만든 Value 클래스의 객체들에 hashCode() 함수를 적용해보자

 

public class Main {

   public static void main(String[] args){
   
       Value v1 = new Value(10);
       Value v2 = new Value(10);

       System.out.println(v1.hashCode());
       System.out.println(v2.hashCode());
   }
}

 

🔔결과

 

이 처럼 값이 같더라도 저장된 객체가 다르기 때문에 다른 해시코드가 생성된다. 앞서 살펴본 것과 같이 클래스의 인스턴스변수 값으로 객체의 같고 다름을 판단해야하는 경우 equals메서드 뿐 만아니라 hashCode메서드도 적절히 오버라이딩해야한다. 

 

public class Main {

   public static void main(String[] args){

       String s1 = "value";
       String s2 = "value";

       System.out.println(s1.hashCode());
       System.out.println(s2.hashCode());
   }
}

 

🔔결과

 

위 결과처럼 String 클래스는 문자열의 내용이 같으면, 동일한 해시코드를 반환하도록 hashCode 메서드가 오버라이딩되어 있다.

 

✏️toString(), 객체 문자 정보

 

toString() 메서드는 인스턴스에 대한 정보를 문자열(String)로 제공할 목적으로 정의되었다. Object 클래스에 정의된 toString은 아래와 같다.

 

public String toString() {
   return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
}

 

클래스를 작성할 때 toString()을 오버라이딩하지 않으면, 클래스이름에 16진수의 해시코드를 얻는다. 이러한 toString() 메서드는 일반적으로 인스턴스나 클래스에 대한 정보 또는 인스턴스 변수들의 값을 문자열로 반환하도록 오버라이딩 된다.

 

아래 예시 처럼 String 클래스의 String()은 String 인스턴스가 갖고 있는 문자열을 반환하도록 오버라이딩되어 있고, Date 클래스의 경우, Date 인스턴스가 갖고 있는 날짜와 시간을 문자열로 변환하여 반환하도록 오버라이딩되어있다.

 

 

 

🔔결과

 

✏️clone(), 객체 복제

 

clone() 메서드는 자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다.

 

 어떤 인스턴스에 대해 작업을 할 때, 원래의 인스턴스는 보존하고 clone메서드를 이용해서 새로운 인스턴스를 생성하여 작업을 하면 작업이전의 값이 보존되므로 작업에 실패해서 원래의 상태로 되돌리거나 변경되기 전의 값을 참고하는 도움이 된다.

 

clone()을 사용하려면, 먼저 복제할 클래스가 Cloneable인터페이스를 구현해야하고, clone()을 오버라이딩하면서 접근 제어자를 protected에서 public으로 변경해야 한다. 그래야 상속관계가 없는 다른 클래스에서 clone()을 호출 할 수 있다. 또한 CloneNotSupportedException 예외처리를 해주어야한다. 

 

public class Main {

   public static void main(String[] args){

       Value value = new Value(10);
       Value copyValue = (Value) value.clone();
       System.out.println(value.value);
       System.out.println(copyValue.value);

   }
}

class Value implements Cloneable{

   int value;

   Value(int value){
       this.value = value;
   }

   public Object clone(){

       Object obj = null;
       
       try{
           obj = super.clone();
       }catch (CloneNotSupportedException e){}

       return obj;

   }
}

 

🔔결과

 

여러 개의 데이터를 다루는 배열, java.util 패키지의 Vector, ArrayList, LinkedList, HashSet, TreeSet, HashMap 등은 clone을 이용해서 간단하게 내용을 복사할 수 있다.

 

public class Main {

   public static void main(String[] args){

       int[] value = {1,2,3,4,5};
       int[] copyValue = value.clone();
       copyValue[0] = 100;

       System.out.println(Arrays.toString(value));
       System.out.println(Arrays.toString(copyValue));

   }
}

 

🔔결과

 

얕은 복사와 깊은 복사

 

Object클래스에 정의된 clone()은 단순히 인스턴스변수의 값만을 복사한다. 때문에 참조 타입의 인스턴스 변수가 있는 클래스는 완전한 인스턴스 복제가 이루어지지 않는다. 원본과 복제복이 같은 객체를 공유하기 때문에 원본을 변경하면 복사본도 영향을 받기 때문이다. 

 

🖍이러한 복제를 얕은 복사(shallow copy)라고 한다.

 

public class Main {

   public static void main(String[] args){

       Circle c1 = new Circle(new Point(1,1), 2.0);
       Circle c2 = c1.clone();
   }
}



class Point{

   int x,y;
   
   Point(int x, int y){
       this.x = x;
       this.y = y;
   }
}



class Circle implements Cloneable{

   Point p;    //원점 - 참조변수
   double r;   //반지름

   Circle(Point p, double r){
       this.p = p;
       this.r = r;
   }

   public Circle clone(){

       Object obj = null;

       try{
           obj = super.clone();
       }catch (CloneNotSupportedException e){}
       return (Circle)obj;
   }
}

 

즉, 위와 같은 코드에서 참조관계는 다음과 같아진다.

 

 

🖍이와 반대로 원본이 참조하고 있는 객체까지 복제하는 것은 깊은 복사(deep copy)라고 한다. 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않는다.

 

 

아래 코드 처럼 원본이 참조하고 있는 객체까지 복사하는 방식으로 깊은 복사를 구현할 수 있다.

 

public class Main {
    public static void main(String[] args){

        Circle c1 = new Circle(new Point(1,1), 2.0);
        Circle c2 = c1.shallowCopy();
        Circle c3 = c1.deepCopy();

        System.out.println("c1="+c1);
        System.out.println("c2="+c2);
        System.out.println("c3="+c3);

        c1.p.x = 9;
        c1.p.x = 9;
        System.out.println("c1 변경후");
        System.out.println("c1="+c1);
        System.out.println("c2="+c2);
        System.out.println("c3="+c3);
    }
}

class Point{
    int x,y;
    Point(int x, int y){
        this.x = x;
        this.y = y;
    }

    public String toString(){
        return "("+x + "," + y+")";
    }
}

class Circle implements Cloneable{
    Point p;    //원점 - 참조변수
    double r;   //반지름

    Circle(Point p, double r){
        this.p = p;
        this.r = r;
    }

    public Circle shallowCopy(){    //얕은 복사
        Object obj = null;
        try{
            obj = super.clone();
        }catch (CloneNotSupportedException e){}
        return (Circle)obj;
    }

    public Circle deepCopy() {  // 깊은 복사
        Object obj = null;
        try{
            obj = super.clone();
        }catch (CloneNotSupportedException e){}
        Circle c = (Circle)obj;
        c.p = new Point(this.p.x, this.p.y);
        return c;
    }

    public String toString(){
        return "[P=" +p + ", r=" + r +"]";
    }
}

 

🔔결과

 

✏️getClass()

 

getClass() 메서드는 자신이 속한 클래스의 Class 객체를 반환한다. Class 객체는 이름이 ‘Class’인 클래스의 객체이다. Class 클래스는 아래와 같이 값이 정의되어 있다.

 

public final class Class implements …{}

 

Class 객체는 클래스의 모든 정보를 담고 있으며, 클래스 당 1개만 존재한다. 또한 클래스 파일이 클래스 로더에 의해서 메모리에 올라갈 때, 자동으로 생성된다.

 

클래스 로더는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 한다. 먼저 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고, 있으면 객체의 참조를 반환하고 없으면 클래스 패스가 지정된 경로를 따라서 클래스 파일을 찾는다.

 

못 찾으면 ClassNotFoundException이 발생되고, 찾으면 해당 클래스 파일을 읽어서 Class 객체로 변환한다.

즉, 파일 형태로 저장디어 있는 클래스를 읽어서 Class클래스에 정의된 형식으로 변환하는 것이다

 

 

클래스의 정보가 필요할 때는 먼저 Class 객체에 대한 참조를 얻어 와야 한다. Class 객체에 대한 참조를 얻는 방법은 여러 가지가 있다.

 

🌈Class cObj = new Card.getClass(); //생성된 객체로 부터 얻어오기

🌈Class cObj = Card.class; //클래스 리터럴(*.class)로 부터 얻어 오기

🌈Class cObj = Class.forName(“Card”); //클래스 이름으로 부터 얻어 오기

 

Class 객체를 이용하면 클래스에 정의된 멤버의 이름이나 개수 등 클래스에 대한 모든 정보를 얻을 수 있다. 때문에 Class 객체를 통해서 객체를 생성하고 메서드를 호출하는 등 동적인 코드를 작성할 수 있다.

 

public class Main {

   public static void main(String[] args) throws IllegalAccessException, InstantiationException {

       Card c = new Card("HEART",3);   //new연산자로 객체 생성
       Card c2 = Card.class.newInstance();        //Class객체를 통해서 객체 생성

       Class cObj = c.getClass();

       System.out.println(c);
       System.out.println(c2);
       System.out.println(cObj.getName());
       System.out.println(cObj.toGenericString());
       System.out.println(cObj.toString());

   }
}



final class Card{

   String kind;
   int num;

   Card(){
       this("SPADE",1);
   }

   Card(String kind, int num){
       this.kind = kind;
       this.num = num;
   }

   public String toString(){
       return kind + ":" + num;
   }

}

 

🔔결과

 

✏️finalize(), 객체 소멸자

 

 

출처: 이것이 자바다, 자바의 정석

반응형

댓글