
Java에서 모든 클래스는 자동으로 Object 클래스를 상속받는다. 즉, 우리가 어떤 클래스를 만들든지 간에 extends를 명시하지 않아도 내부적으로는 Object를 부모로 갖는 구조다.
그래서 클래스에서 Alt + Insert를 누르면 toString(), equals(), hashCode() 같은 메서드들이 기본으로 보이게 된다. 이 메서드들은 모두 Object 클래스에서 상속된 것이다.
Object 클래스는 모든 객체가 공통적으로 가져야 할 기본 동작 규약을 제공한다.
이 메서드들을 Override(재정의)함으로써, 우리가 만든 객체의 출력 방식, 비교 기준, 컬렉션 동작을 직접 제어할 수 있다.
기본적으로 Object의 toString()은 다음과 같은 정보를 반환한다.
하지만 실제 프로그램에서는 객체의 필드 정보를 사람이 읽을 수 있게 출력하고 싶기 때문에, toString()을 Override해서 필요한 필드(name, address 등)를 직접 반환하도록 구현한다.
또한, toString()을 재정의하면 System.out.println(객체)만 호출해도 자동으로 해당 내용이 출력된다.
@Override
public String toString() {
return "이름 : " + name + "\n주소 : " + address;
}
Object의 기본 equals()는 두 객체의 참조 주소(메모리 주소)가 같은지 비교한다.
즉, 내용이 같아도 다른 객체라면 false가 나온다. 하지만 String 클래스처럼 “논리적으로 같은 값인지”를 비교해야 하는 경우, equals()를 재정의해서 필드 값을 기준으로 비교한다.
형식: 객체1.equals(객체2) → “객체1이 객체2와 논리적으로 같은가?”라는 의미의 3형식(SVO) 문장으로 이해하면 된다.
hashCode()는 객체를 정수값(해시값)으로 변환한다. 이 값은 HashMap, HashSet 같은 해시 기반 컬렉션에서 객체를 빠르게 검색하기 위해 사용된다.
중요한 규칙:
따라서 equals()를 Override하면 hashCode()도 반드시 함께 Override해야 한다.
아래 클래스는 Object를 상속받는 일반적인 DTO 구조이며, toString()을 재정의하여 객체 출력 방식을 커스터마이징한 예제이다.
public class ObjectTest {
// field 선언
private String name;
private String address;
public ObjectTest() {
}
public ObjectTest(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void showInfo() {
System.out.println("이름 : " + name + "\n주소 : " + address);
}
@Override
public String toString() {
return "이름 : " + name + "\n주소 : " + address;
}
}
main 메서드에서는 객체 생성, 출력, 그리고 equals와 == 연산자의 차이를 확인한다.
public class ObjectTestMain {
public static void main(String[] args) {
ObjectTest object1 = new ObjectTest();
object1.setName("김일");
object1.setAddress("부산광역시 연제구");
ObjectTest object2 = new ObjectTest("김이", "서울특별시 종로구");
object1.showInfo();
object2.showInfo();
System.out.println(object1); // toString() 자동 호출
System.out.println("------- equals() -------");
String example = "안녕하세요";
boolean result1 = "안녕하세요" == example;
System.out.println(result1); // true (리터럴 풀)
String[] strArray = {"안녕하세요", "안녕"};
boolean result2 = "안녕하세요" == strArray[0];
boolean result3 = example == strArray[0];
System.out.println(result2); // true
System.out.println(result3); // true
ObjectTest test1 = new ObjectTest("안녕하세요", "안녕");
boolean result4 = "안녕하세요" == test1.getName();
System.out.println(result4); // true
String example2 = new String("안녕하세요");
boolean result7 = "안녕하세요" == example2;
System.out.println(result7); // false (새 객체)
boolean result71 = "안녕하세요".equals(example2);
System.out.println(result71); // true (내용 비교)
}
}
== 연산자 → 두 참조 변수가 같은 객체(같은 메모리 주소)를 가리키는지 비교
equals() 메서드 → 두 객체의 내용(논리적 동등성)이 같은지 비교
String 리터럴은 String Pool에 저장되기 때문에 같은 문자열은 동일한 주소를 참조하지만, new String()으로 생성한 객체는 항상 새로운 주소를 가지므로 == 비교는 false가 된다.
이 개념은 단순 문법이 아니라 컬렉션, DTO 비교, 중복 데이터 제거, 캐싱 구조 등에서 핵심 역할을 한다.
즉, Object 메서드 재정의는 Java OOP 설계의 기본기이자, 실무에서도 매우 중요한 부분이다.
Object 클래스는 모든 클래스의 최상위 부모이며, toString(), equals(), hashCode()는 객체 출력, 비교, 컬렉션 동작의 핵심 기준을 담당한다.
특히 equals()를 재정의할 경우 반드시 hashCode()도 함께 재정의해야 하며, 이를 통해 논리적으로 같은 객체를 정확하게 판별할 수 있다.
이 개념이 익숙해지면, 이후 컬렉션 프레임워크, DTO 비교, Spring Boot 엔티티 설계에서도 자연스럽게 연결된다.