공부/Java

자바 Object 클래스

Stair 2024. 9. 8. 12:21
반응형

java.lang패키지 : 자바가 기본으로 제공하는 라이브러리 중에 가장 기본이 되는 것이 java.lang패키지이다.

 

여기서 lang은 Language(언어)의 줄임말이다. 쉽게 이야기해서 자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지를 뜻한다.

 

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

Object : 모든 자바 객체의 부모 클래스

String : 문자열

Integer, Long, Double : 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것

Class : 클래스 메타 정보

System : 시스템과 관련된 기본 기능들을 제공

 

여기 나열한 클래스들은 자바 언어의 기본을 이루기 때문에 반드시 잘 알아두어야한다.

 

 

 

 

1.java.lang

패키지는 모든 자바 애플리케이션에 자동으로 임포트(import)된다. 따라서 임포트 구문을 사용하지 않아도 된다.

 

 

System 클래스는 java.lang 패키지 소속이다. 따라서 다음과 같이 임포트를 생략할 수 있다.

 

 

 

 

2.Object 클래스

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

 

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

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

 

클래스에 상속 받을 부모 클래스가 없으면 묵시적으로 Object 클래스를 상속 받는다.

(쉽게 이야기하여 자바가 extends Object 코드를 넣어준다, 따라서 extends Object는 생략하는 것을 권장한다.)

public class Child extends Parent{

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

 

부모를 상속받는 Child 메서드를 생성하였는데, 이렇게 되면 다음과 같은 구조를 띈다.

 

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

    //toString은 Object 클래스의 메서드
    String string = child.toString();

    System.out.println(string);

}

 

toString()은 Object 클래스의 메서드이므로 child.toString()또한 가능하다.

 

 

자바에서 Object 클래스가 최상이 부모 클래스인 이유

1. 공통 기능 제공

2. 다형성의 기본 구현

 

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

 

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

1. 객체의 정보를 제공하는 toString()

2. 객체의 같음을 비교하는 equals()

3. 객체의 클래스 정보를 제공하는 getClass()

4. ETC...

 

개발자는 모든 객체가 앞서 설명한 메서드를 지원한다는 것을 알고 있다. 따라서 프로그래밍이 단순화되고, 일관성을 가진다.

 

부모는 자식을 담을 수 있다. 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 static void main(String[] args) {
        Dog dog = new Dog();
        Car car = new Car();

        action(dog);
        action(car);


    }

    private static void action(Object object){
//        object.sound(); //Object는 sound()와 move()가 없다.
//        object.move();


        if(object instanceof Dog dog){
            dog.sound();
        } else if (object instanceof Car car) {
            car.move();
        }

    }

 

Object는 부모객체이기에 다운캐스팅을 명시해주지 않으면 메서드를 사용할 수 없다.

((Dog)object).sound()와 같이 사용할 수 있다.

 

Object는 모든 객체의 부모이므로 모든 객체를 대상으로 다형적 참조를 할 수 있지만, sound()와 move()같이 다른 객체의 메서드가 정의되어있지 않기 때문에 메서드 오버라이딩을 활용할 수 없다는 한계가 존재한다.

 

 

Object를 활용하면 모든 타입을 담을 수 있는 배열 또한 생성할 수 있다.

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);
    
}
public static void size(Object[] object){
    System.out.println("전달된 객체의 수는: "+object.length);
}

 

size 메서드는 배열에 담긴 객체의 수를 세는 역할을 담당하고 있다.

Object 타입의 배열은 세상의 모든 객체를 담을 수 있기 때문에, 새로운 클래스가 추가되거나 변경되어도 이 메서드를 수정하지 않아도 된다.

 

만약 Object와 같은 개념이 없다면 어떻게 될까?

void actions(Object)과 같이 모든 객체를 받을 수 있는 메서드를 만들 수 없다.

Object[] objects처럼 모든 객체를 저장할 수 있는 배열을 만들 수 없다.

 

 

 

 

 

3. toString()

object.toString() 메서드는 객체의 정보를 문자열 형태로 제공한다. 그래서 디버깅과 로깅에 유용하게 사용된다. 이 메서드는 Objejct 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있다.

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

Object가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해쉬코드)를 16진수로 제공한다.

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

    System.out.println(string);
    System.out.println(o);
}

 

위 코드의 결과값은 어떻게 나오는지 확인해보자

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

(뒤 참조값은 컴퓨터마다 다르다. o와 string이 동일한 결과값이 나온다는게 중요하다)

 

이 이유는 println에 있는데 println을 파고 들어가보면 결국 o.toString()을 호출하는 코드가 나오기 때문이다.

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

뭐 대충 이런게 있다.

 

따라서 println을 사용할 필요 없이 toString()을 사용하면 된다.

 

 

public class Car {

    private String carName;

    public Car(String carName){
        this.carName = carName;
    }
}
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 +
                '}';
    }
}
public class ObjectPrinter {
    public static void print(Object obj){
        String string = "객체 정보 출력: " +obj.toString();
        System.out.println(string);
    }
}
public static void main(String[] args) {
    Car car = new Car("Model Y");
    Dog dog1 = new Dog("멍멍이1", 2);
    Dog dog2 = new Dog("멍멍이2", 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='멍멍이2', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@4e50df2e
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
3. Object 다형성 활용
객체 정보 출력: lang.object.tostring.Car@4e50df2e
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이2', age=5}

 

다음과 같은 결과값을 출력한다.

Car에서는 Object의 toString메서드를 오버라이딩 하지 않았다. 때문에 Object의 toString()기본 메서드로 참조값이 표시되었다.

Dog에서는 Object의 toString메서드를 오버라이딩 하여 사용하였기 때문에 단순한 참조값이 아닌 객체의 정보들이 표시되게 되었다.

 

 

toString()은 기본적으로 객체의 참조값을 출력한다. 그런데 이 메서드를 재정의 해버리면 객체의 참조값을 출력할 수 없다. 이때는 다음 코드를 사용하여 객체의 참조값을 출력할 수 있다.

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

 

 

 

 

 

4. Object와 OCP

만약 Object가 없고, 또 Object가 제공하는 toString()이 없다면 서로 아무 관계가 없는(공통의 부모가 없는) 객체의 정보를 출력하기 어려울 것이다. 

 

우리가 앞서 만든 ObjectPrinter 클래스는 Car, Dog같은 구체적인 클래스를 사용하는 것이 아니라 ,추상적인 Object 클래스를 사용한다. 이렇게 ObjectPrinter클래스가 Object 클래스를 사용하는 것을 Object 클래스에 의존한다고 표현한다.

 

ObjectPrinter와 Object를 사용하는 구조는 다형성을 매우 잘 활용하고 있다. 다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻이다.

 

ObjectPrinter의 print()메서드와 전체 구조를 분석해보자.

1. 다형적 참조 : print(Object obj), Object타입을 매개변수로 사용해서 다형적 참조를 사용한다. Car, Dog 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있다.

2. 메서드 오버라이딩 : Object는 모든 클래스의 부모이다. 따라서 Dog, Car와 같은 구체적인 클래스는 Object가 가지고 있는 toString() 메서드를 오버라이딩 할 수 있다. 따라서 prin(Object obj) 메서드는 Dog, Car와 같은 구체적인 타입에 의존하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString()을 호출할 수 있다.

 

OCP 원칙

Open : 새로운 클래스를 추가하고 toString()을 오버라이딩해서 기능을 확장할 수 있다.

Closed : 새로운 클래스를 추가해도 Object와 toString()을 사용하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 된다.

 

 

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

 

ObjectPrinter는 모든 타입의 부모인 Object를 사용하고 Object가 제공하는 toString()메서드만 사용한다. 따라서 ObjectPrinter를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있다.

이 ObjectPrinter.print()는 사실 System.out.println()의 작동 방식과 유사하다.

sout 메서드도 Object 매개변수를 사용하고 내부에서 toString()을 호출한다. 따라서 sout을 사용하면 세상의 모든 객체 정보(toString())를 편리하게 출력할 수 있다.

 

*println을 따라가보면 () 안에 Object가 들어가는 것을 확인할 수 있다.

 

 

정적 의존관계 vs 동적 의존관계

정적 의존관계 :컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미한다.

동적 의존관계 : 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계이다.

 

 

 

 

 

5. equals()

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

 

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

1. 동일성 : == 연산자를 사용하여 두 객체의 참조가 동일한 객체를 가지고 있는지 확인

2. 동등성 : equals() 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인

 

"동일"은 완전히 같음을 의미한다. 반면 "동등"은 같은 가치나 수준을 의미하지만 그 형태나 외관 등이 완전히 같지 않을 수 있다.

 

동일성은 물리적으로 같은 메모리에 있는 객체 인스턴스인지 참조값을 확인하는 것이고, 동등성은 논리적으로 같은지 확인하는 것이다.

동일성은 자바 머신 기준(메모리 참조가 기준이므로 물리적)이고, 동등성은 보통 사람이 생각하는 기준(논리적)에 맞추어 비교한다.

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

이러한 경우 물리적으로 다른 메모리에 생성된 다른 객체이지만, 회원번호(id-100)을 기준으로 생각해보면 논리적으로 같은 회원으로 볼 수 있다.

 

따라서 동일성은 다르지만, 동등성은 같다.

String s1 = "hello";
String s2 = "hello";

위와 같은 문자열의 경우도 물리적으로는 다른 메모리에 존재할 수 있지만 논리적으로는 같은 "hello"라는 문자열이다.

(사실 동일한 문자열일 경우엔 자바가 최적화를 진행하여 동일한 메모리를 사용하도록 하게 한다)

 

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)));

}

열심히 이해하고 이 코드를 돌려보면 두 sout 다 false가 나오는것을 보고 멘붕이 온다.

그러나 이것은 둘 다 false가 맞다.

user1의 참조값이 x001이고 user2의 참조값이 x002일때

동일성(==)는 당연히 false이다. 그럼 왜 equals도 false 일까?

 

사실 object가 기본으로 제공하는 equals를 타고 올라가보면 아래와 같다.

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

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

결국 실행 순서가

user1.equals(user2)

return (user1 == user2)

return (x001 == x002)

return false

false

가 되는 것이다.

 

동등성이라는 개념은 각각 클래스마다 다르다. 따라서 동등성 비교를 사용하고 싶다면 equals()메서드를 재정의 해야한다. 그렇지 않으면 Object는 동일성 비교를 기본으로 제공한다.

 

public class UserV2 {

    private String id;

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

    @Override
    public boolean equals(Object obj){
//        obj.id ---- Object 클래스엔 id가 없기에 다운캐스팅 해야함
        UserV2 user = (UserV2) obj;
        return id.equals(user.id);

    }

}
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));
}

 

equals를 오버라이딩 하여 id를 비교하는 메서드로 재정의 하였다.

 

지금의 equals는 이해를 돕기 위해 매우 간단하게 만든 버전이고, 실제로 정확하게 동작하려면 다음과 같이 구현해야 한다. 정확한 equals() 메서드를 구현하는 것은 생각보다 쉽지 않다.

그렇기때문에 intellij의 generate를 사용하여 구현한다. (Alt+insert -> equals() and hashCode())

@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);
}

 

그러면 equals에 관한 메서드가 생성된다.

 

실무에서는 대부분 IDE가 만들어주는 equals()를 사용하므로, 그냥 이런 제약이 있구나 생각하자.

 

 

참고로 동일성 비교가 항상 필요한 것은 아니다. 동일성 비교가 필요한 경우에만 equals()를 재정의하면 된다.

equals()와 hashCode()는 보통 함께 사용된다. 이 부분은 뒤에 컬렉션 프레임워크에서 배운다.

 

 

 

Object의 나머지 메서드

clone() -> 객체를 복사할 때 사용한다. 잘 사용하지 않는다.

hashCode() -> equals()와 종종 함께 사용된다. 컬렉션 프레임워크에서 자세히 알아보자

getClass() -> 뒤에 Class에서 자세히 알아보자

notify(), notifyAll(), wait() ->멀티쓰레드용 메서드이다. 멀티쓰레드에서 알아보자

 

 

 

반응형

'공부 > Java' 카테고리의 다른 글

자바 String 클래스  (1) 2024.09.10
자바 불변 객체  (0) 2024.09.09
자바 다형성 3편 (Polymorphism)  (1) 2024.09.05
자바 좋은 객체 지향 프로그래밍이란?  (1) 2024.09.05
자바 다형성 2편 (Polymorphism)  (1) 2024.09.05