[개념정리] Java "==", Equals와 Hashcode

flaxinger·2022년 1월 7일
0

개념정리

목록 보기
2/3

Java equals와 hashCode

C++을 자주 사용한 사람의 입장에서는 자바의 "==" 연산자와 equals()가 헷갈릴 수 있다. C++에서는 "=="와 compare() 함수가 동작은 조금 다르지만(리턴 값이 다르다) 모두 변수의 값을 비교하기 때문이다. 이와 달리 자바의 "=="와 equals()는 모두 클래스의 주소가 동일한지를 리턴한다. 다만 일부 클래스(해당 글에서는 String과 Integer를 다룬다)의 경우 equals() 함수가 override 되어있어 직관적이지 못하다.

예로 C++로 아래와 같이 코드를 짰다고 하자.

#include <bits/stdc++.h>

using namespace std;

int main(){
  
  string a = string("same");
  string b = string("same");

  // 값 비교
  cout << ((a == b)?"true":"false") << endl;
  // 값 비교
  cout << ((a.compare(b))?"false":"true") << endl;
  // 주소 비교
  cout << ((&a == &b)?"true":"false") << endl;
  
}	

이에 대한 output은 아래와 같다. 두 문자열의 값은 동일하고 주소는 다르다. Compare() 함수는 문자열의 값이 같을 때 0을 반환하므로 위와 같이 처리했다.

true
false

하지만 아래와 같은 자바 코드를 짠다면?

import java.io.*;
import java.util.*;

class Main{
  
  public static void main (String[] args) throws java.lang.Exception{

    String a = new String("same");
    String b = new String("same");
	
    // 주소 비교
    System.out.println(a == b);
    // 주소 비교..??
    System.out.println(a.equals(b));
    // Hash 비교
    System.out.println(a.hashCode() == b.hashCode());
  }
}

아래 출력을 살펴보자. a와 b는 각각 다른 인스턴스이니 주소값이 다르므로 "=="는 false를 리턴한다. 근데 왜 equals와 hashCode()는 다른 값을 리턴하는가!? 주소가 다른데!

false
true
true

많은 자바 클래스가 그렇듯 String 클래스의 equals()와 hashCode() 함수는 String의 값을 비교하도록 override되어 있다. 이와 관련하여 모두 설명하면 너무 길어지니 구체적인 내용은 아래 자료에서 확인할 수 있다. 요약을 하자면 Object의 equals() 함수는 비교하는 두개의 Object 인스턴스가 동일한 인스턴스인지(즉 "=="와 같이 주소가 동일한지)를 확인한다. 단 이는 경우에 따라 override 되어어야하는데, 이 때 equals() 함수를 override하면 hashCode() - HashMap, HashSet 등에서 사용한다 - 함수 또한 변경해주어야 한다. 이는 만약 equals()가 true라면 hashCode() 또한 true여야한다는 규칙에 따른 것이다.

추가 자료
망나니 개발자 - [Java] equals와 hashCode 함수
자바 공식 문서 - Object의 hashCode함수
자바 공식 문서 - String의 equals함수

Java String Constant Pool과 Integer Caching

String Constant Pool

그럼 자바의 "==" 연산자가 힙에 위치한 주소를 비교하는 것과 equals()가 두 문자열의 값을 비교하는 것은 잘 알겠다. 이 때 마지막으로 주의해야하는 부분이 있다.

아래의 코드를 실행하면 어떤 결과가 나올까.

import java.io.*;

class Main{
  
  public static void main (String[] args) throws java.lang.Exception{

    String a = "same";
    String b = "same";

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

우선 a와 b는 동일한 값이므로 equals() 참이 나오는 것을 예상할 수 있다. 근데 문제는 a == b도 true라는 것이다. 왜? 자바 String은 Immutable한 특성을 가지고 있다. 여기서 주의할 것은 Immutable의 정의인데, 아직 구체적인 내용을 모른다면 Ready Kim님의 Java의 String 이야기(1) - String은 왜 불변(Immutable)일까? 글을 참고하길 바란다. Immutable특성 때문에 자바 String은 여러개의 변수가 동일한 힙 주소를 참조할 수 있다. (참고로 자바 String을 선언하면 Stack에는 힙의 주소가 저장되고 힙에 실제 스트링의 값이 저장되는데, 이때 동일한 값의 스트링을 여러개 선언하면 모두 동일한 힙 주소를 참조한다는 말이다) 단 new String("same")을 여러번 사용하면 각각 다른 주소를 보는데, 이는 위에 설명된 이유로 매우 비효율적이다. 이 때 힙에 String의 값을 저장하는 장소를 String Constant Pool이라고 한다.
둘러둘러 설명했는데, 위의 a==b가 true인 이유는 결국 a와 b가 모두 동일한 힙 주소를 참조하고 있기 때문이지 값이 동일해서가 아니다.

String Constant Pool이 왜 필요한지에 대해 간략하게 추가 설명을 하자면, new String()을 사용하면 아래 코드에서 힙에 "repeat" 값의 문자열을 힙에 100,000번 등록해야한다. 극단적인 예지만 어떤 차이인지 쉽게 알 수 있다.

for(int i = 0; i < 100000; i++){
      System.out.println("repeat");               // creates one instance in String Constant Pool
      System.out.println(new String("repeat"));   // creates 100000 insatnces times in heap
    }

Integer Caching

그렇다면 아래 코드는 어떤 결과가 나올까.

import java.io.*;
import java.util.*;

class Main{
  
  public static void main (String[] args) throws java.lang.Exception{

    Integer a = 1;
    Integer b = 1;    

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

  }
}

결론부터 말하자면 true, true가 나온다. 아니 "=="는 주소를 비교한다면서요.. 여기서 "=="는 주소를 비교하는 것이 맞다. 이는 자바에서 Integer값을 String Constant Pool과 유사한 원리로 캐싱하기 때문이다. 이 때 마찬가지로 new Integer()를 사용하면 캐싱하지 않고, 코드가 비효율적이게 된다. 또 이처럼 primitive type을 wrapping할 때 new를 사용하지 않는 것을 autoboxing(wrapper class에서 primitive으로 바꿀 때는 auto-unboxing)이라고 부르는데, 궁금한 분은 찾아보시길 바란다. 마찬가지로 byte, boolean, char 또한 특정 범위의 값으로 autoboxing된다면 자동으로 캐싱된다. 다른 그렇다면 아래 코드는 어떤 결과가 나올까?

import java.io.*;
import java.util.*;

class Main{
  
  public static void main (String[] args) throws java.lang.Exception{

    Integer a = 1000;
    Integer b = 1000;    

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

  }
}

이번에는 true, false가 나온다. 고마해라 제발 이는 캐싱이 -128에서 127까지의 값에만 이루어지기 때문이다. (character도 ASCII 기준 128번째 문자까지 모두 캐싱된다.)

아래는 편안한 double 비교문이다.

    Double a = 1.0;
    Double b = 1.0;    

    System.out.println(a.equals(b));		// true
    System.out.println(a == b);  		// true

결론

  • Java에서 일반적으로 "==" 연산자는 주소를, equals()는 값을 비교한다
  • "==" 연산자를 사용할 때 일부 클래스는 두개의 인스턴스를 선언해도 스택에 동일 주소값이 저장되어 있을 수 있음을 유의해야한다.
  • 두개 클래스의 값은 언제나 equals로 비교하되, 커스텀 클래스의 equals()를 override할 때는 hashCode() 바꿔줘야한다.
    • 단 어디까지나 현업에서 자바를 쓸 때 해당하는 것으로 코딩테스트 등에서 hashCode()가 필요 없을 때는 굳이 바꾸지 않아도 된다.

참고자료

망나니 개발자 - [Java] equals와 hashCode 함수
자바 공식 문서 - Object의 hashCode함수
자바 공식 문서 - String의 equals함수
Java의 String 이야기(1) - String은 왜 불변(Immutable)일까?
Java Integer Cache

profile
부족해도 부지런히

0개의 댓글