Course/Java

[java-mid1] 1. Object 클래스

Lpromotion 2025. 2. 25. 18:11
"김영한의 실전 자바 - 중급편" 내용을 참고하여 정리함.

목차
 

1. java.lang 패키지 소개

`java.lang` 패키지는 자바의 핵심 기능을 제공하는 가장 기본적인 라이브러리 패키지이다.

`lang` 은 Language(언어)의 줄임말로, 자바 언어의 기본적인 클래스들이 포함되어 있다.

 

주요 클래스

  • `Object`: 모든 자바 객체의 부모 클래스
  • `String`: 문자열
  • `Integer`, `Long`, `Double`: 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것
  • `Class`: 클래스의 메타 정보
  • `System`: 시스템 관련 기본 기능 제공 (예: 콘솔 출력, 환경 변수 접근)

 

import 생략 가능

`java.lang` 패키지는 모든 자바 애플리케이션에서 자동으로 포함되므로 별도로 `import` 문을 작성할 필요가 없다.


예를 들어, `System.out.println()` 을 사용할 때 `System` 클래스는 `java.lang` 패키지에 속해 있지만 `import java.lang.System; ` 없이도 사용 가능하다.

import java.lang.System; // 삭제해도 정상 작동 (import 생략 가능)

public class LangMain {

    public static void main(String[] args) {
        System.out.println("hello java");
    }
}

 

 

2. Object 클래스

Object 클래스란

  • 자바에서 모든 클래스의 최상위 부모 클래스
  • 명시적으로 상속받지 않더라도 자동으로 Object를 상속받음 (`extends Object` 생략 가능)
  • 모든 자바 객체가 Object의 기능을 상속받음

 

묵시적  vs 명시적

  • 묵시적 (Implicit): 개발자가 코드에 직접 기술하지 않아도 시스템 또는 컴파일러에 의해 자동으로 수행되는 것을 의미
  • 명시적 (Explicit): 개발자가 코드에 직접 기술해서 작동하는 것을 의미

 

public class Parent {

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

 

위 코드는 아래 코드와 동일함 -> 부모가 없으면 묵시적으로 Object 클래스를 상속받는다. (extends Object가 자동)

public class Parent extends Object {

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

 

toString() 사용 예시

// 명시적으로 Parent를 상속받았기 때문에 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);
    }
}

 

실행 결과

Child.childMethod
Parent.parentMethod
lang.object.Child@4e50df2e

 

  • `toString()` 이 호출되면 객체의 클래스명과 해시코드가 반환됨 (객체의 정보를 제공)
  • 필요하면 `toString()` 을 오버라이딩하여 사용자 정의 출력 가능

 

  • `Parent` 는 `Object` 를 묵시적으로 상속 받았기 때문에 메모리에도 함께 생성된다.
  • 자바에서 모든 객체의 최종 부모는 `Object` 다.

 

Object 클래스가 최상위 부모 클래스인 이유

모든 클래스가 Object 클래스를 상속 받는 이유는 다음과 같다.

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

 

1. 공통 기능 제공

 

  • Object는 모든 객체에게 필요한 공통 기능을 제공한다.
  • 모든 객체가 일관된 방식으로 동일한 기능을 사용할 수 있으며, 개발자가 직접 구현할 필요가 없다.
  • Object 클래스의 주요 기능
    • toString(): 객체의 정보를 문자열로 변환
    • equals(Object obj): 두 객체가 같은지 비교
    • getClass(): 객체의 클래스 정보 반환

 

2. 다형성의 기본 구현

  • Object는 최상위 부모 클래스이기 때문에 모든 객체는 공통 기능을 편리하게 제공(상속) 받을 수 있다.
  • Object는 모든 클래스의 부모이므로, 모든 객체를 Object 타입으로 처리 가능하다.
    • 이는 다양한 타입의 객체를 통합적으로 저장하고 관리하는 데 유용하다.

 

 

3. Object 다형성

Object 클래스의 다형성

  • Object 는 모든 클래스의 최상위 부모이므로, 모든 객체를 Object 타입으로 참조할 수 있다.
  • 즉, 서로 관련 없는 클래스라도 Object 를 통해 다룰 수 있다.

 

예제: Dog과 Car 클래스

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` 가 부모이므로 `Dog` 와 `Car` 객체를 `Object` 타입의 매개변수로 받을 수 있다.
  • 하지만 `Object` 타입으로 참조할 경우, 원래의 메서드(`sound()`, `move()`)를 직접 호출할 수 없고 다운캐스팅이 필요하다.

 

Object 다형성의 장점

  • `action(Object obj)` 같은 메서드는 어떤 객체든지 받을 수 있어 유연성이 높다.
action(dog); // Object obj = dog (Dog)
action(car); // Object obj = car (Car)
  • `Object` 타입을 사용하면 서로 다른 클래스도 동일한 메서드에서 처리 가능하다.

 

Object 다형성의 한계

private static void action(Object obj) {
    obj.sound(); // 컴파일 오류: Object에는 sound() 메서드가 없음
}

  • Object 는 최종 부모이므로 더는 올라가서 찾을 수 없다.
    • `obj` 가 `Object` 타입이므로 `sound()` 를 호출할 수 없음.
  • `Object` 가 모든 객체의 부모이므로 모든 객체를 대상으로 다형적 참조를 할 수 있다.
    하지만 다른 객체의 메서드가 정의되어 있지 않다. 
    • 따라서 원래 객체의 메서드를 사용하려면 다운캐스팅이 필요하다.
    • → 다형적 참조 활용 O, 메서드 오버라이딩 활용 X
if (obj instanceof Dog dog) {
    dog.sound();  // 다운캐스팅 후 메서드 호출
}

 

 

다형성을 제대로 활용하려면 자바 기본편에서 배운 것 처럼 다형적 참조 + 메서드 오버라이딩을 함께 사용해야 한다.

결과적으로, `Object` 는 다형적 참조는 가능하지만, 메서드 오버라이딩이 안되기 때문에 다형성을 활용하기에 한계가 있다.

다음은 `Object`를 활용하는 방법이다.

 

 

4. 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 인스턴스도 만들 수 있음

        Object[] objects = {dog, car, object};
        
        size(objects);
    }

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

 

실행 결과

 
전달된 객체의 수는: 3

 

  • `Object[]` 배열을 사용하면 서로 다른 객체(Dog, Car, Object)를 하나의 배열에 저장 가능하다.

  • `size()` 메서드는 `Object` 타입만 사용하므로, 다양한 객체를 지원하면서도 메서드 수정 없이 확장 가능하다.

 

Object가 없다면?

  • ` void action(Object obj)` 과 같이 모든 객체를 받을 수 있는 메서드를 만들 수 없다.
  • `Object[] objects` 처럼 다양한 객체를 저장하는 배열을 만들 수 없다.
  • 대신 `MyObject` 같은 별도의 공통 부모 클래스를 만들어야 하지만, 전 세계적으로 표준화되지 않아 여러 개발자 간 호환성이 떨어지는 문제가 발생할 것이다.

 

 

5. toString()

Object의 toString() 기본 동작

  • `Object.toString()` 메서드는 객체의 정보를 문자열로 변환하는 역할을 한다.
  • `Object` 클래스에서 기본 제공하며, 모든 클래스에서 상속받아 사용할 수 있다.
public class ToStringMain1 {
    public static void main(String[] args) {
        Object object = new Object();
        System.out.println(object.toString()); // 기본 toString() 호출
        System.out.println(object); // println() 내부에서 toString() 호출
    }
}

 

실행 결과

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

같은 결과값을 반환한다.

 

Object.toString()

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

 

println()과 toString()

// toString() 반환값 출력
String string = object.toString();
System.out.println(string);

// object 직접 출력
System.out.println(object);
public void println(Object x) {
    String s = String.valueOf(x);
    //...
}

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}
  • `System.out.println();` 은 내부적으로 `toString()` 을 호출한다.
    • `System.out.println(object);` 를 하면 자동으로 `toString()` 메서드를 호출해서 결과를 출력한다.
    • `println()` 을 사용할 때, `toString()` 을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있다.

 

toString() 오버라이딩

  • 기본 `toString()`은 클래스 정보와 객체의 참조값만 제공하므로, 유용한 정보를 출력하려면 오버라이딩(재정의)이 필요하다.
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() { // Alt + Windows: generator
        return "Dog{" +
                "dogName='" + dogName + '\'' +
                ", age=" + age +
                '}';
    }
}
  • Dog 클래스에서 `toString()` 을 오버라이딩하여 객체 정보를 더 명확하게 표시함.
public class ToStringMain2 {
    public static void main(String[] args) {
        Dog dog1 = new Dog("멍멍이1", 2);
        Dog dog2 = new Dog("멍멍이2", 5);

        System.out.println(dog1);
        System.out.println(dog2);
    }
}

 

실행 결과

Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
  • Dog 객체의 정보를 명확하게 출력할 수 있다.

 

객체의 참조값 직접 출력

  • `toString()` 을 재정의하면 기본 참조값을 확인할 수 없다.
  • System.identityHashCode()를 사용하면 참조값을 16진수로 출력할 수 있다.
String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue);

 

실행 결과

refValue = 30dae81 // 실행할 때마다 달라질 수 있음

 

 

6. Object와 OCP

Object가 없다면?

  • `Object` 가 없고, ·Object.toString()` 메서드가 제공되지 않는다면,
    서로 아무 관계가 없는 객체의 정보를 출력하는 것이 어려워진다.
  • 즉, 공통 부모 클래스가 없는 객체들의 정보를 출력하려면 각각 별도의 메서드를 만들어야 한다.
  • 예를 들어, Car와 Dog 클래스를 출력하려면 다음과 같은 비효율적인 코드가 필요하다.

 

BadObjectPrinter (잘못된 예시 - 구체적인 것에 의존)

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

    public static void print(Dog dog) { // Dog 전용 메서드
        String string = "객체 정보 출력: " + dog.dogInfo(); // dogInfo() 메서드 필요
        System.out.println(string);
    }
}

 

문제점

  • `BadObjectPrinter` 클래스는 구체적인 타입인 `Car`, `Dog` 에 의존하고 있다.
  • 새로운 클래스를 추가할 때마다 `print()` 메서드를 추가해야 하며, 10개의 클래스가 생기면 `print()` 도 10개 추가해야 한다.
  • `BadObjectPrinter` 는 `Car` , `Dog` 에 의존한다고 표현한다.

 

  • 자바의 `Object` 클래스를 사용하면, 구체적인 타입이 아니라 추상적인 타입(Object)에 의존할 수 있다.
  • `Object`를 활용하면 모든 객체를 받을 수 있는 공통 메서드를 만들 수 있다.

ObjectPrinter (개선된 예시 - 추상적인 것에 의존)

public class ObjectPrinter {
    public static void print(Object obj) {
        String string = "객체 정보 출력: " + obj.toString();
        System.out.println(string);
    }
}
  • `ObjectPrinter.print(Object obj)` 는 구체적인 `Car`, `Dog` 클래스를 직접 사용하지 않고, `Object` 타입을 사용한다.
  • 덕분에 새로운 클래스가 추가되더라도 `ObjectPrinter` 코드를 변경할 필요가 없다.
  • `ObjectPrinter` 클래스가 `Object` 에 클래스에 의존한다고 표현한다.
  • 다형적 참조: `print(Object obj)` , `Object` 타입을 매개변수로 사용해서 다형적 참조를 사용한다.
  • 메서드 오버라이딩: 추상적인 `Object` 타입에 의존하면서 런타임에 각 인스턴스의 `toString()` 을 호출할 수 있다.

 

OCP 원칙 (Open-Closed Principle)

OCP (개방-폐쇄 원칙): 확장에는 열려(Open) 있고, 수정에는 닫혀(Closed) 있어야 한다.

  • Open (확장 가능): 새로운 클래스를 추가하고 `toString()` 을 오버라이딩하면 기능 확장 가능.
  • Closed (수정 불필요): 새로운 클래스를 추가해도 `ObjectPrinter` 코드를 변경할 필요가 없음.

 

OCP 원칙을 만족하는 구조

  1. `ObjectPrinter` 는 `Object` 를 사용하여 모든 객체를 출력할 수 있는 공통 기능 제공.
  2. 각 클래스(`Car`, `Dog` 등)는 필요하면 toString()을 오버라이딩하여 출력 형식을 맞출 수 있음.
  3. 새로운 클래스가 추가되더라도 기존 코드(`ObjectPrinter`)는 변경하지 않아도 됨.

 

System.out.println()

  • `System.out.println()` 도 사실 내부적으로 `toString()` 을 호출하여 객체 정보를 출력한다.
  • `ObjectPrinter.print()` 와 같은 원리로 동작하며, `toString()` 을 오버라이딩하면 원하는 출력 형식을 지정할 수 있다.
System.out.println(car);  // 내부적으로 car.toString() 실행
System.out.println(dog1); // 내부적으로 dog1.toString() 실행

 

이처럼 자바는 객체지향 언어답게 내부적으로도 객체지향 원칙을 잘 활용하고 있다.

 

참고 - 정적 의존관계 vs 동적 의존관계

  • 정적 의존관계 (Static Dependency)
    • 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미함.
    • 예: `ObjectPrinter` 클래스는 `Object` 에 의존함.
  • 동적 의존관계 (Dynamic Dependency)
    • 프로그램 실행 시 결정되는 의존관계. (런타임에 확인할 수 있는 의존관계)
    • 예: `ObjectPrinter.print(Object obj)` 에 전달되는 객체(`Car`, `Dog` 등)는 런타임에 결정됨.
      호출할 때 어떤 객체가 전달될지는 실행해봐야 알 수 있음.

 

 

7. equals() - 1. 동일성과 동등성

동일성과 동등성의 차이

  • 동일성 (Identity): `==` 연산자를 사용하여 두 객체가 같은 메모리 주소(참조값)를 가지는지 확인한다.
  • 동등성 (Equality): `equals()` 메서드를 사용하여 두 객체가 논리적으로 같은지를 비교한다.

 

동일성과 동등성의 개념

비교 기준 동일성 (Identity) 동등성 (Equality)
비교 방법 == 연산자 equals() 메서드
비교 대상 객체의 메모리 주소(참조값) 객체의 내부 값(논리적 동일성)
예시 user1 == user2 user1.equals(user2)

 

예제: 동일성과 동등성 비교

User a = new User("id-100"); // 참조값 x001
User b = new User("id-100"); // 참조값 x002
  • `a == b` → false (서로 다른 객체이므로 동일성이 없음)
  • `a.equals(b)` → 동등성을 정의하지 않으면 기본적으로 false

 

예제 코드: 기본 equals() 동작

 

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
  • `identity = false`: 객체의 참조값이 다름.
  • `equality = false`: `equals()` 를 오버라이딩하지 않았으므로 `Object` 의 `equals()` 가 호출되어 `==` 비교를 수행.

 

 

8. equals() - 2. 구현

equals() 메서드 기본 동작

  • Object의 기본 equals()는 == 연산을 수행한다.
public boolean equals(Object obj) {
    return (this == obj);
}

즉, 기본적으로 equals()는 동일성 비교를 수행하며, 동등성 비교를 하려면 오버라이딩해야 한다.

 

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); // String 클래스의 equals 메서드
    }
    
}
  • `equals()` 를 오버라이딩하여 `id` 값이 같으면 같은 객체로 판단하도록 구현했다.
  • `equals()` 는 `Object` 타입을 매개변수로 사용한다. 따라서 객체의 특정 값을 사용하려면 다운캐스팅이 필요하다.
  • `id.equals(user.id)` 에서 `id` 는 String 타입이므로 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

 

  • identity = false: 참조값이 다름.
  • equality = true: id 값이 같으므로 논리적으로 같은 객체로 판단.

 

Object 클래스의 equals() vs String 클래스의 equals()

• `Object` 클래스의 기본 `equals` 메서드는 참조값을 비교한다. 즉, 두 객체가 같은 메모리 위치를 가리키는지 확인한다. 두 객체가 같은 객체인지 확인하는 방식이다.
• `String` 클래스의 `equals` 메서드는 내용을 비교한다. 즉, 두 문자열이 같은 문자들을 포함하고 있는지 확인한다.

 

정확한 equals() 구현 (IDE 자동 생성)

  • `equals()` 는 단순 비교 외에도 몇 가지 규칙을 지켜야 한다.
  • 대부분의 IDE(IntelliJ, Eclipse)는 `equals()` 를 자동 생성할 수 있다.
  • 단축키: ⌘N (macOS) / Alt+Insert or Alt+Windows (Windows/Linux) → `equals() and hasCode()`
@Override
public boolean equals(Object o) {
    // 1. o가 null인지, 두 객체가 같은 클래스인지 확인
    if (o == null || getClass() != o.getClass()) return false;

    // 2. o를 UserV2 타입으로 캐스팅
    UserV2 userV2 = (UserV2) o;

    // 3. id 값이 같은지 비교
    return Objects.equals(id, userV2.id);
}

 

 

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

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

 

실무에서는 대부분 IDE가 만들어주는 `equals()` 를 사용하므로, 이 규칙을 외우기 보다는 대략 이렇구나 정도로 한번 읽어보고 넘어가면 충분하다.

반응형