[Java의 정석]제9장 java.lang패키지 - 1.Object클래스

류명운

·

2014. 7. 3. 22:36

반응형

java.lang패키지는 자바프로그래밍에 가장 기본이 되는 클래스들을 포함하고 있다. 그렇기 때문에 java.lang패키지의 클래스들은 import문을 사용하지 않고도 사용할 수 있도록 되어 있다. 그 동안 String클래스나 System클래스를 import문을 사용하지 않고도 사용할 수 있었던 이유가 바로 java.lang패키지에 속한 클래스들이기 때문이었던 것이다.
우선 java.lang패키지의 여러 클래스들 중에서도 자주 사용되는 클래스 몇 가지만을 골라서 학습해보도록 하자.

1. Object클래스


Object클래스에 대해서는 클래스의 상속을 학습할 때 배웠지만, 여기서는 보다 자세히 알아보도록 하자. Object클래스는 모든 클래스의 최고 조상이기 때문에 Object클래스의 멤버들은 모든 클래스에서 바로 사용 가능하다.
Object클래스는 멤버변수는 없고 8개의 메서드만 가지고 있다. 이 메서드들은 모든 인스턴스가 가져야 할 기본적인 것들이며, 우선 이 중에서 중요한 몇 가지에 대해서 알아보도록 하자.


1.1 equals메서드

매개변수로 객체의 참조변수를 받아서 비교하여 그 결과를 boolean값으로 알려 주는 역할을 한다. 아래의 코드는 Object클래스에 정의되어 있는 equals메서드의 실제 내용이다.


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


위의 코드에서 알 수 있듯이 두 객체의 같고 다름을 참조변수의 값으로 판단한다. 그렇기 때문에 서로 다른 두 객체를 equals메서드로 비교하면 항상 false를 결과로 얻게 된다.
[참고]객체를 생성할 때, 메모리의 비어있는 공간을 찾아 생성하므로 서로 다른 두 개의 객체가 같은 주소를 갖는 일은 있을 수 없다. 하지만, 두 개 이상의 참조변수가 같은 주소값을 갖는 것(한 객체를 참조하는 것)은 가능하다.

[예제9-1] EqualsEx1.java

class Value {
int value;

Value(int value) {
this.value = value;
}
}

class EqualsEx1
{
public static void main(String[] args)
{
Value v1 = new Value(10);
Value v2 = new Value(10);
if (v1.equals(v2)) {
System.out.println("v1과 v2는 같습니다.");
} else {
System.out.println("v1과 v2는 다릅니다.");
}

v2 = v1;

if (v1.equals(v2)) {
System.out.println("v1과 v2는 같습니다.");
} else {
System.out.println("v1과 v2는 다릅니다.");
}
}
}
[실행결과]
v1과 v2는 다릅니다.
v1과 v2는 같습니다.

value라는 멤버변수를 갖는 Value클래스를 정의하고, 두 개의 Value클래스의 인스턴스 생성한 다음 equals메서드를 이용해서 두 인스턴스를 비교하도록 했다. equals메서드는 주소값으로 비교를 하기 때문에, 두 Value인스턴스의 멤버변수 value의 값이 10으로 서로 같을지라도 equals메서드로 비교한 결과는 false일 수 밖에 없는 것이다.




하지만, v2 = v1;을 수행한 후에는 참조변수 v2는 v1이 참조하고 있는 인스턴스의 주소값이 저장되므로 v2도 v1과 같은 주소값이 저장된다. 그래서 이번에는 v1.equals(v2)의 결과가 true가 되는 것이다.




Object클래스로부터 상속받은 equals메서드는 결국 두 개의 참조변수가 같은 객체를 참조하고 있는지, 즉 두 참조변수에 저장된 값(주소값)이 같은지를 판단하는 기능 밖에 할 수 없다는 것을 알 수 있다. equals메서드 사용해서 Value인스턴스가 가지고 있는 value값을 비교하도록 할 수는 없을까? equals메서드를 Value클래스에서 오버라이딩하여 내용을 변경하면 된다.

[예제9-2] EqualsEx2.java

class Value {
int value;
public boolean equals(Object obj) {
if ( obj instanceof Value) {
return value == ((Value)obj).value;
} else {
return false;
}
}
Value(int value) {
this.value = value;
}
}

class EqualsEx2
{
public static void main(String[] args)
{
Value v1 = new Value(10);
Value v2 = new Value(10);
if (v1.equals(v2)) {
System.out.println("v1과 v2는 같습니다.");
} else {
System.out.println("v1과 v2는 다릅니다.");
}

v2 = v1;

if (v1.equals(v2)) {
System.out.println("v1과 v2는 같습니다.");
} else {
System.out.println("v1과 v2는 다릅니다.");
}
}
}
[실행결과]
v1과 v2는 같습니다.
v1과 v2는 같습니다.

equals메서드가 Value인스턴스가 갖고 있는 멤버변수 value의 값을 비교하도록 하기위해 equals메서드를 다음과 같이 오버라이딩했다.


public boolean equals(Object obj) {
if (obj != null && obj instanceof Value) {
return value == ((Value)obj).value;
} else {
return false;
}
}


이렇게 함으로써 Value클래스의 equals메서드는 기존의 Object클래스에 정의된 equals메서드와는 다르게 주소값이 아닌 두 인스턴스의 value값을 비교하고 그 결과를 돌려주도록 되어 있다. equals메서드의 매개변수 타입이 Object타입이기 때문에 Value타입으로 캐스팅(casting) 해주어야 멤버변수인 value의 값을 참조할 수 있다.
우리가 자주 사용하는 String클래스 역시 Object클래스의 equals메서드를 그대로 사용하는 것이 아니라 이처럼 오버라이딩을 통해서 String인스턴스가 갖는 문자열 값을 비교하도록 되어있다.
그렇기 때문에 같은 내용의 문자열을 갖는 두 String인스턴스에 equals메서드를 사용하면 항상 true값을 얻는 것이다.




1.2 hashCode메서드

이 메서드는 각 인스턴스의 같고다름을 비교하기 위한 인스턴스 구별 값인 해쉬코드(hashcode)를 반환한다. 해쉬코드는 인스턴스의 주소와 관련된 정수값(int)으로 서로 다른 인스턴스는 서로 다른 해쉬코드값을 가질 것을 보장한다. 그래서 서로 다른 두 인스턴스가 같은 해쉬코드값을 갖는 경우는 없다.
하지만, hashCode메서드를 오버라이딩해서 원래의 해쉬코드와는 다른 값을 반환하도록 할 수는 있다.
생성된 인스턴스의 해쉬코드는 프로그램이 실행될 때마다 할당받는 메모리주소가 다를 것이므로 매번 실행할 때마다 다른 값을 가지게 되지만, 적어도 프로그램이 시작된 후부터 종료될 때까지는 같은 값을 유지한다.
Hashtable클래스나 Vector클래스와 같은 인스턴스를 Object타입의 배열에 담아서 작업하는 클래스에서는 인스턴스의 같고 다름을 판단하는데 해쉬코드값을 이용한다.

서로 다른 종류의 인스턴스를 구분할 공통기준이 해쉬코드(주소값)외에는 없기 때문이기도 하지만, 인스턴스의 맴버변수의 값들을 비교하는 것보다는 4byte의 해쉬코드값을 비교하는 것이 더 빠르기 때문이다.




1.3 toString메서드

toString()은 인스턴스에 대한 정보를 문자열(String)로 제공할 목적으로 정의한 것이다. Object클래스에 정의된 toString()은 아래와 같다.


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


클래스를 작성할 때 toString()을 오버라이딩하지 않는다면, 위와 같은 내용이 그대로 사용될 것이다. 즉, toString()을 호출하면 클래스이름에 16진수의 해쉬코드를 얻게 될 것이다.
해쉬코드는 인스턴스의 주소와 관련된 값으로, 서로 다른 인스턴스는 서로 다른 해쉬코드값을 가질 것을 보장한다.
[참고]getClass메서드와 hashCode메서드 역시 Object클래스의 인스턴스메서드이므로 인스턴스 생성없이 바로 호출할 수 있었다.


[예제9-3] CardToString.java

class Card {
String kind;
int number;
Card() {
this("SPADE", 1);
}
Card(String kind, int number) {
this.kind = kind;
this.number = number;
}
}

class CardToString
{
public static void main(String[] args)
{
Card c1 = new Card();
Card c2 = new Card();

System.out.println(c1.toString());
System.out.println(c2.toString());
}
}
[실행결과]
Card@47e553
Card@20c10f

Card인스턴스 두 개를 생성한 다음, 각 인스턴스에 toString()을 호출한 결과를 출력했다. Card클래스에서 Object클래스로부터 상속받은 toString()을 오버라이딩 하지 않았기 때문에, Card인스턴스에 toString()을 호출하면, Object클래스의 toString()이 호출된다.

그래서 위의 결과에 클래스이름과 해쉬코드가 출력되었다. 서로 다른 인스턴스에 대해서 toString()을 호출하였으므로 클래스의 이름은 같아도 해쉬코드값이 다르다는 것을 확인할 수 있다.

[예제9-4] ToStringTest.java

class ToStringTest {
public static void main(String args[]) {
String str = new String("KOREA");
java.util.Date today = new java.util.Date();

System.out.println(str);
System.out.println(str.toString());
System.out.println(today);
System.out.println(today.toString());
}
}
[실행결과]
KOREA
KOREA
Fri May 17 23:26:13 KST 2002
Fri May 17 23:26:13 KST 2002

위의 결과에서 알수 있듯이 String클래스와 Date클래스의 toString()을 호출하였더니 클래스이름과 해쉬코드 대신 다른 결과가 출력되었다.
String클래스의 toString()은 String인스턴스가 갖고 있는 문자열을 반환하도록 오버라이딩되어 있고, Date클래스의 경우, Date인스턴스가 갖고 있는 날짜와 시간을 문자열로 하여 반환하도록 오버라이딩되어 있다.
이처럼 toString()은 보통 인스턴스나 클래스에 대한 정보 또는 인스턴스 변수들의 값을 문자열로 변환하여 반환하도록 오버라이딩된다.

이제 Card클래스에서도 toString()을 오버라이딩해서 보다 쓸모있는 정보를 제공할 수 있도록 바꿔 보자.

[예제9-5] CardToString2.java

class Card {
String kind;
int number;
Card() {
this("SPADE", 1);
}
Card(String kind, int number) {
this.kind = kind;
this.number = number;
}
public String toString() {
// Card인스턴스의 kind와 number를 문자열로 반환한다.
return "kind : " + kind + ", number : " + number;
}
}

class CardToString2
{
public static void main(String[] args)
{
Card c = new Card("HEART", 10);
System.out.println(c.toString());
}
}
[실행결과]
kind : HEART, number : 10


Card클래스의 조상인 Object클래스의 toString()을 오버라이딩해서, Card인스턴스의 toString()을 호출하면, 인스턴스가 갖고 있는 인스턴스변수 kind와 number의 값을 문자열로 변환하여 반환하도록 했다.
Object클래스에 정의된 toString()의 접근제어자가 public이므로 Card클래스에서 오버라이딩할 때도 public으로 했다는 것을 눈 여겨 보도록 하자.
[참고]조상클래스에 정의된 메서드를 자손클래스에서 오버라이딩할 때는 조상클래스에서 정의된 접근범위보다 같거나 더 넓어야 한다. Object클래스에서 toString()이 public으로 선언되어 있기 때문에, 이것을 오버라이딩하는 Card클래스에서는 toString()의 접근제어자를 public으로 할 수 밖에 없다.




1.4 clone메서드

이 메서드는 자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다. 어떤 인스턴스에 대해 작업을 할 때, 원래의 인스턴스는 보존하고 clone메서드를 이용해서 새로운 인스턴스를 생성하여 작업을 하면 작업이전의 값이 보존되므로 작업에 실패해서 원래의 상태로 되돌리거나 변경되기전의 값을 참고하는데 도움이 될 것이다.
Object클래스에 정의된 clone메서드는 단순히 멤버변수의 값만을 복사하기 때문에 배열이나 인스턴스가 멤버로 정의되어 있는 클래스의 인스턴스는 완전한 복제가 이루어지지 않는다.
예를 들어 배열의 경우 복제된 인스턴스도 같은 배열의 주소를 갖기 때문에 복제된 인스턴스의 작업이 원래의 인스턴스에 영향을 미치게 된다.
이런 경우 clone메서드를 오버라이딩해서 새로운 배열을 생성하고 배열의 내용을 복사하도록 해야한다.

[예제9-6] CloneTest.java

// Cloneable인터페이스를 구현한 클래스에서만 clone메서드를 호출할 수 있다.
// 이 인터페이스를 구현하지 않고 clone메서드를 호출하려면 CloneNotSupportedException이 발생한다.
class Point implements Cloneable {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}

public String toString() {
return "x="+x +", y="+y;
}

public Point copy() {
Object obj=null;
try {
// clone메서드에서는 CloneNotSupportedException이 선언되어 있으므로
// 이 메서드를 호출할 때는 try-catch문을 사용해야한다.
obj = clone();
} catch(CloneNotSupportedException e) { }
return (Point)obj;
}
}

class CloneTest {
public static void main(String[] args){
Point original = new Point(3, 5);
Point copy = original.copy();
System.out.println(original);
System.out.println(copy);
}
}
[실행결과]
x=3, y=5
x=3, y=5

clone메서드는 접근제어자가 protected이므로 접근제어자가 public인 새로운 copy메서드를 선언하고 그 내부에서 clone메서드를 통해 인스턴스 복제를 하도록 처리했다.
Cloneable인터페이스를 구현한 클래스의 인스턴스만이 clone메서드를 통한 복제가 가능하다. 인스턴스 복제는 데이터를 복사하는 것이기 때문에 데이터를 보호하기 위해서, 클래스 작성자가 복제를 허용하는 경우, Cloneable인터페이스를 구현한 경우,에만 복제가 가능하도록 하기 위해서이다. 

반응형