1. equals를 재정의 하면 안되는 경우
equals는 재정의하기 쉬워보이지만 곳곳에 함정이 있다. 문제를 회피하는 가장 쉬운 길은 아예 재정의하지 않는 것이다.
ㄱ. 각 인스턴스가 본질적으로 고유할 때
값 표현 객체가 없을 때를 이야기 하는 것 같다. Bean에 등록해두는 객체 repository, controller, service 등이 이에 해당할 것 같다.
DTO, Domain 객체는 값 검증이 필요할 수 있으니 equals를 재정의 해야할 수도 있다.
ㄴ. 인스턴스의 논리적 동치성(logical equality)를 검사할 일이 없을 때
논리적 동치성 검사의 1가지 : Pattern의 인스턴스가 같은 정규 표현식을 나타내는 지 검사
ㄷ. 상위 클래스에서 재정의한 equals가 하위 클래스에도 들어맞을 때
같은 특징을 가지기 때문에 equals를 상속받아 사용하는 걸 권장하는 듯 하다 > 근데 이유가 뒷편에 나옴
ㄹ. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없을 때
// equals가 실수로 호출되는 걸 막고 싶다면
@Override public boolean equals (Object o){
throw new AssertionError();
}
2. equals를 재정의 해야하는 경우
객체 식별성(object identity) : X - 두 객체가 물리적으로 같은가
논리적 동치성(logical equality) : O
객체 식별성이 아니라 논리적 동치성을 확인해야하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 되지 않았을 때. (주로 값 클래스)
public class Fruit{
private String name; // name이 같을 경우 두 객체는 같다 (논리적 동치성)
}
2-1. 값 클래스의 equals를 재정의 할 때 기대 효과
값을 비교하길 원하는 프로그래머의 기대에 부흥
ㄴ. Map, Set의 원소로 사용가능
2-2. 값 클래스여도 equals를 재정의 할 필요가 없을 때
인스턴스 통제 클래스 : 값이 같은 인스턴스가 2개 이상 만들어 지지 않음 (ex Enum)
어차피 논리적으로 같은 값이 0개 혹은 1개 이기 때문에 객체 식별성으로 판단한다. (기존의 equals)
3. Equals 메서드의 규약 - 동치관계
동치 클래스 (equivalence class) : 집합을 서로 같은 원소들로 이루어진 부분집합으로 나누는 연산
> equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환이 가능해야한다.
3-1. 반사성(reflexivity)
> null이 아닌 모든 참조 값 x에 대해 x.equals(y)는 true이다.
객체는 자기 자신과 같아야한다.
public class Fruit{
private String name;
public Fruit(String name){
this.name = name;
}
public static void main(){
List<Fruit> list = new ArrayList<>();
Fruit f = new Fruit("apple");
list.add(f);
list.contains(f); // false일 경우에는 반사성을 만족하지 못하는 경우이다.
}
}
3-2. 대칭성(symmetry)
> null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)가 true이면 y.equals(x)도 true이다.
두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다.
// 대칭성을 위반한 클래스
public final class CaseInsensitiveString{
private final String s;
public CaseInsensitiveString(String s){
this.s = Obejcts.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 cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s); // true
s.equals(cis); // false
//대칭성을 만족하게 수정
@Override public boolean equals(Object o){
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); // String에 대한 instanceof 부분을 빼고 구현한다.
}
3-3. 추이성(trasitivity)
> null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true이고 y.equals(z)도 true이면, x.equals(z)도 true이다.
첫번째 객체와 두번째 객체가 같고, 두번째 객체와 세번째 객체가 같다면 첫번째 객체와 세번째 객체도 같아야 한다. (삼단논법)
이 추이성 조건때문에, equals를 재 정의 하면 안되는 경우에 superclass에서 equals를 정의한 경우를 언급함
[문제점]
1. 대칭성 위배 문제에 빠질 수 있다.
// ColorPoint.java 의 equals
@Override public boolean equals(Object o){
if(!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
public static void main(){
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2, Color.RED);
p.equals(cp); // true (Point의 equals로 계산)
cp.equals(p); // false (ColorPoint의 equals로 계산: color 필드 부분에서 false)
}
2. 추이성 위배 문제에 빠질 수 있다.
//ColorPoint.java의 equals
@Override public boolean equals(Obejct o){
if(!(o instanceof Point))
return false;
if(!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o) && ((ColorPoint) o).color == color;
}
public static void main(){
ColorPoint p1 = new ColorPoint(1,2, Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1,2, Color.BLUE);
p1.equals(p2); // true (ColorPoint의 equals 비교 //2번째 if문에서 Point의 equals로 변환)
p2.equals(p3); // true (Point의 equals 비교 // x,y 같으니 true)
p1.equals(p3); // false (ColorPoint의 equals 비교)
}
3.무한 재귀에 빠질 수 있다.
//SmellPoint.java의 equals
@Override public boolean equals(Obejct o){
if(!(o instanceof Point))
return false;
if(!(o instanceof SmellPoint))
return o.equals(this);
return super.equals(o) && ((SmellPoint) o).color == color;
}
public static void main(){
ColorPoint p1 = new ColorPoint(1,2, Color.RED);
SmellPoint p2 = new SmellPoint(1,2);
p1.equals(p2);
// 처음에 ColorPoint의 equals로 비교 : 2번째 if문 때문에 SmellPoint의 equals로 비교
// 이후 SmellPoint의 equals로 비교 : 2번째 if문 때문에 ColorPoint의 equals로 비교
// 무한 재귀의 상태!
}
구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
그렇다고 instanceof 검사 대신 getClass 검사를 하라는 것은 아니다.
@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의 하위클래스는 정의상 여전히 Point이기 때문에 어디서든 Point로 활용되어야한다.
리스코프 치환원칙 (Liskov substitution principle) : 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다. 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야한다.
Point의 하위클래스는 정의상 여전히 Point이므로 어디서든 Point로 활용가능해야한다.
[우회 방법 : Solution]
3-3-1. 상속대신 컴포지션을 사용하라
public class ColorPoint{
private final Point point;
private final Color color;
public Point asPoint(){ //view 메서드 패턴
return point
}
@Override public boolean equals(Object o){
if(!(o instanceof ColorPoint)){
return false;
}
ColorPoint cp = (ColorPoin) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
ColorPoint - ColorPoint : ColorPoint의 equals를 이용하여 color 값까지 모두 비교
ColorPoint - Point : ColorPoint를 asPoint() 메소드를 이용해 Point로 바꾸어서, Point의 equlas를 이용해 x,y만 비교
Point - Point : Point의 equals를 이용하여 x,y 값 모두 비교
3-3-2. 추상클래스의 하위클래스 사용하기
추상클래스의 하위클래스에서는 equals 규약을 지키면서도 값을 추가할 수 있다.
상위 클래스를 직접 인스턴스로 만드는게 불가능하기 때문에 하위클래스끼리의 비교가 가능해지기 때문인 것 같다.
3-4. 일관성(consistency)
> null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
두 객체가 같다면 (어느 하나 혹은 두 객체 모두가 수정되지 않는한) 앞으로도 영원히 같아야한다.
가변객체 = 비교 시점에 따라 서로 다를 수 있다.
불변객체 = 한번 다르면 끝까지 달라야한다.
equals는 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야한다.
클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들면 안된다 : ex URL과 매핑된 호스트의 IP 주소
3-5. null-아님
> null이 아닌 모든 참조값 x에 대해, x.equals(null)은 false이다.
모든 객체가 null 과 같지 않아야한다.
1) 명시적 null 검사
@Override public boolean equals(Object o){
if( o == null){
return false;
}
}
2) 묵시적 null 검사
@Override public boolean equals(Obejct o){
if(!(o instanceof MyType)) // instanceof 자체가 타입과 무관하게 null이면 false 반환함.
return false;
MyType mt = (MyType) o;
}
4. 양질의 equals 메서드를 구현하는 4단계
4-1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
object identity를 검사한다.
4-2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
올바른 타입의 경우 : equals가 정의된 클래스로 리턴이 되는가
올바른 타입이 아닌경우 ( == 구현한 특정 인터페이스) :구현한 (서로다른) 클래스간 비교가 가능하게 해야함
: 이런 구현체들은 인터페이스의 equals를 이용하여 비교해야한다.
4-3. 입력을 올바른 타입으로 형변환 한다.
2번에서 instatnceof를 이용했기 때문에 100% 성공함
4-4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
2번에서 인터페이스를 사용했다면 입력의 필드값을 가져올 때도 인터페이스 메서드를 사용해야한다.
5. Equals 구현할 때 주의할 추가사항
5-1. 기본타입과 참조타입
기본타입 : == 연산자 비교
참조타입 : equals() 비교
float, double 필드 : Float.compare(float, float), Double.compare(double, double) 비교 (부동 소수 값)
Float.equals(float)나 Double.equals(double) 은 오토 박싱을 수반할 수 있어 성능상 좋지 않다.
배열 필드 : 원소 각각을 지침대로 비교한다. / 모두가 핵심 필드라면 Arrays.equals()를 사용한다.
5-2. null 정상값 취급 방지
Object.equals(object, object) 로 비교하여 NullPointException 발생을 예방하자
5-3. 필드의 표준형을 저장하자.
비교하기 복잡한 필드는 필드의 표준형(canonical form)을 저장한후 비교 : 불변 클래스에 제격이다.
5-4. 필드 비교 순서는 equals 성능을 좌우한다.
다를 가능성이 높은 필드 우선
비교 비용이 싼 필드 우선
핵심필드 / 파생필드 구분하자.
5-5. equals 재정의할 땐 hashCode도 반드시 재정의하자.
5-6. 너무 복잡하게 해결하려 들지 말자.
5-7. Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자
public boolean equals(MyClass o) // @ㅒverrider가 아니다! // 재정의가 아니라 다중정의 상태임!
5-8. AutoValue 프레임워크
https://www.baeldung.com/introduction-to-autovalue
Lombok이랑 비슷한 느낌의 라이브러리인것 같다.
'Reading Record > 이펙티브자바' 카테고리의 다른 글
아이템 [7] - 다 쓴 객체 참조를 해제하라 (0) | 2020.02.08 |
---|---|
아이템 [3] - private 생성자나 열거타입으로 싱글턴임을 보증하라 (0) | 2020.02.08 |
아이템[14] - Comparable을 구현할지 고려하라 (0) | 2020.02.07 |
아이템[11] - equals를 재정의 하려거든 hashCode도 재정의하라 (2) | 2020.01.26 |
아이템 [1] - 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2020.01.20 |