본문 바로가기
개발/Java

자바 기본과 객체 지향 (3) - 동일성과 동등성

by 컴쏘 2024. 10. 31.

Java로 개발 하다보면, 처음에 == 과 eqauls()의 차이에 부딪히게 된다. == 이생각한 대로 동작하지 않아 당황하게 되기 때문이다.

 

이에 대한 차이를 이해하기 위해 동일성과 동등성이 무엇인지 살펴보고, == 과 equals()가 어떻게 다른지 알아보도록 하자.

 

동일성 vs 동등성

  • 동일성: 두 객체가 완전히 같은 경우를 의미한다. 여기서 "완전히 같다"는 것은 두 객체가 사실상 하나의 객체로 간주될 수 있다는 것이다. 즉, 두 객체의 메모리 주소가 같다는 것을 의미한다.
  • 동등성: 두 객체가 같은 정보를 갖고 있는 경우를 의미한다. 객체의 주소가 다르더라도 내용이 같으면 동등하다고 할 수 있다. 즉, 두 객체의 값이 동일하다는 것이다.

 

그렇다면, Java에서 == 은 왜 생각한 대로 동작하지 않을까? 

 

== vs equals()

Java에서의 == 은 두 객체의 참조가 같은지(동일한 객체를 가리키고 있는지)를 확인한다. 이는 == 이 객체의 메모리 주소를 비교하기 때문이다. (객체의 실제 내용보다는 메모리 주소에 초점) 

  • 동일성 비교이다. 
  • 기본 타입의 값이나 객체의 참조 자체를 비교할 때 사용 

반면에, equals()는 객체의 내용이나 상태를 기반으로 두 객체가 같은지를 판단한다. 

  • 동등성 비교이다. 
equals()에서 주의해야 할 점은 equals()를 오버라이딩해서 객체의 상태나 값을 비교하는 로직을 구현해야 한다는 점이다. 

자바에서 모든 객체는 Object 클래스를 상속 받기 때문에 equals() 메소드를 기본적으로 가지고 있다. 
이때 기본 equals() 메소드는 == 과 동일하게 객체의 참조를 비교한다. 

 

 

여기서 equals()를 재정의할 때, 자주 볼 수 있는 것은 hashCode()에 대한 재정의이다. 

hashCode()가 무엇이고, hashCode() 재정의에 대해서 알아보자.

 

HashCode 

HashCode는 객체를 식별하는 하나의 고유 정수 값이다. 

 

따라서, Java의 hashCode()는 객체를 대표하는 정수 값을 반환하며, 이 값은 해시 기반의 Collection에서 객체를 효율적으로 관리하는 데 사용된다. (객체의 주소 값을 이용하여 해싱 기법을 통해 해시 코드를 생성하며 각 객체는 고유한 다른 해시 코드 값을 가진다.)

 

그렇다면 hashCode() 재정의는 무엇일까? 

 

hashCode() 재정의

위에서 언급했듯이, 해시 기반의 Collection을 사용하는 HashMap, HashSet, HastTable 등은 모두 해시 기반으로 hashCode()와 equals()를 동시에 사용하는데, 만약 equals() 만 재정의한 경우, 기존 규칙을 위반하여 원하는 결과가 나오지 않을 수 있다. 

 

hashCode()와 equals() 순서 ❘ 출처 : https://haon.blog/haon/java/equality-identity/

 

 

위의 그림을 보면 hashCode() 리턴값이 같아야, equals() 리턴 값을 비교하게 된다. 

 

만약, hashCode()가 재정의되지 않았다면, HashSet과 같은 자료구조를 사용하였을 때, 같은 내용의 객체를 생성하더라도 다른 HashCode 값이 부여가 되어 다른 값으로 인식해버린다. 따라서, HashSet()이 원하는 대로 동작하지 않을 수 있다. 다음과 같은 코드에서 1이 출력되는 것이 아니라 2가 출력된다. 

 

public static void main(String[] args) {
    Set<Car> cars = new HashSet<>();
    cars.add(new Car("foo"));
    cars.add(new Car("foo"));

    System.out.println(cars.size());
}

예시 출처 

 

따라서, equals()를 재정의할 경우에는 hashCode()도 재정의하는 것을 권장한다고 한다. 

 

꼭 hashCode() 재정의해야 할까?

해시 기반의 Collection을 정말 사용하지 않을 것이라면 필요하지 않을 수 있다. 하지만, 상황은 항상 고정적이지 않고 언제나 변할 수 있기 때문에 만일을 대비해두어서 같이 재정의하는 것을 추천한다. 

 

 

이번에는 ==과 equals()의 차이점에 대해 알아보고, equals()와 hashCode()의 재정의에 대해서도 살펴보았다.

 

재정의와 관련하여 toString() 메서드도 많이 사용되므로, 자세히 알아보고 싶다면 하단의 내용을 확인해보자.

 

추가로 알아보기 - toString() 오버라이딩 

더보기

toString()

 

기본적인 Object 클래스의 toString() 메소드는 해당 인스턴스에 대한 정보를 문자열로 반환한다. 

  • toString()은 인스턴스에 대한 정보를 문자열로 제공할 목적으로 정의되어있다. 
  • 그냥 객체만 출력해도 toString()을 붙인 것과 똑같은 값이 출력되는데, 이는 컴파일러가 객체만 출력할 경우 자동으로 toString()을 붙여주고 컴파일하기 때문이다. 
  • [클래스이름]@[16진수 해시 코드]의 형태로 반환된다. 예시) MyObject@251a69d7
    • (16진수 해시코드는 인스턴스의 주소를 해싱하여 변환한 값)

그렇다면, toString() 오버라이딩은 무엇일까? 

toString() 오버라이딩 

만약, 객체의 이름이나 주소 값이 아닌, 객체의 고유 정보를 출력하고 싶을 때가 있다. 

이때는 오버라이딩을 통해 toString() 메소드를 재정의해주면 된다. 

 

예시) Person 객체의 이름과 나이를 출력하고 싶다면 toString() 재정의를 통해 이름과 나이를 출력하게 할 수 있다. 

 

메서드 오버라이딩 주의사항! 

부모 메서드에 정의된 접근제어자의 범위보다 낮은 범위의 제어자를 지정할수 없다! 

  • protected이면 오버라이딩했을 때 public이나 protected으로 설정해야 한다. (private이나 default이면 컴파일 에러 발생)

 

참고 1
참고 2 
참고 3
참고 4