[Java] Object 클래스

Yujeong·2024년 4월 21일
0

Java

목록 보기
2/22
post-thumbnail

Object 클래스란?

자바에서 모든 클래스의 최상위 부모 클래스는 항상 Object 클래스이다.

  1. 묵시적(implicit)
    묵시적이라는 말은 개발자가 코드를 직접 작성하지 않아도 컴파일러에 의해 자동으로 수행되는 것을 의미한다.
    다음의 코드를 보면, 명시적으로 Object 클래스를 상속받는 코드가 없지만, 자동으로 Object 클래스를 상속받고 있다.
public class Parent {
	
    public void parentMethod() {
    	System.out.println("Parent.parentMethod");
    }
    
}
  1. 명시적(explicit)
    명시적이라는 말은 개발자가 코드를 직접 작성해서 작동하는 것을 의미한다.
    클래스에 상속 받을 부모 클래스를 명시적으로 지정하면 어떨까? 이 경우에는 Object 클래스를 상속받지 않는다.
public class Child extends Parent {
	
    public void childMethod() {
    	System.out.println("Child.childMethod");
    }
    
}
  1. Object가 최상위 부모 클래스인 이유
  • 공통 기능 제공
    • Object는 모든 객체에 필요한 공통 기능을 제공한다.
    • Object는 최상위 부모 클래스이기 때문에 모든 객체가 이 기능들을 상속받을 수 있다.
    • 이유: 객체의 정보 제공, 객체 비교, 객체가 어떤 클래스로 만들어졌는지 확인하는 기능은 모든 객체에 필요한 기본 기능이다. 기본 기능들을 객체를 만들 때마다 새로운 메서드를 정의해서 만드는 것은 매우 번거롭다. 또한, 개발자마다 다른 이름으로의 메서드로 만들어서 일관성이 없을 수 있다.
  • 다형성의 기본 구현
    • 모든 자바 객체는 Object 타입으로 처리될 수 있다.
    • 다양한 타입의 객체를 통합적으로 처리할 수 있다.

Object 클래스의 메서드

Object 클래스는 다양한 메서드를 가지고 있다.

1. toString()

toString()은 객체의 정보를 제공하는 메서드로, 문자열 형태로 제공한다. 그래서 디버깅과 로깅에 유용하게 사용된다.

1) toString() 메서드

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

2) toString() 메서드 사용 예시

public class ToStringMain {

    public static void main(String[] args) {
        Object obj = new Object();
        String str = object.toString();

        System.out.println(str);
        System.out.println(obj);
    }
}
java.lang.Object@a09ee92
java.lang.Object@a09ee92

3) println()

toStirng()의 결과를 출력한 결과와 printlin()에 객체를 직접한 출력한 것이 같았다.
왜 그랬던 것일까? println()은 내부에서 toString()을 호출하기 때문이다.
println() 메서드를 살펴보면 다음과 같은 코드를 수행하는 것을 알 수 있다. 여기서 valueOf()메서드를 자세히 살펴보면 toString() 메서드를 호출하는 것을 확인할 수 있다.

public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

4) 오버라이딩

toString() 메서드가 클래스 정보와 참조값을 제공한다. 하지만, 이 정보만으로는 객체의 상태를 나타내지 못하는 경우도 있기 때문에 보통 toString()을 재정의하여 사용한다.
- 단축키: command + N

public class Dog {

    private String dogName;
    private int age;

    public Dog(String dogName, int age) {
        this.dogName = dogName;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "dogName='" + dogName + '\'' +
                ", age=" + age +
                '}';
    }
}

5) 객체의 참조값 직접 출력

toStirng()은 기본으로 객체의 참조값을 출력한다. 하지만, toString()이나 hashCode()를 재정의하면 객체의 참조값을 출력할 수 없기 때문에 다음의 코드를 활용하여 참조값을 출력할 수 있다.

String refValue = Integer.toHexString(System.identityHashCode(obj));
System.out.println("refValue = " + refValue);

2. equals()

equals()는 객체의 같음을 비교하는 메서드로, 동등성을 비교햔다.
동등성이란 무엇일까?
자바는 두 객체가 같다는 표현을 2가지로 분리해서 제공한다.

  • 동일성(Identity): == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인, 완전히 같음, 물리적 같음
  • 동등성(Equality): equals() 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인, 같은 가치나 수준을 의미하지만 그 형태나 외관 등이 완전히 같지는 않음, 논리적 같음

이렇게 의미를 보면 와닿지 않는다. 동일성은 같은 참조값인지 확인하는 것이고, 동등성은 논리적으로 값은 값인지 확인하는 것이다. 예시로 살펴보자.

  • 예시1
    물리적으로 다른 메모리에 있는 객체이지만, 회원 이름을 기준으로 생각해보면 논리적으로 같은 회원으로 볼 수 있다. 즉, 동일성은 다르지만, 동등성은 같다.
Member member1 = new Member("Kim"); //x001
Member member2 = new Member("Kim"); //x002
  • 예시2
    물리적으로 각각의 "Kim" 문자열이 다른 메모리에 존재할 수 있지만, 논리적으로 같은 "Kim"이라는 문자열이다. 이 경우 또한, 동일성은 다르고 동등성은 같다.
String str1 = "Kim";
String str2 = "Kim";

여기서 이상함을 느껴야 한다. 왜 두 경우 모두 같은 결과가 나오는 것일까?
equals() 메서드를 살펴보자.
Object가 기본으로 제공하는 equals()==으로 동일성 비교를 제공한다. 즉, 동등성 비교를 사용하고 싶으면 equals() 메서드를 재정의해야 한다.

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

1) equals() 재정의해서 사용해보기

- Member의 동등성은 `id`(고객번호)로 비교
- `equals()`는 `Object` 타입을 매개변수로 사용해서 다운캐스팅
- 현재 인스턴스의 `id` 문자열과 비교 대상 객체의 `id` 문자열 비교
- `id`는 `String`으로 선언했기에 `equals()`를 사용하여 비교 
public class Member {

    private String id;

    public Member(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {
        Member member = (Member) obj;
        return Objects.equals(id, member.id);
    }

}
public class EqualsMain {
     public static void main(String[] args) {
         Member member1 = new Member("Kim");
         Member member2 = new Member("Kim");
         System.out.println("identity = " + (member1 == member2));
         System.out.println("equality = " + member1.equals(member2));
     }
}

실행하면 결과는 다음과 같이 나온다.

identity = false
equality = true

2) 정확한 equals() 구현해보기

command+N을 통해 equals() 코드를 자동으로 만들 수 있다.

@Override
public boolean equals(Object o) {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
     User user = (User) o;
     return Objects.equals(id, user.id);
}

3) equals() 메서드를 구현할 때 지켜야 할 규칙

  • 반사성(Reflexivity): 객체는 자기 자신과 동등해야 한다.
  • 대칭성(Symmetry): 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 한다.
  • 추이성(Transitivity): 만약 한 객체가 두 번째 객체와 동일하고, 두 번째 객체가 세 번째 객체와 동일하다면, 첫 번째 객체는 세 번째 객체와도 동일해야 한다.
  • 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한, equals() 메소드는 항상 동일한 값을 반환해야 한다.
  • null에 대한 비교: 모든 객체는 null 과 비교했을 때 false 를 반환해야 한다.

3. getClass()

getClass()는 자신이 속한 클래스의 객체를 반환하는 메서드로, 객체의 클래스 정보 제공한다.

@IntrinsicCandidate
public final native Class<?> getClass();
  • 클래스 당 1개만 존재
  • 클래스 파일이 클래스 로더(ClassLoader)에 의해 메모리에 올라갈 때, 자동으로 생성

예시

 Card card = new Card().getClass();

Object 다형성

CatCar는 아무 관련이 없는 클래스이지만, 둘 다 부모가 없기 때문에 Object를 자동으로 상속받는다.

class Cat {
    public void sound() {
System.out.println("야옹"); }
}
class Car {
    public void move() {
System.out.println("부릉"); }
}

action()에서 각각의 인스턴스의 메서드를 호출하려면 다운캐스팅이 필요하다.

public class ObjectPolyEx {

    public static void main(String[] args) {
        Cat cat = new Cat();
        Car car = new Car();

        action(cat);
        action(car);
    }

    private static void action(Object obj) {

        if (obj instanceof Cat cat) {
            cat.sound();
        } else if (obj instanceof Car car) {
            car.move();
        }
    }
}
  • Object 다형성 활용 예시
public class ObjectPrinter {
    public static void print(Object obj) {
		String string = "객체 정보 출력: " + obj.toString();
        System.out.println(string);
    }
}

장점

  • 어떤 객체든지 인자로 전달할 수 있다.
    ex) ObjectPolyEx 클래스의 action() 메서드

한계

  • action() 메서드 안에서 obj.sound()를 호출하면 오류가 발생한다. 매개변수인 objObject 타입이므로, Object에는 sound() 메서드가 없기 때문이다.
  • obj.sound() 호출 → objObject 타입에서 sound() 찾기 → Object에서 sound() 찾을 수 없으므로 오류 발생
    obj.sound() 호출
action(cat)
private static void action(Object obj) {
obj.sound(); //컴파일 오류, Object는 sound()가 없다.
}

Object 배열

Object[]를 만들면 모든 객체를 담을 수 있는 배열을 만들 수 있다.

public class ObjectPolyExample2 {
    public static void main(String[] args) {
      Cat cat = new Cat();
      Car car = new Car();
      Object object = new Object(); //Object 인스턴스도 만들 수 있다.
      Object[] objects = {cat, car, object};
      /**
      Object[] objects = new Object[3];
      objects[0] = cat;
      objects[1] = car;
      objects[2] = object;
      **/
      size(objects);
    }
    private static void size(Object[] objects) {
  	  System.out.println("전달된 객체의 수는: " + objects.length);
    }
}
전달된 객체 수는: 3

Object가 없다면?

  • void action(Object obj)과 같이 모든 객체를 받을 수 있는 메서드를 만들 수 없다.
  • Object[] objects 처럼 모든 객체를 저장할 수 있는 배열을 만들 수 없다.

Object와 OCP

구체적인 것에 의존하는 것을 추상적인 것에 의존함으로써 해결할 수 있다.
부모타입으로 올라갈수록 개념은 더 추상적이게 되고, 하위 타입으로 내려갈수록 개념은 더 구체적이게 된다.

구체적인 것에 의존

출력해야 할 클래스가 늘어나면 그 수에 맞춰 메서드도 계속 늘어나게 된다.

public class BadObjectPrinter {
    public static void print(Car car) { //Car 전용 메서드
    	String string = "객체 정보 출력: " + car.carInfo();
    }
    public static void print(Dog dog) { //Dog 전용 메서드
    	String string = "객체 정보 출력: " + dog.dogInfo();
    }
}

추상적인 것에 의존

public class ObjectPrinter {
    public static void print(Object obj) {
        String string = "객체 정보 출력: " + obj.toString();
        System.out.println(string);
    }
}

특징

  • 다형적 참조
    • print(Object obj),Object` 타입을 매개변수로 사용해서 다형적 참조를 사용
    • 모든 객체 인스턴스를 인수로 받을 수 있음
  • 메서드 오버라이딩
    • 구체적인 클래스는 Object 가 가지고 있는 toString() 메서드를 오버라이딩 할 수 있음
    • 구체적인 타입에 의존하지 않고, 추상적인 Object 타입에 의존하면서 각 인스턴스의 toString()을 호출할 수 있음

OCP 원칙

  • Open: 새로운 클래스를 추가하고, toString()을 오버라이딩해서 기능을 확장할 수 있다.
  • Closed: 새로운 클래스를 추가해도 ObjecttoString()을 사용하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 된다.

참고
Class Object
Package java.lang
Java의 정석
김영한의 실전 자바 - 중급 1편

profile
공부 기록

0개의 댓글

관련 채용 정보