본문 바로가기
Back-end

java.lang패키지와 유용한 클래스(1)

by 신재권 2021. 6. 23.

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

그동안 String클래스나 System클래스를 import없이 사용할 수 없었던 이유가 바로 java.lang 패키지에 속한 클래스들이기 때문이었던 것이다. 우선 java.lang패키지의 여러 클래스들 중에서 자주 사용되는 클래스 몇가지를 학습한다.

Object클래스

클래스의 상속을 학습할 때 Object클래스에 대해서 이미 배웠지만, 여기서는 보다 자세히 알아보자. Object클래스는 모든 클래스의 최고 조상이기 때문에 Object클래스의 멤버들은 모든 클래스에서 바로 사용이 가능하다.

메서드 :설명

protected Object clone() : 객체 자신의 복사본을 반환한다.

public boolean equals(Object obj) : 객체 자신과 객체 obj가 같은 객체인지 알려준다. (같으면 true)

protected void finalize() : 객체가 소멸될 때 가비지 컬렉터에 의해 자동적으로 호출된다. 이 때 수행되어야하는 코드가 있을 때 오버라이딩 한다.(거의 사용안함)

public Class getClass() : 객체 자신의 클래스 정보를 담고 있는 Class 인스턴스를 반환한다.

public int hashCode() : 객체 자신의 해시코드를 반환한다.

public String toString() : 객체 자신의 정보를 문자열로 반환한다.

public void notify() : 객체 자신을 사용하려고 기다리는 쓰레드를 하나만 깨운다.

public void notifyAll() : 객체 자신을 사용하려고 기다리는 모든 쓰레드를 깨운다.

public void wait(), public void wait(long timeout), public void wait(long timeout, int nanos) : 다른 쓰레드가 notify()나 notifyAll()을 호출할 때까지 현재 쓰레드를 무한히 또는 지정된 시간(timeout, nanos)동안 기다리게 한다. (timeout은 천 분의 1초, nanos는 10^9분의 1초)

Object 클래스는 멤버변수는 없고 오직 11개의 메서드만 가지고 있다. 이 메서드들은 모든 인스턴스가 가져야 할 기본적인 것들이며, 이 중에서 중요한 몇가지만 살펴보자.

  • notify(), notifyAll(), wait은 쓰레드(thread)와 관련된 것이다.

equals(Object obj)

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

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

위의 코드에서 알 수 있듯이 두 객체의 같고 다름을 참조변수의 값으로 판단한다. 그렇기 때문에 서로 다른 두 객체를 equals메서드를 비교하면 항상 false를 결과로 얻게 된다.

  • 객체를 생성할 때, 메모리의 비어있는 공간을 찾아 생성하므로 서로 다른 두 개의 객체가 같은 주소를 갖는 일은 있을 수 없다. 두 개 이상의 참조변수가 같은 주소값을 갖는 것(한 객체를 참조하는 것)은 가능하다.
  • public 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는 다릅니다.");
    		
    	}
    
    }
    
    class Value{
    	int value;
    	
    	Value(int value){
    		this.value = value;
    	}
    }
    ==================================
    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값을 비교하도록 할 수는 없을까? Value클래스에서 equals 메서드를 오버라이딩 하여 주소가 아닌 객체에 저장된 내용을 비교하도록 변경하면 된다.

class Person{
	long id;
	
	public boolean equals(Object obj){
		if(obj instanceof Person)
			return id == ((Person)obj).id;
		else 
			return false;
	}
	
	Person(long id){
		this.id = id;
	}
}


public class EqualsEx2 {
	public static void main(String[] args) {
		
		Person p1 =new Person(8011081111222L);
		Person p2 =new Person(8011081111222L);
		
		if(p1 == p2)
			System.out.println("p1과 p2는 같은 사람입니다.");
		else
			System.out.println("p1과 p2는 다른 사람입니다.");
		
		if(p1.equals(p2))
			System.out.println("p1과 p2는 같은 사람입니다.");
		else
			System.out.println("p1과 p2는 다른 사람입니다.");
		
		
	}

}
====================
p1과 p2는 다른 사람입니다.
p1과 p2는 같은 사람입니다.

equals메서드가 Person인스턴스의 주소값이 아닌 멤버변수 id의 값을 비교하도록 하기위해 equals 메서드를 다음과 같이 오버라이딩했다. 이렇게 함으로써 서로 다른 인스턴스일지라도 같은 id(주민등록번호)를 가지고 있다면 equals메서드로 비교했을 때 true를 결과로 얻게 할 수 있다.

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

String클래스 역시 Object클래스의 equals메서드를 그대로 사용하는 것이 아니라 이처럼 오버라이딩을 통해서 String인스턴스가 갖는 문자열 값을 비교하도록 되어있다. 그렇기 때문에 같은 애용의 문자열을 갖는 두 String인스턴스에 equals메서드를 사용하면 항상 true값을 얻는 것이다.

  • String클래스뿐만 아니라, Data, File, wrapper클래스(Integer, Double 등)의 equals메서드도 주소값이 아닌 내용을 비교하도록 오버라이딩되어 있다. 그러나 의외로 StringBuffer클래스는 오버라이딩되어 있지 않다.

hashCode()

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

해싱은 데이터관리기법 중의 하나인데 다량의 데이터를 저장하고 검색하는 데 유용하다.

해시함수는 찾고자하는 값을 입력하면, 그 값이 저장된 위치를 알려주는 해시코드(hash code)를 반환한다.

일반적으로 해시코드가 같은 두 객체가 존재하는 것이 가능하지만, Object클래스에 정의된 hashCode메서드는 객체의 주소값으로 해쉬코드를 만들어 반환하기 때문에 32bit JVM에서는 서로 다른 두 객체는 결코 같은 해시코드를 가질 수 없었지만, 64bit JVM에서는 8byte주소값으로 해시코드(4 byte)를 만들기 때문에 해시코드가 중복될 수 있다.

앞서 살펴본것과 같이 클래스의 인스턴스변수 값으로 객체의 같고 다름을 판단해야하는 경우라면, equals메서드 뿐 만아니라 hashCode메서드도 적절히 오버라이딩을 해야한다.

같은 객체라면 hashCode메서드를 호출했을 때의 결과값인 해시코드도 같아야 하기 떄문이다.

  • 해싱기법을 사용하는 HashMap이나 HashSet과 같은 클래스에 저장할 객체라면 반드시 hashCode메서드를 오버라이딩 해야한다.
public class HashCodeEx1 {

	public static void main(String[] args) {
		String str1 = new String("abc");
		String str2 = new String("abc");
		
		System.out.println(str1.equals(str2));
		System.out.println(str1.hashCode());
		System.out.println(str2.hashCode());
		System.out.println(System.identityHashCode(str1));
		System.out.println(System.identityHashCode(str2));
		
	}

}
===========================
true
96354
96354
366712642
1829164700

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

반면에 System.identifyHashCode(Object x)는 Object클래스의 hashCode메서드처럼 객체의 주소값으로 해시코드를 생성하기 때문에 모든 객체에 대해 항상 다른 해시코드값을 반환할 것을 보장한다. 그래서 str1과 str2가 해시코드는 같지만 서로 다른 객체라는 것을 알 수 있다.

  • System.identifyHashCode(Object x)의 호출결과는 실행할 때마다 달라질 수 있다.

toString()

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

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

Object클래스에 정의된 toString()은 아래와 같다.

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

클래스를 작성할 때 toString()을 오버라이딩 하지 않는다면, 위와 같은 내용이 그대로 사용될 것이다. 즉 , toString()을 호출하면 클래스이름에 16진수의 해시코드를 얻게될 것이다.

  • getClasS()와 hashCode()역시 Object클래스에 정의된 것이므로 인스턴스 생성없이 바로 호출할 수 있다.
  • class Card{
    	String kind;
    	int number;
    	
    	Card(){
    		this("SPADE", 1);
    	}
    	Card (String kind, int number){
    		this.kind  = kind;
    		this.number = number;
    	}
    }
    
    
    public 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@15db9742
    Card@6d06d69c

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

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

public 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
Wed Jun 23 16:58:25 KST 2021
Wed Jun 23 16:58:25 KST 2021

위의 결과에서 알 수 있듯이 String클래스와 Date클래스의 toString()을 호출하였더니 클래스이름과 해시코드 대신 다른 결과가 출력되었다.

String클래스의 toString()은 String인스턴스가 갖고 있는 문자열을 반환하도록 오버라이딩되어 있고, Date클래스의 경우, Date인스턴스가 갖고 있는 날짜와 시간을 문자열로 변환하여 반환하도록 오버라이딩 되어 있다.

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

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

class Card1{
	String kind;
	int number;
	
	Card1(){
		this("SPADE", 1);
	}
	Card1 (String kind, int number){
		this.kind  = kind;
		this.number = number;
	}
	
	public String toString(){
		return "Kind : "+kind+", number : "+number;
	}
}



public class CardToString2 {

	public static void main(String[] args) {
		Card1 c1 = new Card1();
		Card1 c2 =new Card1("HEART", 10);
		System.out.println(c1.toString());
		System.out.println(c2.toString());
		

	}

}
================
Kind : SPADE, number : 1
Kind : HEART, number : 10

Card 인스턴스의 toString()을 호출하면 인스턴스가 갖고 있는 인스턴스변수 kind와 number의 값을 문자열로 변환하여 반환하도록 toString()을 오버라이딩했다. 오버라이딩할 때 Object클래스에 정의된 toString()의 접근 제어자가 public이므로 Card클래스의 toString()의 접근제어자도 public으로 헀다는 것을 눈 여겨 보자.

조상에 정의된 메서드를 자손에서 오버라이딩할 때는 조상에 정의된 접근범위보다 같거나 더 넓어야하기 때문이다. Object클래스에서 toString()의 접근제어자가 public이므로 , 이를 오버라이딩하는 Card클래스에서는 toString()의 접근 제어자를 public으로 할 수 밖에 없다.

clone()

이 메서드는 자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다. 어떤 인스턴스에 대해 작업을 할 때, 원래의 인스턴스는 보존하고 clone메서드를 이용해서 새로운 인스턴스를 생성하여 작업을 하면 작업이전의 값이 보존되므로 작업에 실패해서 원래의 상태로 되돌리거나 변경되기 전의 값을 참고하는데 도움읻 될 것이다.

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

예를 들어 배열의 경우, 복제된 인스턴스도 같은 배열의 주소를 갖기 때문에 복제된 인스턴스의 작업이 원래의 인스턴스에 영향을 미치게 된다. 이런 경우 clone메서드를 오버라이딩해서 새로운 배열을 생성하고 배열의 내용을 복사하도록 해야한다.

class Point implements Cloneable{ //Cloneable인터페이스를 구현한 클래스에서만 clone()을 호출할 수 있다.
	int x, y;					  //이 인터페이스를 구현하지 않고 clone()을 호출하면 예외가 발생한
	
	Point (int x, int y){
		this.x = x;
		this.y = y;
	}
	
	public String toString(){
		return "x=" + x + ", y="+y;
	}
	
	public Object clone(){
		Object obj =null;
		try{
			obj = super.clone();	//clone()은 반드시 예외처리를 해주어야 한다.
		}catch(CloneNotSupportedException e){ }
		return obj;
	}
}
public class CloneEx1 {

	public static void main(String[] args) {
		Point original = new Point(3, 5);
		Point copy = (Point)original.clone();	//복제(clone)해서 새로운 객체를 형성
		System.out.println(original);
		System.out.println(copy);
	}

}
==============================
x=3, y=5
x=3, y=5

clone()을 사용하려면, 먼저 복제할 클래스가 Cloneable인터페이스를 구현해야 하고, clone()을 오버라이딩하면서 접근 제어자를 protected에서 public으로 변경한다. 그래야만 상속관계가 없는 다른 클래스에서 clone()을 호출 할 수있다.

public class Object{
		...
	protected native Object clone() throws CloneNotSupportedException;
	....
}
  • Object클래스의 clone()은 Cloneable을 구현하지 않은 클래스에 호출되면 예외를 발생시킨다.

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

class Point implements Cloneable{ // 1. Cloneable인터페이스를 구현한다.
	...
	public Object clone(){ //2. 접근 제어자를 public으로 변경
	Object obj = null;
	try{
		obj = super.clone(); //try-catch내에서 조상클래스의 clone()을 호출
	}catch(CloneNotSupportedException e){}
	return obj;
	}
}

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

공변 반환타입

JDK 1.5부터 공변 반환타입(covariant return type)이라는 것이 추가되었는데, 이 기능은 오버라이딩할 떄 조상 메서드의 반환타입을 자손 클래스의 타입으로 변경을 허용하는 것이다.

아래의 코드는 예제 7의 clone()의 반환타입을 Object에서 Point로 변경한 것이다.

즉 , 조상의 타입에서 자손의 타입으로 변경한 것이다. 그리고 return 문에 Point타입으로 형변환도 추가하였다. 예전에는 오버라이딩 할 때 조상에 선언된 메서드의 반환타입을 그대로 사용해야 했다.

public Point clone() { //반환 타입을 Object에서 Point로 변경
	Object obj = null;
	try{
		obj  =super.clone();
	}catch(CloneNotSupportedException e){}
	return (Point)obj;
	}
}

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

Point copy = (Point)orignal.clone(); ->
Point copy = orinal.clone();

예제 9-7이 공변 반환타입으로 사용되도록 직접변경해서 실행해보자

class Point implements Cloneable{ //Cloneable인터페이스를 구현한 클래스에서만 clone()을 호출할 수 있다.
	int x, y;					  //이 인터페이스를 구현하지 않고 clone()을 호출하면 예외가 발생한
	
	Point (int x, int y){
		this.x = x;
		this.y = y;
	}
	
	public String toString(){
		return "x=" + x + ", y="+y;
	}
	
	public Point clone(){
		Object obj =null;
		try{
			obj = super.clone();	//clone()은 반드시 예외처리를 해주어야 한다.
		}catch(CloneNotSupportedException e){ }
		return (Point)obj;
	}
}
public class CloneEx1 {

	public static void main(String[] args) {
		Point original = new Point(3, 5);
		Point copy = original.clone();	//복제(clone)해서 새로운 객체를 형성
		System.out.println(original);
		System.out.println(copy);
	}

}
import java.util.*;

public class CloneEx2 {

	public static void main(String[] args) {
		int [] arr = {1,2,3,4,5};
		int[] arrClone = arr.clone(); //배열 arr를 복제해서 같은 내용의 새로운 배열을만든다.
		arrClone[0] = 6;
		
		System.out.println(Arrays.toString(arr));
		System.out.println(Arrays.toString(arrClone));
		
	}

}
================
[1, 2, 3, 4, 5]
[6, 2, 3, 4, 5]

clone()을 이용해서 배열을 복사하는 예제이다. 배열도 객체이기 때문에 Object클래스를 상속받으며, 동시에 Cloneable인터페이스와 Serializable인터페이스가 구현되어 있다. 그래서 Object클래스의 멤버들을 모두 상속받는다. Object클래스에는 protected로 정의되어 있는 clone()을 배열에서는 public으로 오버라이딩하였기 때문에 예제처럼 직접호출이 가능하다. 그리고 원본과 같은 타입을 반환하므로 형변환이 필요없다.

일반저긍로 배열을 복사할 때 , 같은 길이의 새로운 배열을 생성한 다음에 System.arraycopy()를 이용해서 내용을 복사하지만, 이처럼 clone()을 이용해서 간단하게 복사할 수 있다는 것도 참고로 알아두자.

아래의 두 코드는 같은 결과를 얻는다. 어느 쪽을 사용해도 상관없다.

int [] arr = {1,2,3,4,5};
int[] arrClone = arr.clone();
-------------------------------
int[] arr = {1,2,3,4,5};
int[] arrClone= new int[arr.length]; 
System.arraycopty(arr,0,arrClone,0,arr.length);

배열 뿐 아니라 java.util패키지의 Vector, ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, Calendar, Date와 같은 클래스들이 이와 같은 방식으로 복제가 가능하다.

ArrayList list  = new ArrayList();
...
ArrayList list2 = (ArrayList)list.clone();
  • clone()으로 복제가 가능한 클래스인지 확인하려면 Java API에서 Cloneable을 구현하였는지 확인하면 된다.

'Back-end' 카테고리의 다른 글

java.lang패키지와 유용한 클래스(3)  (0) 2021.06.25
java.lang패키지와 유용한 클래스(2)  (0) 2021.06.24
예외처리(5)  (0) 2021.06.23
예외처리(4)  (0) 2021.06.22
예외처리(3)  (0) 2021.06.21