Crescendo Code
equals() 메서드와 hashCode() 메서드 본문
객체의 비교 : equals() 메서드와 hashCode() 메서드
Java 프로그래밍을 하다 보면 두 값을 서로 비교하기 위해 비교 연산자를 사용하는 경우가 많다.
비교 연산자 중 하나인 '==' 은 리터럴 값이나 기본 자료형의 값이 동등한지 여부를 비교하며, 사용 빈도수 또한 높은 편이다.
▶ 리터럴 값이란 1, 121, 12.3 등 변수에 아직 할당되지 않은 값 그 자체를 뜻한다.
▶ 기본 자료형이란 char, byte, int, double ... 등 문자 / 숫자 / 논리로 구성된 자료형을 뜻한다.
▶ 기본 자료형 중 논리형(boolean)에 해당하는 true / false 도 '==' 연산자로 비교할 수 있다.
그렇다면 참조형 변수, 즉 객체를 비교할 때에도 비교 연산자인 '==' 을 사용할 수 있을까?
결론부터 말하면 비교할 수 있다.
방법으로는 equals() 메서드와 hashCode() 메서드를 호출함으로써 비교할 수 있는데, 객체는 객체의 주소를 나타내는 주소값과 실제 객체 안에 들어있는 내용을 나타내는 값으로 나눌 수 있기 때문에 객체의 비교를 보다 자세히 이해하기 위해선 객체의 흐름과 각 메서드의 기능을 먼저 확실히 이해할 필요가 있다.
equals() 메서드
Java는 Object라는 최상위 클래스가 존재하며 모든 클래스를 하위 클래스로 상속하고 있다. 따라서 모든 클래스는 Object 클래스를 상속받는다. equals() 메서드와 hashCode() 메서드는 이 최상위 클래스인 Object 클래스에 소속되어 있는 메서드이며, 우리가 생성해서 사용하는 클래스도 자연스럽게 Object 클래스를 상속받게 되므로 객체를 비교하는데에 위 두 메서드를 사용할 수 있다.
equals() 메서드의 기능은 두 객체의 주소값을 비교한다.
객체는 new 라는 키워드로 새 객체를 생성할 수 있고, 생성된 객체는 힙(Heap) 메모리 / 스택 메모리에 각각 객체와 그 주소값이 저장된다. 아래는 Example이라는 객체를 생성해 각각 ex01, ex02 변수에 저장하여 변수값 및 비교 결과를 출력한 예제이다.
▶ 비교 연산자 '==' 을 사용한 비교 예제
package test01;
class Example {
}
public class Object01 {
public static void main(String[] args) {
Example ex01 = new Example();
Example ex02 = new Example();
System.out.println( ex01 );
System.out.println( ex02 );
System.out.println( ex01 == ex02 );
}
}
객체를 담은 변수를 출력하면 객체 안의 내용이 아닌 그 객체를 참조하는 참조값. 즉, 객체의 주소를 출력하게 된다. 이 주소값은 객체 생성 시 고유하게 부여되므로 같은 객체를 생성했을지라도 서로 다른 주소값을 가지게 된다.
따라서, 두 값을 '==' 연산자로 비교하면 각 개체의 주소값을 비교해 false가 출력된다.
이번엔 위 코드의 15행 부분을 equals() 메서드로 작성한 후 그 내용을 출력해 보자.
▶ equals() 메서드를 사용한 비교 예제
package test01;
class Example {
}
public class Object01 {
public static void main(String[] args) {
Example ex01 = new Example();
Example ex02 = new Example();
System.out.println( ex01 );
System.out.println( ex02 );
System.out.println( ex01.equals(ex02) );
}
}
출력 결과는 처음 '==' 연산자를 사용했던 결과와 동일하게 false가 출력된다. 즉, Object 클래스의 equals() 메서드는 비교 연산자인 '==' 과 완전히 동일한 기능을 가지고 있으며, 그 기능은 객체의 주소값을 비교한다는 것이다.
실제로 equals() 메서드의 구조를 살펴보면 '==' 연산자를 사용하고 있기 때문에 당연한 결과라고 할 수 있다.
▶ equals() 메서드 구조
public boolean equals(Object obj) {
return (this == obj);
}
그렇다면 객체의 주소가 아닌 객체의 내용 자체를 비교할 수는 없을까?
객체의 내용을 비교하기 위해선 기존 equals() 메서드의 내용 및 구조를 작업 중인 하위 클래스에서 다시 재정의(오버라이딩) 해 사용하는 수밖에 없다.
▶ 오버라이딩은 부모클래스의 메서드를 자식클래스가 입맛에 맞게 다시 덮어서 변경하는 것을 말한다. (단 메서드명과 같은 기본적인 사항은 변경할 수 없다는 제약 조건이 있으며, 안의 내용은 수정할 수 있다.)
그런데 우리가 사용하는 여러 기본형 데이터 타입 중에서 String 타입은 다른 기본형들과는 다르게 대문자로 시작하는 문자열 클래스이다. 즉, 클래스 타입을 변수 데이터 타입으로 가지는 자료형이다.
그렇다면 String 타입의 문자열을 equals() 메서드로 비교하게 되면 어떻게 될까?
각각의 문자열이 저장되어 있는 주소값을 비교하게 될까?
String 클래스는 객체임에도 불구하고 사용빈도가 많아 클래스 내부에서 equals() 메서드가 내용을 변경하는 메서드로 이미 재정의 되어 있다. 따라서, String 타입의 문자열 변수를 equals() 메서드로 비교하게 되면 그 내용 값을 비교한다.
▶ String 타입의 문자열 변수 비교 예제
package test01;
public class Object02 {
public static void main(String[] args) {
String exam01 = "Hello!";
String exam02 = "Hello!";
System.out.println( exam01 );
System.out.println( exam02 );
System.out.println( exam01 == exam02 );
System.out.println( exam01.equals(exam02) );
String exam03 = new String( "Java!" );
String exam04 = new String( "Java!" );
System.out.println( exam03 );
System.out.println( exam04 );
System.out.println( exam03 == exam04 );
System.out.println( exam03.equals(exam04) );
}
}
변수 exam01, exam02는 그 타입이 String 클래스임에도 불구하고 컴파일러가 동일한 리터럴 값을 공유하도록 최적화되어있어 단순 비교 연산자인 '==' 을 사용해도 값이 true가 출력됨을 확인할 수 있다.
변수 exam03, exam04는 new 키워드를 이용해 객체를 생성하는 방식으로 문자열을 주입하였다. 이 경우 단순 비교 연산자인 '==' 은 주소 값만을 비교하기 때문에 false를 출력하고, equals() 메서드를 통해 비교하면 내용 값을 비교해 true를 출력한다.
▶ 모두 String 객체에 한정되어 있는 기능이다.
hashCode() 메서드
hashCode() 메서드는 각각의 객체를 구별하기 위해 사용되는 정수값이다. 앞에서 equals() 메서드를 다룰 때 객체와의 구별을 주소값을 통해 한다고 했었는데, 주소값과 해시코드로 리턴 받는 정수값 모두 객체마다 유일한 값이라는 점에서 어느 정도는 해시코드를 주소값이라고 이해해도 무방하다.
▶ hashCode() 메서드 구조
public native int hashCode();
hashCode 메서드 구조를 살펴보면 native라는 예약어를 확인할 수 있다. 이 native는 자바에서 작성된 코드가 아닌 C, C++, 어셈블리 등의 다른 언어로 작성되어 있다는 의미의 자바 내 특별한 예약어이다. native의 구현은 자바 코드에서 직접 제공되지 않고, 다른 언어로 작성된 native 코드에 의존하므로 우리가 직접 그 구현을 확인할 수는 없다.
우선 hashCode()는 native 코드를 통한 일련의 과정을 통해 고유의 정수값(int)을 리턴 받는다는 것만 확인하도록 하자.
▶ hashCode() 출력 예제
class Exam01{
}
public class HashCode {
public static void main(String[] args) {
Exam01 ex01 = new Exam01();
Exam01 ex02 = new Exam01();
System.out.println( ex01.hashCode() );
System.out.println( ex02.hashCode() );
}
}
그런데 equals() 메서드의 기본 기능이 객체의 고유한 주소 값을 비교하는 것이고, hashCode() 메서드 기능 역시 고유한 정수 값을 비교하는 것이라면 언뜻 비슷해 보이는 이 두 메서드는 언제 어디서 각각 사용되는 것일까?
Set ( HashSet ), Map ( HashMap ), HashTable
자바에서는 대량의 데이터를 효율적으로 이용 및 관리할 수 있는 자료구조 방식으로써 컬렉션 프레임워크라는 것이 존재한다. 그중 Set 계열의 HashSet이나 Map 계열의 HashMap 등은 기본적으로 데이터를 저장하고 관리하는 기능을 가지고 있는데, 이때 데이터 관리에 있어서 중복 데이터를 허락하지 않는다는 특징이 있다.
Set과 Map의 특징을 간략하게 알아보자면, Set은 순서가 없는 공간이기 때문에 순서를 기준으로 데이터를 찾을 수가 없다. Set은 데이터 간의 고유성을 이용해 데이터를 찾기 때문에, 중복 데이터를 배제하는 특징을 가지고 있다.
Map은 키와 값 두 종류의 데이터를 한 쌍으로 묶어서 저장하고 있는데 '값'은 중복될 수 있으며 '키'는 중복될 수 없다.
이처럼 데이터를 저장하거나 색인할 때 데이터의 중복성을 검사해야 하는 경우가 생기는데 이때 필요한 메서드가 equals() 메서드와 hashCode() 메서드이다.
먼저, hashCode() 메서드는 위와 같은 해시 자료구조에서 해시 함수를 통해 해시 코드를 만들고, 생성된 hashCode 값을 검사한다. 그 후 자료를 저장하는 일련의 과정에서 저장소 소단위와 비슷한 개념을 지닌 버킷을 통해 데이터를 저장하게 되는데, 한정된 공간 속에서 데이터를 처리하다 보면 같은 해시 코드를 지니게 되는 해시 충돌이 상황이 발생할 수 있다.
이때 equals() 메서드는 안의 '값'을 비교해 두 데이터 및 객체가 동일한 것인지를 보다 정밀하게 파악한다.
ex01.equals(ex02) && ex01.hashCode() == ex02.hashCode()
이처럼 Java에서 두 객체를 비교할 때, hashCode() 메서드로 먼저 객체를 비교한다. 만약 여기서 hashCode 자체가 다르다면 equals()를 메서드를 수행하지 않고 바로 다른 객체로 판정한다. 이후 hashCode() 메서드 수행 결과로 같다는 결과가 나오면 equals() 메서드로 동일한 객체인지의 여부를 검사하게 된다.
다시 말해 equals() 메서드가 true인 두 객체의 hashCode() 값은 항상 같으며, String과 같은 클래스에서는 이미 equals() 메서드와 hashCode() 메서드가 오버라이딩 되어 있어 두 객체가 같음을 검사할 때 상호 보완하며 실행된다.
이처럼 객체의 동일 / 동등성 비교를 위해서는 하나의 메서드가 아니라 두 메서드를 함께 오버라이딩 해야 하는 상황이 많이 발생한다. 이클립스에서는 자동으로 두 메서드를 오버라이딩 해주는 기능이 있으니 필요하다면 사용하도록 하자.
'Back-End > Java' 카테고리의 다른 글
2진수, 8진수, 16진수, 각 진수와 10진수와의 변환 (0) | 2024.02.06 |
---|