[Java] == 연산자 / equals() 메서드 차이점

dondonee·2023년 12월 1일
0
post-thumbnail

== 연산자 / equals() 메서드


자바에서 '값이 같다'는 기준은 무엇일까? 다음 두 경우를 생각해 볼 수 있다.

  1. 참조 값이 같다. (reference equality)
  2. 내용 값이 같다. (value equality)

그래서 자바에서는 비교 방법이 두 가지가 존재한다. == 연산자와 Object 클래스의 equals() 메소드이다.

  • == 연산자 : 두 대상의 참조가 같은지 비교한다.
  • equals() 메서드 : internal value(내부 값)가 같은지 비교한다.

그런데 자바의 데이터 타입은 primitive type(원시 타입)과 object type(객체 타입) 두 종류가 있다. 원시 타입은 참조를 통하지 않고 직접 값을 가지고, 클래스가 아니기 때문에 equals() 메서드를 호출할 수 없으며, null 값도 가질 수 없다.

따라서 자바에서 비교 연산을 할 때는 다음 세 항목을 고려해야 한다 :

  1. == 연산자 비교인지 equals() 비교인지
  2. 원시 타입인지 객체 타입인지
  3. null 비교인지 아닌지


== 연산자


== 연산자는 참조를 기준으로 두 값을 비교하는 동일성 연산자이다.

(1) Primitive type

int a = 10;
int b = 15;
assertFalse(a == b);

int c = 10;
assertTrue(a == c);

int d = a;
assertTrue(a == d);

원시 타입은 참조를 갖지 않는다. 따라서 ==를 통한 비교는 단순히 값의 비교이다. int d = a와 같이 다른 변수를 대입해도 단순하게 값을 비교한다.

null 비교

원시 타입은 null을 가질 수 없기 때문에 null 비교는 불가능하다.


(2) Object type

두 대상 객체를 == 연산자로 비교하면 참조를 통해 동일성을 비교하고, 내부 값은 무시된다. 비교 결과가 true이면 두 대상 객체는 같은 인스턴스이다.

Person a = new Person("Bob", 20);  //(1)
Person b = new Person("Mike", 40);
assertFalse(a == b);

Person c = new Person("Bob", 20);  //(2)
assertFalse(a == c);

Person d = a;                      //(3)
assertTrue(a == d);

(2)의 경우 ac는 내부 값은 같지만 서로 다른 객체이기 때문에 참조가 다르고, 따라서 비교 결과는 false이다. 한편 (3)의 경우 변수 da의 참조를 할당했다. 두 변수는 같은 객체를 가리키고 있으므로 비교 결과는 true이다.

null 비교

null 비교는 객체 타입에서만 유효하다. 객체 타입 변수가 메모리에 초기화 되어있는지 체크할 때 사용할 수 있다.



equals() 메서드


별개의 객체가 같은 값을 가지고 있는 것을 비교할 때 사용한다.

(1) Primitive type

int a = 10;
Integer b = a;  //캐스팅

assertTrue(b.equals(10));

원시 타입은 값 하나만 가지고 있는 non-class 타입이기 때문에 equals()를 포함해 어떤 메서드도 사용할 수 없다.

하지만 모든 원시 타입은 자신의 객체 타입인 래퍼 클래스(wrapper class)를 가지고 있다. 래퍼 클래스로 캐스팅한다면 equals() 메서드를 사용할 수 있다. (동작은 일반 객체 타입과 같다.)


(2) Object type

public boolean equals(Object obj) {
        return (this == obj);
}

Object 클래스의 equals() 메서드는 기본적으로는 객체의 참조 값만 비교한다. 두 대상을 비교할 때 참조는 물론 내부 값까지 상세하게 비교하고 싶다면 메서드를 오버라이드하면 된다.

equals() 오버라이드

public class Person {
    private String name;
    private int age;

    // constructor, getters, setters...
}
public class Person {
    // other fields and methods omitted

    @Override
    public boolean equals(Object o) {
        if (this == o) 
            return true;
        if (o == null || getClass() != o.getClass()) 
            return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
}

위와 같이 Person 객체를 만들고 equals()를 오버라이드 하였다. 따라서 이제 Person 객체를 비교할 때는 다음과 같은 법칙을 따른다 :

  1. 참조가 같은지 비교한다. 같으면 true 반환.
  2. 같은 클래스인지 비교한다. 다르면 false 반환.
  3. (참조는 다르지만 같은 클래스인 경우) age, name 값이 같은지 비교한다. 모두 같으면 true 반환.

Person a = new Person("Bob", 20);  //(1)
Person b = new Person("Mike", 40);
assertFalse(a.equals(b));

Person c = new Person("Bob", 20);  //(2)
assertTrue(a.equals(c));

Person d = a;                      //(3)
assertTrue(a.equals(d));

재정의한 equals()의 비교 예시는 위와 같다. 만약 오버라이드하지 않았다면 (2)의 결과는 false이다. 내부 값이 같아도 참조가 다르기 때문이다.

  • 참고) 자바의 해시 기반 컬렉션은 참조 비교와 해시코드 비교를 모두 사용하기 때문에 equals(), hashCode()는 함께 오버라이드해야 한다. Lombok의 @EqulasAndHashCode 애노테이션을 사용하면 간편하게 해결할 수 있다(@Data에 포함되어 있음).

null 비교

Person a = new Person("Bob", 20);
Person e = null;
assertFalse(a.equals(e)); //(1)
assertThrows(NullPointerException.class, () -> e.equals(a)); //(2)

오버라이드한 equals()를 통해 위의 비교를 실행했다. (1)의 경우 참조가 다르기 때문에 결과는 false이다. (2)의 경우를 보자. (1)과 같은 비교이지만 순서를 뒤집었더니 NullPointerException이 발생하여 의도한 비교를 수행할 수 없다.

assertFalse(e != null && e.equals(a));

예외를 방지하려면 위와 같이 && 연산을 통해 참조 비교를 함께 묶어주면 된다. e = null이므로 앞 조건이 false이고, 전체 조건이 false가 되어 예외가 발생하지 않고 false를 반환한다.


assertFalse(Objects.equals(e, a));
assertTrue(Objects.equals(null, e));

또한 Java 7 부터는 위와 같이 Objects#equals()를 통해 null-safe 비교가 가능하다. 두 대상이 모두 null인 경우는 true를 반환한다.




정리

대상 데이터 타입비교 기준null 비교
==primitive값 비교
object참조 비교참조 비교(메모리 초기화 여부 체크)
equals()primitive
object@Override → 값 비교참조 비교(NullPointerException 주의)
  • == 연산자는 동일성을 비교한다.
    • 원시 타입의 경우 단순히 값만 가지고 있기 때문에 값이 동일하면 같은 것이다. 객체 타입의 경우 객체를 가리키는 참조를 가지고 있기 때문에 참조 값을 비교하여 동일성을 체크한다.
    • 객체 타입의 참조 값이 같으면 같은 객체이기 때문에 당연히 내부 값도 같다. (반대로 내부 값이 같아도 참조가 다르면 다른 객체이며 ==를 통한 비교 결과는 false가 된다.
  • equlas() 메서드는 기본적으로는 참조만을 비교한다. 그러나 일반적으로 이 메서드를 오버라이드하여 내부 값이 같은지 검사하기 위해 사용한다.
    • 원시 타입은 메서드를 가질 수 없기 때문에 equals()를 통한 비교가 불가능하다.
    • 객체 타입의 경우 오버라이드하여 내부 값을 비교하도록 한다.
    • 객체를 equals()를 통해 null과 동일한지 비교하는 것은, 객체가 메모리에 초기화 되어있는지를 검사하는 의미이다. 단 직접 비교하면 NullPointerException이 발생하므로 주의한다.


🔗 Reference

0개의 댓글