[Java] 동일성과 동등성 (==, equals(), hashcode())

조민서·2023년 3월 5일
5

JAVA

목록 보기
13/17
post-custom-banner

동일성(identity)과 동등성(equality)의 차이점이 뭘까요?

동일성(identity)

사전을 보면 1. 무엇이 무엇과 서로 같다.
즉, 객체 자체가 동일한 경우 동일하다고 합니다.

동등성(equality)

사전을 보면 1. 등급이나 정도가 같다.
즉, 객체의 값이 동일한 경우 동등하다고 합니다.


아래코드는 동등성? 동일성?

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

그럼 아래 코드는 동등성? 동일성?

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

똑같은 Integer인데도 결과가 다르다.


아래 코드는 동등성? 동일성?

User user1 = new User("a", 1000); //name, age
User user2 = new User("a", 1000);
Integer a = new Integer(1);
Integer b = new Integer(1);

System.out.println(user1 == user2); //false
System.out.println(a == b); //false

System.out.println(user1.equals(user2)); //false
System.out.println(a.equals(b)); //true

왜 Object랑 Integer 결과가 다르지?


결론

==, equals() 차이점

==은 참조값을 비교하고, eqauls()는 값을 비교한다.
즉, ==의 목적은 동일성이고, equals()의 목적은 동등성이다.

Object.equals(), Integer.equals(), String.equals() 실제 코드

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

//Integer.equals()
public boolean equals(Object obj) {
	if (obj instanceof Integer) {
		return value == ((Integer)obj).intValue();
	}
	return false;
}

//String.equals()
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;
}

Object.equals(), Integer.equals() 차이점

위에서 equals는 값을 비교한다고 했다. 근데 Object.equals()는 예외적으로참조값을 비교한다.

Integer.equals()는 값만 비교한다.
즉, Object.equlas()는 동일성이 목적, Integer.equlas()는 동등성이 목적이다.
Integer 뿐만 아니라, Boolean, Byte, Long, Double 등의 래퍼클래스Stringequals()값만 비교하도록 오버라이딩 되어 있다.

처음에 똑같은 Integer끼리 동일성을 (==) 비교했을때 왜 결과가 달랐지?

우선 Integer는 래퍼(wrapper)클래스다. 우선 래퍼클래스는 기본적으로 기본형을 감싸고 있다.
래퍼클래스는 스택에 힙메모리에 있는 객체의 주소를 저장해놓고 힙에 있는 객체를 참조합니다.
여기에서 자바는 Integer 클래스는 내부적으로 캐시 기능을 이용하는데, [-128, 127]범위의 숫자는 캐시에 등록해놓고 똑같은 객체를 참조합니다.

주의

Integer a = new Integer(-128);
Integer b = new Integer(-128);
System.out.println(a == b); //false
System.out.println(a.equals(b)); //true
System.out.println(a.hashCode() == b.hashCode()); //true

주의하자. 위와 다르게 a와 b는 new를 통해 힙에 새로운 객체를 생성한 것으로 동일성은 성립하지 않고, 동등성만 성립한다.


Object.equals()는 동일성, Interger.equals()는 동등성 OK! 그럼 hashcode()는 뭔데?

hashCode() 메서드는 객체의 해시 코드를 반환합니다. 해시 코드는 객체를 구분하기 위해 사용되는 값으로, 객체의 내용이 변경되지 않으면 일관된 값을 반환해야 합니다.
일반적으로 해시 코드는 해시 테이블이나 해시 맵 같은 자료구조에서 객체를 검색할 때 사용됩니다.

User user = new User("a", 1000); //name, age
System.out.println(user.hashCode()); //1521118594

user.name = "b";
user.age = 2000;
System.out.println(user.hashCode()); //1521118594

user = new User("b", 2000);
System.out.println(user.hashCode()); //1940030785

위 결과를 보면, 처음 생성한 user객체와 객체의 값을 변경한 경우 결과는 같습니다.
하지만 user에 새로운 객체(new 연산)를 초기화 시켜주면 힙에 있는 다른 메모리를 참조하므로 해시코드 값이 달라집니다.
하지만 실제 프로젝트 에서는 객체의 내부의 값이 변경되면 다른 해시코드가 출력되도록 hascode()를 오버라이딩해서 새로 구현해주는게 좋습니다.
즉, equals()뿐만 아니라 hashcode()도 객체 내부상태에 의존해야 합니다.

만약 해시코드가 같으면 어떡하나요?

해시코드가 같을 수 있습니다. 이런 경우를 해시 충돌이라고 하는데, 이 경우 equals() 메서드를 통해 객체들을 비교합니다.
따라서 hashCode()와 equals() 메서드를 모두 오버라이딩하여 구현해야 하며,
두 메서드 모두 객체의 내부 상태에 의존해야 합니다. 이렇게 구현된 클래스는 해시 테이블에서 안전하게 사용될 수 있습니다.

equals(), hashcode() 오버라이드 예시

@Override
public boolean equals(Object obj) {
	if (this == obj) {
		return true;
	}

	String objName = obj.name;
	int objAge = obj.age;
	if (obj instanceof User) {
		if(objName.equals(this.name)
			&& objAge == this.age) { return true;}
	}

	return false;
}

@Override
public int hashCode() {
	int hash = 17;
	hash = 31 * hash + name.hashCode();
	hash = 31 * hash + age;
	return hash;
}

결과

User userA = new User("A", 0); //name, age
System.out.println("userA.hashCode() = " + userA.hashCode());
userA.age = 20;
System.out.println("userA.age = 20");
System.out.println("userA.hashCode() = " + userA.hashCode());

User userB = new User("A", 20);
System.out.println("userA.equals(userB) = " + userA.equals(userB));
System.out.println("userA.hashCode() = " + userA.hashCode());
System.out.println("userB.hashCode() = " + userB.hashCode());
System.out.print("userA.hashCode() == userB.hashCode() : "
	+ (userA.hashCode() ==  userB.hashCode()));
System.out.println("userA == userB : " + (userA == userB));

출력
userA.hashCode() = 18352
userA.age = 20
userA.hashCode() = 18372
userA.equals(userB) = true

userA.hashCode() = 18372
userB.hashCode() = 18372
userA.hashCode() == userB.hashCode() : true
userA == userB : false

이제는 객체의 값을 변경만 해도 해시코드가 다르게 출력되는 것을 볼 수가 있다.
즉, equals()와 hashcode()는 객체 내부상태에 의존한다.

userA와 userB의 해시코드가 같게 나옵니다. (주의)

왜냐하면 우선 userA와 userB의 이름과 나이는 같습니다.
그럼 hashcode() 계산식에서 차이점은 name.hashCode() 밖에 없는데, 같은 문자열이면 해시코드 값이 같다는 보장은 없지만, 일반적으로 같은 문자열에 대해서는 같은 해시코드 값을 반환합니다.
그래서 위 코드는 좋지 못한 hashcode()입니다. 왜냐하면 충돌이 자주 일어날 수 있으니까요.

@Override
public int hashCode() {
	int hash = 17;
	Random random = new Random();
	int randomHash = random.nextInt(1000);
	hash = 31 * hash + name.hashCode();
	hash = 31 * hash + age + randomHash;
	return hash;
}
/*
출력
userA.hashCode() = 19024
userB.hashCode() = 19271
userA.hashCode() == userB.hashCode() : false
*/

좋은 예시는 아니지만 이런식으로 랜덤을 이용하면 해시코드가 다릅니다.

equals(), hashcode() 둘다 true면 같은 객체?

위를 보면 equals(), hashcode() 둘다 true지만 서로 다른 객체(다른 힙주소를 참조, userA, userB)입니다.
즉 동일하지 않습니다. userA == userB : false

깃허브 코드

코드 보기

profile
내 두뇌는 휘발성 메모리다. 😪
post-custom-banner

0개의 댓글