『자바의 신 3판』 을 읽고 내용 정리 및 공부한 내용을 정리한 글입니다.
서적: 자바의 신 3판 구입처
자바에서는 기본적으로 아무런 상속을 받지 않으면 java.lang.Object 클래스를 확장한다.
이를 확인하기 위해선, Object 클래스에 있는 메소드를 호출해볼 수 있다. 아무 선언도 하지 않은 클래스에서 toString()을 호출해보면 컴파일 및 실행까지 아무런 문제가 없는 것을 확인할 수 있다. 즉, Object 클래스를 상속받았음을 알 수 있다.
그렇다면 extends로 부모 클래스를 상속받을 때는 어떨까? 자바는 한 번에 이중 상속을 받을 순 없지만, 여러 단계로 상속을 받을 수는 있다.
예를 들어 Parent 클래스와 그를 상속받은 Child 클래스가 있다고 하자. Parent 클래스는 아무런 상속을 받지 않았지만, 실제로는 Object 클래스의 상속을 받았다.
그리고 Parent 클래스를 Child 클래스가 상속받았으므로, Child 클래스는 자동으로 Object 클래스의 메소드들을 상속받는다.
다시 말해서 Child 클래스는 Object 클래스의 자식의 자식이다.
Object ← Parent ← Child
가장 큰 이유는 Object 클래스에 있는 메소드들을 통해서 클래스의 기본적인 행동을 정의할 수 있기 때문이다.
“이 정도의 메소드는 정의되어 있어야 하고, 처리해 주어야 한다.”는 것을 정의하는 작업이 필요하기 때문에 Object 클래스를 상속받았다고 생각하면 된다.
Object 클래스에 선언되어 있는 메소드는 객체를 처리하기 위한 메소드와 쓰레드를 위한 메소드로 나뉜다. 쓰레드를 위한 메소드는 25장에서 자세히 살펴본다.
💡 쓰레드란?
프로그램이 실행되는 작은 단위 중 하나.
반환값 | 메소드 | 설명 |
---|---|---|
protected Object | clone() | 객체의 복사본을 만들어 리턴한다. |
public boolean | equals(Object obj) | 현재 객체와 매개 변수로 넘겨받은 객체가 같은지 확인한다. (같으면 True, 다르면 False) |
protected void | finalize() | 현재 객체가 더 이상 쓸모가 없어졌을 때 가비지 컬렉터(garbage collector)에 의해 호출된다. |
public Class<?> | getClass() | 현재 객체의 Class 클래스의 객체를 리턴한다. |
public int | hashCode() | 객체에 대한 해시 코드(hash code) 값을 리턴한다. 해시 코드라는 것은 “16진수로 제공되는 객체의 메모리 주소”를 말한다. |
public String | toString() | 객체를 문자열로 표현하는 값을 리턴한다. |
💡 가비지 컬렉터
자바의 메모리에 있는 쓰레기를 청소하는 로봇이라고 생각하면 된다. 객체를 만들고, 그 객체가 어디에서 쓰인 후 필요가 없어졌을 때 이 로봇이 자바 프로세스 내에 있는 객체들을 뒤져 보면서 어떤 객체를 죽일지 살릴 지를 확인해서 처리해준다.
메소드 | 설명 |
---|---|
void notify() | 이 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨운다. |
void notifyAll() | 이 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨운다. |
void wait() | 다른 쓰레드가 현재 객체에 대한 notify() 메소드나 notifyAll() 메소드를 호출할 때까지 현재 쓰레드가 대기하고 있도록 한다. |
void wait(long timeout) | wait() 메소드와 동일한 기능을 제공하며 매개 변수에 지정한 시간만큼만 대기한다. 매개 변수 시간을 넘어 섰을 때에는 현재 쓰레드는 다시 깨어난다. 여기서의 시간은 밀리초로 1/1,000초 단위다. |
void wait(long timeout, int nanos) | wait() 메소드와 동일한 기능을 제공하며, 밀리초 + 나노초만큼만 대기한다. 뒤에 있는 나노초의 값은 0~999,999 사이의 값만 지정할 수 있다. |
toString() 메소드는 Object 클래스의 메소드 중에서 가장 많이 사용된다. 해당 클래스가 어떤 객체인지를 쉽게 나타낼 수 있는 중요한 메소드다.
이 메소드가 자동으로 호출되는 경우는 다음과 같다.
참조 자료형의 더하기 연산은 String만 가능하다. String을 제외한 참조자료형에 더하기 연산을 수행하면, 자동으로 toString() 메소드가 호출되어 객체의 위치에는 String값이 놓이게 된다.
// 아래 세 가지는 모두 같은 결과를 반환한다.
System.out.println(this);
System.out.println(toString());
System.out.println("" + this);
실제 Object 클래스에 구현되어 있는 toString() 메소드는 다음과 같다.
getClass().getName() + '@' + Integer.toHexString(hashCode())
Object 클래스에 있는 getClass() 결과에 getName() 메소드를 부르면 현재 클래스의 패키지 이름과 클래스 이름이 나온다.
그 다음에는 at(@, 골뱅이)가 붙는다. 이는 앞과 뒤를 구분하기 위한 구분자로, 신경쓰지 않아도 된다.
가장 마지막 부분에는 객체의 해시 코드 값을 출력한다. hashCode()는 int 타입의 값을 리턴해주는데, 그 값을 Integer 클래스에서 제공하는 toHexString() 메소드를 활용해 16진수로 변환하는 작업이 수행된다.
💡 Integer 클래스는 20장에서 설명되어 있다. 간단히 설명하면 Int 값을 보다 쉽게 처리할 수 있도록 도움을 주는 클래스이다.
모든 클래스의 toString()을 오버라이딩할 필요는 없다. 하지만, DTO를 사용할 때는 오버라이딩해 놓는 것이 좋다. 그래야 내용 확인이 쉽기 때문이다.
==와 !=는 기본 자료형에서만 사용할 수 있다. 참조 자료형에서 사용하면 “주소값”을 비교하기 때문에 사용하면 안된다.
안의 속성값들이 같은지 비교할 때는 equals()를 사용해야 한다.
그런데 오버라이딩하지 않은 equals() 메소드는 hashCode() 값을 비교한다.
hashCode() 값은 해당 객체의 주소값을 리턴하므로, 클래스의 인스턴스 변수값들이 같더라도 서로 다른 생성자로 객체를 생성했으면 해시 코드가 다르니 두 객체는 다르다는 결과가 나온다.
따라서 Object 클래스에 선언되어 있는 equals() 메소드를 오버라이딩해 놓아야지만 제대로 된 비교가 가능하다.
이클립스나 인텔리제이같은 IDE에서는 자동으로 생성한 equals() 메소드를 생성해주는 기능이 있다.
public class MemberDTO {
public boolean equals(Object obj) {
if (this == obj) return true; // 주소가 같으므로 true
if (obj == null) return false; // obj가 null이므로 false
// 클래스의 종류가 다르므로 false
if (getClass() != obj.getClass()) return false;
// 같은 클래스이므로 형 변환 실행
MemberDTO other = (MemberDTO) obj;
// 각 인스턴스 변수가 같은지 비교하는 작업 수행
if (name == null) {
// name이 null일 때 비교 대상의 name이 null이 아니면 false
if (other.name != null) return false;
// 두 개의 email 값이 다르면 false
} else if (!name.equals(other.name)) return false;
if (phone == null) {
if (other.phone != null) return false;
} else if (!phone.equals(other.phone)) return flase;
return true;
}
}
equals() 메소드를 Overriding할 때는 반드시 다음 다섯 가지를 만족시켜야 한다.
equals() 메소드를 Overriding할 때에는 hashCode() 메소드도 같이 Overriding해야만 한다.
왜냐하면, equals() 메소드를 오버라이딩해서 객체가 서로 같다고 이야기할 수는 있겠지만, 그 값이 같다고 해서 그 객체의 주소 값이 같지는 않기 때문이다.
다시 말하면, equals() 메소드의 결과가 true인데도 불구하고 hashCode() 메소드의 값은 다르게 된다.
따라서 같은 hashCode() 메소드 결과를 갖도록 하려면 함께 오버라이딩 해줘야 한다.
자동으로 생성한 hashCode() 오버라이딩 결과는 다음과 같다.
public class MemberDTO {
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((email == null) ? 0 : email.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((phone == null) ? 0 : phone.hashCode());
return result;
}
}
💡 equals() 메소드는 객체 비교가 필요할 경우에만 Overriding해주면 된다.
hashCode() 메소드는 기본적으로 객체의 메모리 주소를 16진수로 리턴한다. 만약 어떤 두 개의 객체가 서로 동일하다면, hashCode() 값은 무조건 동일해야만 한다.
따라서, equals() 메소드를 override하면, hashCode() 메소드도 override해서 동일한 결과가 나오도록 만들어야만 한다.
hashCode() 메소드를 오버라이딩할 때는 다음 조건을 따라야 한다.
이러한 제약들 때문에, 직접 두 메소드들을 작성하는 것은 별로 권장하지 않는다. 요즘은 각종 개발 툴에서 자동으로 생성해주는 기능을 제공해주므로, 해당 기능을 사용하는 것을 권장한다.
Me: java.lang
Me: javap
Me: X
Me: 해당 객체를 복사하여 반환한다.
Me: toString()
Me: equals()
Me: int타입
💡 책에 있는 내용이 아닙니다.
책을 읽으며 설명이 더 필요하거나, 추가로 궁금한 점에 대해 질문 형식으로 작성 후, 답을 구해보고 있습니다.
참고한 사이트나 영상은 [출처]로 달아두었으며, 오류 지적은 언제나 환영합니다.
Object 클래스에 있는 메소드이다. 특정 객체에 대한 참조가 더 이상 없다고 판단할 때 가비지 컬렉션이 객체의 finalize를 호출한다. 이 메소드는 비어 있는 메소드일 뿐, 따로 오버라이딩 하지 않을 경우 아무 동작도 하지 않는다.
문서에서는 나와 있듯이 하위 클래스에서 시스템 리소스를 삭제하거나 다른 정리를 수행하기 위해 finalize 메소드를 오버라이딩하여 작성할 수 있다고 한다.
즉, 가비지 수집 대상이 되었을 때 (객체가 소멸되기 전에) 애플리케이션 개발자가 의도한 기능을 수행하여 별도의 리소스 정리 작업을 할 수 있도록 할 때 오버라이딩해서 사용하는 메소드이다.
finalize() 메소드를 오버라이드 하면 해당 객체는 가비지 컬렉터가 조금 특별하게 처리한다. 가비지 수집 즉시 회수되지 않고 종료화 대상으로 먼저 등록된다. 그래서 finalize() 를 오버라이드 하지 않는 객체보다 수명이 한 사이클 정도 더 길다고 볼 수 있다.
좀 더 상세하게 단계별로 정리하면 다음과 같습니다.
가비지 컬렉터가 finalize() 메소드를 오버라이드 한 객체와 그렇지 않는 객체를 구별할 수 있는 이유는, 객체가 finalize() 메소드를 오버라이드 하게되면 해당 객체의 생성자 바디가 성공적으로 반환되는 시점에 해당 객체를 종료화 가능한 객체 목록에 등록하는 식으로 JVM 에 구현되어 있기 때문이다.
이는 아래 문제들을 야기시킬 수 있기 때문이다.
따라서 Closable 인터페이스의 close() 메소드에 객체와 엮여 있는 리소스의 정리를 구현하고, 객체의 사용이 끝났을 때 호출해주는 식으로 정리하는게 맞다.
String 클래스에서 Object.equals()를 오버라이딩한 상태이다. 아래는 자바 API 문서에서 발췌.
뒷장에서는 print()를 통하여 출력할 때 valueOf() 라는 메소드를 출력한다고 되어 있다. valueOf()는 객체가 null일 경우 문자열 “null”을 반환하고, null이 아닐 경우 내부적으로 toString()을 출력하도록 되어 있다.
즉, toString()과 다르게 객체가 null이어도 NPE(NullPointException)을 발생시키지 않으므로, valueOf()를 사용하도록 한 것 같다.
💡 테스트 해봤을 때, 더하기 연산도 마찬가지로 문자열 null을 출력하는 것을 확인했다.
Java 8의 hashCode() 의 설명을 보면 아래와 같이 쓰여 있다.
As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.)
Object 클래스에서 정의한 hashCode 메서드는 가능한 한 실제 객체들에 대해 구별된 정수를 반환하도록 설계되어 있습니다. (이는 일반적으로 객체의 내부 주소를 정수로 변환하는 방식으로 구현되지만, 이 구현 기술은 Java™ 프로그래밍 언어에 의해 필수적으로 요구되지 않습니다.)
참고로 이 설명은 Java 21 버전에서 없어졌다.
설명을 읽어보면 자바에서는 hashCode의 구현 방식에 대한 특정한 기준을 제시하지 않고 있다는 것을 알 수 있다.
자바 언어 명세에서 hashcode()에 대한 구체적인 구현 방법을 명시하지 않기 때문이라고 한다.
즉, hashcode()는 명세에 따라 동일한 동작을 보장하지만, 그 동작을 어떻게 구현할지는 JVM 제조사에게 달려있다는 말이다.
OpenJDK에서는 System.identityHashCode(this)
를 호출해서 정수 값을 생성한다고 한다.
하단 참고 사이트의 첫번째 링크인 How does the default hashCode() work?에서 해당 메소드를 파헤쳐서 분석했는데, 해당 내용을 간략히 정리하면 다음과 같다.
System.identityHashCode(this)
는 내부적으로 총 6가지의 방식을 제공한다.
즉, OpenJDK에서는 최소한 버전 6이후부터는 hashCode와 메모리 주소는 관련이 없다.
다음과 같은 문제가 생길 수 있다.
hashCode는 명세만 정해져 있고 구현은 JVM에 따라 달라진다. 일반적으로는 객체의 내부 식별자(객체의 메모리 주소, 객체의 참조값, 또는 다른 고유한 값)를 기반으로 해시 코드를 반환하는 System.identityHashCode(this)
를 사용한다고 한다.
따라서 객체의 메모리 주소를 사용한다고 확신할 수는 없다. 다만, OpenJDK는 memory 주소와 전혀 관계가 없다.
따라서 equals()를 오버라이딩하면서 hashCode()도 함께 오버라이딩하여 일관성을 유지해야 한다.
컬렉션 프레임워크는 뒷장에서 배우겠지만, 다수의 데이터를 쉽고 효과적으로 처리할 수 있는 표준화된 방법을 제공하는 클래스의 집합이다.
그 중 HashMap에서의 동작을 살펴 보자.
아래는 ChatGPT의 도움을 받았습니다.
즉, hashCode()를 기반으로 데이터를 저장하고, hashCode()는 같은 값이 나올 수 있으므로 equals()로 동등한 지 검사한다.
이처럼 해시 기반의 컬렉션에서는 문제가 생길 수도 있으므로 둘 다 오버라이딩 해야한다.
물론, 사용하지 않는데 모두 오버라이딩 할 필요는 없다. 꼭 필요한 경우에 위 사항들을 고려해서 오버라이딩 하도록 하자.
두 객체에게 할당된 메모리 주소가 같을 때, 두 객체는 동일하다고 말한다.
다시 말해서, 같은 메모리 주소를 가리키고 있고 같은 값을 가지고 있으면 두 객체는 동일하다고 할 수 있다.
두 객체의 메모리 주소가 다르더라도, 객체의 내용이 같을 때 동등하다라고 말한다.
즉, 동일하면 동등하다. 하지만 동등하면 동일하다고 말할 수는 없다.
==
연산자는 객체의 동일성을 판별하기 위해 사용하며, equals()
는 두 객체의 동등성을 판별하기 위해 사용한다.
equals()
는 재정의하지 않으면 내부적으로 ==
연산자와 같은 로직을 수행한다. 따라서 equals()
는 각 객체의 특성에 맞게 재정의를 해야 동등성의 기능을 수행한다.
hashCode()
는 동일성이라고 할 수 있다. equals()
가 같으면 hashCode()
도 같아야 한다. 즉, 객체의 내용이 같으면 그 객체는 동일한 객체이다.
.equals와 .hashCode()는 항상 함께 오버라이딩해야한다.
☕ 자바 equals / hashCode 오버라이딩 - 완벽 이해하기
How does the default hashCode() work?
OKKY - java.lang.Object.hashcode() 값은 실제 메모리 주소와 어떤 연관이 있나요?