
'김영한의 실전 자바 - 중급 1편' 강의를 들으면서 복습할만한 내용을 정리하였다.
자바는 기본으로 제공하는 라이브러리(클래스 모음) 중에 가장 기본이 되는 것이 바로 java.lang 패키지이다. 여기서 lang 은 language (언어)의 줄임말이다. 쉽게 이야기해서 자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지를 뜻한다.
java.lang 패키지의 대표적인 클래스들
Object : 모든 자바 객체의 부모 클래스
String : 문자열
Integer, Long, Double : 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것
Class : 클래스 메타 정보
System : 시스템과 관련된 기본 기능들을 제공
java.lang 패키지는 모든 자바 애플리케이션에 자동으로 임포트(import)된다. 따라서 임포트 구문을 사용하지 않아도 된다.
자바에서 모든 클래스의 최상위 부모 클래스는 항상 Object 클래스이다.

package lang.object;
// 부모가 없으면 묵시적으로 Object 클래스를 상속받는다.
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
Object 클래스를 상속 받는다.extends Object 코드를 넣어준다.extends Object는 생략하는 것을 권장한다.Object 를 상속 받지 않는다.extends Object 코드를 넣지 않는다.묵시적(Implicit) vs 명시적(Explicit)
묵시적 : 개발자가 코드에 직접 기술하지 않아도 시스템 또는 컴파일러에 의해 자동으로 수행되는 것을 의미
명시적 : 개발자가 코드에 직접 기술해서 작동하는 것을 의미

Parent 는 Object 를 묵시적으로 상속 받았기 때문에 메모리에도 함께 생성된다.
모든 클래스가 Object 클래스를 상속 받는 이유는 다음과 같다.
공통 기능 제공
다형성의 기본 구현
객체의 정보를 제공하고, 이 객체가 다른 객체와 같은지 비교하고, 객체가 어떤 클래스로 만들어졌는지 확인하는 기능은 모두 객체에게 필요한 기본 기능이다. 이런 기능을 객체를 만들 때 마다 항상 새로운 메서드를 정의해서 만들어야 한다면 상당히 번거로울 것이다.
그리고 막상 만든다고 해도 개발자마다 서로 다른 이름의 메서드를 만들어서 일관성이 없을 것이다. 예를 들어서 객체의 정보를 제공하는 기능을 만든다고 하면 어떤 개발자는 toString() 으로 또 어떤 개발자는 objectInfo() 와 같이 서로 다른 이름으로 만들 수 있다.
Object 는 모든 객체에 필요한 공통 기능을 제공한다. Object 는 최상위 부모 클래스이기 때문에 모든 객체는 공통 기능을 편리하게 제공(상속) 받을 수 있다.
Object 가 제공하는 기능
toString()equals()getClass()부모는 자식을 담을 수 있다. Object 는 모든 클래스의 부모 클래스이다. 따라서 모든 객체를 참조할 수 있다.
Object 클래스는 다형성을 지원하는 기본적인 메커니즘을 제공한다. 모든 자바 객체는 Object 타입으로 처리될 수 있으며, 이는 다양한 타입의 객체를 통합적으로 처리할 수 있게 해준다.
쉽게 이야기 해서 Object 는 모든 객체를 다 담을 수 있다. 타입이 다른 객체들을 어딘가에 보관해야 한다면 바로 Object 에 보관하면 된다.
Object 는 모든 클래스의 부모 클래스이다. 따라서 Object 는 모든 객체를 참조할 수 있다.

Dog 와 Car 는 서로 아무런 관련이 없는 클래스이다. 둘다 부모가 없으므로 Object 를 자동으로 상속 받는다.
action 메서드
private static void action(Object obj) {
//obj.sound(); //컴파일 오류, Object는 sound()가 없다.
//obj.move(); //컴파일 오류, Object는 move()가 없다.
//객체에 맞는 다운캐스팅 필요
if (obj instanceof Dog dog) {
dog.sound();
}
else if (obj instanceof Car car) {
car.move();
}
}
이 메서드는 Object 타입의 매개변수를 사용한다. 그런데 Object 는 모든 객체의 부모다. 따라서 어떤 객체든지 인자로 전달할 수 있다.
action(dog) //main에서 dog 전달
private static void action(Object obj) {
obj.sound(); //컴파일 오류, Object는 sound()가 없다.
}
action() 메서드안에서 obj.sound() 를 호출하면 오류가 발생한다. 왜냐하면 매개변수인 obj 는 Object 타입이기 때문이다. Object 에는 sound() 메서드가 없다.

Dog 인스턴스의 sound() 를 호출하려면 다음과 같이 다운캐스팅을 해야한다.
if (obj instanceof Dog dog) {
dog.sound();
}
Object 는 모든 객체를 대상으로 다형적 참조를 할 수 있다.
Object 는 모든 객체의 부모이므로 모든 객체를 담을 수 있다.Object 를 통해 전달 받은 객체를 호출하려면 각 객체에 맞는 다운캐스팅 과정이 필요하다.
Object 가 세상의 모든 메서드를 알고 있는 것이 아니다.다형성을 제대로 활용하려면 자바 기본편에서 배운 것 처럼 다형적 참조 + 메서드 오버라이딩을 함께 사용해야 한다. 그러면에서 Object 를 사용한 다형성에는 한계가 있다.
Object 는 모든 객체의 부모이므로 모든 객체를 대상으로 다형적 참조를 할 수 있따. 하지만 Object 에는 Dog.sound() 와 같은 다른 객체의 메서드가 정의되어 있지 않다. 따라서 메서드 오버라이딩을 활용 할 수 없다. 결국 각 객체의 기능을 호출하려면 다운캐스팅을 해야 한다.
결과적으로 다형적 참조는 가능하지만, 메서드 오버라이딩이 안되기 때문에 다형성을 활용하기 에는 한계가 있다.
Object[] 을 만들면 세상의 모든 객체를 담을 수 있는 배열을 만들 수 있다.
...
Object objects[0] = new Dog();
Object objects[1] = new Car();
Object objects[2] = new Object();
size(objects);
}
private static void size(Object[] objects) {
System.out.println("전달된 객체의 수는: " + objects.length);
}

size() 메서드
size(Object[] objects) 메서드는 배열에 담긴 객체의 수를 세는 역할을 담당한다.
이 메서드는 Object 타입만 사용한다. Object 타입의 배열은 세상의 모든 객체를 담을 수 있기 때문에, 새로운 클래스가 추가되거나 변경되어도 이 메서드를 수정하지 않아도 된다. 지금 만든 size() 메서드는 자바를 사용하는 곳이라면 어디든지 사용될 수 있다.
만약 Object 와 같은 개념이 없다면 어떻게 될까?
void action(Object obj) 과 같이 모든 객체를 받을 수 있는 메서드를 만들 수 없다.
Object[] objects 처럼 모든 객체를 저장할 수 있는 배열을 만들 수 없다.
물론
Object가 없어도 직접MyObject와 같이 클래스를 만들고 모든 클래스에서 직접 정의한MyObject를 상속 받으면 된다. 하지만 하나의 프로젝트를 넘어서 전세계 모든 개발자가 비슷한 클래스르 만들 것이고, 서로 호환되지 않는 수 많은XxxObject들이 넘쳐날 것이다.
Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공한다. 그래서 디버깅과 로깅에 유용하게 사용된다.
이 메서드는 Object 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있다.
Object.toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
실행결과
java.lang.Object@a09ee92
Object 가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공한다.println()과 toString()System.out.println() 메서드느 사실 내부에서 toString() 을 호출한다. Object 타입(자식 포함)이 println() 에 인수로 전달되면 내부에서 obj.toString() 메서드를 호출해서 결과를 출력한다.
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
Object.toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못한다. 그래서 보통 toString() 을 재정의(오버라이딩)해서 보다 유용한 정볼르 제공하는 것이 일반적이다.
Dog
public class Dog {
private String dogName;
private int age;
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"dogName='" + dogName + '\'' +
", age=" + age +
'}';
}
}
Dog 는 toString() 을 재정의했다.
toString() 메서드는 IDE의 도움을 받아서 작성하는 것이 매우 편리하다.
ObjectPrinter
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
"객체 정보 출력:" 이라는 문자와 객체의 toString() 결과를 합해서 출력하는 단순한 기능을 제공한다.
Object obj의 인수로 dog(Dog)가 전달 된다.obj.toString() 을 호출한다.obj 는 Object 타입이다. 따라서 Object 에 있는 toString() 을 찾는다.Dog에 재정의된 메서드가 있다.Dog.toString() 을 실행한다.참조 - 객체의 참조값 직접 출력
toString()은 기본으로 객체의 참조값을 출력한다. 그런데 오버라이딩하면 객체의 참조값을 출력할 수 없다. 이때는 다음 코드를 사용하면 된다.String refValue = Integer.toHexString(System.identityHashCode(dog1)); System.out.println("refValue = " + refValue);실행결과
refValue = 72ea2f77
만약 Object 가 없고, 또 Object 가 제공하는 toString() 이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것이다. 여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻이다. 아마도 다음의 BadObjectPrinter 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것이다.
BadObjectPrinter
public class BadObjectPrinter {
public static void print(Car car) { //Car 전용 메서드
String string = "객체 정보 출력: " + car.carInfo(); //carInfo() 메서드 만듬
System.out.println(string);
}
public static void print(Dog dog) { //Dog 전용 메서드
String string = "객체 정보 출력: " + dog.dogInfo(); //dogInfo() 메서드 만듬
System.out.println(string);
}
}
BadObjectPrinter 는 구체적인 타입인 Car, Dog 를 사용한다. 따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 된다. 이렇게 BadObjectPrinter 클래스가 구체적인 특정 클래스인 Car, Dog 를 사용하는 것을 BadObjectPrinter 는 Car, Dog 에 의존한다고 표현한다.
다행히도 자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 Object 클래스와 메서드 오버라이딩 문제를 해결해줄 Object.toString() 메서드가 있다.
위에서 만든 ObjectPrinter 클래스는 Car, Dog 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 Object 클래스를 사용한다. 이렇게 ObjectPrinter 클래스가 Object 클래스를 사용하는 것을 ObjectPrinter 클래스가 Object 클래스에 의존한다고 표현한다.
추상적 : 여기서 말하는 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아니다.
Animal과Dog, Cat의 관계를 떠올려보자.Animal같은 부모 타입으로 올라갈수록 개념은 더 추상적이게 되고,Dog, Cat과 같이 하위 타입으로 내려갈 수록 개념은 더 구체적이게 된다.

ObjectPrinter 와 Object 를 사용하는 구조는 다형성을 매우 잘 활용하고 있다. 다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻이다.
Open : 새로운 클래스를 추가하고, toString() 을 오버라이딩해서 기능을 확장할 수 있다.
Closed : 새로운 클래스를 추가해도 Object 와 toString() 을 사용하는 클라이언트 코드인 ObjectPrinter 는 변경하지 않아도 된다.
다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 Car, Dog 에 의존하는 것이 아니라 추상적인 Object 에 의존하면서 OCP 원칙을 지킬 수 있었다. 덕분에 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있다. 그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter 는 변경할 필요가 없다.
ObjectPrinter 는 모든 타입의 부모인 Object 를 사용하고, Object 가 제공하는 toString() 메서드만 사용한다. 따라서 ObjectPrinter 를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있다.
지금까지 설명한 ObjectPrinter 는 사실 System.out.println() 의 작동 방식을 설명하기 위해 만든 것이다.

자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용한다.
참고 - 정적 의존관계 vs 동적 의존관계
정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미한다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의조관계이다. 쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있다.
동적 의존과계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계이다. 앞서
ObjectPrinter.print(Object obj)에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있다. 이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계이다.
Object 는 동등성 비교를 위한 equals() 메서드를 제공한다.
자바는 두 객체가 같다는 표현을 2가지로 분리해서 제공한다.
동일성(Identity) : == 연산자를 사용해서 두 객체가 참조가 동일한 객체를 가리키고 있는지 확인 (둘이 완전히 같은 객체 인스턴스 인지)
동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인
"동일"은 완전히 같음을 의미한다. 반면 "동등"은 같은 가치나 수준을 의미하지만 그 형태나 외관 등이 완전히 같지는 않을 수 있다.
쉽게 이야기해서 동일성은 물리적으로 같은 메모리에 있는 객체 인스턴스인지 참조값을 확인하는 것이고, 동등성은 논리적으로 같은지 확인하는 것이다.
동일성은 자바 머신 기준이고 메모리의 참조가 기준이므로 물리적이다. 반면 동등성은 보통 사람이 생각하는 논리적인 기준에 맞추어 비교한다.
Object.equals()
public boolean equals(Object obj) {
return (this == obj);
}
Object 가 기본으로 제공하는 equals() 는 == 으로 동일성 비교를 제공한다.
동등성이라는 개념은 각각의 클래스마다 다르다. 어떤 클래스는 주민등록번호를 기반으로 동등성을 처리할 수 있고, 어떤 클래스는 고객의 연락처를 기반으로 동등성을 처리할 수 있다.
따라서 동등성 비교를 사용하고 싶으면 equals() 메서드를 재정의해야 한다. 그렇지 않으면 Object 는 동일성 비교를 기본으로 제공한다.
IntelliJ를 포함한 대부분의 IDE는 정확한 equals() 코드를 자동으로 만들어준다.
// User 클래스의 id를 기반으로 동등성을 비교.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
clone() : 객체를 복사할 때 사용
hashCode() : equals() 와 hasCode() 는 종종 함께 사용된다.
getClass()
notify(), notifyAll(), wait() : 멀티쓰레드용 메서드이다.