equals
는 각 인스턴스가 오직 자기 자신과만 같게 구현되어 있음.public boolean equals(Object obj) {
return (this == obj);
}
Thread
의 경우, 각 인스턴스가 고유하기에 Object.equals
사용례에 맞음.논리적 동치성(Logical equality)
을 검사할 일이 없는 경우java.util.regex.Pattern
에서 Pattern a = Pattern.compile("a");
Pattern b = Pattern.compile("a");
System.out.println(a.equals(b)); // false
논리적 동치성(Logical equality)
를 검사할 필요가 있다는 의미가 된다.equals
가 하위 클래스에도 딱 들어맞는 경우Set
구현체에서 AbstractSet
, List
구현체에서 AbstractList
, Map
구현체들은 AbstractMap
으로부터 상속받아 equals 를 사용하는 경우private
이거나 package-private
이고 equals
메서드를 호출할 일이 없는 경우equals
가 호출되지 않도록 하기 위해 다음과 같이 구현할 수 있음.@Override
public boolean equals(Object o) {
throw new AssertionError(); // 호출 금지!
}
객체 식별성
이 아닌 논리적 동치성
을 확인해야 할 때 재정의한다.equals
가 논리적 동치성을 비교하도록 재정의되지 않았을 때 재정의한다.값 클래스
들이 해당됨.Integer
이나 String
처럼 값을 표현하는 클래스이다.
값 클래스라 해도, 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스
라면 equals
를 재정의하지 않아도 됨.
예 : Enum
클래스
해당 클래스에서 논리적으로 같은 인스턴스가 1개 이상 만들어지지 않는다.
논리적 동치성
과 객체 식별성
이 사실상 동일한 의미가 된다.
따라서 Object
의 equals
가 논리적 동치성까지 확인해준다고 볼 수 있음.
equals
메서드를 재정의시에 반드시 다음의 규약을 따라야함반사성(reflexity)
null
이 아닌 모든 참조 값 x
에 대해 , x.equals(x)
는 true
이다.대칭성(symmetry)
null
이 아닌 모든 참조 값 x
, y
에 대해 , x.equals(y)
가 true
라면y.equals(x)
도 true
추이성(transitivity)
null
이 아닌 모든 참조 값 x
, y
, z
에 대해, x.equals(y)
가 true
이고, y.equals(z)
도 true
면 x.equals(z)
도 true
이다.일관성(consistency)
null
이 아닌 모든 참조 값 x
, y
에 대해, x.equals(y)
를 반복해서 호출하면 항상 true
를 반환하거나 항상 false
를 반환해야 한다.null - 아님:
- null
이 아닌 모든 참조 값 x
에 대해, x.equals(null)
은 false
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;
}
// 나머지 코드는 생략
}
CaseInsensitiveString - equals
- String 은 true
반환
String - equals
- CaseInsensitiveString 는 false
반환
CaseInsensitiveString
을 컬렉션에 넣은 후 contains
메서드를 호출하면 예측할 수 없는 결과가 나온다.
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
list.contains(s); // 결과는 불확실
CaseInsensitiveString
의 equals
를 String
과 연동하려는 시도를 포기해야함.
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
return false;
}
로 수정하면 된다.
상속을 사용하여 클래스를 확장할 떄 발생하는 문제이다.
Point
클래스와 이를 확장한 ColorPoint
클래스 예시 (아래)
2차원 공간에서 점을 나타내는 Point
클래스를 살펴보자.
해당 클래스는 x
와 y
좌표를 가진다, equals
메서드를 재정의하여 두 점이 같은지 비교한다.
public 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 p.x == x && p.y == y;
}
}
ColorPoint
클래스의 확장Point
클래스를 확장하여 색상 정보를 추가한 ColorPoint
클래스 작성public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
equals
메서드 : 대칭성 위배ColorPoint
클래스에서 equals
메서드 재정의 시, 색상 정보까지 비교한다면?@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
Point
클래스 equals 메서드는 색상을 무시하지만,ColorPoint
클래스의 equals 메서드는 색상을 고려하기에,true
를 반환해도 컬러포인트.equals(포인트) 는 false
를 반환한다.equals
메서드 : 추이성 위배@Override
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
if (!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o) && ((ColorPoint) o).color == color;
}
그러나
ColorPoint c1 = new ColorPoint(1, 2, Color.RED);
Point c2 = new Point(1, 2);
ColorPoint c3 = new ColorPoint(1, 2, Color.BLUE);
TRUE
ColorPoint
의 형이 아니기에, return o.equals(this);
로 비교한다.Point
클래스의 동등성 비교를 위해 x, y 값으로 비교하게 되며true
를 반환한다TRUE
Point
클래스의 동등성 메서드를 탄다.true
가 반환된다.FALSE
ColorPoint
클래스이다.ColorPoint
의 equals
메서드가 호출되는데, x,y 좌표가 같아도, 색상이 다르기에 false
를 반환하게 된다.equals
에서 메서드를 잘못 구현하는 경우 추이성을 위배할 수 있다는 문제점이 있다.@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
Point
클래스의 서브 클래스인 ColorPoint
클래스간의 비교를 하는 경우, 둘의 논리적 동치성을 비교하지 않고, 단순 구현 클래스가 다르다는 이유로 같지 않음을 반환해 버린다.Set
을 사용하여 단위 원 내의 점을 포함하도록 초기화private static final Set<Point> unitCircle = Set.of(
new Point(1, 0), new Point(0, 1),
new Point(-1, 0), new Point(0, -1)
);
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
CounterPoint
클래스는 Point
를 상속하여 인스턴스 개수를 센다public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger();
public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet();
}
public static int numberCreated() {
return counter.get();
}
}
CounterPoint
인스턴스를 onUnitCircle
메서드에 넘기면, Point
클래스의 equals
를 getClass
를 사용해 작성한 경우 onUnitCircle
는 false
를 반환한다.Point
를 상속하는 대신 Point
를 ColorPoint
의 private
필드로 설정,ColorPoint
와 같은 위치의 Point
를 반환하는 뷰(view) 메서드를 public
에 추가한다public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
Point
의 기능을 그대로 사용하면서ColorPoint
만의 기능을 추가할 수 있는 장점이 있다고 한다equals
메서드의 추이성을 위배하지 않으면서, ColorPoint
객체를 Point
객체처럼 사용할 수 있다.java.sql.Timestamp
[java.util.Date](http://java.util.Date)
를 확장하여 nanosecond
필드를 추가한 클래스이다.TimeStamp
의 equals
메서드는 대칭성을 위배한다.Date
객체와 TimeStamp
객체를 같은 컬렉션에 넣거나 섞어 사용하면 예기치 않은 동작이 가능하다.equals
규약을 지키면서 값을 추가가 가능하다.태그 달린 클래스보다는 클래스 계층구조를 활용하라
(아이템 23) 에 따라아무런 값을 갖지 않는 추상 클래스인 Shape
를 위에 두고,
- 이를 확장해 radius
필드를 추가한 Circle
클래스,
- length
와 width
필드를 추가한 Rectengle
클래스를 만들 수 있다.
상위 클래스를 직접 인스턴스로 만드는 게 불가능하면, equals 관련 동치성, 추이성에 대한 문제는 발생하지 않음.
일관성은 일반적으로 두 객체가 같고, 수정이 되지 않는다는 전제하에서는 앞으로도 영원히 같아야 한다는 의미이다.
가변 객체를 비교 시점에 따라, 서로 다를 수도 혹은 같은 수도 있는 반면에, 불변 객체는 한번 다르면, 끝까지 달라야 한다.
클래스를 작성시에, 불변 클래스로 만드는 것이 나을지 심사숙고 해야함.
불변 클래스로 만들기로 하였다면, equals 의 판단에 신뢰할 수 없는 자원이 끼어들게 하면 안됨.
ex
java.net.URL
의 equals
의 경우 주어진 URL
와 매핑된 호스트 IP
를 이용하여 비교한다.
- 호스트는 이름을 IP 주소로 변경하려면, 네트워크를 통하여야 하는데, 항상 결과가 같다는 보장이 불가능하다.
- URL
의 equals 가 일반 규약을 어기게 하고, 실무에서 문제가 된다고 한다.
equals
메서드의 동등성 비교에서 null
검사는 필요없음.// 묵시적 null 검사 - 이쪽이 낫다.
@Override
public boolean equals(Object o) {
if (!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}
자기 자신을 참조하는지 확인한다
true
를 반환한다.if (this == o) return true;
타입 확인
instanceof
연산자로 입력이 올바른 타입인지 확인한다false
반환한다equals
가 정의된 클래스지만, 가끔은 그 클래스가 구현한 특정 인터페이스가 될 수도 있음.if (!(o instanceof MyType)) return false;
형변환
instanceof
로 검사했기에, 이 단계는 무조건 성공MyType mt = (MyType) o;
핵심 필드 비교
return this.field1.equals(mt.field1) && this.field2 == mt.field2;
float
혹은 double
을 제외한 기본 타입 필드는 ==
연산자로 비교한다float
과 double
필드는 각각 Float.compare(float, float)
과 Double.compare(double, double)
로 비교한다.Float.NaN
이나 -0.0f
등 특수한 부동소수 값을 다루기 위함임.
Float . compare ( float , float ) 의 장점
Nan 비교
NaN
은 수학적으로 정의되지 않은 값으로서float a = Float.NaN; System.out.println(a == a); // false 가 된다.
[Float.compare](http://Float.compare)
에서는 NaN 값을 같게 처리한다-0.0 과 0.0
- 두 같은 수학적으로는 같은 값이지만, 부동소수점 표현에서는 다르다.
float b = -0.0f; float c = 0.0f; System.out.println(b == c); // true
[Float.compare](http://Float.compare)
에서는-0.0
과0.0
을 다르게 처리한다.- 시각적으로는 같아보이지만, 부동소수점상에서는 다르게 처리되어야 하낟.
Objects.equals(Object, Object) 로 비교하여
인자에서 null 이 들어가도 NPE 가 터지지 않도록 처리함
배열필드는 원소 각각을 앞의 지침대로 비교
배열 모든 원소가 핵심 필드라면 `Arrays.equals` 메서드를 사용