java 강의에서 == 연산자에 대해, 이 글에서 언급을 했었습니다. 그런데, 질문을 받고 답변을 하다 보니, 헷갈리는 점이 있었습니다. 객체에서 다른 객체를 참조하는데 a == b인 경우가 있을까?

 


 사실, 이에 대한 답은 문서에 있습니다. Object 클래스에 있는 equals 메서드에 나와 있는데요.

 

 

 다른 부분은 없고, 초록색 부분과 노란색 부분만 읽어보시면 됩니다. x와 y가 같은 오브젝트를 참조한다를 명제 Q, Object 클래스의 equals 메소드가 true를 리턴한다를 명제 P라 합시다. if and only if는 필요 충분 조건을 나타냅니다. 즉, 저 메소드가 참 값을 리턴했다는 것은, x와 y가 같은 객체를 참조했다는 것입니다.

 

 

 이런 경우에만 참이 된다는 소리입니다. x == y인데 다른 객체를 참조하고 있지는 않는다고 명세에 나와 있습니다. 그런데, equals 메서드하고 객체끼리 비교하는 == 하고 무슨 관계가 있길래 그럴까요?

 

 

 Dog 클래스는 위와 같습니다. equals가 재정의 되어 있지 않습니다.

 

 

Dog 2개가 같은지를 비교하기 위해서, equals 메소드를 호출하면, Object의 equal을 부르는데요.

 

 

 이것은 대상 Object와 인자로 넘어온 Object의 동일성을 비교합니다. == 연산자로요. 즉, 강아지 객체에서 equals를 호출하면 == 연산자로 compare 한다는 명제가 성립합니다.

 

 


 공식 문서에 레퍼런스 타입에서의 == 연산자인 경우, 같은 객체를 참조하면 true를 리턴한다고 나와 있습니다. 그런데, 저는 그걸 봤음에도 뭔가 찝찝한 구석이 있었습니다. 그래서, 테스트 코드를 작성해 보기로 하였습니다.

 

 

 오브젝트를 key값으로 밀어넣었는데요. 파이썬의 deepcopy를 자바에서 구현하려고 할 때, 중요하게 쓰일 수 있는 트릭 중 하나입니다. 왜 이렇게 프로그램을 작성하였는지, 그리고 이것이 어떻게 동작하는 지에 대해서 간단하게 설명해 보도록 하겠습니다.

 

 

 먼저, hash값을 계산하는 것은 이 글에서 설명을 드렸으니 참고하시면 좋을 듯 싶습니다. key는 Dog이고, Dog는 hashCode가 구현되어 있지 않으므로, 오브젝트 클래스에 있는 hashCode를 호출합니다.

 

 

 다음에 putVal 하는 메서드에서, key.equals(k)를 호출하는 구문을 보시면 되는데요. 키 값이 Dog 객체이고, Dog가 equals가 재정의 되어 있지 않으므로, Object의 equals를 호출하게 됩니다. 즉, 대략적인 구조를 도식화 해서 그려보면 아래와 같습니다.

 

 

 그림에서 K는 키값입니다. hash 계열의 자료구조는 hashCode 값으로 어느 버킷인지 찾은 다음에, 그 버킷에서 해당 객체와 동등한 객체를 찾게 되는데요. Dog는 equals이 재정의가 되어 있지 않으므로, == 연산자로 비교하는 Object의 equals를 호출하게 됩니다. 실제로, 디버그 포인트를 찍어보면 아래와 같은 호출 스택을 볼 수 있습니다.

 

 

 여기서 634번째 줄이 뭔가 익숙한데요.

 

 

 버킷에 키가 1개 이상 있을 때 이 메서드를 호출함을 알 수 있습니다. 여기까지 정리해 보면, Dog는 equals가 재정의가 되어 있지 않았고, 버킷에 키가 하나 이상 있었으므로, equals를 호출했다고 할 수 있어요.

 

 

 그리고 hashCode 설명을 보면, same object에 대해서 호출하면, 같은 정수값을 리턴한다는 것을 알 수 있어요. 즉, a.hashCode()와 b.hashCode()가 다른 정수값을 리턴했다면, a와 b는 같은 객체가 아니라는 이야기가 됩니다. 결국 어떤 객체가 hash 값으로 H라는 것을 떨궜다면, hash 값이 H가 아닌 것에 대해서 탐색하는 것은 의미가 없습니다.

 

 저는 단지, hashCode 값이 다르다면 다른 객체다. equals를 재정의 하지 않으면, 두 객체를 비교할 때 ==을 이용한다. 버킷 내에서 비교할 때, equals를 호출한다. 를 이용했습니다. 이 3개의 특성을 효율적으로 활용할 수 있는 것은 HashMap이다였습니다. 결국 저는 다른 객체인데 ==가 참이 나오는 경우가 있는지를 빠르게 판단하기 위해서 Hash 계열의 자료구조를 쓴 셈입니다.

 

 

 5분이 지나도, 10분이 지나도 아무런 반응이 없었습니다. 결론은 공식 문서를 믿자는 것이였습니다.

 

 


 여담이지만, 파이썬에서 deepcopy도 이러한 사실을 절찬리에 잘 써먹고 있습니다. 문서에서 id 메서드의 설명을 보면 같은 life time에 있는 다른 오브젝트 a, b에 대해서 id(a)와 id(b)는 다르다고 되어 있습니다.

 

 

 d값을 어디에 쓸까요?

 

 

 memo 딕셔너리에 쓸 때 key값으로 쓰고 있습니다. 이는 복사해야 하는 객체들의 연결 그래프가 사이클을 포함하는 경우가 있을 수 있기 때문입니다. 이 경우를 처리하기 위해, memo를 두었다는 것을 간파하실 수 있습니다. 여기서 중요한 것은 memo에 객체의 id 값을 넣었다는 점입니다.