Java는 기본적으로 lang 패키지를 제공하며, 이는 자바의 핵심적인 클래스를 보관하는 패키지다. lang 패키지는 별도의 import 없이도 사용할 수 있다.
주요 클래스는 다음과 같다.
이러한 클래스들은 자바에서 가장 기본적인 기능을 담당한다. 예를 들어, 우리가 자주 사용하는 System.out.println(sout)은 System 클래스에 포함된 기능으로, 별도의 import 없이도 사용할 수 있다. 이는 자바가 lang 패키지를 기본적으로 포함시켜 제공하기 때문이다. 이러한 구조 덕분에 자바 개발자는 핵심적인 기능을 자연스럽게 사용할 수 있다.
상속과 다형성을 배울 때, 우리는 최상위 부모 클래스를 우리가 만든 인터페이스나 커스텀 클래스로 인식했지만, 실제로 자바에서 모든 클래스의 최상위 부모 클래스는 Object이다. 즉, 부모 클래스를 명시적으로 지정하지 않은 경우, 묵시적으로 Object를 상속받는다.
모든 클래스에 Object가 최상위 부모로 존재하기 때문에, 이를 통해 공통 기능을 사용할 수 있으며, 다형성을 활용하여 Object 타입으로 모든 객체를 다룰 수도 있다. Object 클래스에서 제공하는 대표적인 공통 기능은 다음과 같다:
toString(): 객체를 문자열로 표현하는 메서드.equals(Object obj): 객체의 동등성을 비교하는 메서드.hashCode(): 객체의 해시코드를 반환하는 메서드.getClass(): 객체의 런타임 클래스를 반환하는 메서드.clone(): 객체의 복제본을 생성하는 메서드 (사용하려면 Cloneable 인터페이스를 구현해야 함).finalize(): 객체가 가비지 컬렉션 대상이 될 때 호출되는 메서드.이처럼 Object 클래스는 자바에서 모든 객체의 공통 기능을 제공하며, 이를 기반으로 다양한 객체 지향 프로그래밍 기능을 구현할 수 있다.
public class Main1 {
public static void main(String[] args) {
Dog dog = new Dog();
Car car = new Car();
action(dog);
action(car);
}
private static void action(Object obj) {
if (obj instanceof Dog dog) {
dog.sound();
} else if (obj instanceof Car car) {
car.move();
}
}
}
위 코드를 보면, action 스태틱 메서드는 Object타입을 입력으로 받는다. dog와 car는 서로 다른 클래스의 인스턴스이지만, 최상위 부모인 Object의 존재 덕분에 action 메서드에 Object 타입의 인자로 들어갈 수 있다. 이것이 다형성 활용의 첫 번째 예시이다.
부모는 항상 자식을 품을 수 있다는 것을 명심하자. 즉, action 메서드의 입구 문지기 역할을 하는 Object는 모든 클래스의 최상위 부모이기 때문에 어떤 객체든 받을 수 있다.
하지만 dog와 car는 각각 Dog와 Car 클래스의 인스턴스로, 각 클래스 고유의 메서드(sound, move)를 사용하려면 타입을 확인해야 한다. 이를 위해 if문과 instanceof를 사용하여 클래스 타입을 확인한다.
원래는 다음과 같이 명시적 다운캐스팅을 해야 했다.
if (obj instanceof Dog) {
Dog dog = (Dog) obj;
dog.sound();
}
그러나 Java 16 이상에서는 타입 매칭이 확장되어 `instanceof` 사용 시 명시적 캐스팅 없이 객체를 바로 사용할 수 있다.
```java
if (obj instanceof Dog dog) {
dog.sound(); // 추가 캐스팅 없이 바로 사용 가능
}
문제는 여전히 다운캐스팅 과정이 포함되며, instanceof로 매번 타입 검증을 진행해야 한다는 점이다.
이러한 문제를 해결하기 위해 오버라이딩(Overriding)을 사용할 수 있다. 부모 클래스나 인터페이스에 공통 메서드를 정의하고 각 클래스에서 이를 재정의(Overriding)하면, instanceof 없이도 다형성을 활용하여 적절한 메서드를 호출할 수 있다.
Object는 자바에서 제공하는 최상위 부모 클래스이다. 따라서 우리가 원하는 방향으로 리팩토링할 수 없으며, Object 자체를 수정하려는 시도는 해서는 안 된다.
다형성을 잘 활용하려면 일반적으로 메서드 오버라이딩(method overriding)을 이용해야 한다. 하지만 Object에 이미 정의된 메서드를 오버라이딩하지 않는 한, 특정 클래스의 고유 기능을 사용하기 위해서는 다운캐스팅이 유일한 방법이 된다.
그렇다면 커스텀 부모 클래스를 두어 Object는 활용하지 않고 다형성을 이용하는 방법이 떠오를 수 있다.(이것이 정답이다.)
Object는 모든 타입의 객체를 담을 수 있기 때문에, 배열을 Object 타입으로 선언하면 모든 타입의 객체를 배열에 담을 수 있다.
만약 Object가 존재하지 않는다면, 배열에 담기는 모든 객체가 동일한 부모를 가지도록 개발자가 직접 최상위 클래스를 만들어야 할 것이다. 하지만 Object와 같은 공통 개념 없이 각 개발자가 각각의 최상위 클래스를 정의한다면, 이름이 다르거나 설계가 일관되지 않아 서로 호환되지 않는 문제가 발생할 수 있다. 이를 방지하기 위해 자바는 Object라는 공통 최상위 클래스를 제공한다.
toString 메서드는 객체의 정보를 문자열 형태로 제공하는 메서드이다. 처음에는 이 메서드가 객체를 단순히 String으로 형변환해주는 기능이라고 오해할 수 있지만, 실제로는 객체의 상태나 정보를 문자열로 표현하는 역할을 한다.
toString은 Object 클래스의 메서드이므로, 모든 클래스에서 상속받아 사용할 수 있다. 필요에 따라 이 메서드를 오버라이딩하여 객체에 대한 사용자 정의 정보를 반환하도록 구현할 수 있다.
println() & toString()
println 메서드의 내부에서는 사실 toString()을 호출한다.
toString() 메서드는 기본적으로 클래스 정보와 참조값을 문자열로 제공하지만, 이는 종종 우리가 원하는 정보와 거리가 있을 수 있다. 이러한 경우, toString() 메서드를 오버라이딩(Overriding)하여 요구에 맞는 정보를 반환하도록 재정의할 수 있다.
일반적으로 개발자는 IDE(예: IntelliJ)의 Generator 단축키를 사용하여 toString() 메서드를 쉽게 오버라이딩한다. 이를 통해 객체의 상태나 주요 데이터를 손쉽게 문자열로 표현할 수 있다.
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", height=" + height +
'}';
}
위 코드처럼 toString() 오버라이딩 메서드를 구성했을 때 역으로 객체의 참조값을 알 수 없다.(기본 Object toString 기능이 사라지기에.)
객체의 참조값은 다음의 코드로 직접 출력도 가능하다. 필요시 커스텀한 toString에 추가해주면 될 듯 하다.
String refValue = Integer.toHexString(System.identityHashCode(yourObject))
System.identityHashCode(yourObject) 이 코드로 참조값을 뽑고 toHexString로 하여금 16진수 변환을 한다.
ex)
refValue = 72ea2f77
만약 Object와 toString이 존재하지 않는다면, 우리는 모든 클래스마다 객체 정보를 제공하는 메서드를 개별적으로 만들어야 할 것이다. 예를 들어, 객체 정보를 출력하는 클래스를 아래와 같이 작성한다고 가정해보자.
public class ObjectPrinter {
/*
// 추상적인 설계
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
*/
// 구체적인 설계
public static void printCat(Cat cat) {
String string = "객체 정보 출력: " + cat.toString();
System.out.println(string);
}
public static void printDog(Dog dog) {
String string = "객체 정보 출력: " + dog.toString();
System.out.println(string);
}
}
위 코드처럼 구체적인 클래스(Cat, Dog)에 의존하여 작성하면, 새로운 클래스가 추가될 때마다 ObjectPrinter에 관련 메서드를 계속 추가해야 한다. 이는 OCP(Open/Closed Principle)를 위반하게 된다.
이와 같은 구체적인 설계는 특정 클래스에 의존하며, 시스템 구조가 복잡해질수록 유지보수가 어려워지는 문제를 초래한다. 반면, 주석 처리된 추상적인 설계처럼 Object 타입에 의존한다면, 어떠한 클래스가 추가되더라도 영향을 받지 않는다.
결론적으로, 설계를 할 때 구체적인 것에 의존하지 않고, 추상적인 것에 의존하도록 만들어야 시스템이 확장 가능하고 유지보수도 용이해진다.
동일은 완전히 같음을 의미하며 동등은 같은 가치나 수준을 의미하지만 외관 등이 완전히 같지는 않은 것을 의미한다.
위 코드를 보면, user1과 user2는 동일한 값을 가지지만, 서로 다른 참조값을 가지고 있다. 따라서 이 둘은 동등하지 않지만 동일하지 않다고 볼 수 있다.
동등하다는 개념은 "가치와 수준이 같다"는 의미를 가지지만, 이는 상대적이고 추상적인 개념이다. 실제로 자바는 동등성의 구현을 프로그래머에게 맡긴다. 즉, 동등성을 구현하기 위해 equals 메서드를 오버라이딩해야 한다.
이를 통해 알 수 있는 것은, Object가 제공하는 기본 메서드들은 개발자가 구체적인 요구사항에 맞게 오버라이딩하여 활용하라는 메시지를 전달한다는 점이다.
equals() 메서드의 내부 코드를 보면, 기본적으로 == 연산자를 사용하여 객체를 비교한다.equals()의 기본 기능은 ==와 동일하며, 동일성(같은 참조값)을 판별한다.==로 비교할 수 있다.예를 들어, user1과 user2는 서로 다른 참조값을 가지지만, id가 같다면 동등하다고 간주할 수 있다. 이러한 동등성은 프로그래머가 직접 정의해야 한다.
equals 메서드 오버라이딩을 자동으로 생성할 수 있다. equals()는 동등성을 구현하기 위한 중요한 메서드로, 개발자는 객체의 가치 비교 기준에 맞게 이를 오버라이딩하여 사용해야 한다.
// 멤버 변수로 width, height를 가짐.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Rectangle rectangle = (Rectangle) o;
return width == rectangle.width && height == rectangle.height;
}