Object
자바는 모든 클래스는 Object를 상속받는다. 즉 Object는 최상위 클래스임.
이로 인해 모든 객체에 필요한 공통 기능 (equals, hashcode, toString …)을 제공하고 모든 객체에 대해 다형적 참조가 가능하여 다형성의 기본적인 매커니즘을 지원한다. 하지만 Object로 모든 객체를 다형적 참조한다면 Object 내부의 메서드만 사용할 수 있기 때문에 다운캐스팅을 해야한다는 단점이 존재한다.
toString()
Object 내에는 다음과 같이 toString() 메서드가 구현되어 있다.
- 패키지 포함 객체 이름@16진수 해시값
- test.Member@30f39991 → src 하위의 test 패키지.클래스이름@해시코드
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
- System.out.println() → 내부에서 toString() 호출
public void println(Object x) {
String s = String.valueOf(x); // <- toString 호출
if (getClass() == PrintStream.class) {
writeln(String.valueOf(s));
} else {
synchronized (this) {
print(s);
newLine();
}
}
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
equals() : Object에서 동등성 비교를 위한 메서드
- 동일성 : == 연산자를 사용하는 것으로 두 객체의 참조가 서로 동일한 객체인지를 확인.
- 같은 메모리에 있는 인스턴스인지 참조값으로 비교
- 동등성 : equals() 메서드를 사용해서 두 객체의 참조가 논리적 동등인지를 확인.
- 논리적인 기준에 맞추어서 같은지 비교
public class Test {
public static void main(String[] args) {
Member member1 = new Member("kim", 25);
Member member2 = new Member("kim", 25);
System.out.println(member1 == member2); // false
System.out.println(member1.equals(member2)); // false
}
}
위 코드에서 equals()는 논리적인 기준으로 비교한다고 하였다. 근데 이름과 나이가 모두 같은 객체인데 왜 false가 나오는 것일까?
equals()는 기본적으로 동일성 비교를 한다.
public boolean equals(Object obj) {
return (this == obj);
}
따라서 equals()를 재정의하여 논리적인 기준을 정의해줘야 한다!
IDE는 equals()를 쉽게 재정의할 수 있도록 도와준다.
public class Member {
private String name;
private int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return age == member.age && Objects.equals(name, member.name);
}
}
이렇게 이름과 나이가 모두 같으면 같은 사람이라고 toString을 재정의하면 처음 코드에서 equals 비교는 동등성 비교로 바뀌게 되어 true가 나오게 된다.
System.*out*.println(member1.equals(member2)); → true
System.out.println(”문자열”) 에서 객체 주소가 아닌 문자열이 출력되는 이유도 String에서 equals()를 오버라이딩 하였기 때문이다.
HashCode()
객체의 주소 값에 해싱을 통해 코드를 만든 후 반환하는 hashcode() 메서드를 지원한다.
(실제 주소값은 아님)
public class Test {
public static void main(String[] args) {
Member member1 = new Member("kim", 25);
Member member2 = new Member("kim", 25);
System.out.println(member1.hashCode());
System.out.println(member2.hashCode());
}
}
출력 결과
821270929
1160460865
해시 코드를 재정의하지 않으면 Object의 기본 해시 코드는 객체의 참조값을 이용해 해시 코드를 생성한다.
위에서 member1, member2는 각각 다른 인스턴스이므로 참조가 다르고 해시 코드 역시 달라지게 된다.
이렇게 만든 해시 코드는 숫자이므로 배열의 인덱스로 사용하는 것이 가능하다. (해시코드 - 음수도 가능함)
→ 배열에서 검색은 순회해야하므로 O(n)
→ 만약 인덱스와 내부의 값이 같다면 검색을 O(1)로 줄일 수 있음
→ 컬렉션 프레임워크에서 HashSet, HashMap의 경우에는 키에 값을 저장할 때 위에서 생성된 해시 코드를 이용하여 해시 인덱스를 생성하고 이 해시 인덱스를 배열의 인덱스로 사용해서 값을 넣는다.
- 해시 함수 : 임의의 데이터를 입력으로 받아서 이를 고정된 길이의 해시 코드로 출력하는 함수
- 해시 충돌 : 입력된 데이터가 다르더라도 해시 코드가 같을 수 있는데 이를 해시 충돌이라고 함.
- 해시 인덱스 : 보통 해시 코드 값에 배열의 크기를 나누어 구하고 이 구해진 결과는 데이터 저장 위치를 결정하는데 사용
해시 코드 역시 equals()처럼 IDE의 도움으로 해시 코드를 쉽게 재정의할 수 있다.
@Override
public int hashCode() {
return Objects.hash(name, age);
}
Objects.hash() → 여러 필드를 받아서 해시 코드 생성
Objects.hashCode() → 단일 필드를 이용해 해시 코드 생성
이펙티브 자바에 따르면(아이템 11) equals()를 재정의하면 hashCode() 역시 재정의를 하라고 하는데 이러한 이유는 무엇일까?
다음과 같은 상황을 보자.
- equals()만 구현한 경우
public class Test {
public static void main(String[] args) {
Set<Member> members = new HashSet<>();
Member member1 = new Member("kim", 25);
Member member2 = new Member("kim", 25);
members.add(member1);
members.add(member2);
System.out.println(member1.equals(member2));
System.out.println(members);
}
}
출력 결과
true
[Member{name='kim', age=25}, Member{name='kim', age=25}]
Set 자료 구조는 중복을 허용하지 않는 자료 구조이다. 근데 Member에서 이름과 나이가 같으면 같은 사람이라는 논리적인 비교 기준을 정의하였고 이로 인해 두 객체는 같다는 결과가 나오지만 HashSet 내부에는 중복된 값이 들어가 있는 것을 확인할 수 있다.
hashCode()만 구현한 경우
메인 메서드는 그대로 하고 멤버 클래스에서 equals()는 주석 처리, hashCode() 메서드만 재정의하면 출력 결과는 다음과 같다.
false
[Member{name='kim', age=25}, Member{name='kim', age=25}]
두 객체는 == 비교를 하였기 때문에 equals() 값은 당연히 false가 나온다. 근데 HashSet 에도 역시 데이터가 중복으로 저장되었다.
Hash 자료 구조는 해시 코드를 가지고 해시 인덱스를 만든 후 이 인덱스에 따라 저장이 된다고 했는데 그럼 중복 저장이 되면 안되는 것이 아닌가? 라고 생각할 수 있다.
이렇게 중복 저장되는 이유는 해시 자료구조는 다음과 같은 순서로 객체를 비교하기 때문이다.
- hashCode() 값 비교.
- 같은 해시 코드라면 equals() 비교
- 2번 역시 같다면 같은 객체임
즉 해시 인덱스는 동일하지만 equals()에서 둘은 다른 객체라고 생각하여 Set은 해당 값을 저장하는 것이다.
equals(), hashCode() 모두 구현한 경우
public class Member {
private String name;
private int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Member{" +
"name='" + name + '\\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return age == member.age && Objects.equals(name, member.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
출력 결과
true
{Member{name='kim', age=25}=1, Member{name='Lee', age=24}=1}
논리적으로 같은 member1, member2 객체의 값이 중복으로 저장되지 않은 것을 확인할 수 있다.
즉 해시 자료 구조에 데이터를 저장하는 경우 hashCode() 를 구현해야하며,
equals()를 재정의하는 경우에도 hashCode() 역시 재정의하여 서로 같은 객체임을 확실히 해야한다.
'JAVA' 카테고리의 다른 글
[자바] 자바의 스레드 생명주기와 메서드 (0) | 2024.12.25 |
---|---|
[자바] Iterable, Iterator, Comparable, Compartor (0) | 2024.12.24 |
[자바] 제네릭 (0) | 2024.12.24 |
[자바] Exception (0) | 2024.12.24 |
[자바] 중첩 클래스 (0) | 2024.12.24 |