[Java] 기본 API 클래스 - Object
java.lang 패키지
java.lang 패키지는 자바 프로그램의 기본적인 클래스를 담고 있는 패키지이다. 그렇기 때문에 java.lang 패키지에 있는 클래스와 인터페이스는 import 없이 사용할 수 있다. 지금까지 사용한 String 과 System 클래스도 java.lang 패키지에 포함되어 있기 때문에 import 하지 않고 사용했다. 다음은 java.lang 패키지에 속하는 주요 클래스와 간략한 용도에 대해 설명한 표이다.
클래스 | 용도 | |
Object | - 자바 클래스의 최상위 클래스로 사용 | |
System | - 표준 입력 장치로부터 데이터를 입력받을 때 사용 - 표준 출력 장치로 출력하기 위해 사용 - 자바 가상 기계를 종료시킬 때 사용 - 쓰레기 수집기를 실행 요청할 때 사용 |
|
Class | - 클래스를 메모리로 로딩할 때 사용 | |
String | - 문자열을 저장하고 여러 가지 정보를 얻을 때 사용 | |
StringBuffer, StringBuilder | - 문자열을 저장하고 내부 문자열을 조작할 때 사용 | |
Math | - 수학 함수를 이용할 때 사용 | |
Wrapper | Byte, Short, Character, Integer, Float, Double, Boolean, Long |
- 기본 타입의 데이터를 갖는 객체를 만들 때 사용 - 문자열을 기본 타입으로 변환할 때 사용 - 입력값 검사에 사용 |
java.util 패키지
java.util 패키지는 자바 프로그램 개발에 조미료 같은 역할을 하는 클래스를 담고 있다. java.util 패키지는 컬렉션 클래스들이 대부분을 차지하고 있다. 컬렉션 클래스들은 많지만 아래 여섯가지 클래스만 소개하려고 한다.
클래스 | 용도 |
Arrays | - 배열을 조작(비교, 복사, 정렬, 찾기)할 때 사용 |
Calendar | - 운영체제의 날짜와 시간을 얻을 때 사용 |
Date | - 날짜와 시간 정보를 저장하는 클래스 |
Objects | - 객체 비교, 널(null) 여부 등을 조사할 때 사용 |
StringTokenizer | - 특정 문자로 구분된 문자열을 뽑아낼 때 사용 |
Random | - 난수를 얻을 때 사용 |
Object 클래스
클래스를 선언할 때 extends 키워드로 다른 클래스를 상속하지 않으면 암시적으로 java.lang.Object 클래스를 상속하게 된다. 따라서 자바의 모든 클래스는 Object 클래스의 자식이거나 자손 클래스이다. Object는 자바의 최상위 부모 클래스에 해당한다. Object 클래스는 필드가 없고, 메소드들로 구성되어 있다. 이 메소드들은 모든 클래스가 Object를 상속하기 때문에 모든 클래스에서 사용이 가능하다.
객체비교(equals())
다음은 Object의 equals() 메소드이다.
public boolean equals(Object obj) { ... }
equals() 메소드의 매개 타입은 Object인데, 이것은 모든 객체가 매개값으로 대입될 수 있음을 말한다. 그 이유는 Object가 최상위 타입이므로 모든 객체는 Object 타입으로 자동 타입 변환될 수 있기 때문이다. Object 클래스의 equals() 메소드는 비교 연산자인 == 과 동일한 결과를 리턴한다. 두 객체가 동일한 객체라면 true를 리턴하고 그렇지 않으면 false를 리턴한다.
Object obj1 = new Object();
Object obj2 = new Object();
boolean result = obj1.equals(obj2);
boolean result = (obj1 == obj2);
자바에서는 두 객체를 동등 비교할 때 equals() 메소드를 흔히 사용한다. equals() 메소드를 두 객체를 비교해서 논리적으로 동등하면 true를 리턴하고, 그렇지 않으면 false를 리턴한다. 논리적으로 동등하다는 것은 같은 객체이건 다른 객체이건 상관없이 객체가 저장하고 있는 데이터가 동일함을 뜻한다. 예를 들어 String 객체의 equals() 메소드는 String 객체의 번지를 비교하는 것이 아니고, 문자열이 동일한지 조사해서 같다면 true를 리턴하고, 그렇지 않다면 false를 리턴한다. 이것이 가능한 이유는 String 클래스가 Object의 equals() 메소드를 오버라이딩해서 번지 비교가 아닌 문자열 비교로 변경했기 때문이다.
Object의 equals() 메소드는 직접 사용되지 않고 하위 클래스에서 재정의하여 논리적으로 동등 비교할 때 이용된다. 예를 들어 Member 객체는 다르지만 id 필드값이 같으면 논리적으로 동등한 객체로 취급하고 싶을 경우 Object의 equals() 메소드를 재정의해서 id 필드값이 같음을 비교하면 된다.
equals() 메소드를 오버라이딩할 때는 매개값이 기준 객체와 동일한 타입의 객체인지 먼저 확인해야 한다. Object 타입의 매개 변수는 모든 객체가 매개값으로 제공될 수 있기 때문에 instanceof 연산자로 기준 객체와 동일한 타입인지 제일 먼저 확인해야 한다. 만약 비교 객체가 다른 타입이라면 equals() 메소드는 false를 리턴해야 한다. 비교 객체가 동일한 타입이라면 기준 객체 타입으로 강제 타입 변환해서 필드값이 동일한지 검사하면 된다. 필드값이 모두 동일하다면 true를 리턴하고 그렇지 않으면 false를 리턴하도록 작성한다. 다음 예제는 Member 클래스에서 equals() 메소드를 오버라이딩한 것이다. Member 타입이면서 id 필드값이 같은 경우는 true를 리턴하고, 그 이외의 경우는 false를 리턴하도록 했다.
// Member.java -- 객체 동등 비교
public class Member {
public String id;
public Member(String id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Member) {
Member member = (Member) obj;
if(id.equals(member.id)) {
return true;
}
}
return false;
}
}
객체 해시코드(hashCode())
객체 해시코드란 객체를 식별한 하나의 정수값을 말한다. Object의 hashCode() 메소드는 객체의 메모리 번지를 이용해서 해시코드를 만들어 리턴하기 때문에 객체마다 다른 값을 가지고 있다. 논리적 동등 비교 시 hashCode()를 오버라이딩할 필요성이 있는데, 컬렉션 프레임워크에서 HashSet, HashMap, Hashtable은 두 객체가 동등한지 비교한다. 우선 HashCode() 메소드를 실행해서 리턴도니 해시코드 값이 같은지를 본다. 해시코드 값이 다르면 다른 객체로 판단하고, 해시코드 값이 같으면 equals() 메소드로 다시 비교한다. 그렇기 때문에 hashCode() 메소드가 true가 나와도 equals()의 리턴값이 다르면 다른 객체가 된다.
다음 예제를 보면 Key 클래스는 equals() 메소드를 오버라이딩해서 number 필드값이 같으면 true를 리턴하도록 했다. 그러나 hashCode() 메소드를 오버라이딩하지 않았기 때문에 Object의 hashCode() 메소드가 사용된다.
// Key.java -- hashCode() 메소드를 오버라이딩하지 않음
public class Key {
public int number;
public Key(int number) {
this.number = number;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Key) {
Key compareKey = (Key) obj;
if(this.number == compareKey.number) {
return true;
}
}
return false;
}
}
이런 경우 HashMap의 식별키로 Key 객체를 사용하면 저장된 값을 찾아오지 못한다. 왜냐하면 number 필드값이 같더라도 hashCode() 메소드에서 리턴하는 해시코드가 다르기 때문에 다른 식별키로 인식하기 때문이다. 다음 예제는 "new Key(1)" 객체로 "홍길동"을 저장하고, 다시 "new Key(1)"객체로 저장된 "홍길동"을 읽으려고 했지만 결과는 null이 나온다.
// KeyExample.java -- 다른 키로 인식
public class KeyExample {
public static void main(String[] args) {
// Key 객체를 식별키로 사용해서 String 값을 저장하는 HashMap 객체 생성
HashMap<Key, String> hashMaop = new HashMap<Key, String>();
// 식별키 "new Key(1)" 로 "홍길동"을 저장함
hashMap.put(new Key(1), "홍길동");
// 식별키 "new Key(1)"로 "홍길동"을 읽어옴
String value = hashMap.get(new Key(1));
System.out.println(value);
}
}
의도한 대로 "홍길동"을 읽으려면 다음과 같이 오버라이딩한 hashCode() 메소드를 Key 클래스에 추가하면 된다. hashCode()의 리턴값을 number 필드값으로 했기 때문에 저장할 때의 "new Key(1)"과 읽을 때의 "new Key(1)"은 같은 해시코드가 리턴된다.
// Key.java -- hashCode() 메소드 재정의 추가
public class Key {
...
@Override
public int hashCode() {
return number;
}
}
저장할 때의 new Key(1)과 읽을 때의 new Key(1)은 사실 서로 다른 객체이지만 HashMap은 hashCode()의 리턴값이 같고, equals() 리턴값이 true가 나오기 때문에 동등 객체로 평가한다. 즉, 같은 식별키로 인식한다는 뜻이다. 결론적으로 말해서 객체의 동등 비교를 위해서는 Object의 equals() 메소드만 오버라이딩하지 말고 hashCode() 메소드도 오버라이딩해서 논리적 동등 객체일 경우 동일한 해시코드가 리턴되도록 해야 한다. 다음은 이전 예제에서 사용한 Member 클래스를 보완하는 측면에서 hashCode() 메소드를 오버라이딩한 것이다. id 필드값이 같은 경우 같은 해시코드를 리턴하도록 하기 위해 String의 hashCode() 메소드의 리턴값을 활용했다. String의 hashCode()는 같은 문자열일 경우 동일한 해시코드를 리턴한다.
// Member.java -- hashCode() 메소드 재정의 추가
public class Member {
public String id;
public Member(String id) {
this.id = id;
}
@Override
public boolean equals(Obejct obj) {
if(obj instanceof Member) {
Member member = (Member) obj;
if(id.equals(member.id)) {
return true;
}
}
return false;
}
}
객체 문자 정보(toString())
Object 클래스의 toString() 메소드는 객체의 문자 정보를 리턴한다. 객체의 문자 정보란 객체를 문자열로 표현한 값을 말한다. 기본적으로 Object 클래스의 toString() 메소드는 "클래스명@16진수해시코드"로 구성된 문자 정보를 리턴한다.
Object obj = new Object();
System.out.println(obj.toString());
// 실행 결과
java.lang.Object@de6ced
Object의 toString() 메소드의 리턴값은 자바 애플리케이션에서는 별 값어치가 없는 정보이므로 Obejct 하위 클래스는 toString() 메소드를 오버라이딩하여 간결하고 유익한 정보를 리턴하도록 되어 있다. 예를 들어 java.util 패키지의 Date 클래스는 toString() 메소드를 오버라이딩하여 현재 시스템의 날짜와 시간 정보를 리턴한다. 그리고 String 클래스는 toString() 메소드를 오버라이딩해서 저장하고 있는 문자열을 리턴한다. 다음 예제는 Object 클래스와 Date 클래스의 toString() 메소드의 리턴값을 출력해본 것이다.
// ToStringExample.java
public class ToStringExample {
public static void main(String[] args) {
Object obj1 = new Object();
Date obj2 = new Date();
System.out.println(obj1.toString())
System.out.println(obj2.toString())
}
}
우리가 만드는 클래스도 toString() 메소드를 오버라이딩해서 좀 더 유용한 정보를 리턴하도록 할 수 있다. SmartPhone 클래스에서 toString() 메소드를 오버라이딩해서 제작회사와 운영체제를 리턴하도록 만들어보자.
// SmartPhone.java
public class SmartPhone {
private String company;
private String os;
public SmartPhone(String company, String os) {
this.company = company;
this.os = os;
}
@Override
public String toString() {
return company + ", " + os;
}
}
// SmartPhoneExample.java
public class SmartPhoneExample {
public static void main(String[] args) {
SmartPhone myPhone = new SmartPhone("구글", "안드로이드");
String strObj = myPhone.toString();
System.out.println(strObj);
System.out.println(myPhone);
}
}
우리는 지금까지 콘솔에 출력하기 위해 System.out.println() 메소드를 사용해 왔다. 이 메소드의 매개값은 콘솔에 출력할 내용인데, 매개값이 기본 타입일 경우, 해당 값을 그대로 출력한다. 만약 매개값으로 객체를 주면 객체의 toString() 메소드를 호출해서 리턴 값을 받아 출력하도록 되어 있다.
객체 복제(clone())
객체 복제는 원본 객체의 필드값과 동일한 값을 가지는 새로운 객체를 생성하는 것을 말한다. 객체를 복제하는 이유는 원본 객체를 안전하게 보호하기 위해서이다. 신뢰하지 않는 영역으로 원본 객체를 넘겨 작업할 경우 원본 객체의 데이터가 훼손될 수 있기 때문에 복제된 객체를 만들어 신뢰하지 않는 영역으로 넘기는 것이 좋다. 복제된 객체의 데이터가 훼손되더라도 원본 객체는 아무런 영향을 받지 않기 때문에 안전하게 데이터를 보호할 수 있다. 객체를 복제하는 방법에는 얕은 복제와 깊은 복제가 있다.
객체 복제 - 얕은 복제(this clone)
얕은 복제(thin clone)란 단순히 필드값을 복사해서 객체를 복제하는 것을 말한다. 필드값만 복제하기 때문에 필드가 기본 타입일 경우 값 복사가 일어나고, 필드가 참조 타입일 경우에는 객체의 번지가 복사된다.
Object의 clone() 메소드는 자신과 동일한 필드값을 가진 얕은 복제된 객체를 리턴한다. 이 메소드로 객체를 복제하려면 원본 객체는 반드시 java.lang.Coneable 인터페이스를 구현하고 있어야 한다. 메소드 선언이 없음에도 불구하고 Cloneable 인터페이스를 명시적으로 구현하는 이유는 클래스 설계자가 복제를 허용한다는 의도적인 표시를 하기 위해서이다. 클래스 설계자가 복제를 허용하지 않는다면 Cloneable 인터페이스를 구현하지 않으면 된다. Cloneable 인터페이스를 구현하지 않으면 clone() 메소드를 호출할 때 CloneNotSupportedException 예외가 발생하여 복제가 실패한다. clone()은 CloneNotSupportedException 예외 처리가 필요한 메소드이기 때문에 try-catch 구문이 필요하다.
try {
Object obj = clone();
}
catch(CloneNotSupportedException e) { }
다음 예제를 보면 Member 클래스가 Cloneable 인터페이스를 구현했기 때문에 getMember() 메소드에서 clone() 메소드로 자신을 복제한 후, 복제한 객체를 외부로 리턴할 수 있다.
// Member.java
public class Member implments Cloneable {
public String id;
public String name;
public String password;
public int age;
public boolean adult;
public Member(String id, String name, String password, int age, boolean adult) {
this.id = id;
this.name = name;
this.password = password;
this.age = age;
this.adult = adult;
}
public Member getMember {
Member cloned = null;
try {
cloned = (Member) clone();
}
catch(CloneNotSupportedException e) { }
return cloned;
}
}
객체 복제 - 깊은 복제(deep clone)
얕은 복제(thin clone)의 경우 참조 타입 필드는 번지만 복제되기 때문에 원본 객체의 필드와 복제 객체의 필드는 같은 객체를 참조하게 된다. 만약 복제 객체에서 참조 객체를 변경하면 원본 객체도 변경된 객체를 가지게 된다. 이것인 얕은 복제의 단점이다. 얕은 복제의 반대 개념은 깊은 복제(deep clone)이다. 깉은 복제란 참조하고 있는 객체도 복제하는 것을 말한다.
깊은 복제를 하려면 Object의 clone() 메소드를 오버라이딩해서 참조 객체를 복제하는 코드를 직접 작성해야 한다. 다음 예제를 보면 Member 클래스에 int[ ] 배열과 Car 타입의 필드가 있다. 이 필드들은 모두 참조 타입이므로 깊은 복제 대상이 된다. Member 클래스는 Object의 clone() 메소드를 오버라이딩해서 int[ ] 배열과 Cat 객체를 복제한다.
// Member.java
public class Member implements Cloneable {
public String name;
public int age;
public int[] scores;
public Car car;
public Member(String name, int age, int[] scores, Car car) {
this.name = name;
this.age = age;
this.scores = scores;
this.car = car;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// 먼저 얕은 복사를 해서 name, age를 복제한다.
Member cloned = (Member) super.clone();
// scores 를 깊은 복제한다.
cloned.scores = Arrays.copyOf(this.scores, this.scores.length);
// car 를 깊은 복제한다.
cloned.car = new Car(this.car.model);
// 깊은 복제된 Member 객체를 리턴
return cloned;
}
public Member getMember() {
Member cloned = null;
try {
cloned = (Member) clone();
}
catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return cloned;
}
}
// Car.java
public class Car {
public String model;
public Car(String model) {
this.model = model;
}
}
객체 소멸자(finalize())
참조하지 않는 배열이나 객체는 쓰레기 수집기(Garbage Collector)가 힙 영역에서 자동적으로 소멸시킨다. 쓰레기 수집기는 객체를 소멸하기 직전에 마지막으로 객체의 소멸자(finalize())를 실행시킨다. 소멸자는 Object의 finalize() 메소드를 말하는데, 기본적으로 실행 내용이 없다. 만약 객체가 소멸되기 전에 마지막으로 사용했던 자원(데이터 연결, 파일 등)을 닫고 싶거나, 중요한 데이터를 저장하고 싶다면 Object의 finalize()를 오버라이딩 할 수 있다. 다음은 finalize() 메소드를 오버라이딩한 클래스이다. finalize() 메소드가 실행되면 번호를 출력하게 해서 어떤 객체가 소멸되는지 확인할 수 있도록 했다.
// Counter.java
public class Counter {
private int no;
public Counter(int no) {
this.no = no;
}
@Override
protected void finalize() throws Throwable {
System.out.println(no + "번 개체의 finalize()가 실행됨");
}
}
다음은 객체가 소멸될 때 finalize()가 실행되는 것은 확인시켜주는 예제이다. 한두 개의 개체를 쓰레기로 만들었다고 해서 쓰레기 수집기가 실행되는 것은 아니기 때문에 반복해서 객체를 생성하고 쓰레기로 만들었다. 그리고 반복한 때마다 System.gc()를 호출해서 쓰레기 수집기를 가급적 빨리 실행해 달라고 JVM에게 요청했다.
// FinalizeExample.java
public class FinalizeExample {
public static void main(Strnig[] args) {
Counter counter = null;
for(int i=1; i<=50; i++) {
counter = new Counter(i);
counter = null; // Counter 객체를 쓰레기로 만듦
System.gc(); // 쓰레기 수집기 실행 요청
}
}
}
실행 결과를 보면 순서대로 소멸시키지 않고, 무작위로 소멸시키는 것을 볼 수 있다. 그리고 전부 소멸시키는 것이 아니라 메모리의 상태를 보고 일부만 소멸시킨다. 예제에서는 System.gc()로 쓰레기 수집기를 실행 요청하였으나, 쓰레기 수집기는 메모리가 부족할 때 그리고 CPU가 한가할 때에 JVM에 의해서 자동으로 실행된다. 그렇기 때문에 finalize() 메소드가 호출되는 시점은 명확하지 않다. 프로그램에 종료될 때 즉시 자원을 해제하거나 즉시 데이터를 최종 저장해야 한다면, 일반 메소드에서 작성하고 프로그램이 종료될 때 명시적으로 메소드를 호출하는 것이 좋다.