JAVA에서 == 와 .equals()에 대한 고찰
고찰하게된 배경
프로그래밍을 입문한 python도 그렇고 javascript도 같음을 비교할 때 == 하나로 처리하는데, 자바에서는 equals()메소드로 비교해야할 경우와 ==으로 비교해야할 경우에 대해 잘 이해하고 있어야합니다. 아무생각없이 ==으로만 같음과 다름을 비교하고 싶은데, 어떤 이유로 자바에서는 equals()를 통해 같음을 판별하는지 실험실을 만들어 자세히 들여다 보고 싶었습니다.
== 의 정의 vs equals() 의 정의
- == : 변수가 가리키고있는 주소가 같으면 true
- equals() : Override 하지 않는한, == 와 동일하게 판별
primitive type에서의 ==과 equals()
당연한 결과같지만.. 아래 실험과 같이 primitive type에서는 == 비교는 값의 비교로 확인됩니다.
@Test
void primitive_type_는는비교() {
// given
int a = 1;
int b = 1;
// when
boolean actual = (a == b);
// then
assertTrue(actual);
}
primitive type에서의 equals()는?? 당연히 안됩니다. 왜냐하면 class도 아니고 Object를 상속받지도 않았으니까… 그래서 검증불가!!
@Test
void primitive_type_이퀄비교() {
int a = 1;
int b = 1;
// boolean actual = (a.equals(b)); //a는 primitive type이라 equals메소드가 없다 -> 빨간줄 간다
// assertTrue(actual);
}
결론
- prmitive type에서는 ==비교만 가능하고, 주소 비교가 아닌 값의 비교로 동작합니다.
일반클래스에서의 == 과 equals()
실험을 위해 간단한 상품클래스를 만들었습니다.
@Getter
class Item {
private long id;
private String name;
private int price;
@Builder
public Item(long id, String name, int price) {
this.id = id;
this.name = name;
this.price = price;
}
}
모든 항목의 값이 동일하게 맞추어두고 == 비교를 해보았습니다.
@Test
void 일반클래스타입_는는비교() {
// given
Item airpot1 = Item.builder().id(1).name("에어팟프로").price(320000).build();
Item airpot2 = Item.builder().id(1).name("에어팟프로").price(320000).build();
// when
boolean actual = (airpot1 == airpot2);
// then
assertFalse(actual); // 클래스 타입으로 생성된 객체의 는는 비교는 주소(reference)의 비교라서, 각각 생성된 객체의 주소값은 당근 서로 다를 것이다. 그러니까 false
}
결과는 서로 같지않다!!! 본 글의 앞 부분에서 정의한대로 일반 클랙스로 생성한 객체(변수)의 == 비교는 각 객체가 가리키고 있는 주소의 비교이기 때문에 각각 생성된 객체의 주소는 서로 다를테니 당연히 ==비교의 결과는 false
Item class는 equals를 override하지 않았기에 Object class의 Equals를 그대로 상속받을 것입니다. 그러면 Object class의 equals메소드는 어떻게 구현되어있을까요?? 아래처럼 비교대상과 == 비교하는 끝…두둥…
...
public boolean equals(Object obj) {
return (this == obj);
}
...
그래서 equals로 비교하기 테스트를 실행
@Test
void 일반클래스타입_이퀄비교_equal을override하지않음() {
// given
Item airpot1 = Item.builder().id(1).name("에어팟프로").price(320000).build();
Item airpot2 = Item.builder().id(1).name("에어팟프로").price(320000).build();
// when
boolean actual = (airpot1.equals(airpot2));
// then
assertFalse(actual); // 같을 것 같지만... Object class의 equals()는 this == obj 라서 사실상 는는 비교다. 결국 객체의 주소(reference)를 서로 비교하니, 당근 "서로 다르다"로 되는 것
}
결과는 == 하고 동일했습니다. 당연하겠죠.. 둘다 결국 == 비교일테니까…
결론
- 일반 클래스에서의 ==과 equals는 동일하게 동작한다.
equals를 override해서 비교해보기
equals를 모든 항목이 같을 때, 같다!! 라고 재정의한 새로운 Item class를 만들어보았습니다. 간단히 설명하면.. ItemEnhanced 타입이 아니면 무조건 같지 않다라고 처리하고, ItemEnhanced 타입일 경우 각 항목을 일일히 비교해서 모든 값이 같으면 같다라고 true로 리턴하는 방식입니다.
@Getter
class ItemEnhanced {
private long id;
private String name;
private int price;
@Builder
public ItemEnhanced(long id, String name, int price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ItemEnhanced) {
ItemEnhanced target = (ItemEnhanced) obj;
boolean isSameId = this.id == target.getId();
boolean isSameName = this.name.equals(target.getName());
boolean isSamePrice = this.price == target.getPrice();
return isSameId && isSameName && isSamePrice;
} else {
return false;
}
}
}
==은 바로 위 실험에서 증명되었으니까, 재정의한 equals로 어떻게 동작하는지만 실험해보겠습니다.
@Test
void 일반클래스타입_이퀄비교_equal을override해서_모든항목의값이같을때_같다_라고_정의() {
// given
ItemEnhanced airpot1 = ItemEnhanced.builder().id(1).name("에어팟프로").price(320000).build();
ItemEnhanced airpot2 = ItemEnhanced.builder().id(1).name("에어팟프로").price(320000).build();
// when
boolean actual = (airpot1.equals(airpot2));
// then
assertTrue(actual); // 모든 항목의 값이 모두 같을때 "같다"라고 equals를 override 했으므로, 두 객체의 equals비교의 결과는 true
}
결과는 당연히 서로 같다!! 라고 결과가 나왔습니다. equals는 주관적입니다. equals 재정의를 이름만 같아도 같다!! 라고 정의하면 가격이 달라도 같다!!라고 판별되도록 처리할 수있습니다.
결론
- equals를 재정의하면 다양한 형태로 같음을 판별하도록 할 수있습니다.
그렇다면 String은??
먼저, String은 primitive type일까요?? 땡!! String은 엄연히 클래스입니다. 일단, String은 두가지 방법으로 선언할 수 있습니다.
- 리터럴 방식
- new 방식
리터털 방식은 대체로 흔히 사용하는 방법으로 String a = “안녕”;의 형태를 뜻합니다. 리터럴 방식으로 간단한 실험을 해보았습니다
@Test
void 그렇다면_String클래스의리터럴선언방식에서_는는비교는() {
// given
String a = "123";
String b = "123";
// when
boolean actual = (a == b);
assertTrue(actual);
}
분명 클래스인데,== 비교가 같다!! 로 판별되었습니다… 헉.. 그래서 찾아보니, 리터럴선언 시 intern()메소드가 호출되면서 string constant pool에 이미 동일한 문자열이 존재하는 확인해보고, 있으면 재활용하기때문에, 위 케이스처럼 “123”으로 문자열이 같으면 pool내의 동일한 녀석을 가리키고 있을 것. 그러니까 는는비교도 같다고 판단되는 것입니다.
리터럴로 선언된 값이 같은 두 변수를 equals로 비교해보겠습니다.
@Test
void 그렇다면_String클래스의리터럴선언방식에서_이퀄즈비교는() {
// given
String a = "123";
String b = "123";
// when
boolean actual = (a.equals(b));
assertTrue(actual); //
}
결과는 서로 같다.. 인데요.. String클래스에서 재정의한 equals가 주소비교인 == 비교가 아닌 데이터값 비교로 같음을 판별하기 때문입니다. 겸사겸사 String class의 오버라이드한 equals 메소드를 보겠습니다. 주소가 같은 경우를 빼고는 인코딩 타입에 따라 별도 equals가 수행되는데, .value부분을 보면 값을 가지고 같음을 판별한다고 간접적으로 유추할 수 있습니다.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
리터럴의 2가지 케이스를 확인해보았으니, 이제는 new로 생성하는 방식을 실험해보겠습니다. new로 생성한 뒤 ==비교를 하면, 서로 같지 않다!!라고 판별됩니다. new로 생성하면 heap에 각각 다른 주소에 할당이 될테니 == 비교는 주소를 비교하는 판별식이니까 당연히 == 비교의 결과는 같지 않음!!이 되어야합니다.
@Test
void 그렇다면_String클래스를new로생성_는는비교는() {
// given
String a = new String("123");
String b = new String("123");
// when
boolean actual = (a == b);
// then
assertFalse(actual); // 힙에 각각 생성되니까, 당근 주소가 서로 다를테고, 그러니까 는는비교 시 false가 됨
}
new생성한 String 을 equals비교로 해보겠습니다. 결과는 같다!! 인데요.. 이유는 이전 실험에서 이미 설명했던 String class에서 재정의(오버리이딩)한 equals가 주소비교가 아닌 데이터값 비교이기 때문에 주소가 다르더라도 값은 같으니까 같음의 판별은 true가 됩니다.
@Test
void 그렇다면_String클래스를new로생성_이퀄즈비교는() {
// given
String a = new String("123");
String b = new String("123");
// when
boolean actual = (a.equals(b));
// then
assertTrue(actual); // 같다. 이유는 String class에서 override한 equals는 주소비교가 아닌 데이터값 비교이기때문에 당근 같다.
}
마무리
- ==비교와 equals는 그때그때 달라요
- 기본적으로 ==은 주소비교이나, primitive type일때는 값비교로 동작합니다.
- Object class에서 상속해주는 equals는 == 하고 같습니다.
- equals를 override해서 어떠한 경우가 같다라고 판단할 수 있는지 개발자가 정의해줘야한다.
- String은 리터럴 방식일 경우와 new로 생성하는 방식에 따라 ==의 동작이 서로 다릅니다.
- 하지만 equals는 String class에서 재정의한 equals를 따르기에 둘다 동일하게 동작합니다.