각 인스턴스가 본질적으로 고유하다.
Person p1 = new Person("daisy");
Person p2 = new Person("daisy");
Person 객체는 이름이 같아도 본질적으로 고유해서 객체 참조 비교를 사용하는게 옳다.
논리적 동치성을 검사할 필요가 없다.
PersonName p1 = new PersonName("daisy");
PersonName p2 = new PersonName("daisy");
위는 논리적 동치성을 검사해야 하는 예제로
PersonName 객체는 이름이 같으면 논리적으로 같다. 두개의 객체는 같다고 보는 것이 맞으니 재정의를 해야한다.
상위 클래스의 equals을 사용해도 논리적으로 잘 맞다.
클래스가 private이거나 package-private(default)이고 equals을 사용할 일이 없다.
x.equals(x) = true.x.equals(y) = y.equals(x).x.equals(y) = y.equals(z) = A라면 z.equals(x) = A이다.x.equals(y)를 반복 호출하면 항상 같은 결과를 반환한다.x.equals(null) == false는 항상 옳다.아래는 Intellij에서 자동으로 생성해주는 equals함수다.
class PersonName {
private String name;
public PersonName(String name) {
this.name = name;
}
public String get() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonName that = (PersonName) o;
if (name != null ? !name.equals(that.name) : that.name != null)
return false;
return true;
}
@Override
public int hashCode() {
return name.hashCode();
}
}
반사성, 대칭성, 추이성, 일관성, null이 아님 을 모두 만족했다.
이러면 된걸까?
상속을 생각하지 않는 경우는 괜찮겠지만, 상속을 하게 된다면 문제가 생길 것이다.
아래의 예제를 보자.
class SuperPersonName extends PersonName {
public SuperPersonName(String name) {
super(name);
}
}
SuperPersonName객체 끼리 비교하면 상관없지만, PersonName과 SuperPersonName을 비교하면 안된다.
public static void main(String[] args) {
List<PersonName> personNameList = new ArrayList<>();
personNameList.add(new SuperPersonName("liubei"));
System.out.println("liubei라는 사람이 있나? : " + personNameList.contains(new PersonName("liubei")));
}
위의 예제를 실행하면 결과는 아래와 같다.
liubei라는 사람이 있나? : false
이렇게 된 이유는 PersonName과 SuperPerson의 getClass()값이 다르기 때문이다.
고치자면 getClass가 아닌 instanceof를 사용하면 된다.
아래는 고친 PersonName클래스의 equals함수다.
class PersonName {
...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PersonName)) return false;
PersonName that = (PersonName) o;
if (name != null ? !name.equals(that.name) : that.name != null)
return false;
return true;
}
...
}
main함수를 돌리면 출력은 아래와 같다.
liubei라는 사람이 있나? : true
책에서는 리스코프 치환 원칙에 따르면 SuperPersonName이 곧 PersonName이니 SuperPersonName과 PersonName을 비교할 수 있어야 한다고 했다.
getClass를 쓰면 다형성을 쓸 수가 없다는 의미다.
이번엔 SuperPerson에 새로운 필드를 넣어보자.
아래와 같이 작성했다.
class SuperPersonName extends PersonName {
private String houseName;
public SuperPersonName(String houseName, String name) {
super(name);
this.houseName = houseName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SuperPersonName)) return false;
if (!super.equals(o)) return false;
SuperPersonName that = (SuperPersonName) o;
if (!houseName.equals(that.houseName)) return false;
return true;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + houseName.hashCode();
return result;
}
}
main 함수도 아래와 같이 변경해야 주었다.
public static void main(String[] args) {
List<PersonName> personNameList = new ArrayList<>();
personNameList.add(new SuperPersonName("liu", "liubei"));
System.out.println("liubei라는 사람이 있나? : " + personNameList.contains(new PersonName("liubei")));
}
결과는 아래와 같다.
liubei라는 사람이 있나? : true
하위 클래스에 필드가 생기면 생기는 문제로,
이와 같은 예제가 억지스럽긴 하지만, 일어날 수 있는 일이다.
liu의 가문의 liubei와 그냥 liubei는 같은 것이라고 할 수 없는데, 같은 것으로 인식했다.
사실 가장 문제는 personName.equals(SuperPersonName) != SuperPersonName.equals(personName)의 문제가 생긴다.
equals의 원칙 중 하나인 대칭성이 깨진다는 것이다.
책에서는 그래도 instanceof를 사용하라고 하고, 대칭성이 깨지는 문제는 객체지향 언어의 근본적인 문제라고 했다.
이런 문제를 피하기 위해 하위 클래스에 필드를 추가하지 않는 것이 좋으며
우회법으로 상속이 아닌 컴포지션을 쓰자.
책에서는 equals 메서드 구현 방법을 단계적으로 정해놓았다.
equals(PersonName o)은 재정의가 아니다. equals(Object o)가 맞다.