이 블로그는 개인의 공부 목적으로 작성된 블로그입니다. 왜곡된 정보가 포함되어 있을 수 있습니다
3장 모든 객체의 공통 메서드
3장부터는 자바의 최상위 부모 객체인 Object에 대해서 equals, hashCode, toString 과 같은 재정의 메서도들의 올바른 사용 방법에 대해 알아보자
10 equals는 일반 규약을 지켜 재정의하라
public boolean equals(Object obj) {
return (this == obj);
}
위코드는 object의 메소드 equals로 객체가 참조하는 인스턴스를 비교 하기 위해 사용할 수 있다.
책에서는 equals를 다음의 경우 재정의하지 않는것을 권장하고 있다.(아에 사용하는 것을 금지 해야하는 경우)
- 각 인스턴스가 본질적으로 교유할 경우로 값을 표현하는 것이 아닌 동작하는 개체를 표현하는 클래스인 경우
- 인스턴스의 논리적 동치성을 검사할 일이 없는 경우
- 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는경우
- 클래스가 private이거난 package-private이고 equals 메소드를 호출할 일이 없는 경우
아래와 같이 예외를 발생시켜 호출을 막을 수 있다.
@Override
public boolean equals(Object o){
throw new AssertionError();
}
equals 재정의해야 하는 경우는 객체가 논리적 동치성을 확인해야할때 특히 기존의 equals가 논리적 동치성을 비교할 수 없는 경우에 재정의 해야한다.
값 클래스(Integer String)이나 아이템 1에서 인스턴스를 통제할 수 있는 상태라면 equals를 재정의 할 필요없이 객체를 식별하는 것 많으로 논리적 동치성이 보장된다.(다른 것도 보장되지 않나?)
그러면 우리가 신경 써야하는 경우는 어떤 경우인가? Object에서 정한 equals 규약을 살퍼보자
equals 메소드는 동치관계를 구현하여, 다음을 만족한다.
- 반사성(reflexivity): 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.equal(z) 도 true이면 x.equals(z)도 true이다.
- 일관성(consistency): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반봅해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
- null-아님: null 이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다.
하나씩 살퍼보자
반사성
본인은 본인과 같다라는 아주 단순해보이는(?) 성질이다. 너무 직관적으로 이해되서 반례를 찾기 힘들다 .
대칭성
동치 관계와 상관없이 서로에 대한 equals는 같아야한다는 성질이다. 다음 코드를 보자
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;
}
}
CaseInsensitiveString 클래스에 String s를 맴버변수로 넣어 다른 CaseInsensitiveString 인스턴스간에 s 값으로 비교하고, 더 나아가 type이 String 인 경우에도 s와 String을 비교하는 유연한 equals함수를 재정의하였다. 그러나 위의 경우 문제가 발생하는데 CaseInsensitiveString 클래스에서 재정의 된 equals은 가능하지만 String 클래스에서는 equals이 재정의 되지 않아 반사성을 만족하지 않는 경우가 발생한다.
public class Item10 {
public static void main(String[] args) {
CaseInsensitiveString caseInsensitiveString=new CaseInsensitiveString("hi");
String str="hi";
System.out.println(caseInsensitiveString.equals(str));
System.out.println(str.equals(caseInsensitiveString));
}
}
따라서 이경우 아래와 같이 재정의 하여 CaseInsensitiveString 클래스간 비교가 가능하는 equals로 변경하여야한다고 한다. String 클래스에도 CaseInsensitiveString 에대한 처리를 하는 방법은 책에서 지양하는 것을 주장하고 있다.(상상만해도 어지럽다.)
@Override
public boolean equals(Object o){
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}
추이성
삼단 논법으로 A와 B가 같고 B와 C가 같으면 A와 C도 같아야하는 성질이다. 바로 코드로 넘어가 보자
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;
}
}
x축 y축을 맴버로 가지는 평범한 코드이다 equals에도 문제가 없다. 다음 코드를 보자
public class ColorPoint extends Point{
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
Point를 상속하는 ColorPoint이다. 여기서 문제가 발생하는데 이경우 equal를 따로 재정의하지 않고 부모 Point클래스의 equals를 그래로 사용하여 color에 대한 비교가 일어나지 않는다는 것이다.
@Override
public boolean equals(Object o){
if(!(o instanceof ColorPoint))
return false;
return super.equals(o)&&((ColorPoint)o).color==color;
}
만약 위와 같이 코드를 변경하면 해결될거 같지만...
public static void main(String[] args) {
Point point=new Point(1,2);
ColorPoint cp=new ColorPoint(1,2,Color.BLUE);
System.out.println(point.equals(cp));
System.out.println(cp.equals(point));
}
대칭성을 위배하게 된다. (상속의 특징 때문에 부모 Point 입장에서 ColorPoint와의 비교는 x,y만 하게 된다)
그러면 ColorPoint와 Point까리의 비교만 예외처리해주면 되지 않을까?
@Override
public boolean equals(Object o){
if(!(o instanceof ColorPoint))
return false;
if(!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o)&&((ColorPoint)o).color==color;
}
이경우 추이성을 만족하지 못하는데, Point 인스턴스 A 1개와 ColorPoint 인스턴스 B,C 2개에 대해서 ColorPoint 인스턴스의 색깔이 다르고 나머지 x,y는 같을때 추이성을 위반한다(A=B,A=C,B!=C)
게다가 Point의 자식이 여러개일경우도 문제가 생긴다.
책에서는 이현상에 대해서 객체 지향 언어의 동치관계에서 나타나는 문제로 확장성을 유지하면서 해결할 수 있는 해답이 존재하지 않는다고 말한다.
@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을 상속하지 않고 필드에 넣어 view 메서드를 추가하는 방식이다.
public class ColorPoint {
private final Color color;
private final Point point
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);
}
}
그러나 이방법도 ColorPoint와 Point 간의 비교가 불가능한데 사용할 수 있을까?
일관성
두 객체가 같다면 계속 같아야한다는 성질이다. 불변 객체, 가변 객체 모두 해당된다. 결국 equals에 신뢰할 수 없는 자원이 들어가면 안된다는 것이다. java.net.URL의 equals에서 IP주소를 네트워크를 통해 받아와야하므로 때때로 일관성이 보장 되지 않는 경우가 발생했다고 한다.
Null - 아님
모든 객체가 null이 아니라는 성질이다. instance==null와 같은 명시적인 비교 방법 보다는 instance of 와 같은 묵시적인 비교방법을 권장하고 있다.
정릴 하자면 equals를 재정의 하는 방법은 다음과 같은 프로세스로 진행된다.
- == 연산자로 자기 자신의 참조인지 확인 (굳이 할필요가 있을까?)
- instance of로 타입 확인
- 입력 인스턴스를 옯바른 type으로 casting
- 객체의 맴버 변수를 일일히 검사
기본 타입은 ==, 참조타입은 equals, float와 double은 compare로 비교 (Float.equal의 오토박싱을 주의...)
위에 나온 비교하기 어려운 객체들은 표준형을 지정하여 표준형을 계속 갱신하면서 비교하는 방식도 사용할 수 있다.
또한 어떤 값을 먼저 비교 하느냐에 따라 성능이 달라질 수 있다.
실제 개발에 있어서 equal에 대해서 단위테스트를 필요하다. 다음 결과를 예측해보자
public class Item10 {
public static void main(String[] args) {
Integer a = 5;
Integer b = 5;
System.out.println(a == b);
System.out.println(a.equals(b));
Integer c = 500;
Integer d = 500;
System.out.println(c == d);
System.out.println(c.equals(d));
}
}
당연히 true, true, true, true를 예상했지만 그렇지 않다.(나도 처음에 그렇게 생각했다) Integer 래퍼클래스의 캐시 범위 가 -128 ~ 127 으로 범위에 벗어나는 경우(500) 새로운 인스턴스를 생성하게 된다.
다시 돌아와서 equals를 재정의하기 위해서는 주의사항이 필요하다.
- equals를 재정의 할때 hashCode를 재정의할것
- 복잡하지 않게 동치성 검사로 단순하게 접근할 것
- Object 외의 type에 대해서 equals 메소드를 선언하지 말 것(하위 클래스와 같이 명시적인 객체 비교를 선언하는 상황)
AutoValue통해 자동 생성하는 방식도 좋다고 한다(나중에 찾아보겠다)
11 equals를 재정의하려거든 hashCode도 재정의하라
equals를 재정의한 클래스 모두에서 hashCode를 재정의해야 한다.
Object 명세에는 hashCode에 대해 다음과 같이 말하고 있다.
- equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야한다
- equals(Object)가 두 객체를 같다고 판단했으면, 두 객체의 hashCode는 똑같은 값을 반환해야한다.
- equals(Object)가 두 객체를 다르다고 판단했다면, 두 객체의 hashCode는 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아한다.
아이템 10에서는 우리는 논리적으로 같은 두 객체에 대해서 hashCode는 같은 값을 반환해야하지만 그렇지 않다. 이경우, 해시버킷을 같은 곳에 담더라도 해시코드가 다르면 동치성 비교를 하지 않는 HashMap에 의해 문제가 발생한다.
@Override
public int hashCode(){
return 11;
}
위와 같은 모두 같은 값을 넘겨 주는 경우 하나의 해시 버킷에 자원이 집중되어 시간 측면에서 비효율적인 문제가 발생한다.
따라서 우리는 서로 다른 인스턴스에 대해서는 다른 해시값을 할당하여 균등하게 해시 버킷을 사용하도록 기대해야한다.
'Java' 카테고리의 다른 글
[이펙티브 자바] 아이템 47, 48, 49 (2) | 2024.04.27 |
---|---|
[이펙티브 자바] 아이템 39, 40, 41 (1) | 2024.03.24 |
[이펙티브 자바] 아이템 7,8,9 (1) | 2024.01.13 |
[이펙티브 자바] 아이템 3,4,5,6 (0) | 2024.01.07 |
[이펙티브 자바] 아이템 1, 2 (0) | 2024.01.04 |