일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 큐
- Simple Brute-Force Algorithm
- 개념
- 스택
- 완전탐색
- 매개 변수 탐색
- Queue
- 알고리즘잡스
- basic data-structure
- 우선순위 큐
- Adv. recursive function
- dfs
- 선형자료구조
- 고급정렬
- Divide and Conquer
- Sort
- hint
- Stack
- 내돈후기
- 간단한 완전탐색
- Advanced Sort
- parametric search
- 정렬
- 동적계획법
- binary search
- heap
- 기본자료구조
- 이진탐색
- 힙
- 깊이우선탐색
- Today
- Total
루시와 프로그래밍 이야기
ITEM10. equals는 일반 규약을 지켜 재정의하라 본문
재정의하지 않는 경우
- 각 인스턴스가 본질적으로 고유하다.
클래스는 값 표현x 동작하는 개체를 표현 (ex. Thread) - 인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없다.
- 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
- 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.
위험 회피 스타일 / 실수로라도 호출되는 것이 싫다면 다음과 같이 구현하자
@Override public boolean equals(Object o){
throw new AsserionError(); //호출금지!
}
Q. 언제 equals를 재정의해야 할까?
A. 논리적 동치성을 확인해야할 때, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때
즉, 논리적으로 같은 값인지 확인하고 싶을 때!
[동일성 vs 동등성] (https://sedangdang.tistory.com/224)
동일성(identical): 두개의 오브젝트가 완전히 동일함. (== 비교)
동등성(equivalent): 동일한 정보를 담고 있음 (equals() 비교)
- 두 오브젝트가 서로 동일하다면 사실 오브젝트는 하나만 존재하고 있는것이다. 같은 주소값을 참조하고 있는 것.
- 동일한 오브젝트는 동등하다. 하지만 동등한 오브젝트가 동일하지는 않다.
String a = "1234";
System.out.println(a.equals("1234")); //true
System.out.println(a.equals(1234)); //false
equals 규약
- equals 메서드는 동치관계를 구현한다.
- 반사성(reflexivity) : null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
- 대칭성(symmetry) : null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
- 추이성(transivity) : 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다.
2. 대칭성 위배 : cis.equals(s) == true / s.equals(cis) == false
package effectivejava.chapter3.item10;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// 코드 10-1 잘못된 코드 - 대칭성 위배! (54-55쪽)
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// 대칭성 위배!
// cf) equalsIgnoreCase = 대소문자 구분없이 문자열 자체만으로 비교
@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;
}
// // 수정한 equals 메서드 (56쪽)
// @Override public boolean equals(Object o) {
// return o instanceof CaseInsensitiveString
// && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
// }
// 문제 시연 (55쪽)
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
System.out.println(list.contains(s));
//equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
}
}
- cis.equals(s); // true
- cis가 CaseInsensitiveString클래스 이므로
- CaseInsensitiveString클래스의 오버라이드된 equals 함수를 타서
- if (o instanceof String) 를 통해 true 리턴
- s.equals(cis); // false
- s가 String클래스 이므로
- String클래스의 기존 equals 함수를 타서 다음 코드 참고
- CaseInsensitiveString instanceof String이 아니므로 false
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
※ 해결방법 : CaseInsensitiveString와 String을 equals로 연동하겠다는 허황된 꿈을 버리면 된다
cis.equals(s) == false / s.equals(cis) == false
3. 추이성 위배
상위 클래스에는 없는 새로운 필드를 하위 클래스에 추가하는 상황을 생각해보자.
package effectivejava.chapter3.item10;
// 단순한 불변 2차원 정수 점(point) 클래스 (56쪽)
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;
}
// // 잘못된 코드 - 리스코프 치환 원칙 위배! (59쪽)
// @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;
// }
// 아이템 11 참조
@Override public int hashCode() {
return 31 * x + y;
}
}
package effectivejava.chapter3.item10.inheritance;
import effectivejava.chapter3.item10.Color;
import effectivejava.chapter3.item10.Point;
// Point에 값 컴포넌트(color)를 추가 (56쪽)
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
// 코드 10-2 잘못된 코드 - 대칭성 위배! (57쪽)
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
// // 코드 10-3 잘못된 코드 - 추이성 위배! (57쪽)
// @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) {
// 첫 번째 equals 메서드(코드 10-2)는 대칭성을 위배한다. (57쪽)
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp) + " " + cp.equals(p));
// 두 번째 equals 메서드(코드 10-3)는 추이성을 위배한다. (57쪽)
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.printf("%s %s %s%n",
p1.equals(p2), p2.equals(p3), p1.equals(p3));
}
}
1. 첫번째 대칭성 위배
- p.equals(cp) // true
- Point의 equals는 색상을 무시해서 같다고 나오고
- 직관적으로 생각할때도 Point 기준으로 p와 cp의 위치가 같으므로 같은 값인 true 반환
- cp.equals(p) //false
- ColorPoint의 equals는 입력 매개변수의 클래스 종류가 다르다며 매번 false 반환
- 직관적으로 생각할때도 ColorPoint기준으로 (Ponint) p에는 컬러가 없으니까 다른값인 false 반환
※ 대칭성 위배 해결방법 : o가 일반 Point면 색상을 무시하고 비교한다.
이 방식은 대칭성은 지켜주지만, 추이성을 깨버린다.
2. 두번째 추이성 위배
- p1.equals(p2)
- p2가 Point이므로 색상 무시하고 비교하여 ture (o가 일반 Point면 색상을 무시하고 비교)
- p2.equals(p3)
- Point인 p2기준에서 p3는 위치가 같으므로 true
- p1.equals(p3))
- ColorPoint인 p1기준에서 ColorPoint p3과 색까지 같아야 하는데 달라서 false
※ 추이성 위배 해결방법 : 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 X
→ 다음 코드로는 p1.equals(p3)) 가 Point로서만 체크하므로 true 나옴.
@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로써 활용될 수 있어야 한다"
package effectivejava.chapter3.item10.inheritance;
import effectivejava.chapter3.item10.Point;
import java.util.concurrent.atomic.*;
// Point의 평범한 하위 클래스 - 값 컴포넌트를 추가하지 않았다. (59쪽)
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(); }
}
package effectivejava.chapter3.item10.inheritance;
import effectivejava.chapter3.item10.Point;
import java.util.*;
// CounterPoint를 Point로 사용하는 테스트 프로그램
public class CounterPointTest {
// 단위 원 안의 모든 점을 포함하도록 unitCircle을 초기화한다. (58쪽)
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);
}
public static void main(String[] args) {
Point p1 = new Point(1, 0);
Point p2 = new CounterPoint(1, 0);
// true를 출력한다.
System.out.println(onUnitCircle(p1));
// true를 출력해야 하지만, Point의 equals가 getClass를 사용해 작성되었다면 그렇지 않다.
System.out.println(onUnitCircle(p2));
}
}
set을 포함한 대부분의 컬렉션은 contains에서 equals를 이용하는데
CounterPoint(p2)의 인스턴스는 어떤 Point와도 같을 수 없기 때문에 onUnitCircle(p2) false 출력
※ 해결방법 : Point의 equals를 instanceof기반으로 올바로 구현했다면 제대로 동작할 것
→ 우회방법 : ITEM 18 "상속 대신 컴포지션을 사용하라"
Point를 상속하는 대신 Point를 ColorPoint의 private 필드로 두고,
ColorPoint와 같은 위치의 일반 Point를 반환하는 뷰 메서드(ITEM 6)를 public으로 추가하는 방식 ??
package effectivejava.chapter3.item10.composition;
import effectivejava.chapter3.item10.Color;
import effectivejava.chapter3.item10.Point;
import java.util.Objects;
// 코드 10-5 equals 규약을 지키면서 값 추가하기 (60쪽)
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);
}
/**
* 이 ColorPoint의 Point 뷰를 반환한다.
*/
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();
}
}
4. 일관성 : 두 객체가 같다면 앞으로도 영원히 같아야한다 (가변이든 불변이든)
5. null-아님 : 모든 객체가 null과 같지 않아야 한다
NullPointException을 던지는 코드에서도 if(o==null) return false로 보호해야한다.
→ 그러나 instance검사를 하면 알아서 null검사가 진행된다.
※최종 정리 : equals 메서드 구현 방법
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환한다.
- 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
- 자문하기
- 대칭적인가?
- 추이성이 있는가?
- 일관적인가?
package effectivejava.chapter3.item10;
// 코드 10-6 전형적인 equals 메서드의 예 (64쪽)
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int 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 pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// 나머지 코드는 생략 - hashCode 메서드는 꼭 필요하다(아이템 11)!
}
마지막 주의사항
- equals를 재정의할 땐 hashCode도 반드시 재정의하자(ITEM 11)
- 너무 복잡하게 해결하려 들지 말지
- Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자
입력타입이 달라지므로 재정의가 아니라 다중정의(ITEM 52) - 꼭 필요한 경우가 아니면 equals를 재정의하지 말자
'스터디 > 이펙티브 자바' 카테고리의 다른 글
ITEM12. toString을 항상 재정의하라 (0) | 2022.08.31 |
---|---|
ITEM11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2022.08.31 |
3장 모든 객체의 공통 메서드 (0) | 2022.08.30 |
2장 객체 생성과 파괴 (0) | 2022.08.30 |
1장 들어가기 (0) | 2022.08.18 |