자바에서 만드는 모든 클래스는 결국
Object클래스를 부모로 가진다.
다른 클래스를 직접 상속하지 않아도 컴파일 과정에서extends Object가 붙는다고 이해하면 된다.
그래서Object는 단순히 가장 위에 있는 클래스가 아니라, 모든 객체가 공통으로 가지는 기능의 출발점이다.
객체를 공부할 때Object를 먼저 보는 이유는 분명하다.
객체를 같은 것으로 볼지 판단할 때는equals()가 필요하고, 해시 기반 컬렉션에서 객체를 빠르게 찾을 때는hashCode()가 필요하며, 객체를 출력했을 때 어떤 문자열로 보일지는toString()이 결정한다.
이 메서드들은 모두Object에서 시작한다.
Object를 이해한다는 것은 객체를 비교하고, 저장하고, 출력하는 기본 원리를 이해하는 것과 같다.
이번 글에서는Object의 모든 메서드를 넓게 다루지 않는다.
객체 비교와 출력에서 가장 자주 만나는equals(),hashCode(),toString()을 중심으로 정리한다.
핵심 특징
Object클래스의 핵심은 모든 객체가 공통으로 사용할 수 있는 기본 메서드를 제공한다는 점이다.
초보자는 이 메서드들을 처음부터 외우려고 하기보다 “객체를 어떻게 비교하고 출력하는가” 흐름으로 이해하는 것이 좋다.
Object는 모든 클래스의 공통 부모다.- 다른 클래스를 상속하지 않아도 결국
Object를 상속받는다.equals()는 두 객체를 같은 것으로 볼 기준을 정한다.hashCode()는 해시 기반 컬렉션에서 객체를 빠르게 찾기 위한 정수값을 만든다.toString()은 객체를 출력했을 때 어떤 문자열로 보일지 정한다.- 직접 만든 객체를 값 기준으로 비교하려면
equals()와hashCode()를 함께 재정의해야 한다.정리하면
Object는 “모든 객체가 기본으로 물려받는 공통 기능 모음”이다.
그중 객체 비교와 컬렉션 저장 흐름에서는equals()와hashCode()가 특히 중요하고, 출력 확인 흐름에서는toString()이 중요하다.
Object에는 여러 메서드가 있지만, 처음에는 세 가지를 먼저 잡으면 된다.
바로equals(),hashCode(),toString()이다.
이 세 메서드는 서로 따로 떨어진 기능처럼 보이지만 실제로는 자주 함께 연결된다.
equals()
equals()는 두 객체를 같은 것으로 볼지 판단하는 메서드다.
여기서 “같다”는 말은 무조건 안에 든 값이 같다는 뜻이 아니다.
기본 상태의equals()는 내용 비교가 아니라, 두 참조변수가 같은 객체를 가리키는지 확인하는 방식이다.
Object의 기본equals()구조를 단순하게 보면 아래와 같다.
이 코드는 실제 실행용 파일이 아니라,Object안의 기본 비교 구조를 초보자 기준으로 단순화한 조각 코드다.// ObjectEqualsStructure.java public boolean equals(Object obj) { // this와 obj가 같은 객체를 가리키는지 확인함 return (this == obj); }기본
equals()는 내부적으로this == obj를 비교한다.
즉, 객체 안의 값이 같은지 보는 것이 아니라 참조값이 같은지, 쉽게 말해 같은 객체를 가리키는지 본다.
예를 들어 사람 두 명이 같은 이름표를 들고 있다고 해서 같은 사람은 아니다.
이름표의 글자는 같을 수 있지만 실제 사람은 다를 수 있다.
객체도 마찬가지다.
객체 안의 값이 같아 보여도new로 따로 만들었다면 서로 다른 객체다.
hashCode()
hashCode()는 객체를 정수값으로 바꿔 돌려주는 메서드다.
이 값은HashSet,HashMap처럼 해시 기반 컬렉션에서 객체를 빠르게 찾기 위한 기준으로 먼저 사용된다.
다만 이 정수값을 실제 메모리 주소 그 자체라고 외우면 안 된다.
초보자 단계에서는 객체를 빠르게 분류하고 찾기 위해 사용하는 번호라고 이해하면 충분하다.
쉽게 말하면hashCode()는 물건을 창고에 넣기 전에 붙이는 분류 번호와 비슷하다.
창고에 물건이 아주 많을 때 물건 하나하나를 처음부터 끝까지 다 뒤지면 오래 걸린다.
그래서 먼저 분류 번호로 비슷한 위치를 찾고, 그다음 진짜 같은 물건인지 확인한다.
이때 최종 확인을 맡는 것이equals()다.
그래서 해시 기반 컬렉션은 보통 아래 흐름으로 객체를 판단한다고 보면 된다.
- 먼저
hashCode()로 비교 후보 위치를 찾는다.- 후보가 있으면
equals()로 정말 같은 객체인지 확인한다.- 새 객체라고 판단되면 저장하고, 이미 같은 객체라고 판단되면 저장하지 않는다.
값 기준으로 같은 객체라고 판단하고 싶다면
equals()와hashCode()의 기준을 반드시 맞춰야 한다.
equals()는 이름과 나이를 비교하는데hashCode()는 전혀 다른 기준으로 만들면, 해시 기반 컬렉션에서 중복 판단이 어긋날 수 있다.
toString()
toString()은 객체를 문자열처럼 보여줄 때 어떤 형태로 출력할지 정하는 메서드다.
객체를System.out.println()에 넣으면 내부적으로 그 객체의toString()결과가 출력된다.
Object의 기본toString()구조를 단순하게 보면 아래와 같다.
이 코드도 실제 실행용 파일이 아니라, 기본 출력 문자열이 어떻게 만들어지는지 보여주는 조각 코드다.// ObjectToStringStructure.java public String toString() { // 클래스 이름과 16진수 해시코드를 합쳐 문자열로 돌려줌 return getClass().getName() + "@" + Integer.toHexString(hashCode()); }그래서
toString()을 재정의하지 않은 객체를 출력하면클래스이름@해시값처럼 보인다.
예를 들어Card@452b3a41같은 형태다.
이 출력은 객체 안의 값을 보여주는 것이 아니라, 기본 형태의 객체 식별 문자열에 가깝다.
반대로toString()을 재정의하면 객체 안에 들어 있는 값을 사람이 읽기 쉬운 문자열로 보여줄 수 있다.
그래서 출력 결과를 확인하거나 디버깅할 때 유용하다.
예제별 역할
이 글의 예제는 단순히 코드를 많이 보여주기 위한 것이 아니다.
객체 비교가 어떤 단계로 확장되는지 순서대로 확인하기 위한 예제다.
ObjectTest1: 같은 객체와 같은 내용의 차이를 확인한다.EqualsTest1: 문자열 비교에서equals()가 왜 필요한지 확인한다.ObjectTest2: 직접 만든 객체에서equals()만 재정의했을 때의 한계를 확인한다.ObjectTest3:equals()와hashCode()를 함께 재정의해HashSet중복 판단을 완성한다.ToStringExample:toString()을 재정의하면 객체 출력이 어떻게 달라지는지 확인한다.HashCodeExample: 클래스마다hashCode()기준이 다를 수 있음을 보충한다.이 흐름을 따라가면 “객체 안의 값이 같은데 왜 다르다고 나오지?”에서 시작해서 “직접 만든 객체를 값 기준으로 비교하려면 무엇을 고쳐야 하지?”까지 자연스럽게 이어진다.
객체 비교에서 가장 먼저 잡아야 하는 개념은 같은 내용과 같은 객체가 다르다는 점이다.
값이 같아 보여도new를 두 번 사용하면 서로 다른 객체가 만들어진다.
반대로 새 객체를 만들지 않고 기존 객체를 다른 변수에 대입하면 두 변수는 같은 객체를 함께 가리킨다.
쉽게 말하면 같은 정보가 적힌 회원 가입 신청서가 두 장 있을 수 있다.
두 신청서에 적힌 내용은 같지만 종이는 두 장이다.
이때 “내용이 같은가”와 “종이 자체가 같은가”는 다른 질문이다.
객체 비교도 이와 비슷하다.
예제: ObjectTest1
// ObjectTest1.java package day10; class Member1 { private int id; private String name; private String password; Member1(int id, String name, String password) { // 회원 정보를 객체 안에 저장함 this.id = id; this.name = name; this.password = password; } @Override public String toString() { // 객체를 출력할 때 읽기 쉬운 형태로 보여줌 return "Member1 [id=" + id + ", name=" + name + ", password=" + password + "]"; } } public class ObjectTest1 { public static void main(String[] args) { Member1 obj1 = new Member1(10, "자바", "duke"); Member1 obj2 = new Member1(10, "자바", "duke"); // 값은 같아 보이지만 new를 두 번 사용했으므로 서로 다른 객체임 System.out.println(obj1.equals(obj2)); System.out.println(obj1 == obj2); Member1 obj3 = obj2; // 새 객체를 만든 것이 아니라 obj2가 가리키는 객체를 obj3도 함께 가리킴 System.out.println(obj3.equals(obj2)); System.out.println(obj3 == obj2); } }// 출력결과 // false // false // true // true
obj1과obj2는 안에 들어 있는 값이 같아 보인다.
둘 다id는10,name은"자바",password는"duke"다.
하지만new를 두 번 사용했기 때문에 메모리에는 서로 다른 객체가 만들어진다.
그래서obj1 == obj2는false다.
==는 두 참조변수가 같은 객체를 가리키는지 묻기 때문이다.
그리고Member1은 아직equals()를 직접 재정의하지 않았으므로obj1.equals(obj2)도 기본equals()처럼 같은 객체인지 확인한다.
결과는false다.
반대로Member1 obj3 = obj2;는 새 객체를 만든 것이 아니다.
obj2가 가리키던 객체를obj3도 같이 가리키게 만든 것이다.
그래서obj3.equals(obj2)와obj3 == obj2는 둘 다true가 된다.
여기서Member1안에toString()이 들어 있지만, 이 예제의 핵심은toString()이 아니다.
toString()은 객체를 출력할 때 어떤 문자열로 보여줄지 정하는 메서드이고, 뒤에서 따로 다시 확인한다.
지금은 먼저equals()와==가 같은 객체를 기준으로 판단한다는 점에 집중하면 된다.
이 예제에서 꼭 잡아야 하는 기준은 아래와 같다.
==는 두 변수가 같은 객체를 가리키는지 확인한다.- 기본
equals()도 내용 비교가 아니라 같은 객체인지 확인하는 쪽이다.- 직접 만든 객체에서 내용 비교를 원하면
equals()를 재정의해야 한다.
equals()라는 이름만 보고 무조건 내용 비교라고 생각하면 안 된다.
기본 상태에서는==와 비슷하게 같은 객체인지 확인한다고 먼저 이해하면 된다.
문자열을 함께 보는 이유는 내용 비교가 가장 분명하게 드러나기 때문이다.
String은Object의 기본equals()를 그대로 쓰지 않는다.
문자열 내용을 비교하도록equals()가 이미 재정의되어 있다.
그래서String에서는 서로 다른 객체로 만들어져도 글자 내용이 같으면equals()결과가true가 나온다.
이 부분은 직접 만든 객체와 비교해서 봐야 이해가 쉽다.
예제: EqualsTest1
// EqualsTest1.java package day9; import java.util.Scanner; public class EqualsTest1 { public static void main(String[] args) { String s3 = new String("가나다"); String s4 = new String("가나다"); // 같은 문자열이지만 new를 두 번 사용했으므로 서로 다른 객체임 System.out.println(s3 == s4); System.out.println(s3.equals(s4)); // String은 equals()가 내용 비교로 재정의되어 있음 Scanner scan = new Scanner(System.in); System.out.print("문자열 입력 : "); String inputStr = scan.next(); if (s3.equals(inputStr)) { // 입력한 문자열 내용이 같으면 실행됨 System.out.println("\"가나다\"를 입력했군요..."); } else { System.out.println("\"가나다\"가 아니군요..."); } scan.close(); } }// 출력결과 // false // true // 문자열 입력 : 가나다 // "가나다"를 입력했군요...
s3와s4는new를 두 번 사용해서 만들었기 때문에 서로 다른 객체다.
그래서s3 == s4는false다.
하지만 두 객체가 담고 있는 문자열 내용은"가나다"로 같다.
String은 문자열 내용을 비교하도록equals()가 재정의되어 있으므로s3.equals(s4)는true가 된다.
입력값 비교도 같은 원리다.
사용자가"가나다"를 입력했다고 해서 그 입력값이s3와 같은 객체라는 뜻은 아니다.
그래서 문자열 내용 비교는==가 아니라equals()를 사용해야 한다.
여기서 초보자가 많이 헷갈리는 질문이 나온다.
“왜Member1은 내용이 같아도false인데,String은 내용이 같으면true일까?”
답은 단순하다.
Member1은equals()를 재정의하지 않았고,String은 이미 재정의해 두었기 때문이다.
즉,equals()는 자동 내용 비교 메서드가 아니다.
클래스가 어떤 기준으로equals()를 다시 만들어 두었는지에 따라 결과가 달라진다.
직접 만든 클래스는 그냥 두면
Object의 기본equals()를 사용한다.
기본equals()는this == obj를 비교하므로 객체 안의 값이 같아도 서로 다른 객체면false가 나온다.
그래서 직접 만든 객체를 값 기준으로 비교하고 싶다면equals()안에 “어떤 값이 같으면 같은 객체로 볼 것인지”를 직접 적어야 한다.
회원 객체라면id,name,password가 모두 같을 때 같은 회원 정보로 볼 수 있다.
이 기준은 클래스마다 달라질 수 있다.
예제: ObjectTest2
// ObjectTest2.java package day10; import java.util.HashSet; class Member2 { private int id; private String name; private String password; Member2(int id, String name, String password) { // 회원 정보를 객체에 저장함 this.id = id; this.name = name; this.password = password; } @Override public boolean equals(Object o) { if (this == o) { // 완전히 같은 객체면 바로 true return true; } if (!(o instanceof Member2)) { // null이거나 Member2 타입이 아니면 비교 대상이 아님 return false; } Member2 m = (Member2) o; // 세 필드가 모두 같으면 같은 값의 객체로 판단함 return id == m.id && name.equals(m.name) && password.equals(m.password); } } public class ObjectTest2 { public static void main(String[] args) { Member2 obj1 = new Member2(10, "자바", "duke"); Member2 obj2 = new Member2(10, "자바", "duke"); Member2 obj3 = new Member2(20, "자바", "duke"); System.out.println(obj1.equals(obj2)); // 값 기준으로 같으므로 true System.out.println(obj1 == obj2); // 서로 다른 객체이므로 false System.out.println(obj1.equals(null)); // null은 비교 대상이 아니므로 false System.out.println(obj1.equals(new java.util.Date())); // 다른 타입 객체이므로 false System.out.println(obj1.equals(obj3)); // id가 다르므로 false HashSet<Member2> set = new HashSet<>(); System.out.println(set.add(obj1)); System.out.println(set.add(obj2)); System.out.println(set.add(obj3)); System.out.println(set.size()); // hashCode()를 재정의하지 않았으므로 obj1과 obj2가 함께 저장될 수 있음 } }// 출력결과 // true // false // false // false // false // true // true // true // 3이 예제에서는
equals()안에 비교 기준을 직접 넣었다.
id,name,password가 모두 같으면 같은 값의 객체로 보겠다는 뜻이다.
그래서obj1.equals(obj2)는true가 된다.
하지만obj1 == obj2는 여전히false다.
왜냐하면obj1과obj2는new를 두 번 사용해서 만든 서로 다른 객체이기 때문이다.
여기서equals()코드 흐름을 번호로 보면 더 쉽다.
this == o이면 같은 객체이므로 바로true를 반환한다.o가Member2타입이 아니면 비교할 수 없으므로false를 반환한다.- 같은 타입이면
Member2로 형변환한 뒤 필드 값을 비교한다.id,name,password가 모두 같으면true를 반환한다.이제
equals()는 값 기준 비교가 된다.
하지만 아래쪽의HashSet결과를 보면obj1,obj2,obj3가 모두 저장된다.
obj1과obj2는equals()로 보면 같은데도HashSet에서는 둘 다 들어간다.
이유는 아직hashCode()를 재정의하지 않았기 때문이다.
즉, 해시 기반 컬렉션에서는equals()만 재정의하면 부족하다.
HashSet은 중복을 허용하지 않는 컬렉션이다.
다만 저장 순서를 보장하지는 않는다.
이번 글에서는 순서보다 중복 판단 기준이 핵심이므로, 어떤 기준으로 같은 객체를 찾는지에 집중하면 된다.
HashSet은 중복인지 확인할 때 눈에 보이는 값을 바로 비교하는 것이 아니다.
먼저hashCode()로 저장 위치와 비교 후보를 찾고, 그다음equals()로 최종 확인한다.
이 흐름은 사물함 번호로 생각하면 쉽다.
사물함이 아주 많을 때 어떤 물건이 이미 있는지 확인하려면 모든 사물함을 다 열어보는 것보다 먼저 사물함 번호를 확인하는 편이 빠르다.
hashCode()는 이 사물함 번호 같은 역할을 한다.
그리고 같은 번호 쪽에서 실제 물건이 같은지 확인하는 과정이equals()다.
HashSet.add()의 반환값도 같이 알아야 한다.
- 새 객체로 판단되어 저장되면
true를 반환한다.- 이미 같은 객체가 있다고 판단되어 저장하지 않으면
false를 반환한다.그래서
add()결과를 보면HashSet이 그 객체를 새 데이터로 봤는지, 중복 데이터로 봤는지 확인할 수 있다.
hashCode()를 재정의할 때 지켜야 하는 핵심 규칙은 아래 세 가지다.
- 같은 객체에서
hashCode()를 여러 번 호출하면 같은 값이 나와야 한다.equals()가true인 두 객체는hashCode()도 같아야 한다.hashCode()가 같다고 해서 반드시equals()가true인 것은 아니다.첫 번째 규칙에는 전제가 있다.
equals()비교에 사용되는 필드 값이 바뀌지 않았을 때 같은hashCode()가 나와야 한다는 뜻이다.
프로그램을 새로 실행했을 때까지 무조건 같은 값이 나와야 한다는 뜻은 아니다.
두 번째 규칙이 가장 중요하다.
equals()에서id,name,password를 기준으로 비교한다면hashCode()도 같은 필드를 기준으로 만들어야 한다.
그래야HashSet이 두 객체를 같은 후보로 보고equals()까지 비교할 수 있다.
반대로equals()와hashCode()의 기준이 서로 다르면 값이 같아 보이는 객체가 중복으로 저장될 수 있다.
이제 앞의 문제를 해결한 완성 예제를 보자.
이번에는equals()와hashCode()를 같은 필드 기준으로 함께 재정의한다.
Objects.hash()는 여러 값을 넣으면 그 값들을 기준으로 해시코드를 만들어 주는 메서드다.
직접 복잡한 계산을 만들 필요 없이,equals()에서 비교한 필드를 그대로 넣으면 기준을 맞추기 쉽다.
예제: ObjectTest3
// ObjectTest3.java package day10; import java.util.HashSet; import java.util.Objects; class Member3 { private int id; private String name; private String password; Member3(int id, String name, String password) { // 회원 정보를 객체에 저장함 this.id = id; this.name = name; this.password = password; } @Override public boolean equals(Object o) { if (this == o) { // 완전히 같은 객체면 바로 true return true; } if (!(o instanceof Member3)) { // null이거나 Member3 타입이 아니면 false return false; } Member3 m = (Member3) o; // equals()의 비교 기준은 id, name, password임 return id == m.id && name.equals(m.name) && password.equals(m.password); } @Override public int hashCode() { // equals()에서 비교한 필드와 같은 기준으로 hashCode()를 만듦 return Objects.hash(id, name, password); } @Override public String toString() { // HashSet 출력 시 객체 내용을 읽기 쉽게 보여줌 return "(" + id + ":" + name + ":" + password + ")"; } } public class ObjectTest3 { public static void main(String[] args) { Member3 obj1 = new Member3(10, "자바", "duke"); Member3 obj2 = new Member3(10, "자바", "duke"); Member3 obj3 = new Member3(20, "자바", "duke"); HashSet<Member3> set = new HashSet<>(); System.out.println(set.add(obj1)); System.out.println(set.add(obj2)); System.out.println(set.add(obj3)); System.out.println("저장된 데이터의 개수 : " + set.size()); } }// 출력결과 // true // false // true // 저장된 데이터의 개수 : 2이번에는
obj1과obj2가 같은 값의 객체로 판단된다.
둘 다id,name,password가 같고,equals()와hashCode()가 같은 필드 기준으로 만들어졌기 때문이다.
그래서set.add(obj1)은 처음 저장이므로true다.
set.add(obj2)는 이미 같은 값의 객체가 있다고 판단되므로 저장하지 않고false다.
set.add(obj3)은id가 다르므로 새 객체로 판단되어true다.
결과적으로HashSet에 저장된 데이터 개수는2다.
이 예제의 핵심은 하나다.
직접 만든 객체를HashSet에서 값 기준으로 중복 제거하려면equals()와hashCode()를 같은 기준으로 함께 재정의해야 한다.
toString()은 객체를 출력했을 때 어떤 문자열로 보여줄지 정하는 메서드다.
객체를 그냥 출력하면 내부적으로toString()이 호출된다.
그래서 객체가null이 아니라면System.out.println(obj)와System.out.println(obj.toString())은 같은 결과처럼 이해하면 된다.
기본toString()은 객체의 중요한 값을 보여주지 않는다.
클래스이름@해시값형태에 가까운 문자열을 보여준다.
그래서 객체 안에 어떤 값이 들어 있는지 확인하기 어렵다.
직접 만든 클래스에서는 보통toString()을 재정의해서 사람이 읽기 쉬운 형태로 바꾼다.
예제: ToStringExample
// ToStringExample.java package day10; class Card { private int number; private String kind; Card(int number, String kind) { // 카드 숫자와 모양을 저장함 this.number = number; this.kind = kind; } @Override public String toString() { // 객체 안의 값을 사람이 읽기 쉬운 문자열로 돌려줌 return "Card[number=" + number + ", kind=" + kind + "]"; } } public class ToStringExample { public static void main(String[] args) { Card card = new Card(7, "HEART"); System.out.println(card); // println()은 내부적으로 card.toString() 결과를 출력함 System.out.println(card.toString()); } }// 출력결과 // Card[number=7, kind=HEART] // Card[number=7, kind=HEART]
System.out.println(card)를 실행했는데도 객체 주소처럼 보이지 않고 카드 정보가 출력된다.
그 이유는Card클래스에서toString()을 재정의했기 때문이다.
이 메서드를 재정의하지 않았다면day10.Card@해시값같은 형태로 보였을 수 있다.
그 출력만 보고는 카드 번호가7인지, 모양이HEART인지 바로 알기 어렵다.
그래서toString()은 디버깅할 때 특히 유용하다.
객체를 출력하기만 해도 현재 객체가 어떤 값을 가지고 있는지 확인할 수 있기 때문이다.
또 하나 기억할 점이 있다.
Object의toString()은public메서드다.
오버라이딩할 때는 조상 메서드보다 접근 범위를 좁힐 수 없다.
그래서 직접 만든 클래스에서toString()을 재정의할 때도public으로 작성해야 한다.
hashCode()는 클래스마다 기준이 다를 수 있다.
직접 만든 클래스가hashCode()를 재정의하지 않으면Object의 기본hashCode()를 사용한다.
이 기본값은 보통 객체마다 다르게 보이는 정수값으로 나오지만, 실제 메모리 주소 그 자체라고 외우는 것은 정확하지 않다.
초보자 단계에서는 “서로 다른 객체를 구분하는 데 쓰이는 기본 번호” 정도로 이해하면 된다.
반면String,Integer같은 클래스는 내용이나 값 기준으로hashCode()가 이미 재정의되어 있다.
예제: HashCodeExample
// HashCodeExample.java package day10; class Name { String firstName; String lastName; Name(String firstName, String lastName) { // 이름과 성을 저장함 this.firstName = firstName; this.lastName = lastName; } } public class HashCodeExample { public static void main(String[] args) { Name name1 = new Name("길동", "고"); Name name2 = new Name("길동", "고"); // 직접 만든 Name은 equals()와 hashCode()를 재정의하지 않음 System.out.println(name1 == name2); System.out.println(name1.equals(name2)); String str1 = new String("길동고"); String str2 = new String("길동고"); // String은 내용 기준 equals()와 hashCode()가 재정의되어 있음 System.out.println(str1 == str2); System.out.println(str1.equals(str2)); System.out.println(str1.hashCode() == str2.hashCode()); Integer num1 = Integer.valueOf(100); Integer num2 = Integer.valueOf(100); // Integer도 값 기준 hashCode()가 정의되어 있음 System.out.println(num1.hashCode() == num2.hashCode()); } }// 출력결과 // false // false // false // true // true // true
name1과name2는 안에 들어 있는 값이 같아 보이지만 직접 만든Name클래스에서equals()를 재정의하지 않았다.
그래서name1.equals(name2)는 기본equals()처럼 같은 객체인지 확인하고, 결과는false가 된다.
반면str1과str2는new를 두 번 사용했으므로 서로 다른 객체다.
그래서str1 == str2는false다.
하지만 문자열 내용이 같으므로str1.equals(str2)는true이고,String의hashCode()도 내용 기준으로 만들어져 있으므로 두 해시코드 비교 결과도true다.
Integer도 값 기준으로hashCode()가 정의되어 있다.
그래서 같은 값100을 가진Integer값은 같은 해시코드를 가진다.
여기서는 두 변수가 같은 객체인지보다, 같은 값이면hashCode()기준도 같아진다는 점을 확인하면 된다.
이 예제에서 중요한 결론은 이것이다.
모든 클래스가 자동으로 내용 기준 비교를 해주는 것은 아니다.
String과Integer처럼 이미 잘 재정의된 클래스도 있지만, 직접 만든 클래스는 필요한 기준에 맞게 직접 재정의해야 한다.
객체 비교는 처음 볼 때 이름 때문에 헷갈리기 쉽다.
특히equals()라는 이름 때문에 자동으로 내용 비교를 해준다고 생각하기 쉽다.
하지만 실제 기준은 클래스마다 다르다.
1. 같은 내용과 같은 객체는 다르다
값이 같아 보여도
new를 두 번 사용하면 서로 다른 객체다.
==는 두 객체의 내용이 같은지 보는 것이 아니라 같은 객체를 가리키는지 본다.
2. 기본 equals()는 내용 비교가 아니다
Object의 기본equals()는this == obj를 반환한다.
그래서 직접 만든 클래스에서equals()를 재정의하지 않으면 기본적으로 같은 객체인지 확인하는 방식으로 동작한다.
3. String은 이미 equals()가 재정의되어 있다
String은 문자열 내용을 비교하도록equals()가 재정의되어 있다.
그래서 문자열 비교에서는==가 아니라equals()를 사용해야 한다.
4. equals()만 재정의하면 HashSet에서는 부족할 수 있다
HashSet은 먼저hashCode()로 비교 후보를 찾고, 그다음equals()로 최종 확인한다.
그래서 값 기준 중복 제거를 원한다면equals()와hashCode()를 함께 재정의해야 한다.
그리고HashSet은 저장 순서를 보장하지 않으므로, 이 글에서는 출력 순서보다 중복 판단 기준을 중심으로 보면 된다.
5. hashCode()가 같다고 무조건 같은 객체는 아니다
hashCode()는 비교 후보를 빠르게 찾기 위한 값이다.
최종 판단은equals()가 한다.
따라서hashCode()가 같아도equals()가false이면 같은 객체가 아니다.
6. toString()은 객체 값을 읽기 쉽게 보여주기 위한 메서드다
객체를 출력했을 때
클래스이름@해시값처럼 보이면 객체 안의 값을 알기 어렵다.
toString()을 재정의하면 객체 안의 중요한 값을 읽기 쉬운 문자열로 확인할 수 있다.
7. Object의 모든 메서드를 한 번에 다 외울 필요는 없다
Object에는clone(),getClass(),wait(),notify()같은 메서드도 있다.
하지만 지금 글의 핵심은 객체 비교와 출력이다.
따라서 먼저equals(),hashCode(),toString()을 정확히 잡는 것이 좋다.
참고
Object를 이해할 때는 메서드 이름을 외우는 방식보다 흐름으로 잡는 것이 좋다.
1. 모든 객체는 결국Object에서 시작한다.
2. 그래서 모든 객체는equals(),hashCode(),toString()을 물려받는다.
3. 기본equals()는 내용 비교가 아니라 같은 객체인지 확인한다.
4. 직접 만든 객체를 값 기준으로 비교하려면equals()를 재정의해야 한다.
5. 해시 기반 컬렉션에서 값 기준 중복 제거까지 하려면hashCode()도 함께 재정의해야 한다.
6. 객체를 출력했을 때 읽기 쉽게 보이게 하려면toString()을 재정의한다.
즉,Object클래스는 모든 객체가 공통으로 가지는 기능의 출발점이다.
이번 글에서는 그중에서도 객체 비교와 출력에 직접 연결되는equals(),hashCode(),toString()이 핵심이다.
직접 만든 객체를 제대로 비교하고 컬렉션에서 중복까지 처리하려면equals()와hashCode()를 같은 기준으로 함께 맞춰야 한다.