본문 바로가기

Java

[Java] Object 클래스

Java.lang 패키지

자바가 기본으로 제공하는 라이브러리(클래스 모음) 중에 가장 기본이 되는 것이 java.lang 패키지 입니다.

자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지를 뜻합니다.

 

java.lang 패키지의 대표적인 클래스들

  • Object: 모든 자바 객체의 부모 클래스
  • String: 문자열
  • Interger, Long, Double: 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것
  • Class: 클래스 메타 정보
  • System: 시스템과 관련된 기본 기능들을 제공

 

import 생략 가능

java.lang 패키지는 모든 자바 애플리케이션에 자동으로 임포트(import) 됩니다.

따라서 임포트 구문을 사용하지 않아도 됩니다.

package lang;

import java.lang.System; // 생략가능

public class LangMain {

    public static void main(String[] args) {
        System.out.println("hello java.lang");
    }
}
  • import java.lang.System; 코드를 삭제해도 정상 작동한다.

 

Object 클래스

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

 

// 부모가 없으면 묵시적으로 Object 클래스를 상속받는다.
public class Parent{

    public void parentMethod() {
        System.out.println("Parent.parentMethod");
    }
}
public class Parent extends Object{

    public void parentMethod() {
        System.out.println("Parent.parentMethod");
    }
}

 

클래스에 상속 받을 부모 클래스가 없으면 묵시적으로 Object 클래스를 상속 받습니다. (생략권장)

public class Child extends Parent{ // 명시적 상속

    public void childMethod() {
        System.out.println("Child.childMethod");
    }
}
public class ObjectMain {

    public static void main(String[] args) {
        Child child = new Child();
        child.childMethod();
        child.parentMethod();

        // toString()은 Object 클래스의 메서드
        // 객체에 대한 정보를 출력해줌(참조 값)
        String string = child.toString();
        System.out.println(string);
    }
}

 

Parent는 Object를 묵시적으로 상속 받았기 때문에 메모리에도 함께 생성됩니다.

  1.  child.toString( )을 호출한다.
  2. 먼저 본인 타입인 Child 에서 toString( )을 찾아보지만 없어서 부모 타입으로 올라가서 찾는다.
  3. 부모 타입인 Parent에서 찾아보지만 없어서 부모 타입으로 올라가서 찾는다.
  4. 부모 타입인 Object에서 찾는다. Object에 toString( ) 메서드를 호출한다.

 

"그렇다면 자바에서 Object 클래스가 최상위 부모 클래스인 이유는 뭘까?"

  • 공통 기능 제공
  • 다형성의 기본 구현

Object는 모든 객체에 필요한 공통 기능을 제공합니다.

Object는 최상위 부모 클래스이기 때문에 모든 객체는 공통 기능을 편리하게 제공(상속) 받을 수 있습니다.

 

공통 기능 제공

toString( ) 같은 기능의 객체를 만들 때 마다 항상 새로운 메서드를 정의해서 만들어야 한다면 개발자마다

서로 다른 이름의 메서드를 만들어서 일관성이 깨질 수 있습니다.개발자는 모든 객체가 앞서 설명한 메서드를

지원한다는 것을 알고 있다면, 프로그래밍이 단순화되고, 일관성을 가지게 됩니다.

 

Object가 제공하는 기능은 다음과 같습니다.

  • 객체의 정보를 제공하는 toString( )
  • 객체의 같음을 비교하는 equals( )
  • 객체의 클래스 정보를 제공하는 getClass( )
  • 기타 여러가지 기능

 

다형성의 기본 구현

Object는 모든 클래스의 부모 클래스입니다. 따라서 모든 객체를 참조할 수도 있습니다.

Object 클래스는 다형성을  지원하는 기본적인 메커니즘을 제공하고 있습니다.

모든 자바 객체는 Object 타입으로 처리될 수 있으며, 이는 다양한 타입의 객체를 통합적으로 처리할 수 있게 도와줍니다. 쉽게 이야기하면 Object는 모든 객체를 다 담을 수 있고, 타입이 다른 객체들을 어딘가에 보관해야 한다면

Object에 보관하면 됩니다.

 

Object 다형성

Object는 모든 클래스의 부모 클래스입니다.

그림과 같이 Object는 모든 객체를 참조할 수 있습니다.

 

 

public class Car {
    
    public void move() {
        System.out.println("자동차 이동");
    }
}
public class Dog {
    
    public void sound() {
        System.out.println("개 소리");
    }
}
public class ObjectPolyExample1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Car car = new Car();

        action(dog);
        action(car);
    }
    private static void action(Object obj) {
        //obj.sound(); 컴파일 오류, Object는 sound()가 없다.
        //obj.move(); 컴파일 오류, Object는 move()가 없다.

        //객체에 맞는 다운 캐스팅 필요
        if (obj instanceof Dog dog) {
            dog.sound();
        } else if (obj instanceof Car car) {
            car.move();
        }
    }
}

 

Object는 모든 타입의 부모입니다.

부모는 자식을 담을 수 있으므로 앞의 코드를 다음과 같이 변경해도 됩니다.

Object dog = new Dog();
Object car = new Car();

 

 Object 다형성의 장점

  • action(Object obj) 메서드는 Object 타입의 매개변수를 사용한다.
  • 따라서 어떤 객체든지 인자로 전달할 수 있다.
action(dog) // main에서 dog 전달
void action(Object obj = dog) // Object는 자식인 Dog 타입을 참조할 수 있다.

actionc(car) // main에서 car 전달
void action(Object obj = car) // Object는 자식인 Car 타입을 참조할 수 있다.

 

Object 다형성의 한계

//main에서 dog, car 전달
private static void action(Object obj) {
        obj.sound(); 컴파일 오류, Object는 sound()가 없다.
        obj.move(); 컴파일 오류, Object는 move()가 없다.

 

  • action( ) 메서드 안에서 obj.sound( )를 호출하면 오류가 발생한다.
  • 왜냐하면 매개 변수인 obj는 Object 타입이기 떄문이다.
  • Object에는 sound( ) 메서드가 없다.

 

obj.sound( ) 호출

  • obj.sound( ) 호출
  • obj는 Object 타입이므로 Object 타입에서 sound( )를 찾는다.
  • Object에서 sound( )를 찾을 수 없다. Object는 최종 부모이므로 더 이상 올라가서 찾을 수 없다

Dog 인스턴스의 sound( ) 를 호출하려고 하면 다운캐스팅을 해야한다.

if (obj instanceof Dog dog) {
        dog.sound();
}

  • Object obj 의 참조값을 Dog dog 로 다운캐스팅 하면서 전달한다.
  • dog.sound( ) 를 호출하면 Dog 타입에서 sound( )를 찾아서 호출한다.

 

Object를 활용한 다형성의 한계

 

Object는 모든 객체를 대상으로 다형적 참조를 할 수 있다.

  • Object는 모든 객체의 부모이므로 모든 객체를 담을 수 있다.

Object를 통해 전달 받은 객체를 호출하려면 각 객체에 맞는 다운캐스팅 과정이 필요하다.

  • Object가 세상의 모든 메서드를 알고 있는 것은 아니다.

Object 본인이 보유한 toString( ) 같은 메서드는 자식 클래스에서 오버라이딩 할 수 있지만, sound( ), move( ) 같은

Object에 속하지 않은 메서드는 메서드 오버라이딩을 활용할 수 없습니다. 그렇기 때문에 다형성을 활용하기에는

한계가 있습니다. 그렇다면 Object를 언제 활용하면 좋을까요?

 

Object 배열

Object는 모든 타입의 객체를 담을 수 있었습니다.

그렇다면 Object [ ] 을 만들면 세상의 모든 객체를 담을 수 있는 배열을 만들 수 있지 않을까요?

public class ObjectPolyExample2 {

    public static void main(String[] args) {
        Dog dog = new Dog();
        Car car = new Car();
        Object object = new Object();

        Object[] objects = {dog, car, object};
        size(objects);
    }
    private static void size(Object[] objects) {
        System.out.println("전달된 객체의 수는: " + objects.length);
    }
}

 

size( ) 메서드

  • 이 메서드는 Object 타입만 사용한다.
  • Object 타입의 배열은 세상의 모든 객체를 담을 수 있기 때문에 새로운 클래스가 추가되거나 변경되어도 이 메서드를 수정하지 않아도 된다.
  • 지금 만든 size( ) 메서드는 자바를 사용하는 곳이라면 어디든지 사용할 수 있다.

 

Object가 없다면?

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

물론 Object가 없어도 직접 클래스를 만들고 모든 클래스에서 직접 정의한 클래스를 상속 받으면 됩니다.

하지만 하나의 프로젝트를 넘어서 전세계 모든 개발자가 비슷한 클래스를 만들 것이고, 서로 호환되지 않는

직접 정의한 수 많은 Object 클래스들이 넘쳐날 것입니다.

 

 

toString( )

 

Object.toString( ) 메서드는 객체의 정보를 문자열 형태로 제공해줍니다.

그래서 디버깅과 로깅에 유용하게 사용되곤 합니다.

이 메서드는 Object 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다.

public class ToStringMain1 {

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

        //ToString() 반환 값 출력
        System.out.println(string);

        //object 직접 출력
        System.out.println(object);
    }
}

 

실행 결과

java.lang.Object@b4c966a
java.lang.Object@b4c966a

 

Object.toString( )

public String toString() {
	return getClass().getName() + "@" + Integer.toHexString(hasCode());
}
  • Object가 제공하는 toString( ) 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)을 16진수로 제공합니다.

 

"println( )과 toString( ) 의 값은 왜 같을까?"

사실 System.out.println( ) 메서드는 내부에서 toString( ) 을 호출합니다.

Object 타입(자식 포함) 이 println( )에 인수로 전달되면서 내부에서 obj.toString( ) 메서드를 호출해서 결과를 출력합니다.

public void println(Object x) {
	String s = String.valueOf(x);
    // ....
}
public static String valueOf(Object obj) {
	return (obj == null) ? "null" : obj.toString();
}

 

 

toString( ) 오버라이딩

 

Object.toString( ) 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나태지 못합니다.

그래서 보통 toString( ) 을 재정의(오버라이딩) 해서 보다 유용한 정보를 제공하는 것이 일반적입니다.

 

public class Dog {

    private String dogName;
    private int age;

    public Dog(String dogName, int age) {
        this.dogName = dogName;
        this.age = age;
    }
    // Alt + Insert -> toString()
    @Override
    public String toString() {
        return "Dog{" +
                "dogName='" + dogName + '\'' +
                ", age=" + age +
                '}';
    }
}
public class Car {

    private String carName;

    public Car(String carName) {
        this.carName = carName;
    }
}
package lang.object.tostring;

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

public class ToStringMain2 {

    public static void main(String[] args) {

        Car car = new Car("ModelY");
        Dog dog1 = new Dog("멍멍이1", 2);
        Dog dog2 = new Dog("멍멍이1", 5);

        System.out.println("1. 단순 toString 호출");
        System.out.println(car.toString());
        System.out.println(dog1.toString());
        System.out.println(dog2.toString());
        
        System.out.println("2. println 내부에서 toString 호출");
        System.out.println(car);
        System.out.println(dog1);
        System.out.println(dog2);

        System.out.println("3. Object 다형성 활용");
        ObjectPrinter.print(car);
        ObjectPrinter.print(dog1);
        ObjectPrinter.print(dog2);
    }
}

 

실행 결과

1. 단순 toString 호출
lang.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이1', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이1', age=5}
3. Object 다형성 활용
객체 정보 출력: lang.object.tostring.Car@4e50df2e
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이1', age=5}

 

  • Car 인스턴스는 toString( )을 재정의 하지 않았다. 따라서 Object가 기본적으로 제공하는 toString( ) 메서드를 사용한다.
  • Dog 인스턴스는 toString( )을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있다.

 

ObjectPrinter.print(car) 분석

ObjectPrinter.print(car); // main에서 호출

void print(Object obj = car) { // 인수 전달(Car타입)
	String string = "객체 정보 출력: " + obj.toString();
}
  1. Object obj의 인수로 car(Car) 가 전달된다.
  2. 메서드 내부에서 obj.toString( )을 호출한다.
  3. obj는 Object 타입이다. 따라서 Object에 있는 toString( )을 찾는다.
  4. 이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아본다. 재정의된 메서드가 없다.
  5. Object.toString( )을 실행한다.

 

ObjectPrinter.print(dog) 분석

ObjectPrinter.print(dog) 분석

void print(Object obj = dog) { // 인수 전달(Dog 타입)
    String string = "객체 정보 출력: " + obj.toString();
}
  1. Object obj의 인수로 dog(Dog) 가 전달된다.
  2. 메서드 내부에서 obj.toString( )을 호출한다.
  3. obj는 Object 타입이다. 따라서 Object에 있는 toString( )을 찾는다.
  4. 이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아본다. 재정의된 메서드가 있다.
  5. Dog.toString( )을 실행한다.

 

참고 - 객체의 참조값 직접 출력

toString( )은 기본적으로 객체의 참조값을 출력하게 되어 있습니다.

toString( )이나, hasCode( ) 메서드를 재정의 하면 참조값을 출력할 수 없습니다.

아래 코드를 사용하면 직접 객체의 참조값을 출력할 수 있습니다.

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

 

 

Object와 OCP

만약 Object가 없고, 또 Object가 제공하는 toString( )이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것 입니다.(공통의 부모가 없는 경우) 각각의 클래스마다 별도의 메서드를 작성해야 합니다.

 

 

반면 ObjectPrinter 와 Object를 사영하는 구조는 다형성을 매우 잘 활용하고 있습니다. 

다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻입니다.

 

ObjectPrinter의 print( )

  • 다형적 참조: print(Object obj), Object 타입을 매개 변수로 사용해서 다형적 참조를 사용한다. Car, Dog 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있다.
  • 메서드 오버라이딩: Object는 모든 클래스의 부모이다. 따라서 Dog, Car 와 같은 구체적인 클래스는 Object가 가지고 있는 toString( ) 메서드를 오버라이딩 할 수 있다. 따라서 pring(Object obj) 메서드는 Dog, Car 와 같은 구체적인 타입에 의존(사용) 하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString( )을 호출할 수 있다.

OCP 원칙

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

다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 Car, Dog 에 의존하는 것이 아니라 추상적인 Object 에 의존하면서 OCP 원칙을 지킬 수 있었습니다. 덕분에 새로운 클래스를 추가하고 toString( ) 메서드를 새롭게 오버라이딩 해서 기능을 확장 할 수 있습니다. 그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter는 변경할 필요도 없죠.

 

equals( )

Object는 동등성 비교를 위한 equals( ) 메서드를 제공합니다.

 

자바는 두 객체가 같다라는 표현을 2가지로 분리해서 제공합니다.

  • 동일성(indentity): == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
  • 동등성(Equality): equals( ) 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인

예를 들어 같은 회원 번호를 가진 객체가 2개 있다고 가정하겠습니다.

User a = new User("id-100"); // 참조 x001
User b = new User("id-100"); // 참조 x002

 

이 경우 물리적으로 다른 메모리에 있는 다른 객체이지만, 회원번호를 기준으로 생각해보면 논리적으로는 같은 회원으로 볼 수 있습니다. 따라서 동일성은 다르지만, 동등성은 같습니다.

 

문자의 경우도 마찬가지 입니다.

String str1 = "hello";
String str2 = "hello";

 

이 경우 물리적으로는 각각의 "hello" 문자열이 다른 메모리에 존재할 수 있지만, 논리적으로는 같은 "hello" 라는 문자열 입니다. (이 경우에는 자바가 같은 메모리를 사용하도록 최적화 한다고 합니다.)

 

public class UserV1 {
    private String id;

    public UserV1(String id) {
        this.id = id;
    }
}
public class EqualsMainV1 {

    public static void main(String[] args) {
        UserV1 user1 = new UserV1("id-100");
        UserV1 user2 = new UserV1("id-100");

        System.out.println("identity = " + (user1 == user2));
        System.out.println("equality = " + (user1.equals(user2)));
    }
}

 

실행 결과

identity = false
equality = false

 

 

 

동일성 비교

user1 == user2
x001 = x002
false //결과

 

동등성 비교

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

 

Object가 기본으로 제공하는 equals( )는 == 으로 동일성 비교를 제공합니다.

 

user1.equals(user2)
return (user1 == user2) // Object.equals 메서드안
return (x001 == x002) // Object.equals 메서드안
return false

 

동등성이라는 개념은 각각의 클래스 마다 다릅니다 .어떤 클래스는 주민등록번호를 기반으로 동등성을 처리할 수도 있고,

어떤 클래스는 고객의 연락처를 기반으로 동등성을 처리할 수 있습니다. 어떤 클래스는 회원 번호를 기반으로 동등성을 처리할 수 있습니다. 따라서 동등성 비교를 사용하고 싶다면 equals( ) 메서드를 재정의 해야 합니다.

그렇지 않으면 Object는 동일성 비교를 기본으로 제공하게 됩니다.

 

equals( ) 구현

public class UserV2 {
    private String id;

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

    @Override
    public boolean equals(Object obj) {
        UserV2 user = (UserV2) obj;
        return id.equals(user.id);
    }
}
  • Object의 equals( ) 메서드를 재정의 했다.
  • UserV2의 동등성은 id(고객번호)로 비교한다.
  • equals( )는 Object 타입을 매개 변수로 사용한다. 따라서 객체의 특정 값을 사용하려면 다운캐스팅이 필요하다.
  • 여기서는 현재 인스턴스(this)에 있는 id 문자열과 비교 객체의 id 문자열을 비교한다.
  • UserV2에 있는 id는 String 이다. 문자열 비교는 == 이 아니라 equals( )를 사용해야 한다.
public class EqualsMainV2 {

    public static void main(String[] args) {
        UserV2 user1 = new UserV2("id-100");
        UserV2 user2 = new UserV2("id-100");

        System.out.println("identity = " + (user1 == user2));
        System.out.println("equality = " + (user1.equals(user2)));
    }
}

 

실행 결과

identity = false
equality = true
  • user1, user2 는 서로 다른 객체이지만 둘 다 같은 id(고객 번호)를 가지고 있으므로 동등하다.

위에 재정의한 equals( )는 매우 간단히 만든 버전이고, 실제로 정확하게 동작하려면 다음과 같이 구현해야 합니다.

대부분의 IDE는 정확한 equals( ) 코드를 자동으로 만들어줍니다.

 

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

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

 

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

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

정리

  • 동등성 비교가 항상 필요한 것은 아니다. 동등성 비교가 필요한 경우에만 equals( )를 재정의하면 된다.
  • equals( )와 hasCode( )는 보통 함께 사용된다.

 

 

'Java' 카테고리의 다른 글

[Java] String 클래스가 불변 객체 라고?  (0) 2024.04.02
[Java] 불변 객체, 불변 클래스  (1) 2024.04.01
[Java] 다형성이 중요한 이유?  (0) 2024.03.26
[Java] 인터페이스  (0) 2024.03.21
[Java] 다형성의 활용  (0) 2024.03.19