글 작성자: Key Ryung

 

 

Java에서의 동등성? 동일성?

 

Introduction

C와는 다르게 Java는 Pointer(주소)에 대한 정보가 내부적으로 숨겨져 있기 때문에 비교에 있어서 차이가 난다. 언어적인 부분에 있어서 포인터를 숨김으로써 얻을 수 있는 장점도 있었지만 주소의 비교와 값의 비교에 있어서 구분을 해줘야 하는 직관을 포기할 수 밖에 없었다는 생각이 이 부분을 살펴보면서 들게 되었다. 그리고 어느 순간 동일성에 대해서 동등성 설명을 하고 동등성에 대해서 동일성으로 말하는 실수를 자주 저지르는 것 때문에 이번에 한국어로도 제대로 정리해야겠다 싶어 정리하게 되었다.
 

Class Type, Primitive Type

Java 세계에서는 두 가지 타입이 있다. Class Type과 Primitive Type!
타입에 따라 저장되는 메모리가 다르기도 하지만 그런 차이는 Wrapper Type(Integer, Long, Double)과 Primitive(int, long, double) Type을 살펴보면서 다뤄보도록 하겠다.
 

Object

Java 모든 Class Type의 부모는 Object이므로 많은 메서드들을 상속받게 된다. 그 중 equals() 메서드를 통해 같은지 판단해주는데 여기서 같다고 말하는 것은 무엇일까?
 

Object의 Equals

public boolean equals(Object obj) {
    return (this == obj);
}
 
궁금하면 Intellij에서 직접 살펴보면 된다. 코드를 보니 this 나 자신과 들어온 Object 인스턴스와 == 연산을 수행하여 결과를 반환한다. == 연산은 말 그대로 같은지 비교 하는 연산인데 Class Type의 instance 끼리 ==연산을 하는 것은 주소를 비교하는 것이다. 다른 프로그래밍 언어, 예를 들면 C++, Kotlin에서는 연산자 오버라이딩을 통하여 연산자의 동작을 바꿀 수도 있지만 Java에서는 지원하지 않으니 논외로 생각하자!
결국 주소를 비교한다는 것을 통해 기본적으로 Object가 같은지 판단하고 있다. 개념적으로 생각해보면 같은 주소에 놓인 데이터는 항상 **"동일"**하다. 코드적으로 살펴보자!
 
public static void main(String args[]) {
    Person key = new Person();
    Person lani = new Person();
}
 
이 코드에서 key라는 Person Type의 instance 하나와 lani라는 instance를 생성자를 호출하여 선언했다. 여기서 keylani라는 변수에는 어떤 값이 들어있을까? 앞서 말했듯 Java에서는 Pointer(주소)를 내부적으로 숨기고 있다. 위 두 변수 안에는 실제 정보를 저장하고 있는 주소를 가지고 있다. 임의로 주소를 표시해본다면 다음과 같이 나타낼 수 있다.
 
public static void main(String args[]) {
    // 1. 두 명의 사람을 선언
    Person key = new Person();   // key : 0x04
    Person lani = new Person();  // lani : 0x08
    
    // 2. 두 사람을 프린해보기
    System.out.println(key);
    System.out.println(lani);
}
keylani는 전혀 다른 곳에 저장된 데이터다. 실제로 2번과 같이 프린트해본다면 어떤 숫자들이 출력되는데 지금은 주소와 관련된 값이 출력된다라고 간단하게 알고 넘어가자! 따라서 이 상황에서 equals() 메서드와 ==연산자를 사용해보면 결과가 어떻게 나올까?
 
public static void main(String args[]) {

    Person key = new Person();   // key : 0x04
    Person lani = new Person();  // lani : 0x08    

    // 두 사람을 비교해보기
    System.out.println(key.equals(lani)); ??
    System.out.println(key == lani);      ??
}
현재 상황에서는 당연히 둘 모두 false로 출력될 것이다. (왜냐하면 둘(equals,==) 모두 주소를 비교하고 다른 주소를 가리키고 있기 때문에 false라는 결과가 나온다.)
그렇다면 하고 싶은 연산이 두 사람이 같은 이름을 갖고 있는가에 대한 비교나 두 사람이 같은 나이를 갖고 있는가 라면 어떻게 해야될까? 바로 equals() 메서드에 대한 "오버라이딩"이다. 주소의 비교는 이미 == 연산자로 가능하고 바꿀 수도 없으니 equals()라는 메서드를 통해 실제 비교하고 싶은 값의 판단을 해야 한다.
 
 

Object의 Equals의 5가지 조건

이렇게 값에 대한 비교를 진행할 때 JavaDoc에서는 5가지 조건을 만족하도록 비교하라고 명시해놨다.
  • 재귀(reflexive) : null이 아닌 x라는 개체의 x.equlas(x) 결과는 항상 true여야 한다.
  • 대칭(symmetric) : null이 아닌 x와 y 객체가 있을 대 x.equals(y)가 true를 리턴했다면, y.equals(x)도 true를 리턴해야 한다.
  • 타동적(transitive) : null이 아닌 x,y,z가 있을 때 x.equals(y)가 true를 리턴하고, y.equals(z)가 true를 리턴하면, x.equals(z)는 반드시 true를 리턴해야만 한다.
  • 일관(consistent): null이 아닌 x와 y가 있을 때 객체가 변경되지 않은 상황에서는 몇 번을 호출하더라도 x.equals(y)의 결과는 항상 true이거나 항상 false여야만 한다.
  • null과의 비교 : null이 아닌 x라는 객체의 x.equals(null) 결과는 항상 false여야만 한다.

 

 

One More Thing

그리고 가장 중요한 것이 있다.
🚧 바로 equals() 메서드를 Overriding 할 때에는 hashCode() 메서드도 항상 같이 Overriding 해야한다

 

 

동일성, 동등성

앞서 설명했던 주소의 같음동일성, 값의 같음동등성 이라고 말한다. 영어로 말하면 identity, equality라고 말하면서 구분이 쉬웠는데 막상 한국어로 설명하려니 어려웠었다. 근데 다시 한자로 생각해보면 동일하다는 것은 같은 주소에 있는 완전 무결하게 같은 것이라는 점에서 이해가 쉽게 되었다.