1. 3장 시작 : 모든 객체의 공통 메서드
- Object의 final이 아닌 메서드(eqauls, hashcode, toString, clone, finalize)는 모두 재정의를 염두해 두고 설계된 것임.
- Object를 상속하는 즉, 모든 클래스는 해당 메서드를 재정의해야 한다.
- 잘못 구현시 해당 메서드들을 재정의할 때, 지켜야할 규약을 잘 지켰다고 가정하는 클래스(HashMap, HashSet 등)에 오작동을 초래
2. Item10 : eqauls는 일반 규약을 지켜 재정의하라.
- 일반적으로, 재정의를 안하게 되면 자기 자신과 같은지만 비교, 즉 인스턴스가 같은지만 비교한다.
3. eqauls를 재정의하지 않아도 되는 상황
3-1. 각 인스턴스가 본질적으로 고유하다.
- 값을 나타내는 것이 아닌 동작 개체를 표현하는 클래스
- 예 : Thread
3-2. 인스턴스의 논리적 동치성을 고려할 필요가 없다.
3-3. 상위클래스에서 재정의한 eqauls가 하위 클래스에도 딱 들어맞는다.
- 예 : Set은 AbstractSet, List는 AbstractList, Map은 AbstractMap의 eqauls를 그대로 사용중이다.
3-4. 클래스가 private 이거나 private-package인 경우 equals를 사용할 일이 없다.
- 이러한 경우 eqauls 내부에 throw new AssertionError()를 작성해 확실히 하는 것이 좋다.
4. eqauls를 재정의 하는 상황
- 객체 식별성(두 객체가 물리적으로 같은가)를 비교하는 것이 아닌 논리적 동치성을 비교해야 하는데, 상위 클래스의 eqauls가 논리적 동치성을 비교하도록 재정의 되지 않았을 때 재정의한다.
- 주로 값 클래스들(String, Integer)이 해당 된다.
- 재정의를 해야 객체를 Map의 키, Set의 원소 등으로 사용할 수 있다.
- 값 클래스라도 값이 같은 인스턴스가 2개 이상 만들어지는 것을 방지하는 통제 클래스라면 굳이 작성할 필요가 없다.
(enum, String 등)
5. eqauls를 재정의하기 위한 규칙 -> 해당 규칙을 어긴다면 오류 발생 주의
5-1. 반사성
// 3-1. 반사성 : null이 아닌 모든 참조 값 x에 대하여 x.equals(x)는 true 이다.
// - 객체는 자기 자신과 같아야 한다.
5-2. 대칭성
// 3-2. 대칭성 : null이 아닌 모든 참조 값 x,y에 대하여 x.equals(y)는 true이면 y.equals(x)도 true이다.
// - 잘못된 예시
// -> equals는 대소문자를 무시함.
// -> 위치를 바꾸었을때, 같은 결과를 출력하지 않음
CaseInsesitiveString obj = new CaseInsesitiveString("Polish");
String s = "polish";
System.out.println(obj.equals(s));
System.out.println(s.equals(obj));
// - equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응하는지 알 수 없다.
// -> 어떤 JDK에서는 false를 반환하기도 함.
List<CaseInsesitiveString> list = new ArrayList<>();
list.add(obj);
System.out.print("포함하고 있는가? ");
System.out.println(list.contains(obj));
// - 이를 해결하기 위해서는 String의 equals와 연동되어야 하는데 말도 안되는 얘기이다.
package Item10;
import java.util.Objects;
public class CaseInsesitiveString {
private final String s;
public CaseInsesitiveString(String s){
this.s = Objects.requireNonNull(s);
}
// @Override
// public boolean equals(Object o){
// if(o instanceof CaseInsesitiveString){
// return s.equalsIgnoreCase(((CaseInsesitiveString) o).s);
// }
// if(o instanceof String){
// return s.equalsIgnoreCase(((String) o));
// }
// return false;
// }
// 수정
@Override
public boolean equals(Object o){
return o instanceof CaseInsesitiveString && ((CaseInsesitiveString) o).s.equalsIgnoreCase(s);
}
}
5-3. 추이성
// 3-3. 추이성 : null이 아닌 모든 참조 값 x,y,z에 대하여 x.equals(y)는 true, y.equals(x)도 true이면 x.equals(z)도 true이다.
// 첫번째 객체와 두번째 객체가 같고 두번째 객체와 세번째 객체가 같으면 첫번째와 세번째 객체도 같아야 한다.
예시 : 최상단 클래스를 상속해 필드를 늘려가는 클래스 구현
- 최상단 클래스 : Point(x, y)
- 하위클래스 : ColorPoint(x, y, Color)
- 하위클래스끼리만 비교 : 대칭성이 위배 됨.
// 3-3-1. ColorPoint 객체끼리만 비교
// 대칭성 위배
// System.out.println(point1.equals(point2));
// System.out.println(point2.equals(point1));
- 둘의 결과가 같지 않음 : Point의 eqauls를 써서 ColorPoint와 비교시 같지만 ColorPoint의 eqauls를 사용해 비교시 다르다고 나옴
@Override
public boolean equals(Object o){
if(!(o instanceof ColorPoint)){
return false;
}
return super.equals(o) && ((ColorPoint) o).color.equals(this.color);
}
- 새로 생성된 필드는 비교 안함 : 추이성이 위배 됨.
// 3-3-2. 색상 무시 비교
// 추이성 위배
// 스택오버플로우도 발생가능 - 다른 하위클래스 만들어 해당 하위클래스와 equals하는 경우
- a=b, b=c a=c?가 성립이 안됨 : ColorPoint p1, Point p2, ColorPoint p3가 있을 때, 색상을 무시하고 x, y 좌표만 비교시 p1과 p2는 같고 p2와 p3는 같으므로 p1=p3여야하지만 실제 Color는 다름.
@Override
public boolean equals(Object o){
// 아무곳에도 속하지 않는 경우 : false 반환
if(!(o instanceof Point)){
return false;
}
// Point지만 ColorPoint는 아닌 경우 : Color 빼고 비교
if(!(o instanceof ColorPoint)){
return o.equals(this);
}
// ColorPoint인 경우 : 색상까지 전체 비교
return super.equals(o) && ((ColorPoint) o).color.equals(this.color);
}
- 최상단 클래스의 eqauls를 getClass()를 통해 구현 : 해당 방법은 리스코프 치환 원칙 위배
// 3-3-3. Point의 equals 메서드를 getClass()를 통해 처리
// - 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로 활용이 가능해야한다.
// - 위의 기본적인 조건이 위배됨
public boolean equals11(Object o){
if(o == null | o.getClass() != getClass()){
return false;
}
Point p = (Point) o;
return p.x == this.x && p.y == this.y;
}
// 리스코프 치환 원칙 : 어떤 타입에 있어서 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다.
// 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 작동해야 한다.
- ColorPoint는 여전히 Point인데, 비교가 안됨
- 추가적으로 컬렉션들은 eqauls를 통해 비교하는데, 상위클래스로 정의된 컬렉션안에 하위 클래스를 넣을 수 없는 것이된다.
// 다른 예시
// - java.sql.Timestamp는 java.util.Date를 확장한 후 nanoseconds 필드를 추가하였다.
// - 결과적으로, Timestamp의 eqauls는 대칭성을 위배, Date의 객체와 섞여 한 컬렉션에 들어가면 동작 오류 발생
// - TimeStamp를 이렇게 설계한 것은 실수이니 따라하면 안된다.
- 결론
// 결론 : 구체 클래스를 확장해 새로운 값을 추가해가며 equals 규약을 만족시킬 방법은 존재하지 않는다.
- 우회방법
// 우회 방법
// "상속대신 컴포지션을 사용하라"
// - Point를 상속하는 대신 Point를 ColorPoint의 Private Field로 두고, ColorPoint와 같은 위치의
// 일반 Point를 반환하는 View 메서드를 public으로 추가하는 방식
// - 아래의 경우는 eqauls 규약을 지키면서 상위클래스에 값을 추가한 하위클래스를 만드는 경우이다.
// - 위에서 나온 문제들을 해당 클래스는 하위클래스가 아님을 명시하며 해소하는 느낌이다.
package Item10;
public class newColorPoint {
private final Point point;
private final Color color;
public newColorPoint(Point point, Color color){
this.point = point;
this.color = color;
}
public Point asPoint(){
return this.point;
}
@Override
public boolean equals(Object o){
if(!(o instanceof newColorPoint)){
return false;
}
newColorPoint p = (newColorPoint) o;
return p.point.equals(this.point) && p.color.equals(this.color);
}
}
5-4. 일관성
// 3-4. 일관성 : null이 아닌 모든 참조 값 x,y에 대하여 x.equals(y)를 반복해도 호출해도 같은 값이 나온다.
// - 두 객체가 같다면 앞으로도 영원히 같아야 한다.
// - 가변 클래스는 비교 시점에 따라 다를 수 있지만 불변 클래스는 한번 다르면 끝까지 달라야한다.
// - 클래스가 불변이든 가변이든 eqauls의 판단에 신뢰할 수 없는 자원이 끼어들어서는 안된다.
// - equals 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야 한다.
5-5. null이 아님.
// 3-5. null이 아님 : null이 아닌 모든 참조값 x에 대하여 x.equals(null)은 false이다.
// - 모든 객체가 null과 같지 않아야 한다.
// if(0 == null)과 같은 코드는 필요하지 않다. if(!(o instanceof ClassName))으로 묵시적 검사를 진행한다.
6. equals 메서드를 구현하는 방법
// 4-0. equals의 매개변수는 Object로 설정한다.
// 4-1. == 연산자를 활용해 입력이 자기자신의 참조인지 확인한다.
// 4-2. instanceof 연산자로 입력이 올바를 타입인지 확인한다.
// 4-3. 입력을 올바른 형태로 형변환한다.
// 4-4. 입력 객체와 자기 자신의 대응하는 '핵심'필드들이 모두 일치하는지 하나씩 검사한다.
// (+) 필드 비교 방법
// - float, double 비교 : Float.compare(float, float), Double.compare(double, double) 사용
// (Float.equals, Double.equals는 오토박싱을 사용하기에 성능이 좋지 않음)
// - 나머지 기본 타입 : ==로 비교
// - 참조 타입 필드 : equals 메서드로 비교
// (+) null도 정상값으로 취급하는 참조 타입 필드
// - 정적 메서드인 Objects.eqauls(Object, Object)를 활용해 NullPointException을 예방하자.
System.out.println(Objects.equals(newP2, newP3));
// (+) 어떤 필드를 먼저 비교하느냐에 따라 성능차이 발생 가능
// - 다를 가능성이 더 크거나 비용이 싼 필드를 먼저 비교
// (그래야 비용이 비싼 필드는 나중에 비교하게 되고 속도가 향상 됨)
// - 동기화용 락 필드와 같이 객체의 논리적 상태와 관련없는 필드는 굳이 비교하지 않는다.
7. 최종 결론
// 최종 결론
// "equals를 다 구현했다면 세가지만 자문하자! 대칭성, 추이성, 일관성"
// 주의사항
// - equals를 정의할 때는 hashcode도 반드시 같이 정의하자.
// - 너무 복잡하게 해결하려 들지 말자.
// (예를들어, File 비교시 심볼릭 링크를 비교해 같은 파일을 가리키는지 확인할 필요가 없다.)
// - equals의 매개변수로는 무조건 Object로만 설정한다.
// - 사실, 자동생성 프로그램에 맡기는것이 실수도 적고 안전하다.
https://github.com/mokjaemin/EffectiveJAVA
'Language > Java Plus' 카테고리의 다른 글
Effective JAVA - Item12 : toString을 항상 재정의하라. (0) | 2023.09.19 |
---|---|
Effective JAVA - Item11 : equals를 재정의하려거든 hashcode도 재정의하라. (0) | 2023.09.18 |
Effective JAVA - Item9 : try-finally 보다는 try-with-resources를 사용하라. (0) | 2023.09.13 |
Effective JAVA - Item8 : finalizer와 cleaner 사용을 피하라 (0) | 2023.09.13 |
Effective JAVA - Item7 : 다 쓴 객체 참조를 해제하라 (0) | 2023.09.12 |