관리 메뉴

나구리의 개발공부기록

프로젝트 환경 구성 및 java.lang 패키지 소개, Object 클래스,Object 다형성, Object 배열, toString(), Object와 OCP, equals()(동일성과 동등성, 구현) 본문

인프런 - 실전 자바 로드맵/실전 자바 - 중급 1편

프로젝트 환경 구성 및 java.lang 패키지 소개, Object 클래스,Object 다형성, Object 배열, toString(), Object와 OCP, equals()(동일성과 동등성, 구현)

소소한나구리 2025. 1. 16. 15:49

출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 프로젝트 환경 구성 및 java.lang 패키지 소개

1) 프로젝트 환경 구성

(1) 프로젝트 생성

  • Name: java-mid1
  • Location: 원하는 위치
  • Build system: IntelliJ
  • JDK: 자바 17 or 21

2) java.lang 패키지 소개

(1) java.lang

  • 자바가 기본으로 제공하는 라이브러리(클래스 모음) 중에 가장 기본이 되는 것이 java.lang 패키지임
  • lang은 Language(언어)의 줄임말로 자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지임

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

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

(3) import 생략 가능

  • java.lang 패키지는 모든 자바 애플리케이션에 자동으로 임포트되기 때문에 임포트 구문을 사용하지 않아도 됨
  • 해당 패키지의 클래스들은 너무 자주 사용하기 때문에 거의 자바 언어와 동일한 수준으로 취급하기 때문임
  • System 클래스를 예로 설명해보면 System.out.println() 출력 코드를 호출할 때 import 구문이 없이 사용할 수 있음
package lang;

public class LangMain {

    public static void main(String[] args) {

        System.out.println("Hello java!");

    }
}

2. Object 클래스

1) Object 클래스

(1) Object 클래스 예제

  • 자바에서 모든 클래스(객체)의 최상위 부모 클래스는 항상 Object 클래스임
  • 클래스에 상속받을 부모 클래스가 없으면 묵시적으로 Object 클래스를 상속 받는데, 자바가 extends Object 코드를 자동으로 넣어주기 때문에 해당 코드는 생략하는 것을 권장함
  • Child 클래스 처럼 상속 받을 부모 클래스를 명시적으로 지정하면 자바가 Object 클래스를 자동으로 상속 받지 않음
  • 즉, 상속 구조의 클래스에서 계속 위로 따라가다보면 언젠가 아무것도 상속받지 않는 클래스가 나올텐데, 그 클래스는 묵시적으로 Object 클래스를 상속받은 것임
package object;

// 묵시적 상속
public class Parent {

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

// 위 코드는 아래 코드와 동일함
public class Parent extends Object{ ... }

package object;

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

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

 

** 묵시적(Implicit) vs 명시적(Explicit)

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

(2) ObjectMain

  • toString()은 Object 클래스의 메서드로 객체의 정보를 제공함
  • Parent는 Object를 묵시적으로 상속 받았기 때문에 메모리에도 함께 생성이 됨
  • child.toString()을 호출하면 본인 타입인 Child에서 toString()을 찾는데 없기 때문에 부모로 올라가고, 부모인 Parent에도 없기때문에 Parent의 부모인 Object로 올라가서 찾음
  • Object에는 toString()이 있으므로 해당 메서드가 호출됨
package object;

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
object.Child@4f023edb
*/

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

(1) 공통 기능 제공

  • 객체의 정보를 제공하고 이 객체가 다른 객체와 같은지 비교하고 객체가 어떤 클래스로 만들어졌는지 확인하는 기능은 모든 객체에 필요한 기본 기능인데 이런 기능을 객체를 만들 때 마다 항상 새로운 메서드를 정의해서 만들어야 한다면 매우 번거로운 일이 될 것임
  • 그리고 직접 만든다 해도 개발자마다 같은 기능을 개발자마다 toString(), objectInfo() 등등 처럼 각각 메서드의 이름을 다르게 만들 수도 있어서 일관성이 없음
  • Object 클래스는 모든 객체에 필요한 공통 기능을 제공하고 최상위 부모 클래스에 위치하기 때문에 모든 객체는 이 기능을 편리하게 제공(상속)받아서 사용할 수 있게 됨
  • toString(): 객체의 정보를 제공
  • equals(): 객체의 같음을 비교
  • getClass(): 객체의 클래스 정보를 제공
  • 위의 주요 외에 다른 추가 기능도 있으며 개발자들은 모든 객체가 위와 같은 메서드를 지원한다고 알고 있기 때문에 프로그래밍이 단순화되고 일관성을 가지게 됨

(2) 다형성의 기본 구현

  • Object는 모든 클래스의 부모클래스이므로 모든 객체를 참조할 수 있음
  • 다형성을 지원하는 기본적인 메커니즘을 제공하기 때문에 모든 자바 객체는 Object 타입으로 처리될 수 있어 다양한 타입의 객체를 통합적으로 처리할 수 있게 해줌
  • 즉, Object는 모든 객체를 다 담을 수 있으므로 타입이 다른 객체들을 어딘가에 보관해야 한다번 Object에 보관하면 됨

3. Object 다형성

1) Object 다형성

(1) 예제 설명

  • Dog와 Car는 서로 아무런 관련이 없는 클래스지만 둘다 부모가 없으므로 Object를 자동으로 상속 받음

 

(2) Car, Dog

  • 전혀 관계가없는 Car와 Dog클래스이지만 최고조상에 Object 클래스를 상속받고 있음
package lang.object.poly;

public class Car {
    public void move() {
        System.out.println("자동차 이동");
    }
}

package lang.object.poly;

public class Dog {
    public void sound() {
        System.out.println("멍멍");
    }
}

 

(3) ObjectPolyExample

  • Object는 모든 타입의 부모이기 때문에 인스턴스 생성시 Object로 다형적 참조가 가능함
  • action(Object obj) 메서드에서 오브젝트를 매개변수를 사용하므로 어떤 타입이든지 전달할 수 있음
package lang.object.poly;

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

        // Object 타입으로 다형적 참조 가능
        Object objDog = new Dog();
        Object objCar = new Car();

        action(dog);
        action(car);
    }

    private static void action(Object obj) {
        // 컴파일 오류, Object 클래스는 sound(), move()가 없음
//        obj.sound();
//        obj.move();

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

 

(4) Object 다형성의 한계

좌) obj.sound() 호출 불가 그림 / 우) 다운캐스팅

  • 하지만 action() 메서드 안에서 obj.sound()를 호출하면 오류가 발생하는데 매개변수인 obj는 Object타입인데 Object에는 sound()가 없기 때문임
  • obj.sound()메서드를 호출하면 Object에서 sound()를 찾는데, Object에서 없으면 조상 클래스에서 메서드를 찾아야 하지만 Object는 최고 조상이므로 더 찾을 곳이 없어서 오류가 발생함
  • 자바 메커니즘상 아래로는 찾지 않고 유일하게 아래로 탐색하는 방법은 메서드 오버라이딩임
  • sound()를 호출하려면 instanceof로 인스턴스를 확인하고 안전하게 다운캐스팅을 한 뒤 메서드에 접근해야함

(5) Object를 활용한 다형성의 한계

  • Object는 모든 객체를 대상으로 다형적 참조할 수 있지만 Object를 통해 전달 받은 객체를 호출하려면 각 객체에 맞는 다운캐스팅 과정이 필요함
  • 즉, Object는 모든 객체의 부모이므로 모든 객체를 담을 수 있지만 Object가 세상의 모든 메서드를 알고있지 않기 때문에 다시 원래의 타입으로 형변환을 해주어야 함
  • 다형성을 제대로 활용하려면 자바 기본편에서 배운 것 처럼 다형적 참조 + 메서드 오버라이딩을 함께 사용해야 함
  • 그러나 Object는 모든 객체의 부모이므로 모든 객체를 대상으로 다형적 참조를 할 수는 있지만 각 객체가 가진 고유한 기능(메서드)을 호출하려면 다운 캐스팅을 해야함
  • 오버라이딩을 하고싶어도 Object가 가진 메서드는 자식 클래스에서 오버라이딩이 가능한데, Object 자체가 가지지 않은 메서드는 애초에 오버라이딩을 할 수도 없음
  • Object를 언제 활용하면 좋은지는 강의에서 하나씩 배움

4. Object 배열

1) Object 배열

(1) ObjectPolyExample2

  • Object는 모든 타입의 객체를 담을 수 있기 때문에 Object[]을 만들면 세상의 모든 객체를 담을 수 있는 배열을 만들 수 있음
  • Object도 인스턴스를 생성할 수 있음
  • size(Object[] objects)로 Object[] 배열을 매개변수로하여 다양한 타입의 객체들을 메서드에 전달할 수 있음

package lang.object.poly;

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
*/

 

(2) size() 메서드 설명

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

(3) Object와 같은 개념이 없으면?

  • 모든 객체를 받을 수 있는 메서드를 만들 수 없고, 모든 객체를 저장할 수 있는 배열을 만들 수 없어 전반적으로 공통으로 사용할 메서드나 배열을 만들 수 없음
  • 물론 XxxObject와 처럼 직접 클래스를 만들고 애플리케이션의 모든 클래스에 직접 정의한 XxxObject를 상속 받으면 되긴 하지만 매우 불편할 것임
  • 그리고 나를 프로젝트를 넘어서 전세계 모든 개발자가 비슷한 클래스를 만들 것이고 서로 호환되지 않는 수많은 XxxObject 클래스들이 코드에 존재하게 될 것임 

5. toString()

1) toString()

(1) ToStringMain1

  • Object의 toString() 메서드는 객체의 정보를 문자열 형태로 제공하여 디버깅과 로깅에 유용하게 사용됨
  • Object 클래스에 정의되어 있으므로 모든 클래스에서 사용할 수 있음
  • Object의 인스턴스를 생성하여 해당 참조 변수를 toString()으로 변환하여 출력한 것과 object를 바로 출력한 결과가 동일함
package lang.object.tostring;

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@23fc625e
java.lang.Object@23fc625e
*/

 

(2) Object.toString()

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

** 참고

  • 해시코드에 대한 정확한 내용은 이후에 별도로 다룰 예정이므로 지금은 객체의 참조값 정도로 생각하면 됨
  • 해시코드는 원래는 숫자 타입인데 toHexString()으로 hashCode를 변환하여 16진수로 변경하여 toString()메서드로 출력됨
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

 

(3) pringln()과 toString()

  • toString()의 결과를 출력한 코드와 object를 바로 println()에 직접 출력한 코드의 결과가 완전히 같은데 System.out.println() 메서드는 내부에서 toString()을 호출하기 때문임
  • println() 메서드를 타고 들어가다보면 아래와 같은 코드에서 obj.toString()을 호출하고 있는 것을 확인할 수 있음
  • 그래서 println()을 사용할 때 toString()을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있음
// println() 메서드를 타고 들어가다보면 아래와 같은 코드를 볼 수 있음
public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

2) toString() 오버라이딩

(1) 설명

  • Object의 toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지는 못함
  • 그래서 보통 toString()을 재정의(오버라이딩)해서 정보를 제공함

(2) Car, Dog

  • Car는 toString()을 재정의 하지 않고 Dog는 IDE에서 지원하는 toString() 만들기 기능을 통해 toString() 메서드를 오버라이딩하여 재정의
  • toString()를 재정의하고싶을 때에는 대부분 IDE의 도움을 받고 만약 원하는 형식이 있으면 직접 오버라이딩하면됨
package lang.object.tostring;

public class Car {
    private String carName;

    public Car(String carName) {
        this.carName = carName;
    }
}

package lang.object.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() {
        return "Dog{" +
                "dogName='" + dogName + '\'' +
                ", age=" + age +
                '}';
    }
}

 

(3) ObjectPrinter

    • 객체 정보 출력: 이라는 문자와 매개변수로 넘어온 객체의 toString() 결과를 합해서 출력하는 기능을 제공 
package lang.object.tostring;

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

 

(4) ToStringMain2

  • Dog 클래스에서 toString()메서드를 오버라이딩 하였기 때문에 Dog 인스턴스를 생성한 변수는 toString()메서드를 호출 시 오버라이딩된 메서드가 호출되고 Car는 그대로 Object의 toString()이 호출되는 것을 확인할 수 있음
  • 직접 만든 ObjectPrinter.printer() 메서드도 동일하게 출력되는 것을 확인할 수 있음
package lang.object.tostring;

public class ToStringMain {
    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@4f023edb
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@4f023edb
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
3. Object 다형성 활용
객체 정보 출력:lang.object.tostring.Car@4f023edb
객체 정보 출력:Dog{dogName='멍멍이1', age=2}
객체 정보 출력:Dog{dogName='멍멍이2', age=5}
*/

 

(5) ObjectPrinter.print(...) 분석

좌) Car인스턴스로 호출 / 우) Dog인스턴스로 호출

 

  • print(...)의 인수로 car(Car 타입 참조변수)가 전달되면 메서드 내부에서 obj.toString()을 호출하고, Car 인스턴스에는 toString()이 없기 때문에 조상인 Object 타입에서 toString()을 찾으고 오버라이딩 된 메서드도 있는지 찾음
  • 오버라이딩 된 메서드가 없으므로 조상인 Object의 toString()이 호출됨
  • print(...)의 인수로 dog(Dog 타입 참조변수)가 전달되면 메커니즘은 똑같이 진행하지만 Dog에는 toString()이 오버라이딩 되어있으므로 오버라이딩 된 toString()이 호출됨

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

  • toString()이나 hashCode()를 재정의하면 원래 Object 클래스에서 제공하고 있던 기능이였던 객체의 참조값을 출력할 수 없는데, 아래처럼 코드를 작성하면 객체의 참조값을 출력할 수 있음
  • System.identityHashCode(객체): 숫자로 실제 참조값을 출력
  • Integer.toHexString(숫자): 숫자를 16진수로 변경
System.out.println(Integer.toHexString(System.identityHashCode(car)));

// 실행결과: 4f023edb

6. Object와 OCP

1) Object와 OCP

(1) BadObjectPrinter

  • 만약 Object가 없고 Object가 제공하는 toString()이 없다면 공통 부모가 없기 때문에 서로 아무 관계가 없는 객체의 정보를 출력하기가 어려울 것임
  • 아래의 코드처럼 각각의 클래스 전용의 메서드를 작성해야 됨
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);
    }
}

 

(2) 구체적인 것에 의존

  • BadObjectPrinter는 구체적인 타입인 Car, Dog를 사용하므로 이후에 출력해야할 구체적인 클래스가 10개로 늘어나면 이에 맞추어 메서드도 10개로 계속 늘어나야 함
  • 이렇게 BadObjectPrinter클래스가 구체적인 특정 클래스인 Dog, Car를 사용하는 것을 BadObjectPrinter 클래스가 Car, Dog에 의존한다고 표현함
  • 그러나 자바는 객체의 정보를 사용할 때 다형적 참조 문제를 해결해줄 Object 클래스와 메서드 오버라이딩 문제를 해결해줄 Object.toString()메서드가 존재하고 있음
  • 물론 직업 Object와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있지만 번거로워짐

(3) 추상적인 것에 의존

  • 앞에서 만든 ObjectPrinter 클래스는 Car, Dog 처럼 구체적인 클래스를 사용하는 것이 아니라 추상적인 Object 클래스를 사용함
  • 즉, ObjectParinter클래스가 Object 클래스에 의존한다고 표현하며 구체적인 것에 의존지 않고 추상적인 것에 의존함
  • ObjectPrinter와 Object를 사용하는 구조는 다형적 참조와 메서드 오버라이딩을 적절하게 사용하여 다형성을 잘 활용하고 있는 코드임
  • 다형적 참조: print(Object obj) 메서드의 매개변수에 Object 타입을 사용하여 다형적 참조를 사용함
  • 메서드 오버라이딩: Object는 모든 클래스의 부모이므로 구체적인 클래스는 Object가 가지고 있는 toString() 메서드를 오버라이딩할 수 있기 때문에 추상적인 Object타입에 의존하면서 런타임시에 각 인스턴스의 toString()을 호출할 수 있음

** 추상적

  • 여기서 말하는 추상적이라는 뜻은 추상 클래스나 인터페이스만 뜻하는 것은 아님
  • Animal과 Dog, Cat의 관계를 예를 들면 Animal 처럼 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고 dog, Cat과 같이 하위 타입으로 갈 수록 구체적이게 됨

(3) OCP 원칙

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

(4) System.out.println()

  • ObjectPrinter는 System.out.println()의 작동 방식을 설명하기 위해 만든것인데 System.out.println()메서드도 Object 매개 변수를 사용하고 내부에서 toString()을 호출하며 이 출력 코드를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있음
  • 자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용하고 있음
  • 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 오버라이딩해서 사용할 수 있도록 설계되어 있음

** 참고 - 정적 의존관계 VS 동적 의존관계

  • 정적 의존관계: 컴파일 시간에 결정되며 주로 클래스간의 관계를 의미함
    - 앞서 의존 관계 그림이 정적 의존관계이며 프로그램을 실행하지 않고 클래스 내에서 사용하는 타입들로만 보면 쉽게 의존관계를 파악할 수 있음
  • 동적 의존관계: 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계
    - ObjectPrinter.print(Object obj)에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있음
  • 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻함
    - ex) ObjectPrinter는 Object에 의존

7. equals()

1) 동일성과 동등성

(1) 두 객체가 같다는 표현의 2가지

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

(2) 단어 정리

  • "동일"은 완전히 같음을 의미하는 반면 "동등"은 같은 가치나 수준을 의미하긴 하지만 그 형태나 외관 등이 완전히 같지는 않을 수 있음
  • 즉, 동일성은 물리적으로 같은 메모리에 있는 객체 인스턴스인지 참조값을 확인하는 것이고 동등성은 논리적으로 같은지를 확인하는 것임
  • 동일성은 자바 머신 기준(메모리의 참조)이므로 물리적인 반면 동등성은 보통 사람이 생각하는 논리적인 기준에 맞추어서 비교함
  • 아래처럼 동일한 회원번호를 가진 2개의 회원 객체가 있을 때 물리적으로는 다른 메모리에 있기 때문에 서로 다른 객체이지만 회원 번호를 기준으로 생각해보면 논리적으로는 같은 회원으로 볼 수 있으므로 동일성은 다르지만 동등성은 같음
User a = new User("id-100") //참조 x001
User b = new User("id-100") //참조 x002

 

(3) UserV1, EqualsMainV1

  • UserV1 클래스의 인스턴스를 동일한 값으로 2개 생성하여 동일성과 동등성을 비교
  • 실행 결과를 보면 동등성, 동일성을 비교한 결과값이 모두 false가 나옴
package lang.object.equals;

public class UserV1 {
    private String id;

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

package lang.object.equals;

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
*/

 

(4) 설명

  • 동일성 비교는 당연히 user1과 user2는 서로 다른 참조값을 가지고 있기 때문에 비교 결과는 false가 나옴
  • 동등성 비교를 하는 equals() 메서드를 들어가서 살펴보면 Object가 기본으로 제공하는 equals()는 == 비교를 하고 있기 때문에 당연히 false가 결과로 나온 것임
  • 동등성이라는 개념은 어떤 클래스는 주민번호를 기반으로, 어떤 클래스는 고객 연락처를 기반으로 각각의 클래스마다 다르게 동등성을 처리할 수 있는데 Object 클래스가 이것을 모두 알 수는 없음
  • 즉, 논리적인 동등성 비교를 사용하고 싶다면 equals() 메서드를 오버라이딩을 하여 사용해야 하며 그렇지 않으면 Object의 equals() 메서드는 동일성 비교를 기본으로 제공함

2) 구현

(1) UserV2

  • Object의 equals()메서드를 재정의하여 UserV2의 동등성 비교를 Id로 비교하도록 함
  • equals() 메서드는 Object 타입을 매개변수로 사용하므로 객체의 특정 값을 사용하려면 다운캐스팅이 필요함
  • 현재 인스턴스의 id 문자열과 비교 대상으로 넘어온 객체의 id 문자열을 비교 (문자열 비교는 equals()를 사용해야함)
package lang.object.equals;

import java.util.Objects;

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

 

(2) EqualsMainV2

  • 동일성 비교는 객체의 참조가 다르므로 당연히 false가 반환됨
  • 그러나 동등성 비교는 UserV2에서 비교대상인 객체가 같은 id를 가지고 있으면 같은 User라고 판단하도록 equals()메서드를 오버라이딩한 덕분에 true로 반환됨
package lang.object.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
*/

 

(3) 정확한 equals() 구현

  • 앞서 Userv2에서 구현한 equals() 메서드는 이해를 돕기위해 매우 같단히 만든 버전이고 실제로 정확하게 동작하려면 아래와 같이 구현해야하며, 정확한 equals() 메서드를 구현하는 것은 생각보다 쉽지 않음
  • 대부분의 IDE는 정확한 equals() 코드를 자동으로 만들어주는 기능을 지원함
@Override
public boolean equals(Object o) {
    if (o == null || getClass() != o.getClass()) return false;
    UserV2 userV2 = (UserV2) o;
    return Objects.equals(id, userV2.id);
}

 

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

  • 실무에서는 대부분 IDE가 만들어주는 equals()를 사용하므로 이 규칙을 외우기 보다는 한번 읽어보고 넘어가면 됨
  • 반사성(Reflexivity): 객체는 자기 자신과 동등해야 함
    - ex) x.equals(x)는 항상 true
  • 대칭성(Symmetry): 두 객체가 서로에 대해 동일하다고 판단하면 이는 양방향으로 동일해야 함
    - ex) x.equals(y)가 true이면 y.equals(x)도 true
  • 추이성(Transitivity): 만약 한 객체가 두 번째 객체와 동일하고 두 번째 객체가 세 번째 객체와 동일하면 첫 번째 객체는 세 번째 객체와도 동일해야 함
  • 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한 equals() 메소드는 항상 동일한 값을 반환해야 함
  • null에 대한 비교: 모든 객체는 null과 비교했을 때 false를 반환해야 함

** 참고

  • 동등성 비교가 항상 필요한 것은 아니며 동등성 비교가 꼭 필요한 경우에만 equals()를 재정의하면 됨
  • equals()와 hashCode()는 보통 함께 사용되는데 이 부분은 뒤에 컬렉션 프레임워크에서 자세히 설명함(IDE에서 지원하는 기능이 보통 equals()와 hashCode()를 같이 구현해줌)

8. 문제와 풀이

1) toString(), equals() 구현하기

(1) 문제 설명

  • 다음 코드와 결과를 참고하여 Rectangle 클래스를 생성
  • Rectangle 클래스에 IDE 기능을 사용해 toString()과 equals() 메서드를 실행 결과에 맞도록 재정의
  • rect1과 rect2는 넓이(width)와 높이(height)를 가지며 넓이와 높이가 모두 같다면 동등성 비교에 성공해야 함
더보기
package lang.object.test;

public class RectangleMain {
    public static void main(String[] args) {
        Rectangle rect1 = new Rectangle(100, 20);
        Rectangle rect2 = new Rectangle(100, 20);
        System.out.println(rect1);
        System.out.println(rect2);
        System.out.println(rect1 == rect2);
        System.out.println(rect1.equals(rect2));
    }
}

 

실행 결과

Rectangle{width=100, height=20}

Rectangle{width=100, height=20}

false

true

 

(2) 정답

더보기
package lang.object.test;

import java.util.Objects;

public class Rectangle {

    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Rectangle rectangle = (Rectangle) o;
        return width == rectangle.width && height == rectangle.height;
    }

    @Override
    public int hashCode() {
        return Objects.hash(width, height);
    }

    @Override
    public String toString() {
        return "Rectangle{" +
                "width=" + width +
                ", height=" + height +
                '}';
    }
}

 

실행결과는 동일

2) 정리

(1) Object의 나머지 메서드

  • clone(): 객체를 복사할 때 사용, 거의 사용하지 않음
  • hashCode(): equals()와 hashCode()는 종종 함께 사용됨, 컬렉션 프레임워크에서 자세히 설명
  • getClass(): 뒤에서 Class에서 설명
  • notify(), notifyAll(), wait(): 멀티쓰레드용 메서드로 멀티쓰레드에서 다룸