참조 객체를 다룰 때 가장 많이 사용되는 메소드중 하나가 equals 메소드일 것입니다.
이 메소드는 객체간의 관계를 다루는만큼 재정의 시 주의를 요합니다.
이펙티브 자바 규칙 8 에서는 이 메소드를 정의할 때의 규약을 다루가 있습니다.
먼저 equals 메소드를 재정의 하지 않아도 되는 경우는
1. 각각의 개체가 고유한 값을 가질 때 기본형 값(value)로 이루어진 변수 대신 활성개체(active entity)를 나타내는 스레드로 이루어진 클래스는 단순한 실행단위이기 때문에 . 이런 클래스는 Object의 equals 메소드를 그대로 사용해도 됩니다. 2. 클래스에서 논리적 동일성 검사를 요구하지 않는다. 예로 java.util.Random 클래스가 있다. 이 클래스에서 equals 는 랜덤 객체가 같은 난수열로 만들어졌는지 확인할 수 있게끔 재정의 될 수 있었는데, 이 클래스를 만든 사람들은 그런 기능을 클라이언트가 원할 것이라고 생각하지 않았습니다. 이럴 경우 그냥 Object에서 상속받아서 사용하면 됩니다. 3. 상위 클래스에서 재정의한 equals가 하위 클래스에서도 적당합니다. 상위에서 상속받은 equals가 하위에서 사용하기 무리없을 때는 equals를 재정의 하지 않아도 됩니다. 예로 대부분으 Set클래스(HashSet, TreeSet 등)은 abstractSet의 equals 메소드를 그대로 사용합니다 마찬가지로 Map클래스도 AbstractMap의 equals를 그대로 사용합니다. 4. 클래스가 private 또는 package-private로 선언되었고 equals를 사용할 일이 없습니다. 내부 클래스로 사용하기 위해 클래스를 private로 선언할 경우 equals를 사용할 일이 없습니다. 이럴경우 equals를 재정의 하지 않아도 되지만, 혹시나의 실수를 막기위해 아래의 방법으로 재정의를 해주는 것이 좋습니다 @Override public Boolean equals(Object o){ |
이러한 경우는 euals 재정의를 하지 않아도 됩니다.
그렇다면 어느 경우에 사용해주는 편이 좋을까요?
1. 객체의 동일성이 아닌, 논리적 동일성을 비교하고자 할 때 객체의 동일성이라함은 두 객체의 주소의 값이 같음을 이야기합니다 String one = "나"; 당연하지만 이럴 경우 True가 나옵니다. 객체의 동일성이 입증된 것입니다 논리적 동일성의 경우, 각 객체를 구성하는 값이 클라이언트가 원하는 바에 맞추어 동일성을 입증해야하는 경우를 말합니다 사각형을 구성할때 옆 변의 두개가 같으면 같은 사각형이라 보고싶은 클라이언트가 있다고 가정해봅시다 이럴 경우 사용자의 요구에 맞추어 equals를 재정의해야 합니다 2. 상위 클래스의 equals가 하위 클래스의 요구를 충족하지 못할 때 보통 값(value) 클래스의 경우입니다. 값 클래스는 Integer나 Date처럼 특정 값을 표현하는데 보통 사용자는 이러한 값의 동질성을 비교하고 싶지, 객체간의 주소값이 같은지를 비교하지는 않습니다, 이 경우 재정의를 해주면, 이러한 요구를 만족시킬 수 있습니다. equals 메소드를 재정의하지 않아도 되는 값 클래스들도 있는데, 싱글톤 포스트에서 다른 enum 클래스가 대표적입니다. 이러한 객체들은 한 개의 객체만 사용되게끔 설계되었기 때문에 비교대상이 없어 객체 동일성이 바로 논리 동일성으로 이어집니다. |
필요에 의해 equals를 재정의 할 때의 일반 규약은 다음과 같습니다.
이는 Java의 Object 클래스 명세에서 기술하고 있습니다.
반사성
null이 아닌 참조 x가 있을 때, x.equals(x)는 true를 반환한다.
- 즉 자기자신과 자기자신을 비교할 때 True가 나와야합니다.
대칭성
null이 아닌 참조 x와 y가 있을 때, x.equals(y)는 y.equals(x)가 true 일 때만 true를 반환한다.
- 순서가 뒤바뀌어도 결과는 같아야 합니다.
추이성
null이 아닌 참조 x, y, z가 있을 때 x.equals.(y)가 참이고 y.equals(z)가 참이면 x.equals(z)도 참이어야 한다.
- 연속성, 상호성이 보장되어야 합니다.
일관성
null이 아닌 참조 x와 y가 있을 때 equals를 통해 비교되는 정보에 변화가 없다면, 몇번 반복하더라도 같은 결과를 유지해야한다.
하나씩 살펴보도록 합시다.
먼저 아래의 코드를 보죠.
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = 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;
}
}
생성자로 문자열을 받아 저장하는 클래스입니다
equalsignoreCase 메소드는 대소문자를 구분하지 않는 equals 메소드 입니다.
이 클래스의 equals 정의에는 문제가 있습니다.
CaseInsensitiveString의 equals 메소드는 String 객체에 대해 알지만
String의 equals는 CaseInsensitiveString이 뭔지 모른다는 것입니다.
String 내부에 우리가 만든 클래스가 정의해있을리 만무하니까요.
이럴 경우 대칭성이 깨지게 됩니다.
이 상황을 인지하지 못한 채 set이나 map, list 등에 이용하면 큰 곤욕을 치룰 수 있습니다.
CaseInsensitiveString cis = new CaseInsensitiveString("l");
List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();
list.add(cis);
System.out.println(list.contains(cis));
이러한 상황에서 contains 메소드를 사용하게 되면 어떤 결과가 나올까요?
일단 제 자바 환경에서는 true가 나옵니다만, JVM의 버전별로 true가 나올 수도 있고 false가 나올 수도 있다고합니다.
프로그래머의 제어에 일정한 결과를 반환해주지 못하는 프로그램은 프로그램으로서 치명적이겠죠.
때문에 재정의 시에는 항상 대칭성을 염두에 두는 것이 좋습니다.
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
이 코드는 equals가 String 객체와 상호작용하지 않게끔 고쳐주면 됩니다.
또 다음 코드를 한번 보죠.
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;
}
}
2차원 공간의 좌표를 찍는 코드입니다.
이 클래스를 상속하여 좌표에 이어 색을 추가한 클래스와 equals를 만들어 봅시다.
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 ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
equals는 상위 클래스의 equals와 칼라 정보를 골라내게끔 하는 기능을 합니다.
이 equals 재정의의 문제는 순서를 바꾸는 순간 같은 결과를 내지 못한다는 것입니다.
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
p.eqauls(cp);
cp.equals(p);
위 코드로 한번 생각해볼까요.
p의 equals를 호출하는 순간 자바의 다형성에 의해 ColorPoint는 Point로도 여겨지니 if문은 가볍게 통과하고
x와 y값도 비교해봤을 때 문제가 없어 true를 반환할 것입니다.
반면 cp의 equals는 Point는 ColorPoint로 여기질 수 없어 if문을 통과하지 못하고 false를 뱉어낼 것이고요.
이렇게 해서 대칭성이 깨지게 됩니다.
그렇다면 cp의 equals 결과도 true로 바꾸기 위해 색상 정보를 무시하게끔 코드를 짜면 어떨까요?
다음은 위의 ColorPoint의 equals를 수정한 것입니다.
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
if (!(o instanceof ColorPoint)) // o가 Point 객체이면 색상은 비교하지 않음
return o.equals(this);
// o가 ColorPoint이므로 모든 정보 비교
return super.equals(o) && ((ColorPoint)o).color == color;
}
매개변수로 들어올 수 있는 의미있는 경우의 수는 세개입니다.
1. Point -> 두번째 if에서 매개변수 자신의 equals를 호출하여 ColorPrint와 비교하게 됩니다.
위 경우에는 Point는 color를 다루지 않으니 true가 나오겠죠.
2. ColorPrint -> 마지막 return 에서 상위 클래스(Point)의 equals과 매개변수와 자기자신의 color를 비교해서 리턴합니다.
3. 그 외의 참조 객체 -> 첫번째 if에서 false를 리턴합니다.
이렇게 고치면 대칭성은 유지가 되지만, 추이성은 깨지게 됩니다.
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를 리턴 할 것입니다.
p1과 p2는 색상정보를 빼고 비교했기에 true였지만 p1과 p3는 색상정보까지 비교했거든요.
이러한 문제는 본질적인 문제로서 객체 지향의 추상화의 혜택을 포기하면 모를까
객체 생성 가능 클래스를 계승하여 새로운 값 컴포넌트를 추가하면서 equals 규약을 어기지 않을 방법은 없다고 합니다.
이펙티브 자바에서는 이러한 고민을 피해가는 디자인 패턴을 소개하고 있습니다.
public class ColorPoint{
private final Point point;private final Color color;
public ColorPoint(int x, int y, Color color){
if(color == null)
throw new NullPointerException();
point = new Point(x,y);
this.color = 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);
}
}
일반적으로 Java에서 사용되는 상속이 아닌
ColorPrint라는 객체를 Point 객체 + color 라는 속성으로 구성하는 방식입니다.
색이 있는 점(ColorPoint)는 점과 색으로 이루어져 있겠죠?
객체가 equals에 들어오게 되면 먼저 ColorPoint가 맞는지 검사를 합니다.
만약 Point가 equals로 들어오게 되면 false를 리턴합니다.
Point는 ColorPoint가 아니니까요.
그리고 매개변수의 Point와 자신의 Point를 비교한 후, 매개변수의 color와 자신의 color를 비교합니다.
두개 다 일치할 경우 true 아닐 경우 false를 리턴하게 되죠.
모든 요구를 만족시키는 바람직한 하나의 코드가 없는 이상, 이 구성 패턴을 취한 코드가 가장 이상적으로 보이긴 합니다.
일관성은 대부분의 환경에서 인정되지만
객체의 값이 변화하는 경우, 인정이 안될 수도 있습니다.
대표적인 예로 java.net.URL의 equals 메소드 입니다.
이 메소드에서는 URl에 대응되는 호스트의 IP주소를 비교하여 equals의 반환값을 결정합니다.
문제는 호스트명을 IP 주소로 변환하려면 네트워크에 접속해야하는데, 이는 언제나 같은 결과를 보장하지 않는 다는 점입니다.
당장 네이버, 다음 도메인의 IP도 자주 바뀌던데요 ^^;;
마지막으로, 이 모든 내용을 종합하여 이펙티브 자바의 저자는 equals 재정의 시 지켜야 할 점을 요약하고 있습니다.
1. == 연산자를 사용하여 equals의 인자가 자기 자신인지 검사하라.
== 연산자로 자기자신임이 판단되서 바로 리턴해버리면, 뒤의 과정을 거칠 필요가 없어서 컴퓨터 자원이 절약되겠죠?
2. instanceof 연산자를 사용하여 인자의 자료형이 정확한지 검사하라.
instanceof에서 false가 나온 객체가 equals에서 true가 나올 수 있을까요. 자료 객체 검사를 꼭 해주도록 합시다.
3. equals의 인자를 정확한 자료형으로 변환하라.
형변환을 정확하게 해야지 앞서 했던 instanceof의 의미가 있습니다.
4. "중요"필드 각각이 매개변수로 주어진 객체의 해당 필드와 일치하는지 검사한다.
앞서 좌표의 x, y 값을 일일히 검사했듯이
파라미터로 넘겨온 객체가 가지고 있는 필드값과, 현 클래스의 필드값이 일치하는지 검사합시다.
기본자료형은 기본적으로 == 연산자로 비교하면 되지만
float 필드는 Float.compare 메소드를, double 필드는 Double.compare 메소드를 이용해야합니다
또한 객체 중에는 null이 허용되는 값들도 종종 존재할 것입니다.
그 때 널 예외가 나오는 것을 막기 위해서는
1. (field == null ? o.field == null : field.equals(o.field))
2. (field == o.field || (field != null && field.equals(o.field)))
1번과 2번과 같은 숙어(idiom)을 사용하시면 됩니다.
5. equals 메소드 구현을 끝냈다면 대칭성 추이성 일관성의 세 속성이 만족되는지 검토하라.
6. equals를 재정의 할 때는 hashCode도 재정의하라.
7. equals 메소드의 매개변수를 Object에서 다른 것으로 바꾸지 마라
'프로그래밍 > Java' 카테고리의 다른 글
이펙티브 자바 규칙 9 - equal 메소드를 재정의 할 때 hashCode()도 재정의 하자 (1) | 2018.02.05 |
---|---|
이펙티브 자바 규칙 7 - 종료자 사용을 피하라 (0) | 2018.02.02 |
이펙티브 자바 규칙 6 - 유효기간이 지난 객체는 폐기하자 (0) | 2018.01.30 |