자바에서 '값이 같다'는 기준은 무엇일까? 다음 두 경우를 생각해 볼 수 있다.
그래서 자바에서는 비교 방법이 두 가지가 존재한다. ==
연산자와 Object
클래스의 equals()
메소드이다.
==
연산자 : 두 대상의 참조가 같은지 비교한다.equals()
메서드 : internal value(내부 값)가 같은지 비교한다.그런데 자바의 데이터 타입은 primitive type(원시 타입)과 object type(객체 타입) 두 종류가 있다. 원시 타입은 참조를 통하지 않고 직접 값을 가지고, 클래스가 아니기 때문에 equals()
메서드를 호출할 수 없으며, null
값도 가질 수 없다.
따라서 자바에서 비교 연산을 할 때는 다음 세 항목을 고려해야 한다 :
==
연산자 비교인지 equals()
비교인지null
비교인지 아닌지==
연산자는 참조를 기준으로 두 값을 비교하는 동일성 연산자이다.
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
비교는 불가능하다.
두 대상 객체를 ==
연산자로 비교하면 참조를 통해 동일성을 비교하고, 내부 값은 무시된다. 비교 결과가 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)의 경우 a
와 c
는 내부 값은 같지만 서로 다른 객체이기 때문에 참조가 다르고, 따라서 비교 결과는 false
이다. 한편 (3)의 경우 변수 d
에 a
의 참조를 할당했다. 두 변수는 같은 객체를 가리키고 있으므로 비교 결과는 true
이다.
null
비교는 객체 타입에서만 유효하다. 객체 타입 변수가 메모리에 초기화 되어있는지 체크할 때 사용할 수 있다.
별개의 객체가 같은 값을 가지고 있는 것을 비교할 때 사용한다.
int a = 10;
Integer b = a; //캐스팅
assertTrue(b.equals(10));
원시 타입은 값 하나만 가지고 있는 non-class 타입이기 때문에 equals()
를 포함해 어떤 메서드도 사용할 수 없다.
하지만 모든 원시 타입은 자신의 객체 타입인 래퍼 클래스(wrapper class)를 가지고 있다. 래퍼 클래스로 캐스팅한다면 equals()
메서드를 사용할 수 있다. (동작은 일반 객체 타입과 같다.)
public boolean equals(Object obj) {
return (this == obj);
}
Object
클래스의 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
객체를 비교할 때는 다음과 같은 법칙을 따른다 :
true
반환.false
반환.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
에 포함되어 있음).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
이 발생하므로 주의한다.