Object 클래스의 equals함수는 객체의 주소값을 비교햔다.
즉, 같은 값을 가졌더라도 따로 생성되었다면 False를 출력한다.
null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true라면 y.equals(x)도 true다.
package effectivejava;
import java.util.Objects;
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
@Override
public boolean equals(Object o) {
if(o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if(o instanceof String) { //한 방향으로만 작동!!
return s.equalsIgnoreCase((String) o);
}
return false;
}
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Test");
String test = "test";
System.out.println(cis.equals(test)); //true
System.out.println(test.equals(cis)); //false
}
}
CaseInsensitiveString클래스의 equals는 재정의하여 대소문자를 무시하고 값을 비교하도록 정의했다.
따라서 cis.equals(test)는 재정의 된 메소드안에서의 비교이기 때문에 Test와 test가 대소문자를 무시하고 비교하게 되어 true를 리턴하게 되는데
일반적인 test.equals(cis)는 Test와 test를 자체로 비교하기 때문에 대소문자를 가려 false를 리턴하게 된다.
이 경우, 두 객체가 서로의 동치여부에 다른 결과를 내고 있으므로 일관성이 위배됐다고 할 수 있다.
null이 아닌 모든 참조 값 x,y,z에 대하여 x.equals(y)가 true고 y.equals(z)가 true라면 x.equals(z) 또한 true이다.
ColorPoint a = new ColorPoint(1, 2, Color.RED);
Point b = new Point(1, 2);
ColorPoint c = new ColorPoint(1, 2, Color.BLUE);
위와 같은 인스턴스 a,b,c가 존재한다고 할 때,
a.equals(b)와 b.equals(c)에서 a.equals(c)가 되는 과정을 살펴보자
class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point)) return false;
Point p = (Point) o;
return this.x == p.x && this.y == p.y;
}
}
class ColorPoint extends Point {
private final Color color;
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint)) return false;
return super.equals(o) && this.color == ((ColorPoint) o).color;
}
}
//위처럼 equals()를 재정의했다면
ColorPoint a = new ColorPoint(1, 2, Color.RED);
Point b = new Point(1, 2);
System.out.println(a.equals(b)); //false
System.out.println(b.equals(a)); //true
a.equals(b)를 보면 a는 ColorPoint 클래스에서 재정의 된 equals 메서드를 타게 된다. 이렇게 되면 첫번째 if 조건에서 걸리게 된다. b는 Point이지만 ColorPoint는 아니기 떄문이다. 따라서 a.equals(b)는 false가 된다.
b.equals(a)를 보면 b는 Point클래스의 equals메서드를 타게 된다.
이렇게 되면 ColorPoint는 Point클래스를 상속하고 있기 때문에 첫번째 if조건을 통과하게 되고, int x, int y값을 기준으로만 비교하기 떄문에 값이 참이 된다.
class ColorPoint extends Point {
private final Color color;
@Override
public boolean equals(Object o) {
if(!(o instanceof Point)) return false;
//o가 일반 Point이면 색상을 무시햐고 x,y정보만 비교한다.
if(!(o instanceof ColorPoint)) return o.equals(this);
//o가 ColorPoint이면 색상까지 비교한다.
return super.equals(o) && this.color == ((ColorPoint) o).color;
}
}
ColorPoint a = new ColorPoint(1, 2, Color.RED);
Point b = new Point(1, 2);
ColorPoint c = new ColorPoint(1, 2, Color.BLUE);
System.out.println(a.equals(b)); //true
System.out.println(b.equals(c)); //true
System.out.println(a.equals(c)); //false
class ColorPoint extends Point {
private final Color color;
@Override
public boolean equals(Object o) {
if(!(o instanceof Point)) return false;
if(!(o instanceof ColorPoint)) return o.equals(this);
return super.equals(o) && this.color == ((ColorPoint) o).color;
}
}
class SmellPoint extends Point {
private final Smell smell;
@Override
public boolean equals(Object o) {
if(!(o instanceof Point)) return false;
if(!(o instanceof SmellPoint)) return o.equals(this);
return super.equals(o) && this.smell == ((SmellPoint) o).smell;
}
}
Point cp = new ColorPoint(1, 2, Color.RED);
Point sp = new SmellPoint(1, 2, Smell.SWEET);
System.out.println(cp.equals(sp)); // ?
cp.equals(sp) 코드는 ColorPoint 클래스의 재정의 된 equals 메서드를 타게 된다. 이렇게 되면 두번째 if에서 걸리게 된다. 왜냐하면 o는 SmellPoint 타입이기 때문에 !(o instanceof ColorPoint) 이 조건식이 true가 된다.
그렇기 때문에 o.equals(this)가 실행되게 되면 결과적으로 o는 SmellPoint 클래스의 인스턴스이기 때문에 SmellPoint 클래스의 재정의된 equals메서드를 타게 된다.
다시 SmellPoint클래스의 입장에서 보게되면 두번째 if에서 걸리게 된다.
여기서 o는 ColorPoint타입이기 때문에 !(o instanceof ColorPoint) 이 조건식이 true가 된다.
그렇기 때문에 o.equals(this)가 실행되게 되면 결과적으로 다시 ColorPoint 클래스의 재정의된 equals메서드를 타게 된다.
LISCOV치환 : 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다.
//주어진 점이 반지름이 1인 원 안에 포함이 되는지를 확인하는 로직
class Point {
private final int x;
private final int y;
private static final Set<Point> unitCircle = Set.of(new Point(0, -1),
new Point(0, 1),
new Point(-1, 0),
new Point(1, 0)
);
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
@Override
public boolean equals(Object o) {
if(o == null || o.getClass() != this.getClass()) {
return false;
}
Point p = (Point) o;
return this.x == p.x && this.y = p.y;
}
}
//ColorPoint는 Point를 상속받은 클래스
ColorPoint cp = new ColorPoint(1, 0, Color.RED);
System.out.println(Point.onUnitCircle(cp)); //false
위의 Point클래스의 contains() 메소드 안에서 equals() 메소드를 사용하는 비교가 일어나게 되는데, cp의 경우 재정의 된 메소드를 통과하지 못한다.
조건문의 o.getClass()에서는 ColorPoint.getClass가 되고, this.getClass()에서는 Point.getClass가 되기 때문에 둘은 같지 않다.
//cf)재정의 한 equals의 if문이 instanceof로 타입을 비교하는 형태였다면 통과할 수 있었다!
@Override
public boolean equals(Object o) {
if(o == null || !(o instanceof Point)) {
return false;
}
Point p = (Point) o;
return this.x == p.x && this.y = p.y;
}
null이 아닌 모든 참조값 x,y에 대해서 x.equals(y)를 반복해서 호출하면 항상 true 또는 false를 출력한다.
null이 아닌 모든 참조값에 대하여 x.equals(null)은 항상 false를 반환한다.
==
연산자를 사용하여 입력되는 파라미터와 자기자신이 같은 객체인지 아닌지를 검사한다.
instanceof
를 사용하여 파라미터의 타입이 올바른지 체크(null 체크의 의미도 있음)
입력을 올바른 타입으로 형변환해준다. (2번을 거쳤기 때문에 무조건 성공)
파라미터의 Object 객체와 자기자신의 대응되는 핵심필드들이 모두 같은지를 비교한다.
(하나라도 다르면 false 리턴)
기본타입은 ==
로 비교, 참조타입은 equals()
로 비교
단 float 과 double은 float.compare
double.compare
를 사용하여 비교