본문 바로가기

Java/java.lang패키지와 유용한 클래스

[Java] java.lang패키지(Object 클래스) - 코딩밥상

java.lang패키지

 java.lang패키지는 자바프로그래밍에 가장 기본이 되는 클래스들을 포함하고 있다. 그렇기 때문에 패키지에 포함되는 클래스들은 import문 없이도 사용할 수 있게 되어 있다.

그 동안 String, System클래스 등을 import문 없이도 사용할 수 있었던 이유가 이들이 java.lang패키지에 속한 클래스들이기 때문이었다.

우선 패키지에 포함되어있는 여러 클래스들 중 자주 사용되는 몇 가지만을 골라서 정리해보자.

Object클래스

Object클래스는 모든 클래스의 최고 조상이기 때문에 Object클래스의 멤버들은 모든 클래스에서 바로 사용 가능하다.

앞서 정리한바와 같이 Object클래스는 멤버변수는 없고 오직 11개의 메서드만 갖는다. 이 메서드들은 모든 인스턴스가 가져야 할 기본적인 것들이다.

proctected Object clone() 객체의 복사본을 만들어 리턴한다.
public boolean equals(Object obj) 현재 객체와 매개 변수로 넘겨 받은 객체가 같은지 확인하다. 같으면 true를 다르면 false를 리턴한다.
protected void finalize() 현재 객체가 더 이상 쓸모가 없어졌을 때 가비지 컬렉터에 의해서 이 메소드가 호출된다.
public Class<?> getClass() 현재 객체의 Class 클래스의 객체를 리턴한다.
public int hashCode() 객체에 대한 해시코드값을 리턴한다. 해시코드란 16진수 객체 메모리의 주소를 말한다.
public String toString() 객체를 문자열로 표현하는 값을 리턴한다.

Object 클래스 메소드들 중 스레드 처리를 위한 메소드에는 다음과 같은 것들이 있다.

public void notify() 이 객체의 모니터에 대기하고 있는 단일 스레드를 깨운다.
public void notifyAll() 이 객체의 모니터에 대기하고 있는 모든 스레드를 깨운다.
public void wait() 다른 스레드가 현재 객체에 대한 notify() 메소드나 notifyAll() 메소드를 호출할 때까지 현재 스레드가 대가하고 있도록 한다.
public void wait(long timeout) wait() 메소드와 동일한 기능을 제공하며, 매개 변수에 지정한 시간만큼만 대기한다. 즉, 매개변수 시간을 넘어 섰을 때에는 현재 스레드는 다시 깨어난다. 시간은 밀리초 단위다.
public void wait(long timeout, int nanos) wait()메소드와 동일한 기능을 제공한다. 이 메소드는 밀리초 + 나노초(1/1,000,000,000)만큼만 대기한다.

 

 

-equals(Object obj)

매개변수로 객체의 참조변수를 받아서 비교하여 그 결과를 boolean값으로 알려 주는 역할을 한다.

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

Object클래스에 정의되어있는 equals메서드의 실제 내용이다. 두 객체의 같고 다름을 참조변수의 값으로 판단한다.

그렇기 때문에 서로 다른 두 객체를 equals메서드로 비교하면 항상 false를 결과로 얻게 된다.

 

 항상 false값을 갖게 된다면, 비교의 기능을 수행할 수 없고 함수를 사용하는 의미가 없어진다. 또한 객체의 주소값을 비교하는 경우보다 인스턴스의 value값을 비교하는 경우를 훨씬 많이 만날 것이다. 

그렇다면 equals메서드로 인스턴스가 가지고 있는 value값을 비교하려면 어떻게 해야할까?

 

 바로 클래스에서 equals메서드를 오버라이딩하여 주소값이 아닌 객체에 저장된 내용을 비교하도록 변경하면 된다.

즉 의미있게 사용 가능하도록 커스텀하는 것이다. 

public boolean equals(Object obj){	//예시
	if(obj!=null && obj instanceof 객체명){
    	return 멤버값 == ((객체명)obj).멤버값;
    }
    else return false;
}

String클래스 역시 Object클래스의 equals메서드를 이처럼 오버라이딩을 통해서 String인스턴스가 갖는 문자열 값을 비교하도록 되어있다.


-hashCode()

 이 메서드는 해싱(hashing)기법에 사용되는 '해시함수(hash function)'를 구현한 것이다.

hashCode메서드는 객체의 주소값으로 해시코드를 만들어 반환한다. 때문에 클래스의 인스턴스변수 값으로 객체의 같고 다름을 판단해야하는 경우라면 equals메서드 뿐 아니라 hashCode메서드도 적절하게 오버라이딩 해야한다. 같은 객체라면 hashCode를 호출했을 때의 결과값인 해시코드도 같아야 하기 때문이다.

 

String클래스는 문자열의 내용이 같으면, 동일한 해시코드를 반환하도록 hashCode메서드가 오버라이딩되어 있기 때문에, 문자열의 내용이 같은 두 인스턴스에 대해 hashCode()를 호출하면 항상 동일한 해시코드값을 얻는다.

 

반면에 System.identityHashCode(Object x)는 Object클래스의 hashCode메서드처럼 객체의 주소값으로 해시코드를 생성하기 때문에 모든 객체에 대해 항상 다른 해시코드값을 반환할 것을 보장한다.


-toString()

 이 메서드는 인스턴스에 대한 정보를 문자열(String)로 제공할 목적으로 구현한 것이다.

인스턴스의 정보를 제공한다는 것은 대부분의 경우 인스턴스 변수에 저장된 값들을 문자열로 표현한다는 뜻이다.

public String toString(){
	return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

 Object클래스에 정의되어있는 toString()의 실제 내용이다. 클래스를 작성할 때 toString()을 오버라이딩하지 않는다면, 위 코드 그대로 클래스이름에 16진수의 해시코드를 얻게 된다.

 따라서 toString()또한 일반적으로 인스턴스나 클래스에 대한 정보 또는 인스턴스 변수의 값을 문자열로 변환하여 반환하도록 오버라이딩되는 것이 일반적이다.


-clone()

 이 메서드는 자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다.

Object클래스에 정의된 clone()은 단순히 인스턴스변수의 값만 복사하기 때문에 참조타입의 인스턴스 변수가 있는 클래스는 완전한 복제가 이루어지지 않는다. 

 

예를 들어 배열의 경우, 복제된 인스턴스도 같은 배열의 주소를 갖고 복제되기 때문에 인스턴스의 작업이 원래의 인스턴스에 영향을 미치게 되고 함수를 사용하는 의미를 잃어버리게 된다.

따라서 이런경우 오버라이딩하여 새로운 배열을 생성하고 배열의 내용을 복사하도록 해야 한다.

 

clone()을 사용하려면

1. 먼저 복제할 클래스가 Cloneable인터페이스를 구현해야하고,

2. clone을 오버라이딩하면서 접근 제어자를 protected에서 public으로 변경한다. 그래야만 상속관계가 없는 다른 클래스에서 clone()을 호출할 수 있게된다.

3. 마지막으로 조상클래스의 clone()을 호출하는 코드가 포함된 try-catch문을 작성한다.

class 클래스명 implements Cloneable{	//예시
	...
    public 클래스명(공변반환타입) clone(){
    	Object obj = null;
        try{
        	obj = super.clone();
        }
        catch(CloneNotSupportedException e){}
        reeturn (클래스명)obj;
    }
}

Cloneable인터페이스를 구현한 클래스의 인스턴스만 clone()을 통한 복제가 가능한데, 그 이유는 인스턴스의 데이터를 보호하기 위해서이다. 따라서 Cloneable인터페이스가 구현되어있다는 것은 클래스 작성자가 복제를 허용한다는 의미이다.

 

여기서 알아두면 좋은 개념이 '공변환 타입(covariant return type)'이다.

이는 오버라이딩할 때 조상 메서드의 반환타입을 자손 클래스의 타입으로 변경을 허용하는 것이다.

즉, 조상의 타입이 아닌, 실제로 반환되는 자손 객체의 타입으로 반환할 수 있어서 번거로운 형변환이 줄어든다는 장점이 있다.

 

 배열은 이 모든 조건을 만족하도록 구현되어있다. 배열도 객체이기 때문에 Object클래스를 상속받으며, 동시에 Cloneable인터페이스와 Serializable인터페이스가 구현되어있다. 따라서 배열은 clone()을 이용해서 간단하게 복사할 수 있다.

int[] arr = {1,2,3,4,5};
int[] arrClone = arr.clone();

(배열뿐만 아니라 java.util 패키지의 Vector, ArrayList, LinkedList, HashSet, TreeSet 등 자주 사용되는 자료구조 클래스들도 이와 같은 방식으로 복제가 가능하다.)

 

그렇다면 객체배열을 clone()으로 복제하는 경우에는 어떻게 될까?

 clone()은 단순히 객체에 저장된 값을 그대로 복제할 뿐, 객체가 참조하고 있는 객체까지 복제하지는 않는다.원본과 복제본이 같은 객체를 공유하게되므로 완전한 복제라고 보기 어렵다. 이러한 복제(복사)를 '얕은 복사(shallow copy)'라고 하며 얕은 복사에서는 원본을 변경하면 복사본도 영향을 받는다.

 

반면에 원본이 참조하고 있는 객체까지 복제하는 것을 '깊은 복사(deep copy)'라고 하며, 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않는다.

public Circle shallowCopy(){	//얕은복사
	Object obj = null;
    
    try{
    	obj = super.clone();
    }
    catch(CloneNotSupportedException e){}
    
    return (Circle obj);
}
public Circle shallowCopy(){	//깊은복사
	Object obj = null;
    
    try{
    	obj = super.clone();
    }
    catch(CloneNotSupportedException e){}
    
    Circle c = (Circle)obj;
    c.p = new Point(this.p.x, this.p.y);
    
    return c;
}

깊은 복사를 하는 방법은 일차원적이다. 객체를 새롭게 만들어주고 그 객체가 복제된 인스턴스를 참조하도록 하면 된다.


-getClass()

이 메서드는 자신이 속한 클래스의 Class객체를 반환하는 메서드인데, Class객체는 이름이 'Class'인 클래스의 객체이다. Class클래스는 아래와 같이 정의되어있다.

public final class Class implements ... {	//Class클래스
	...
}

 Class객체는 클래스의 모든 정보를 담고 있으며, 클래스 당 1개만 존재한다. 그리고 클래스 파일이 '클래스 로더(ClassLoader)'에 의해서 메모리에 올라갈 때, 자동으로 생성된다.

 클래스 로더는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 한다. 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고, 있으면 객체의 참조를 반환하고 없으면 패스에 지정된 경로를 따라서 클래스 파일을 찾는다. 

찾는다면 해당 클래스 파일을 읽어서 Class객체로 변환하고, 못찾는 경우 ClassNotFoundException가 발생한다.

즉 클래스 파일을 읽어서 사용하기 편한 형태로 저장해 놓은 것이 클래스객체이다.

 

 여기까지 Class클래스에 대해서 정리해보았고, 이 Class객체를 얻는 방법으로는 다음과 같은 방법들이 있다.

Class cObj = new Card().getClass();	//생성된 객체로 부터 얻는 방법
Class cObj = Card.class;			//클래스 리터럴(*.class)로 부터 얻는 방법
Class cObj = Class.forName("Card"); //클래스 이름으로 부터 얻는 방법

특히 forName()은 특정 클래스 파일, 예를 들어 데이터베이스 드라이버를 메모리에 올릴 때 주로 사용한다.

Class객체를 이용하면 클래스에 정의된 멤버의 이름이나 개수 등, 클래스에 대한 모든 정보를 얻을 수 있기 때문에 Class객체를 통해서 객체를 생성하고 메서드를 호출하는 등 보다 동적인 코드를 작성할 수 있다.