-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Labels
🐏 03 Common Methods3장 모든 객체의 공통 메서드3장 모든 객체의 공통 메서드
Description
Chapter : 3. 모든 객체의 공통 메서드
Item : 10. equals는 일반 규약을 지켜 재정의하라
Assignee : hyunsoo10
🍑 서론
equals : 두 객체가 동등한지 여부를 확인(두 객체가 메모리 상에서 동일한 위치)
== : 실제 값이 같은지 확인
- 각 인스턴스가 본질적으로 고유할 때
- 값을 표현하는 것이 아닌 동작하는 개체를 표현하는 클래스인 경우(Thread)
- 인스턴스의 논리적 동치성을 검사할 필요가 없을 때
- 논리적 동치성을 검사할 필요가 없을 때는 Object의 기본 equals만으로해결가능
- 상위 클래스에서 재정의한 equals가 하위클래스에도 딱 들어맞을 때
- 클래스가 private이거나 package-private(default)이고 equals 메서드를 호출할 일이 없을 때
@Override
public boolean equals(Object o) {
throw new AssertionError(); // 호출 금지
}🍑 본론
그렇다면 equals를 재정의해야 할 때는 언제인가
🔷 객체 식별성이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때
값 객체는 Integer와 String처럼 값을 표현하는 클래스를 말하는데, 두 값 객체를 equals로 비교하는 프로그래머는 객체가 같은 것인지가 아니라 값이 같은지를 알고 싶을 것이다.
❗equals 메서드를 재정의할 때 지켜야하는 일반 규약
-
반사성(reflexivity):
- null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
- 객체는 자기 자신과 같아야 한다.
-
대칭성(symmetry):
- null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
public 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 class Main { public static void main(String[] args) { CaseInsensitiveString cis = new CaseInsensitiveString("Polish"); String s = "polish"; System.out.println("cis.equals(s) = " + cis.equals(s)); //true System.out.println("s.equals(cis) = " + s.equals(cis)); //false } }
CaseInsensitiveString의 equals를 String과의 연동을 포기하면 equals를 아래처럼 사용할 수 있다.
@Override public boolean equals(Object o) { return o instanceof CaseInsensitiveString &&((CaseInsensitiveString) o).s.equalsIgnoreCase(s); }
CaseInsensitiveString cis = new CaseInsensitiveString("Polish"); CaseInsensitiveString cis2 = new CaseInsensitiveString("polish"); System.out.println("cis.equals(cis2) = " + cis.equals(cis2)); //true System.out.println("cis2.equals(cis) = " + cis2.equals(cis)); //true
-
추이성(transitivity):
- null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true면, x.equals(z)도 true다.
- 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다.
//부모 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) { System.out.println("Point.equals"); if (!(o instanceof Point)) return false; Point p = (Point)o; return p.x == x && p.y == y; } } //자식 public class ColorPoint extends Point { private final Color color; public ColorPoint(int x, int y, Color color){ super(x,y); this.color = color; } @Override public boolean equals(Object o) { if (!(o instanceof Point)) return false; //o가 일반 Point면 색상을 무시하고 비교한다. if (!(o instanceof ColorPoint)) return o.equals(this); // o가 ColorPoint면 색상까지 비교한다. return super.equals(o) && ((ColorPoint) o).color == color; } }
public static void main(String[] args) { 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)와 p2.equals(p3)는 색상을 무시하여 true 반환 //p1.equals(p3)는 색상을 고려하게 되어 false 를 반환 //추이성 위반 System.out.println("p1.equals(p2) = " + p1.equals(p2)); //true System.out.println("p2.equals(p3) = " + p2.equals(p3)); //true System.out.println("p1.equals(p3) = " + p1.equals(p3)); //false }
해결 방안으로는 상속 대신 컴포지션을 활용하라(아이템 18)를 참고
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); } @Override public int hashCode() { return 31 * point.hashCode() + color.hashCode(); } }
-
일관성(consistency):
- null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
- 두 객체가 같다면 (수정 되지 않는 한) 앞으로도 영원히 같아야 한다.
- 예를들어 java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교한다. 호스트 이름을 IP 주소로 바꾸려면 네트워크를 통해야 하는데, 그 결과가 항상 같다고 보장할 수 없음(잘못 설계된 것임)
-
Not null:
- null이 아닌 모든 참조값 x에 대해, x.equals(null)은 false다.
- 말 그대로 모든 객체과 null과 같지 않아야 한다.
🍑 결론
1️⃣ == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
자기 자신이면 true를 반환, 단순한 성능최적화 용도로 비교 작업이 복잡한 상황일 때 유용하다.
2️⃣ instanceof 연산자로 입력이 올바른 타입인지 확인한다.
이때의 올바른 타입은 equals가 정의된 클래스인 것이 보통이지만 가끔은 그 클래스가 구현한 인터페이스가 될 수도 있다.
3️⃣ 입력을 올바른 타입으로 형변환한다.
앞서 2번에서 instanceof로 검사를 했기 때문에 이 단계는 100% 성공
4️⃣ 입력 객체와 자기 자신의 대응되는 "핵심" 필드들이 모두 일치하는지 하나씩 검사한다.
모든 필드가 일치하면 true, 하나라도 다르면 false를 반환
- float과 double은 compare 메서드
- float, double을 제외한 기본 타입 필드는 ==
- 참조 타입 필드는 각각의 equals 메서드
- 어떤 필드를 먼저 비교하느냐가 equals의 성능을 좌우하기도 한다. 다를 가능성이 크거나 비교하는 비용이 싼 필드를 먼저 비교하다.
- 동기화용 락(lock) 필드 같이 객체의 논리적 상태와 관련 없는 필드는 비교하면 안된다.
- 핵심 필드로부터 계산해낼 수 있는 파생 필드 역시 굳이 비교할 필요는 없지만, 파생 필드를 비교하는 쪽이 더 빠를 때도 있다. 파생 필드가 객체 전체의 상태를 대표하는 상황이 그렇다.
✏️ equals를 다 구현했으면 세가지 자문을 해보자
1. 대칭적인가?
2. 추이성이 있는가?
3. 일관적인가?
이상의 비법에 따라 작성한 PhoneNumber 클래스용 equals 메서드
public class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(short areaCode, short prefix, short lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if(val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override
public boolean equals(Object o) {
if(o == this)
return true;
if(!(o instanceof PhoneNumber))
return false;
PhoneNumber phoneNumber = (PhoneNumber) o;
return phoneNumber.lineNum == lineNum
&& phoneNumber.prefix == prefix
&& phoneNumber.areaCode == areaCode;
}
}❕마지막 주의사항
- equals를 재정의할 땐 hashCode도 반드시 재정의 하자(아이템11)
- Object외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자
- 매개변수를 Object가 아닌 다른 타입으로 받으면 해당 equals는 equals 메서드를 재정의(오버 라이딩)한게 아니라 다중정의(오버로딩)한 것이다.
- 꼭 필요한 경우가 아니면 equals를 재정의하지말자. 많은 경우에 Object의 equals는 우리가 원하는 비교를 정확하게 수행해준다.
Referenced by
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
🐏 03 Common Methods3장 모든 객체의 공통 메서드3장 모든 객체의 공통 메서드