3장. 모든 객체의 공통 메소드 구현하기

항목 7. equals 메소드를 재정의할 때 표준 구현계약을 지켜라.

Object 클래스의 디폴트 equals() 메소드는 참조값을 비교한다.(연산자 == 와 같다)

{code}
class Circle{
Circle a = new Circle();
Circle b = new Circle();

System.out.println("a.equals(b):" + a.equals(b));
System.out.println("a==b" + (a==b));
}

|

결과는?

\[WEBSTUDY:물음\]
 연산자 == 를 사용하면 되는데 equasl()메소드를 사용하는가?    <-- 이것이 equals() 메소드의 존재의 이유..ㅋㅋ
 
 기본 데이타 타입의 비교 값이 하나라면 == 연산자를 사용해도 되지만 여러 멤버변수를 가지고 있는 객체를 비교할 때는 어떤 멤버변수를 비교해야 될지
 모호해 진다. 그래서 equals()메소드를 재정의 하여 사용자가 원하는 특정 멤버변수를 비교하여 사용하면 된다.
 



import java.util.*;

public class Wallet {

private int money; //멤버변수
private String card; //멤버변수

public Wallet(int m, String c){
this.money = m;
this.card = c;
}

public boolean equals(Object o){
if(!(o instanceof Wallet))
return false;

Wallet a = (Wallet)o;
return a.money == money && a.card == card;
}

public int hashCode(){
int result = 17;
result = 37 * result + money;

return result;
}

public String toString(){
return "card is " + card;
}

public static void main(String[] args) {

Wallet a = new Wallet(1000, "BC_Card");
Wallet b = new Wallet(1000, "BC_Card");
Wallet c = new Wallet(2000, "BC_Card");

System.out.println(a==b);

System.out.println(a.equals(b));
System.out.println(b.equals©);

System.out.println(a.hashCode());
System.out.println(b.hashCode());

//hashCode() 테스트
Map m = new HashMap();
m.put(new Wallet(1000,"BC_Card"), "My");

System.out.println(m.get(new Wallet(1000,"BC_Card"))); //결과는?

m.put(a,"My2");
System.out.println(m.get(a));//결과는?

System.out.println(a.equals(null));

System.out.println(a.toString()); //Wallet

System.out.println(a.card); //String

System.out.println(a.card.toString()); //String

}
}



h4. 디폴트 equals()

* 두 객체의 참조값 비교

h4. 재정의된 equals()

* 사용자 정의 객체비교


한글 API 참고 http://pllab.kw.ac.kr/j2seAPI/api/

	h4. equals를 재정의하지 않아도 되는 클래스
		* 각 인스턴스가 원래부터 유일한 경우
			 인스턴스가 값을 표현하기 보다는 Thread처럼 행동하는 존재를 표현할때
		* 논리적인 동등성 검사가 큰 의미가 없는 경우 
			 Random 클래스의 equals 메소드는 같은 순서의 난수를 발생시키는지 검사 할 수도 있었지만 이 클래스 설계자는 이런 기능은 별로 쓸모가 없다고 생각했다.
		* 상위클래스에서 이미 적절하게 equals를 재정의했고 이것을 그대로 쓰면 되는경우
			 컬렉션 프레임워크의 구현 클래스들은 	  대부분 컬렉션 프레임워크의 인터페이스의 기본 구현체인 AbstractSet, AbstractList, AbstractMap 에 정의한 equals 메소드를 그대로 쓸 수 있다.
		* private이거나 package-private 클래스이고 equals가 절대 호출되지 않는 경우
		
h2. equals 메소드 재정의 표준 구현 계약 사항
	   
h4. ① 반사성(Reflexivity) : 모든 객체는 자기 자신과 동등해야 한다.

*	x.equals( x ) 는 true 를 리턴해야 한다.
	
h4. ② 대칭성(Symmetry) :  어떤 두 객체가 서로에 대해 동등성을 검사하면 그 결과는 같아야 한다.

* x.equals\(y) 가 true 면 y.equals\(x ) 는 true 를 리턴해야 한다.
	
||변경 전||변경 후||
|

public final class CaseInsensitiveString {

private String s;

public CaseInsensitiveString(String s){
if(s == null)
throw new NullPointerException();
this.s = s;
}
//틀린 구현 - equals의 대칭성이 깨졌다.
public boolean equals(Object o){
if( o instanceof CaseInsensitiveString){
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
}
//CaseInsensitiveString은 String을 처리하지만
//String은 CaseInsensitiveString을 처리하지 않는다.
if(o instanceof String)
return s.equalsIgnoreCase((String)o);
return false;
}

cis.equals(s); //true
s.equals(cis); //false

|

public final class CaseInsensitiveString {

private String s;

public CaseInsensitiveString(String s){
if(s == null)
throw new NullPointerException();
this.s = s;
}

//수정 소스
public boolean equals(Object o){
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}

|





h4. ③ 추이성(Transitivity) : 첫번째 객체가 두번째 객체와 같고 두번째 객체가 세번째 객체와 같다면,첫번째 객체는 세번째 객체와 같아야한다.

*	x == y (true) && y == z (true) ---> x == 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;
}

public boolean equals(Object o){
if(!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
}

public class ColorPoint extends Point{
private Point point;

public ColorPoint(int x, int y, Color color){
super(x,y);
this.color = color;
}

..//이하 생략
}

//ColorPoint의 equals 메소드는 Point 클래스의 equals 상속 받고 있으므로
//색깔은 비교 대상이 아니다. 색깔을 비교하지 않는 것은 잘못된 것이다.

|

public class Point {
private final int x;
private final int y;

public Point(int x, int y){
this.x = x;
this.y = y;
}

//틀린 구현 - 대칭성이 깨졌다
public boolean equals(Object o){
if(!(o instanceof ColorPoint))
return false;
ColorPoint cp =(ColorPoint)o;
return super.equals(0) && cp.color == color;
}
}

public class ColorPoint extends Point{
private Point point;

public ColorPoint(int x, int y, Color color){
super(x,y);
this.color = color;
}

//ColorPoint 인스턴스를 만들어서 확인해보자
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);

p.equals(cp); // true
cp.equals(p); // false

}

|

//틀린구현 - 추이성이 깨졌다.
public boolean equals(Object o){
if(!(o instanceof Point))
return false;
//o가 Point일 때는 색깔은 비교하지 않고 위치만 비교한다.
if(!(o instanceof ColorPoint))
return o.equals(this);
//o가 ColorPoint인 경우 색깔과 위치를 모두 비교한다.
ColorPoint cp =(ColorPoint)o;
return super.equals(0) && cp.color == color;
}

{code}
문제 해결 소스
{code}
//ColorPoint와 Point의 상속관계를 끊는다.
public class ColorPoint {
//private 멤버 필드로 정의 뷰 메소드 제공
private Point point;
private Color color;

public ColorPoint(int x, int y, Color color){
point = new Point(x, y);
this.color = color;
}
//ColorPoint 인스턴스의 Point로서의 모습을 리턴한다.
public Point asPoint(){
return point;
}
public boolean equals(Object o){
if(!(o instanceof ColorPoint))
return false;
ColorPoint cp =(ColorPoint)o;
return cp.point.equals(point) && cp.color.equals(color);
}

}

|

h4. ④ 일관적(consistent) : x.equals( y )의 결과가 true면 계속 true, false면 계속 false 일관성있어야 한다.

h4. ⑤ x.equals(null)은 반드시 false를 리턴한다.


h3. 좋은 equals 메소드를 만드는 비법

*  == 연산자를 써서 인자가 this 객체를 참조하는지 검사한다. (p40 하단내용)
* instanceof 연산자를 써서 인자의 타입이 올바른지 검사한다.  (두 객체의 형이 동일한지를 비교하는 instanceof)
* 인자를 정확한 타입으로 변환한다. ( 이 항목은 instanceof로 검사했기 때문에 항상 성공)
* 주요필드에 대해 인자의 필드와 this 객체의 해당 필드의 값이 동등한 지 검사한다.
   null에 대한 참조가 허용된 객체 참조 필드에 대한 비교는 NullPointerException을 막기 위해 다음과 같은 구현패턴을 써서 비교한다.
    (field == null ? o.field == null : field.equals(o.field))
   this 객체의 필드와 인자가 가리키는 객체의 필드가 동일한 객체를 참조하는 경우는 다음과 같이 비교하는 것이 빠르다
     (field == o.field ||(field != null && field.equals(o.field)))
* equals 메소드를 다 만들었다면, 대칭성, 매개성, 일관성을 지키는지 확인한다.
* equals 메소드를 재정의하면 hashCode 메소드도 반드시 재정의한다.
* 불안정한 자원에 의존하는 equals 메소드를 작성하지 마라.  (ex: java.net.URL의 equals 메소드는 해당 URL을 가진 호스트의 IP주소비교)
* equals 메소드의 인자를 Object 타입이 아닌 다른 타입으로 선언하지 마라.

h2. 항목 8. equals 메소드를 재정의하면 hashCode메소드도 반드시 재정의하라.

참고 url : http://cafe.naver.com/javathink/4

h3. hashCode 메소드의 구현 계약은 다음과 같다.(java.lang.Object의 hashCode 메소드 명세) 

* 애플리케이션이 일단 실행된 다음 동일한 객체의 hashCode를 여러 번 호출하더라도 equals 메소드에서 
  비교하는 필드의 값을 바꾸지 않는다면, 항상 같은 정수 값을 리턴해야 한다. 
  하지만, 이 정수 값은 애플리케이션이 다시 실행되면 바뀔 수도 있다. 
* equals(Object) 메소드의 리턴 값이 true인 두 객체의 hashCode 메소드는 같은 정수 값을 리턴해야 한다. 
* equals(Object) 메소드의 리턴 값이 flase인 두 객체의 hashCode 메소드가 반드시 다른 정수 값을 리턴할 필요는 없다. 
하지만, 해시(hash) 알고리즘을 쓰는 컬렉션의 성능을 향상시키려면 동등하지 않은 객체는 다른 정수 값을 리턴하는 것이 좋다. 
* 논리적으로 등등한 객체는 반드시 해시코드가 같아야한다.

h3. 간단하면서도 거의 균등한 해시 코드 값을 만드는 hashCode 메소드 구현법

1. result라는 int 타입 변수에 0이 아닌 상수 값(예를 들면, 17과 같은 값)을 저장한다. 
2. 객체의 주요 필드(equals 메소드에서 비교하는 필드)인 모든 f들에 대해 각각 다음 작업을 수행한다. 
* a. 각 필드의 해시 코드 c를 다음과 같이 계산한다. 
  ⅰ.   f가 boolean인 경우, (f ? 0 : 1)를 계산한다. 
  ⅱ.   f가 byte, char, short, int인 경우, (int)f를 계산한다. 
  ⅲ.   f가 long인 경우, (int)(f ^ (f>>>32))를 계산한다. 
  ⅳ.   f가 boolean인 경우, (f ? 0 : 1)를 계산한다. 
  ⅴ.   f가 double인 경우, Double.doubleToLongBits(f)를 계산한 후, 2.a.iii와 같이 계산한다. 
  ⅵ.   f가 객체 참조이고, 이 클래스의 equals 메소드에서 비교하는 대상이라면, f의 hashCode 메소드의 리턴값을 계산한다. 
         만약, 더 복잡한 비교가 필요다면 f를 표준 형식(canonical form)으로 바꿔서 해시 코드를 계산한다. 
         만약, f가 null이면 0으로 계산한다(아무 상수나 괜찮지만 0으로 계산하는 것이 관례이다.) 
  ⅶ.   f가 배열인 경우, 각 구성요소 하나하나를 하나의 필드처럼 처리한다. 
         위의 규칙을 적용하여 배열의 주요 구성요소의 해시 코드를 모두 계산한 후 2.b 단계에 따라 한 번 더 계산한다. 
* b. a에 따라 계산한 해시 코드인 c와 1에서 정의한 result를 다음과 같이 더한다. 
	result = 37*result + c;

 
* c. result를 리턴한다. 

|

import java.util.HashMap;
import java.util.Map;
public class PhoneNumber {

private final short areaCode;
private final short exchange;
private final short extension;

public PhoneNumber(int areaCode, int exchange, int extension){
rangeCheck(areaCode, 999, "area Code");
rangeCheck(exchange, 999, "exchange");
rangeCheck(extension, 9999, "extension");

this.areaCode = (short) areaCode;
this.exchange = (short) exchange;
this.extension = (short) extension;
}
private static void rangeCheck(int arg, int max, String name){
if(arg < 0 || arg > max)
throw new IllegalArgumentException(name + ":" + arg);
}

public boolean equals(Object o){
if(o == this)
return true;
if(!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.extension == extension && pn.exchange == exchange && pn.areaCode == areaCode;
}

public int hashCode(){
int result = 17;

result = 37 * result + areaCode;
result = 37 * result + exchange;
result = 37 * result + extension;

return result;
}

public static void main(String[] args){
Map m = new HashMap();
m.put(new PhoneNumber(400,800,5000), "Jenny");
System.out.println("test:" + m.get(new PhoneNumber(400,800,5000))); //test:Jenny
}
}

|

page 64 ~ 66 해시 원리 참고


h2. 항목 9. toString 메소드는 항상 재정의 하는 것이 좋다.

* toString() 은 \[WEBSTUDY:해당 클래스 이름@해시코드의 부호 없는 16진수\] 로 표현한다.
* toString 메소드의 구현 계약 중에는 모든 클래스는 toString 메소드를 재정의하는 것이 좋다 라고 정의한다.
* toString 메소드는 객체가 가진 관심 있는 모든 정보를 리턴해야 한다.
* 문자열 형식을 명세화하지 않더라도 toString 메소드의 의도는 명확히 문서화하는 것이 좋다.
* toString 메소드가 리턴하는 문자열에 포함된 모든 정보에 접근할 수 있는 방법을 제공하는 것이 좋다.
	
h2. 항목 10. clone 메소드는 신중하게 재정의하라.
	
	* clone()의 사용 
	protected이기 때문에 상속된 상태에서 Object 소속의 clone()을 사용할 수 있다
	Cloneable 인터페이스가 구현된 상태에서 clone()을 사용할 수 있다.
	
	* 메모리 복사를 위한 구현 
	

class TestClone extends Object implements Cloneable{
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}





|

class Mimic extends Object implements Cloneable{//Cloneable 명시
private int id;
private String name;
public Mimic(int id, String name){
this.id = id;
this.name = name;
}
//clone() 메서드의 재정의
public Object clone() throws CloneNotSupportedException{
return super.clone(); //Object의 clone()을 이용한 메모리 복제
}
//toString() 메서드의 재정의
public String toString(){
return this.id + ":" + this.name;
}
}

public class MimicMain{
public static void main(String[] args) throws CloneNotSupportedException{
Mimic m = new Mimic(1000, "호랑이"); //객체 생성
Mimic n = (Mimic)m.clone(); //복사를 위한 메서드 사용
System.out.println(m.hashCode() + " " + m); //참조값 출력 12677476 1000:호랑이
System.out.println(n.hashCode() + " " + n); //참조값 출력 33263331 1000:호랑이
}
}

|

참조 사이트 : http://www.jabook.org 

h2. 항목 11. Comparable 인터페이스의 구현을 고려하라.

* Comparable을 구현하고 있는 클래스들은 같은 타입의 인스턴스끼리 서로 비교할 수 있는 클래스들, 주로 Integer와 같은 wrapper클래스(Boolean제외) 와 String, Date,File 같은 것들이며 오름차순이다.
* Comparable : 기본정렬기준을 구현하는데 사용
* Comparator : 기본정렬기준외에 다른 기준으로 정렬하고자 할 때 사용

- Wrappler 클래스란 : 정수형이나 실수형등의 기본형(primitive data type)인 데이타를 Object 형태의 클개스로 바꾸어주는 클래스


h3. CompareTo 메소드의 표준 구현계약(equals메소드 표준 구현계약과 비슷)

* this객체와 인자로 받은 객체 사이의 순서를 비교한다. 
  this 객체 > 인자 ==> 음수 , this 객체 == 인자  ==> 0, this 객체 < 인자 ==> 양수
* 모든 x,y 에 대해 sgn(x.compareTo( y )) == -sgn(y.compareTo( x )) 임을 보장해야한다.
  (이 식은 y.compareTo( x )가 예외를 던질 때에만 x.compareTo( y )도 같은 예외를 던져야 한다는 뜻이다.)
  1. 추이성 관계 보장해야한다.
  2. x.compareTo( y ) == 0 이면, 모든 z에 대해 sgn(x.compareTo( z )) == sgn(y.compreTo( z ))라는 뜻이다.
  3. (x.compareTo( y ) ==0) == (x.equals( y ))인 것이 좋다.


h2. 문서에 대하여

* 최초작성자 : [임영미]
* 최초작성일 : 2008년 03월 05일
* 이 문서의 내용은 [자바 유창하게 말하기 - Effective Java|http://book.naver.com/bookdb/book_detail.php?bid=130814] 교재를 스터디 하면서 정리한 내용 입니다.
* 이 문서는 [오라클클럽|http://www.gurubee.net] [자바 웹개발자 스터디|제3차 자바 웹개발자 스터디] 모임에서 작성하였습니다.
* 이 문서를 다른 블로그나 홈페이지에 퍼가실 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^\^