본문 바로가기
Java/Java Basic

[Java Basic] 25. java.lang.Object 클래스

by Rosmary 2022. 8. 12.
728x90
반응형

 

 

 

Java에서 유일하게 import 없이 사용할 있는 패키지는 java.lang 패키지다. 기본 패키지인만큼 많이 사용하는 String, System 등의 클래스들이 대거 포함되어 있다. 이번 포스팅에서는 java.lang 패키지의 기장 기본적인 클래스인 Object를 비롯하여, 자주 사용하는 java.lang 패키지 클래스와 매서드에 대해 알아보려 한다. 먼저 최상위 클래스인 Object부터 살펴보자.

 

[ java.lang.Object 클래스 ]

 

java.lang.Object 클래스는 Java의 최상위 클래스이기 때문에, Java에서 생성된 모든 클래스의 상위 클래스로 지정된다. 따라서 Object 클래스에 정의된 모든 매서드는 하위 클래스에서 사용 및 오버라이딩(Override)이 가능하다. Object 클래스에 정의된 매서드는 아래와 같다. 

 

 

중복내용을 제외하면 8개의 매서드가 Object에 정의되어 있는데, 이 중 notify(), notifyall()과 wait() 매서드는 추후 필자가 작성할 쓰레드(Thread) 관련 포스팅에서 다시 다룰 예정이다.

 

 

1. equals() 매서드

 

equals 매서드는 서로 다른 두 객체, Object 타입을 비교하는 매서드다. 특정 객체를 참조하여 매서드를 호출하며, 인자는 다른 객체의 인스턴스를 지정하면 된다. String 사용 시 자주 사용했던 매서드라 친숙한데, String 클래스의 equals()는 Object 클래스의 equals()를 오버라이딩 했기 때문에 결과에서는 조금 차이가 있다.

 

Object의 equals를 먼저 보자. 필자는 의미없는 클래스를 하나 정의하고, 이 클래스의 인스턴스 2개를 생성해서 equals()로 비교해보려한다.

 

 

 

위에서 필자가 정의한 Ex 클래스로부터 생성된 두 개의 인스턴스인 ex1, ex2는 인스턴스멤버 변수를 저장하는 메모리 주소가 각기 다르다. 동일한 클래스에서 생성된 인스턴스임에도 불구하고 참조변수인 ex1, ex2가 가르키는 주소가 다르기 때문에 당연히 비교 결과가 false 값으로 나온다. 

 

ex2 변수의 참조 메모리 주소를 ex1 변수의 참조 매모리 주소와 동일하도록 값을 설정하고 비교 코드를 실행하면, 방금 전의 결과가 달리 true 값이 나타난다.

 

이번에는 equals() 매서드로 인스턴스의 value 값을 비교해보자. 그냥 생각하기에는 아무 에러가 발생하지 않을 듯 하지만 의외로 컴파일도 진행되지 않는다.

 

 

 

이 원인은 equals() 매서드에 정의된 인자 때문이다. equals() 매서드는 Object 객체 타입을 인자로 가진다. 하지만 인스턴스 변수는 기본형 int이므로 Object 객체가 아니기 때문에 당연히 에러가 발생하게 된다. Java Documentation에서 equals() 매서드가 정의된 형태는 아래와 같다.

 

 

 

정리하자면 equals() 매서드는 Object 클래스 또는 해당 클래스를 상속받는 클래스 인스턴스를 비교하여 결과를 반환하는 기능을 한다.

 

그런데, 한 가지 의문이 생긴다. String 객체에서 동일한 문자열을 비교하면, 항상 참 값이 반환되는데, 비교하는 두 문자열 역시 서로 다른 인스턴스이기 때문에 변수가 참조하는 주소값이 다르다. 우선 코드를 작성해서 결과를 보자.

 

 

 

분명 equals() 매서드는 인스턴스의 매모리 주소를 비교하는 기능을 가지기 때문에 위에서 필자가 정의한 두 매서드인 compareAdd, compareContent는 모두 false 코드가 나타나야 할 것이라 생각하게 된다. 하지만 str1 == str2가 false 값으로 나타났음에 반해, equals() 매서드 결과는 true 값이 나타났다. 

 

이는 String 클래스에 존재하는 equals() 매서드가 Object 클래스의 equals() 매서드를 바로 사용하는 것이 아니라 String 클래스에 정의된 오버라이딩 된 equals() 매서드를 사용하기 때문이다. String의 equals() 매서드는 인자값의 주소를 비교하는 것이 아니라 문자열의 순서를 비교한 결과를 반환한다. 따라서 다른 클래스의 equals() 매서드와 다른 결과를 나타내게 되는 것이다.

 

 

 

 

Ex 클래스 내에 있는 value 값이 동일하면 equals() 매서드 결과값이 true로 나타나게 만드는 코드는 String에서 equals() 매서드를 오버라이딩 한 코드와 동일한 코드다.

 

 

 

 


2. toString() 매서드

toString 매서드 역시 앞선 여러 포스팅에서 예시를 통해 사용 예를 알아보았다. 실제로 Java의 toString은 인스턴스 객체가 유래한 클래스의 이름 + @ + 메모리 주소를 16진법으로 전환한 문자열을 표시한다. 아무런 오버라이딩이 되지 않은 이 toString() 매서드는 인스턴스 참조변수를 System.out.printf()로 출력한 것과 동일한 결과가 나타난다.


 

 

toString() 매서드의 정의를 Java Documentation에서 확인해보면 아래와 같다.

 

 

toString() 매서드는 오버라이딩을 통해 클래스에 대한 정보를 출력하는 것 외에는 사실 크게 확인할 내용이 없다. 그럼에도 불구하고 포스팅에서 소개한 이유는, Java Documentation의 안에 정의된 toString의 내용 때문이다. 결과값이 "클래스이름@메모리주소" 형태로 반환되는데, 그 값이 문서상에 정의되어 있다. 

 

getClass().getName() + '@' + Integer.toHexString(hashCode())

 

이 한 줄의 문구에서 필자는 hashCode()와 getClass()에 대해 추가 설명을 더 진행해보려 한다.

 

 

3. hashCode() 매서드

 

Object 클래스에 존재하는 hashCode() 매서드는 객체의 메모리 주소를 hash 값을 반환하는 함수를 적용한 결과를 돌려주는 기능을 한다. 저 hash라는 것을 조금 쉽게 설명하자면, 특정한 값을 일정한 절차(함수)에 따라 암호화 한 결과라 보면 된다. IT에서는 보통 특정 파일의 위/변조 여부를 확인하거나 계정 비밀번호를 암호화하는데 사용한다. 조금 더 자세한 내용은 Python 카테고리이긴 하지만 이 포스팅을 참조하자.

 

 

hashCode()는 public으로 정의되어 있는 Object 클래스 매서드이므로, Java의 어느 클래스에서도 이 매서드를 사용하는 것이 가능하다. Java에서의 hashCode() 매서드는 생성된 인스턴스가 저장되는 메모리의 주소를 hash 값으로 변환 및 반환한다. 

 

 

 

실제로 toString() 호출 시 나타나는 Integer.toHexString() 매서드의 인자로 각 인스턴스의 hashCode() 변환값을 넣으면, 인스턴스 toString() 출력 시 나타나는 @ 뒤의 문자열이 동일하게 출력되는 것을 확인할 수 있다.

 

 

 

* 2022.08.14 추가 내용

 

테스트를 진행해보니, String 객체에 대해서는 hashCode()가 메모리 주소를 반환하지 않고, 문자열 자체를 hashing 한 값을 반환한다. 

 

 

 

이를 통해 유추할 수 있는 사실은, hashCode()가 인스턴스 객체의 이름정보 및 메모리 주소를 반환하는 것이 아니라, toString() 값을 hashing 한 값을 반환한다는 것이다. 실제 위의 예제에서도 보면 str1, str2는 각기 다른 인스턴스이므로 메모리 주소가 다르기 때문에 비교 연산자를 사용하면 같지 않다(false)는 결과가 나타나는 것을 확인할 수 있다.

 

 

 

 

4. getClass() 매서드

 

다음으로 toString의 반환값 앞단에 정의된 getClass() 매서드에 대해 알아보자. getClass() 매서드 역시 인스턴스가 유래한 클래스에 대한 정보를 반환하는 기능을 한다.

 

 

특이하게도 이 getClass() 매서드는 반환값이 Class라는 이름의 클래스 객체다. 이 클래스는 Java에서 생성한 각 클래스의 정보를 담고 있는 객체로 각 클래스마다 1개씩 생성된다. 생성한 클래스 파일이 실행되면 클래스 로더(Class Loader)를 통해 메모리에 올라가게 되는데, 이 때 Class 객체가 자동으로 생성된다. 즉, 클래스 파일의 내용을 읽어서 일정 포맷으로 변환한 것이 Class 객체라고 보면 된다.

 

getClass() 매서드를 인스턴스를 통해 호출하고, System.out.print() 문 인자로 확인해보면 "class + 유래한 클래스명"이 문자열 형태로 반환되는 것을 알 수 있다.

 

 


toString() 반환값 정의 부분을 보면 getClass().getName() 으로 기록된 부분이 보이는데, getName()은 getClass() 매서드 반환값 중 class를 제외한 이름만 추출하는 역할을 한다.

참고로 인스턴스를 참조하지 않고 클래스명을 직접 참조하여 동일한 결과를 반환하는 방법은 아래와 같다.

 

 

 

5. clone() 매서드

 

Object 클래스의 매서드 중 마지막으로 살펴볼 매서드는 clone()이다. 이 매서드는 매서드를 호출한 인스턴스를 복제하여 새로운 인스턴스를 생성한다. 

 

 

이 매서드는 CloneNotSupportedException 예외가 throws로 정의되어 있기 때문에 사용을 위해 try-catch 문을 사용해야 한다. 동시에 반환되는 값이 Object 객체라는 것도 문서 상으로 확인할 수 있다.

 

 

또한 해당 매서드의 Documentation을 보면 Cloneable이라는 인터페이스를 반드시 구현하도록 되어 있다. 즉, 이 매서드는 Cloneable 인터페이스가 구현된 특정 클래스 내에 매서드 오버라이딩을 통해 사용을 해야 하는, 상당히 까다로운 매서드다.

 

단순한 Clone() 사용 시 컴파일 에러가 발생한다.

 

 

clone() 매서드의 사용 예시를 살펴보자.

 

 

clone() 매서드는 Object 객체를 반환해야하기 때문에 반드시 Object 클래스 인스턴스가 오버라이딩 클래스 내부에 정의되어야한다. 또한 clone() 매서드의 예외처리도 진행해야하므로 Obejct 인스턴스의 초기화는 반드시 null로 진행되어야 한다. 그렇지 않은 경우, Object 객체를 반환할 수 있는 방법이 없기 때문이다. 이렇게 반환된 Object 객체는 반드시 clone() 객체를 호출한 코드에서 클래스 형 변환을 진행해주어야 한다. 

 

참고로 Java의 배열 객체는 Cloneable 인터페이스를 구현하며, clone() 매서드도 정의되어 있기 때문에 clone()을 사용하여 배열 복사를 쉽게 진행할 수 있다.

 

 

 

*  JDK 1.5부터는 공변 반환타입(Covariant Return Type)이라고 불리는 기능이 추가되었는데, 이 기능으로 인해 clone() 매서드의 호출 코드가 아니라  clone() 매서드 내의 return 코드에서 형변환을 진행하더라도 에러가 발생하지 않는다고 한다. 참고로 공변 반환은 상속관계의 클래스에서 메서드가 오버라이딩 될 때, 더 narrow한 타입으로 교체할 수 있는 것을 의미한다.


(1) 얕은 복사와 깊은 복사

위에서 사용한 clone() 매서드의 예제로 사용한 인스턴스가 유래한 클래스는 기본형인 인스턴스 변수만 존재한다. 따라서 clone()으로 복제를 하더라도 인스턴스 변수가 복제되는 것에 있어 문제가 없다. 

 

하지만 클래스 내에 참조 변수가 하나 존재한다고 가정해보자. 이 때 clone() 함수로 인스턴스를 복사하게 되면 참조변수는 단순히 자신의 메모리 주소만을 복사하기 때문에 복사된 클래스 인스턴스 내의 참조변수는 동일한 객체를 가르키게 된다.

 

 

따라서 위의 예제에서 ex2.exValue 값을 변경하면, ex1.exValue와 다른 값이 나오지만, 참조변수 내 인스턴스 변수값인 ex2.refex1.refExValue를 변경하면 ex1, ex2 모두 해당 값이 변경된다.

 

 

 

위의 예시는 인스턴스 내 참조변수가 단순히 메모리 주소만을 복사하였기 때문에 완전한 복사가 이루어지지 않은 상태다. 이를 Java에서 얕은 복사(Shallow Copy)라고 칭한다. 반대로, 저 참조변수를 다른 메모리 주소에 완전하게 복사까지 하게 되면 이를 깊은 복사(Deep Copy)라고 한다. 그럼 깊은 복사는 어떻게 진행해야 할까?  간단히 clone() 매서드 내에서 반환되는 obj 객체의 참조변수에 새 인스턴스를 부여해주는 과정이 필요하다. 이렇게 될 경우, clone()에 의해 참조변수 메모리 주소가 복사되더라도 새 인스턴스 생성 코드로 인해 이 참조변수가 메모리의 다른 부분을 가리키게 되기 때문이다.

 

 

깊은 복사로의 코드 변환 결과를 살펴보자.

 

 

 

얕은 복사와 깊은 복사는 Java의 참조변수와 메모리의 관계가 머릿속에 정립되어 있지 않으면 이해하기 어렵다. 관련 내용이 필요한 분들은 이 포스팅을 참고하자.

 

 


 

 

 

다음 포스팅에서는 java.lang 패키지의 String 클래스에 속한 매서드에 대해 알아보려한다.

 

 

 

 

Fin.





반응형

댓글