공공부하자개발 · 영어 학습 노트
자바
중급객체지향과 코어 라이브러리0/9 완료
  • 01상속, 다형성, 오버라이딩
  • 02추상 클래스 vs 인터페이스
  • 03예외 처리
  • 04java.lang 심화
  • 05컬렉션 프레임워크 딥다이브
  • 06메서드 활용 패턴 (중급)
  • 07Object 메서드와 비교
  • 08java.time 실무 날짜 계산
  • 09HTTP 와 JSON 기초
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 중급 › 07 / 9

Object 메서드와 비교

equals, hashCode, toString, Comparable
섹션 7진행 0 / 9
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 == 와 equals: 같은 객체인가, 같은 내용인가

비교 뜻 기본 동작
a == b 두 참조가 같은 객체를 가리키는가 재정의 불가
a.equals(b) 두 객체가 같은 내용인가 Object.equals 는 == 와 동일. 클래스가 직접 정의해야 의미가 생긴다

String, Integer, LocalDate, BigDecimal, 모든 컬렉션은 equals 를 내용 기준으로 정의해 두었습니다. 그래서 "a".equals("a") 는 true 입니다. 우리가 만든 클래스는 정의하기 전까지 == 입니다.

동등성 기준을 무엇으로 할지가 설계 결정입니다. 회원은 id 가 같으면 같은 회원이고(이름은 개명될 수 있음), 좌표는 x·y 가 모두 같아야 같은 점이며, 금액은 값이 같으면 같습니다. "이 객체를 무엇으로 식별하는가" 를 정하고 그 필드만 비교합니다. 이 레슨의 Member 는 id 만, Point 는 x·y 를, record Order 는 모든 필드를 씁니다.

2.2 equals 의 규약과 관용적 구현

java
@Override
public boolean equals(Object o) {
    if (this == o) return true;                          // ① 자기 자신이면 바로 true (성능 + 재귀 방지)
    if (!(o instanceof Member other)) return false;      // ② null 이거나 다른 타입이면 false. instanceof 는 null 에 false 를 준다
    return id == other.id;                               // ③ 식별 필드만 비교. 참조 타입은 Objects.equals(a, b) 로 null 안전하게
}

매개변수 타입이 Object 여야 합니다. equals(Member o) 로 쓰면 오버라이드가 아니라 오버로드가 되어 컬렉션은 여전히 Object.equals 를 부릅니다. @Override 를 붙이면 이 실수를 컴파일러가 잡습니다.

규약은 반사성(자기 자신과 같다), 대칭성(a=b 면 b=a), 추이성(a=b, b=c 면 a=c), 일관성(필드가 안 바뀌면 결과도 안 바뀜), null 과는 항상 false 입니다. 관용적 구현을 따르면 자동으로 만족하고, 상속 관계에서 하위 클래스가 필드를 추가하며 equals 를 또 정의할 때만 대칭성이 깨질 수 있습니다.

그런 경우 getClass() != o.getClass() 비교로 "정확히 같은 클래스" 만 같다고 보는 선택지가 있습니다.

2.3 hashCode: 같은 객체는 같은 해시를 내야 한다

HashMap 은 키의 hashCode() 로 버킷 번호를 먼저 정하고, 그 버킷 안에서만 equals 로 찾습니다. 그래서 규약이 이렇습니다.

a.equals(b) 가 true 면 a.hashCode() == b.hashCode() 여야 한다. (역은 성립하지 않아도 된다: 해시가 같아도 다른 객체일 수 있다 = 충돌)

equals 만 만들고 hashCode 를 안 만들면, 내용이 같은 두 객체가 다른 버킷에 들어갑니다. HashSet 은 둘 다 받아 중복이 생기고, map.get(new Point(1,2)) 는 엉뚱한 버킷을 뒤져 null 을 돌려줍니다. 예제 2 가 이것을 그대로 보여줍니다. List 는 해시를 안 쓰니 멀쩡해서 더 발견하기 어렵습니다.

구현 규칙은 하나입니다. equals 에 쓴 필드로만 계산한다. 필드 하나면 Long.hashCode(id), 여럿이면 Objects.hash(a, b, c). equals 에 없는 필드를 해시에 넣으면 "같은데 해시가 다른" 객체가 생깁니다. 해시 충돌은 성능만 떨어뜨리지 정확성은 해치지 않으므로, 완벽한 분산보다 규약 준수가 우선입니다.

2.4 해시 컬렉션의 키는 바꾸지 않는다

객체를 HashSet 에 넣은 뒤 해시에 쓰인 필드를 바꾸면 그 객체는 원래 버킷에 남아 있는데 새 해시는 다른 버킷을 가리키므로 contains 가 false 가 되고 remove 도 안 됩니다. 컬렉션 레슨의 실수 2 입니다. 해법은 둘 중 하나입니다. 키를 불변 필드(id)로만 식별하거나(이 레슨의 Member 는 이름을 바꿔도 안전), 키 클래스 자체를 불변으로 만듭니다(record).

2.5 record: 세 메서드를 자동으로

java
public record Order(long no, String customer, BigDecimal amount, LocalDate date) { }
// 자동 생성: 모든 필드 기준 equals·hashCode, "Order[no=1, customer=kim, ...]" 형태 toString, 접근자 no(), customer() ...

"모든 필드가 식별자인 불변 값" 이면 record 가 정답입니다. DTO, 좌표, 금액과 통화, 조회 결과 행이 여기 해당합니다. 반대로 Member 처럼 일부 필드만 식별자이고 나머지는 바뀌는 엔티티는 일반 클래스로 두고 직접 정의합니다.

record 의 자동 equals 는 필드의 equals 를 그대로 쓰므로 BigDecimal 필드는 1000 과 1000.0 을 다르게 봅니다(예제 3). 금액이 식별에 들어가면 스케일을 통일하거나 compareTo 기준의 별도 메서드를 두어야 합니다.

2.6 toString: 로그와 디버거의 얼굴

기본 toString 은 클래스명@해시코드16진수 라 정보가 없습니다. 오버라이드하면 문자열 결합, printf, 로그 프레임워크, 디버거 변수 창, 컬렉션 출력([Member{...}, ...]) 전부에 반영됩니다. 규칙은 식별에 필요한 필드를 넣고 민감 정보(비밀번호, 주민번호, 카드번호)는 빼는 것입니다.

실무에서 toString 에 비밀번호가 들어가 로그 파일로 유출되는 사고가 실제로 납니다. record 는 모든 필드를 넣으므로 민감 필드가 있는 record 는 toString 을 다시 정의합니다.

2.7 Comparable 과 Comparator: 정렬 기준을 어디에 두는가

Comparable<T> Comparator<T>
위치 클래스 안 compareTo(T o) 클래스 밖, 별도 객체
개수 클래스당 하나(자연 순서) 필요한 만큼
쓰이는 곳 Collections.sort, list.sort(null), TreeSet 기본 list.sort(cmp), new TreeSet<>(cmp), sorted(cmp)
예 String(사전순), Integer, LocalDate 금액 내림차순, 이름 다음 날짜 등

compareTo 는 음수(앞), 0(같음), 양수(뒤)를 돌려줍니다. 필드를 빼서 만들지 마세요. this.value - o.value 는 큰 양수에서 큰 음수를 빼면 int 가 넘쳐 부호가 뒤집힙니다(예제 6). Integer.compare, Long.compare, a.compareTo(b) 를 씁니다.

Comparator 는 조합이 핵심입니다.

java
Comparator.comparing(Order::amount).reversed().thenComparing(Order::no)          // 금액 내림차순, 같으면 번호 오름차순
Comparator.comparing(Order::customer, Comparator.nullsLast(Comparator.naturalOrder()))   // null 고객명은 마지막으로

comparing 에 키 추출 함수를 주고 reversed, thenComparing, nullsFirst/nullsLast 를 이어 붙입니다. 같은 클래스에 정렬 기준이 여럿이면 Comparator 상수로 이름을 붙여 둡니다(Order.BY_AMOUNT_DESC).

2.8 TreeSet 과 TreeMap 은 compareTo 로 중복을 판단한다

HashSet 은 equals/hashCode, TreeSet 은 compareTo(또는 생성자에 준 Comparator) 로 "같은 원소" 를 정합니다. 금액 기준 Comparator 로 만든 TreeSet 에 금액이 같은 주문 두 건을 넣으면 하나가 조용히 사라집니다(예제 5).

BigDecimal 의 1.0 과 1.00 은 equals 가 false 인데 compareTo 가 0 이라, HashSet 에는 둘 다 들어가고 TreeSet 에는 하나만 들어갑니다.

규약은 "compareTo 가 0 이면 equals 도 true 가 되게 하라(권장)" 이지만 BigDecimal 이 이를 어기는 대표 사례입니다. 금액 비교는 항상 compareTo() == 0 으로 합니다.

핵심 원리
  • 2.1 == 와 equals: 같은 객체인가, 같은 내용인가
  • 2.2 equals 의 규약과 관용적 구현
  • 2.3 hashCode: 같은 객체는 같은 해시를 내야 한다
  • 2.4 해시 컬렉션의 키는 바꾸지 않는다
  • 2.5 record: 세 메서드를 자동으로
  • 2.6 toString: 로그와 디버거의 얼굴
  • 2.7 Comparable 과 Comparator: 정렬 기준을 어디에 두는가
  • 2.8 TreeSet 과 TreeMap 은 compareTo 로 중복을 판단한다
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제