1. java.lang 패키지 & 불변 객체
java.lang 패키지
자바는 자바 언어를 이루는 가장 기본이 되는 클래스를 보관한 java.lang 패키지를 제공한다. 보관된 대표적인 클래스들은 아래와 같다.
- Object = 모든 자바 객체의 부모 클래스
- String = 문자열
- Wrapper(Integer, Long 등) = 기본형 데이터 타입을 객체로 만든 것
- Class = 클래스 메타 정보 저장
- System = 시스템과 관련된 기본 기능들을 제공
이번 섹션과 다음 섹션에서 자바 언어의 기본을 이루는 클래스들을 하나씩 살펴본다. 확실하게 학습하고 넘어가자.
참고
java.lang 패키지는 모든 자바 애플리케이션에 자동으로 import 된다. (생략 가능)
불변 객체
a. 공유 참조와 사이드 이펙트
자바의 데이터 타입을 가장 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.
- 기본형(Primitive Type)
- 항상 값을 복사해서 대입하기 때문에 하나의 값을 여러 변수에서 절대 공유하지 않는다.
- 참조형(Reference Type)
- 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
- 참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 된다. 따라서 객체의 공유를 막을 수 있는 방법이 없다.
- 그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생한다.
- 사이드 이펙트의 근원은 객체 자체를 공유하는 것이 아니라, 공유된 객체의 값을 변경한 것에 있다. 따라서 객체의 값을 변경하지 못하게 설계한다면 사이드 이펙트가 발생하는 것을 막을 수 있다.
b. 불변 객체(Immutable Object) - final
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라고 한다.
- 간단하게 객체 내부 값을 변경할 수 없도록 필드를 final로 선언하고, 생성자를 통해서만 값을 설정할 수 있게 바꾸면 된다.
public class ImmutableObject {
private final String value;
public ImmutableObject(String value) {
this.value = value;
}
...
}
c. 불변 객체(Immutable Object) - 값 변경
불변 객체를 설계할 때, 기존 값을 변경해야 하는 메서드가 필요할 수 있다. 이때는 기존 객체의 값을 그대로 두는 대신, 아래처럼 변경된 결과를 새로운 객체에 담아서 반환하면 된다.
public class ImmutableObject {
...
public ImmutableObject plus(String plus) {
String result = value + plus;
return new ImmutableObject(result);
}
}
참고
불변 객체에서 값을 변경하는 경우, withYear()처럼 "with"로 시작하는 경우가 많다.
자바에서 가장 많이 사용되는 String 클래스가 바로 불변 객체이다. 뿐만 아니라 자바가 기본으로 제공하는 Integer, LocalDate 등 수 많은 클래스가 불변 객체로 설계돼 있다. 따라서 불변 객체의 기초를 확실하게 이해해야 이런 기본 클래스들도 제대로 이해할 수 있다.
참고
모든 클래스를 불변으로 만드는 것은 아니다. 불변 클래스는 값을 변경하면 안되는 특별한 경우에만 만들어서 사용하면 된다. 때로는 같은 기능을 하는 클래스를 하나는 불변으로, 하나는 가변으로 각각 만드는 경우도 있다.
참고
클래스를 불변으로 설계하는 이유는 더 많다.
- 캐시 안정성
- 멀티 쓰레드 안정성
- 엔티티의 값 타입
2. Object 클래스
최상위 부모 클래스
자바에서 모든 클래스의 최상위 부모 클래스는 항상 Object 클래스이다.
- 클래스에 상속받을 부모 클래스가 없으면, 묵시적으로 Object 클래스를 상속받는다.
- 묵시적 = 개발자가 코드에 직접 기술하지 않아도, 시스템 또는 컴파일러에 의해 자동으로 수행되는 것
// 코드를 따로 적지 않아도 Object 클래스를 자동으로 상속(extends)받음
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
- 클래스에 상속받을 부모 클래스를 명시적으로 지정하면, Object 클래스를 상속받지 않는다.
- 명시적 = 개발자가 코드에 직접 기술해서 작동하는 것
// 상속받을 클래스를 지정하면 Object 클래스를 자동으로 상속(extends)받지 않음
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
그림으로 나타내면 아래와 같다.

모든 클래스가 Object 클래스를 상속받는 이유는 다음과 같다. 아래에서 더 자세하게 살펴보자.
- 공통 기능 제공
- Object는 모든 객체에 필요한 공통 기능을 제공한다. Object는 최상위 부모 클래스이기 때문에 Object에서 제공하는 기능은 모든 클래스에서 사용할 수 있다.
- Object가 제공하는 기능은 아래와 같다. 자세한 기능은 아래에서 알아본다.
- toString() = 객체 정보 제공
- equals() = 객체 동등성 비교
- getClass() = 객체 클래스 정보 제공
- 기타 기능
- 다형성의 기본 구현
- Object는 최상위 부모 클래스이기 때문에 모든 객체를 참조할 수 있다.
- Object는 다형성을 지원하는 기본적인 메커니즘을 제공한다. 모든 자바 객체는 Object 타입으로 처리될 수 있으며, 이는 다양한 타입의 객체를 통합적으로 처리할 수 있게 해 준다.
Object 다형성
Object는 모든 객체를 대상으로 다형적 참조를 할 수 있다. 쉽게 말하면 Object는 모든 객체의 부모이므로, 모든 객체를 담을 수 있다.
- 다형성을 제대로 활용하려면 다형적 참조와 메서드 오버라이딩을 함께 사용해야 한다. 그런 면에서 Object를 사용한 다형성에는 한계가 있다.
- 아래 코드를 통해 살펴보자.
- Bird 클래스엔 fly() 메서드가 있고, Cat 클래스엔 sound() 메서드가 있다. Object는 이 두 클래스의 공통 부모이다.
- action() 메서드에서 Object 타입 매개변수 obj에 Bird와 Cat 객체를 넘겨도, 매개변수인 obj는 Object 타입이기 때문에 fly()와 sound() 메서드가 정의돼 있지 않다.
- 따라서 Object 객체를 Bird와 Cat에 맞게 각각 다운캐스팅 해야 한다.
public class ObjectPoly {
public static void main(String[] args) {
Bird bird = new Bird();
Cat cat = new Cat();
action(bird);
action(cat);
}
private static void action(Object obj) {
// obj.fly(); // 컴파일 오류 -> Object에는 fly() 메서드가 없음
// obj.sound(); // 컴파일 오류 -> Object에는 sound() 메서드가 없음
// Object를 객체에 맞춰 다운캐스팅한 뒤에 메서드를 호출해야 함
if (obj instanceof Bird bird) {
bird.fly();
} else if (obj instanceof Cat cat) {
cat.sound();
}
}
}
참고
Object 타입을 사용하면 모든 객체를 담을 수 있는 배열을 만들 수 있다.
ex. Object[] objects = {dog, cat, bird};
toString() 메서드
Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공하므로, 디버깅과 로깅에서 유용하게 사용된다.
- 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시 코드)을 16진수로 제공한다.
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
- System.out.println() 메서드도 내부에서 tostring()을 호출한다.
- 정확하게는 Object 타입(자식 포함)이 println()에 인수로 전달되면 내부에서 obj.toString()을 호출해 결과를 출력한다. 즉, 객체를 바로 전달하면 객체의 정보를 출력할 수 있다.
public void println(Object x) {
String s = String.valueOf(x);
// ...
}
///
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
위에서 기본적으로 제공하는 정보만으론 객체의 상태를 적절하게 나타낼 수 없기 때문에 보통은 아래처럼 toString()을 오버라이딩해서 사용한다.
- System.out.println()에 dog.toString()을 넘기든 dog를 넘기든, 모두 dog가 오버라이딩 한 toString()이 호출된다.
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 아래처럼 ObjectPrinter 클래스를 만들고 Dog 객체를 인수로 넘기면 어떻게 출력될까? 내부 코드를 분석해 보자.
- ObjectStringMain에서 print(Object obj)의 인수로 dog(Dog)가 전달된다.
- 메서드 내부에서 obj.toString()을 호출한다.
- obj는 Object 타입이므로 우선 Object에 있는 toString()을 찾는다.
- 이때 자식에 오버라이딩된 메서드가 있는지 찾아본다.
- Dog에 오버라이딩된 메서드가 있으므로 Dog.toString()을 실행한다.
public class ObjectPrinter {
public static void print(Object obj) {
String str = "객체 정보 출력: " + obj.toString();
System.out.println(str);
}
}
///
public ObjectStringMain {
Dog dog = new Dog("dogA", 1);
ObjectPrinter.print(dog);
}
참고
toString()은 기본으로 객체의 참조값(16진수)을 출력한다. 그런데 toString()이나 hashCode()를 재정의하면 객체의 참조값을 출력할 수 없게 된다. 이때는 아래 코드를 사용하면 객체의 참조값을 출력할 수 있다.
public class refValue {
public static void main(String[] args) {
Dog dog = new dog("dogA", 1);
String refValue = Integer.toHexString(System.identityHashCode(dog));
System.out.println("refValue = " + refValue);
}
}
Object와 OCP(Open-Closed Principle)
만약 Object가 없고, Object가 제공하는 toString()이 없다면 서로 공통된 부모가 없는 객체의 정보를 출력하기 어려울 것이다.
- Object가 없다면, Object를 대신할 공통 부모 클래스를 만들어야 한다. 이때, 이 클래스는 구체적인 클래스에 대한 정보를 모두 갖고 있어야 하기 때문에 클래스가 늘어날수록 구체적인 것에 의존하게 된다.
- 그에 비해, 위에서 만든 ObjectPrinter 클래스는 추상적인 Object 클래스에 의존한다. 여기서 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아니다. 개념은 부모 타입으로 올라갈수록 더 추상적이게 되고, 하위 타입으로 내려갈수록 더 구체적이게 된다.

ObjectPrinter와 Object를 사용하는 구조는 다형적 참조와 메서드 오버라이딩을 적절하게 사용하고 있다.
- 다형적 참조
- print(Object obj), Object 타입을 매개변수로 사용해서 다형적 참조를 사용한다. 따라서 모든 객체 인스턴스를 인수로 받을 수 있다.
- 메서드 오버라이딩
- Object는 모든 클래스의 부모이기 때문에 구체적인 클래스들은 Object가 갖고 있는 toString() 메서드를 오버라이딩할 수 있다.
- 따라서 print(Object obj) 메서드는 구체적인 타입에 의존하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString()을 호출할 수 있다.
또, 클라이언트 코드가 구체적인 것에 의존하는 것이 아니라 추상적인 Object에 의존하면서 OCP(Open-Closed Principle)도 잘 지킬 수 있다.
- Open
- 새로운 클래스를 추가하고, toString()을 오버라이딩해서 기능을 확장할 수 있다.
- Closed
- 새로운 클래스를 추가해도, Object와 toString()을 사용하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 된다.
위에서 만들어본 ObjectPrinter.print()는 사실 System.out.println()의 작동 방식과 거의 동일하다.
- System.out.println() 메서드도 Object를 매개변수로 사용하고, 내부에서 toString()을 호출한다.

정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미한다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계다. 즉, 프로그램을 실행하지 않고, 클래스 내부에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있다.
동적 의존관계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계다. 예를 들어, 런타임에 Object obj에 어떤 객체 인스턴스( ex. Dog, Cat)가 넘어올지는 런타임에 결정된다.
참고로 단순히 의존관계 또는 어디에 의존한다고 표현하면 주로 정적 의존관계를 뜻한다.
ex. ObjectPrinter는 Object에 의존한다.
equals() 메서드
a. 개념
자바는 '두 객체가 같다'라는 표현을 2가지로 분리해서 제공한다.
- 동일성(Identity)
- == 연산자를 사용해서. 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
- 자바 머신을 기준으로 하며, 메모리의 참조가 기준이므로 물리적이다.
- 동등성(Equality)
- equals() 메서드를 사용해서. 두 객체가 논리적으로 동등한 지 확인
- 보통 사람이 생각하는 논리적인 기준에 맞춰 비교한다.
예를 들어 같은 주민등록번호를 가진 회원 객체가 2개 있다고 가정해 보자. 이 경우 물리적으로 다른 메모리에 있는 객체지만, 주민등록번호를 기준으로 생각해 보면 논리적으로는 같은 회원으로 볼 수 있다.
- 즉, 동등성은 같지만 동일성은 다르다.
public class UserMain {
public static void main(String[] args) {
// 참조 x001
User a = new User("xxxxxx-xxxxxxx");
// 참조 x002
User b = new User("xxxxxx-xxxxxxx");
System.out.println("Identity = " + (a == b)); // false
System.out.println("Equality = " + a.equals(b)); // false
}
}
b. 구현
동등성이라는 개념은 각각의 클래스마다 다를 수 있다. 예를 들어 주민등록번호나 핸드폰 번호 등 다양한 논리적인 조건을 기반으로 동등성을 처리할 수 있다. 따라서 동등성 비교를 하고 싶다면 equals() 메서드를 오버라이딩해야 한다.
- 그렇지 않으면 Object는 동일성 비교를 기본으로 제공한다.
// Object가 기본으로 제공하는 equals()는 ==으로 동일성 비교를 제공함
public boolean equals(Object obj) {
return (this == obj);
}
고객 번호가 같으면 논리적으로 같은 객체로 정의하도록 User 클래스를 만들어 보자.
- equals() 메서드를 오버라이딩했기 때문에 동등성 비교를 하면 true가 출력된다.
public class User {
private String id;
public User(String id) {
this.id = 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);
}
}
///
public class UserMain {
public static void main(String[] args) {
// 참조 x001
User a = new User("101");
// 참조 x002
User b = new User("101");
System.out.println("Identity = " + (a == b)); // false
System.out.println("Equality = " + a.equals(b)); // true
}
}
참고로 eqauls() 메서드를 구현할 때 지켜야 하는 규칙이 몇 가지가 있다.
- 반사성(Reflexivity)
- 객체는 자기 자신과 동등해야 한다.
- x.equals(x)는 항상 true이다.
- 대칭성(Symmetry)
- 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 한다.
- x.equals(y)가 true이면, y.equals(x)도 true이다.
- 추이성(Transitivity)
- 만약 한 객체가 두 번째 객체와 동일하고, 두 번째 객세가 세 번째 객체와 동일하다면, 첫 번째 객체는 세 번째 객체와도 동일해야 한다.
- x.equals(y)와 y.equals(z)가 true이면, x.equals(z)도 true이다.
- 일관성(Consistency)
- 두 객체의 상태가 변경되지 않는 한, equals() 메서드는 항상 동일한 값을 반환해야 한다.
- null에 대한 비교
- 모든 객체는 null과 비교했을 때 false를 반환해야 한다.
참고
실무에서는 대부분 IDE가 만들어주는 equals()를 사용하므로, 이 규칙은 읽어 보고 넘어가도 된다.
참고
동등성 비교가 필요한 경우에만 equals()를 오버라이딩하면 된다.
Object의 나머지 메서드
clone()
- 객체를 복사할 때 사용된다. 잘 사용하지 않으므로 다루지 않는다.
hashCode()
- equals()와 hashCode()는 종종 함께 사용된다. hashCode()는 뒤에 컬렉션 프레임워크에서 자세히 설명한다.
getClass()
- 래퍼, Class 클래스 섹션에서 설명한다.
notify(), notifyAll(), wait()
- 멀티쓰레드용 메서드다.
1. java.lang 패키지 & 불변 객체
java.lang 패키지
자바는 자바 언어를 이루는 가장 기본이 되는 클래스를 보관한 java.lang 패키지를 제공한다. 보관된 대표적인 클래스들은 아래와 같다.
- Object = 모든 자바 객체의 부모 클래스
- String = 문자열
- Wrapper(Integer, Long 등) = 기본형 데이터 타입을 객체로 만든 것
- Class = 클래스 메타 정보 저장
- System = 시스템과 관련된 기본 기능들을 제공
이번 섹션과 다음 섹션에서 자바 언어의 기본을 이루는 클래스들을 하나씩 살펴본다. 확실하게 학습하고 넘어가자.
참고
java.lang 패키지는 모든 자바 애플리케이션에 자동으로 import 된다. (생략 가능)
불변 객체
a. 공유 참조와 사이드 이펙트
자바의 데이터 타입을 가장 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.
- 기본형(Primitive Type)
- 항상 값을 복사해서 대입하기 때문에 하나의 값을 여러 변수에서 절대 공유하지 않는다.
- 참조형(Reference Type)
- 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
- 참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 된다. 따라서 객체의 공유를 막을 수 있는 방법이 없다.
- 그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생한다.
- 사이드 이펙트의 근원은 객체 자체를 공유하는 것이 아니라, 공유된 객체의 값을 변경한 것에 있다. 따라서 객체의 값을 변경하지 못하게 설계한다면 사이드 이펙트가 발생하는 것을 막을 수 있다.
b. 불변 객체(Immutable Object) - final
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라고 한다.
- 간단하게 객체 내부 값을 변경할 수 없도록 필드를 final로 선언하고, 생성자를 통해서만 값을 설정할 수 있게 바꾸면 된다.
public class ImmutableObject {
private final String value;
public ImmutableObject(String value) {
this.value = value;
}
...
}
c. 불변 객체(Immutable Object) - 값 변경
불변 객체를 설계할 때, 기존 값을 변경해야 하는 메서드가 필요할 수 있다. 이때는 기존 객체의 값을 그대로 두는 대신, 아래처럼 변경된 결과를 새로운 객체에 담아서 반환하면 된다.
public class ImmutableObject {
...
public ImmutableObject plus(String plus) {
String result = value + plus;
return new ImmutableObject(result);
}
}
참고
불변 객체에서 값을 변경하는 경우, withYear()처럼 "with"로 시작하는 경우가 많다.
자바에서 가장 많이 사용되는 String 클래스가 바로 불변 객체이다. 뿐만 아니라 자바가 기본으로 제공하는 Integer, LocalDate 등 수 많은 클래스가 불변 객체로 설계돼 있다. 따라서 불변 객체의 기초를 확실하게 이해해야 이런 기본 클래스들도 제대로 이해할 수 있다.
참고
모든 클래스를 불변으로 만드는 것은 아니다. 불변 클래스는 값을 변경하면 안되는 특별한 경우에만 만들어서 사용하면 된다. 때로는 같은 기능을 하는 클래스를 하나는 불변으로, 하나는 가변으로 각각 만드는 경우도 있다.
참고
클래스를 불변으로 설계하는 이유는 더 많다.
- 캐시 안정성
- 멀티 쓰레드 안정성
- 엔티티의 값 타입
2. Object 클래스
최상위 부모 클래스
자바에서 모든 클래스의 최상위 부모 클래스는 항상 Object 클래스이다.
- 클래스에 상속받을 부모 클래스가 없으면, 묵시적으로 Object 클래스를 상속받는다.
- 묵시적 = 개발자가 코드에 직접 기술하지 않아도, 시스템 또는 컴파일러에 의해 자동으로 수행되는 것
// 코드를 따로 적지 않아도 Object 클래스를 자동으로 상속(extends)받음
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
- 클래스에 상속받을 부모 클래스를 명시적으로 지정하면, Object 클래스를 상속받지 않는다.
- 명시적 = 개발자가 코드에 직접 기술해서 작동하는 것
// 상속받을 클래스를 지정하면 Object 클래스를 자동으로 상속(extends)받지 않음
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
그림으로 나타내면 아래와 같다.

모든 클래스가 Object 클래스를 상속받는 이유는 다음과 같다. 아래에서 더 자세하게 살펴보자.
- 공통 기능 제공
- Object는 모든 객체에 필요한 공통 기능을 제공한다. Object는 최상위 부모 클래스이기 때문에 Object에서 제공하는 기능은 모든 클래스에서 사용할 수 있다.
- Object가 제공하는 기능은 아래와 같다. 자세한 기능은 아래에서 알아본다.
- toString() = 객체 정보 제공
- equals() = 객체 동등성 비교
- getClass() = 객체 클래스 정보 제공
- 기타 기능
- 다형성의 기본 구현
- Object는 최상위 부모 클래스이기 때문에 모든 객체를 참조할 수 있다.
- Object는 다형성을 지원하는 기본적인 메커니즘을 제공한다. 모든 자바 객체는 Object 타입으로 처리될 수 있으며, 이는 다양한 타입의 객체를 통합적으로 처리할 수 있게 해 준다.
Object 다형성
Object는 모든 객체를 대상으로 다형적 참조를 할 수 있다. 쉽게 말하면 Object는 모든 객체의 부모이므로, 모든 객체를 담을 수 있다.
- 다형성을 제대로 활용하려면 다형적 참조와 메서드 오버라이딩을 함께 사용해야 한다. 그런 면에서 Object를 사용한 다형성에는 한계가 있다.
- 아래 코드를 통해 살펴보자.
- Bird 클래스엔 fly() 메서드가 있고, Cat 클래스엔 sound() 메서드가 있다. Object는 이 두 클래스의 공통 부모이다.
- action() 메서드에서 Object 타입 매개변수 obj에 Bird와 Cat 객체를 넘겨도, 매개변수인 obj는 Object 타입이기 때문에 fly()와 sound() 메서드가 정의돼 있지 않다.
- 따라서 Object 객체를 Bird와 Cat에 맞게 각각 다운캐스팅 해야 한다.
public class ObjectPoly {
public static void main(String[] args) {
Bird bird = new Bird();
Cat cat = new Cat();
action(bird);
action(cat);
}
private static void action(Object obj) {
// obj.fly(); // 컴파일 오류 -> Object에는 fly() 메서드가 없음
// obj.sound(); // 컴파일 오류 -> Object에는 sound() 메서드가 없음
// Object를 객체에 맞춰 다운캐스팅한 뒤에 메서드를 호출해야 함
if (obj instanceof Bird bird) {
bird.fly();
} else if (obj instanceof Cat cat) {
cat.sound();
}
}
}
참고
Object 타입을 사용하면 모든 객체를 담을 수 있는 배열을 만들 수 있다.
ex. Object[] objects = {dog, cat, bird};
toString() 메서드
Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공하므로, 디버깅과 로깅에서 유용하게 사용된다.
- 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시 코드)을 16진수로 제공한다.
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
- System.out.println() 메서드도 내부에서 tostring()을 호출한다.
- 정확하게는 Object 타입(자식 포함)이 println()에 인수로 전달되면 내부에서 obj.toString()을 호출해 결과를 출력한다. 즉, 객체를 바로 전달하면 객체의 정보를 출력할 수 있다.
public void println(Object x) {
String s = String.valueOf(x);
// ...
}
///
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
위에서 기본적으로 제공하는 정보만으론 객체의 상태를 적절하게 나타낼 수 없기 때문에 보통은 아래처럼 toString()을 오버라이딩해서 사용한다.
- System.out.println()에 dog.toString()을 넘기든 dog를 넘기든, 모두 dog가 오버라이딩 한 toString()이 호출된다.
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 아래처럼 ObjectPrinter 클래스를 만들고 Dog 객체를 인수로 넘기면 어떻게 출력될까? 내부 코드를 분석해 보자.
- ObjectStringMain에서 print(Object obj)의 인수로 dog(Dog)가 전달된다.
- 메서드 내부에서 obj.toString()을 호출한다.
- obj는 Object 타입이므로 우선 Object에 있는 toString()을 찾는다.
- 이때 자식에 오버라이딩된 메서드가 있는지 찾아본다.
- Dog에 오버라이딩된 메서드가 있으므로 Dog.toString()을 실행한다.
public class ObjectPrinter {
public static void print(Object obj) {
String str = "객체 정보 출력: " + obj.toString();
System.out.println(str);
}
}
///
public ObjectStringMain {
Dog dog = new Dog("dogA", 1);
ObjectPrinter.print(dog);
}
참고
toString()은 기본으로 객체의 참조값(16진수)을 출력한다. 그런데 toString()이나 hashCode()를 재정의하면 객체의 참조값을 출력할 수 없게 된다. 이때는 아래 코드를 사용하면 객체의 참조값을 출력할 수 있다.
public class refValue {
public static void main(String[] args) {
Dog dog = new dog("dogA", 1);
String refValue = Integer.toHexString(System.identityHashCode(dog));
System.out.println("refValue = " + refValue);
}
}
Object와 OCP(Open-Closed Principle)
만약 Object가 없고, Object가 제공하는 toString()이 없다면 서로 공통된 부모가 없는 객체의 정보를 출력하기 어려울 것이다.
- Object가 없다면, Object를 대신할 공통 부모 클래스를 만들어야 한다. 이때, 이 클래스는 구체적인 클래스에 대한 정보를 모두 갖고 있어야 하기 때문에 클래스가 늘어날수록 구체적인 것에 의존하게 된다.
- 그에 비해, 위에서 만든 ObjectPrinter 클래스는 추상적인 Object 클래스에 의존한다. 여기서 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아니다. 개념은 부모 타입으로 올라갈수록 더 추상적이게 되고, 하위 타입으로 내려갈수록 더 구체적이게 된다.

ObjectPrinter와 Object를 사용하는 구조는 다형적 참조와 메서드 오버라이딩을 적절하게 사용하고 있다.
- 다형적 참조
- print(Object obj), Object 타입을 매개변수로 사용해서 다형적 참조를 사용한다. 따라서 모든 객체 인스턴스를 인수로 받을 수 있다.
- 메서드 오버라이딩
- Object는 모든 클래스의 부모이기 때문에 구체적인 클래스들은 Object가 갖고 있는 toString() 메서드를 오버라이딩할 수 있다.
- 따라서 print(Object obj) 메서드는 구체적인 타입에 의존하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString()을 호출할 수 있다.
또, 클라이언트 코드가 구체적인 것에 의존하는 것이 아니라 추상적인 Object에 의존하면서 OCP(Open-Closed Principle)도 잘 지킬 수 있다.
- Open
- 새로운 클래스를 추가하고, toString()을 오버라이딩해서 기능을 확장할 수 있다.
- Closed
- 새로운 클래스를 추가해도, Object와 toString()을 사용하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 된다.
위에서 만들어본 ObjectPrinter.print()는 사실 System.out.println()의 작동 방식과 거의 동일하다.
- System.out.println() 메서드도 Object를 매개변수로 사용하고, 내부에서 toString()을 호출한다.

정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미한다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계다. 즉, 프로그램을 실행하지 않고, 클래스 내부에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있다.
동적 의존관계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계다. 예를 들어, 런타임에 Object obj에 어떤 객체 인스턴스( ex. Dog, Cat)가 넘어올지는 런타임에 결정된다.
참고로 단순히 의존관계 또는 어디에 의존한다고 표현하면 주로 정적 의존관계를 뜻한다.
ex. ObjectPrinter는 Object에 의존한다.
equals() 메서드
a. 개념
자바는 '두 객체가 같다'라는 표현을 2가지로 분리해서 제공한다.
- 동일성(Identity)
- == 연산자를 사용해서. 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
- 자바 머신을 기준으로 하며, 메모리의 참조가 기준이므로 물리적이다.
- 동등성(Equality)
- equals() 메서드를 사용해서. 두 객체가 논리적으로 동등한 지 확인
- 보통 사람이 생각하는 논리적인 기준에 맞춰 비교한다.
예를 들어 같은 주민등록번호를 가진 회원 객체가 2개 있다고 가정해 보자. 이 경우 물리적으로 다른 메모리에 있는 객체지만, 주민등록번호를 기준으로 생각해 보면 논리적으로는 같은 회원으로 볼 수 있다.
- 즉, 동등성은 같지만 동일성은 다르다.
public class UserMain {
public static void main(String[] args) {
// 참조 x001
User a = new User("xxxxxx-xxxxxxx");
// 참조 x002
User b = new User("xxxxxx-xxxxxxx");
System.out.println("Identity = " + (a == b)); // false
System.out.println("Equality = " + a.equals(b)); // false
}
}
b. 구현
동등성이라는 개념은 각각의 클래스마다 다를 수 있다. 예를 들어 주민등록번호나 핸드폰 번호 등 다양한 논리적인 조건을 기반으로 동등성을 처리할 수 있다. 따라서 동등성 비교를 하고 싶다면 equals() 메서드를 오버라이딩해야 한다.
- 그렇지 않으면 Object는 동일성 비교를 기본으로 제공한다.
// Object가 기본으로 제공하는 equals()는 ==으로 동일성 비교를 제공함
public boolean equals(Object obj) {
return (this == obj);
}
고객 번호가 같으면 논리적으로 같은 객체로 정의하도록 User 클래스를 만들어 보자.
- equals() 메서드를 오버라이딩했기 때문에 동등성 비교를 하면 true가 출력된다.
public class User {
private String id;
public User(String id) {
this.id = 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);
}
}
///
public class UserMain {
public static void main(String[] args) {
// 참조 x001
User a = new User("101");
// 참조 x002
User b = new User("101");
System.out.println("Identity = " + (a == b)); // false
System.out.println("Equality = " + a.equals(b)); // true
}
}
참고로 eqauls() 메서드를 구현할 때 지켜야 하는 규칙이 몇 가지가 있다.
- 반사성(Reflexivity)
- 객체는 자기 자신과 동등해야 한다.
- x.equals(x)는 항상 true이다.
- 대칭성(Symmetry)
- 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 한다.
- x.equals(y)가 true이면, y.equals(x)도 true이다.
- 추이성(Transitivity)
- 만약 한 객체가 두 번째 객체와 동일하고, 두 번째 객세가 세 번째 객체와 동일하다면, 첫 번째 객체는 세 번째 객체와도 동일해야 한다.
- x.equals(y)와 y.equals(z)가 true이면, x.equals(z)도 true이다.
- 일관성(Consistency)
- 두 객체의 상태가 변경되지 않는 한, equals() 메서드는 항상 동일한 값을 반환해야 한다.
- null에 대한 비교
- 모든 객체는 null과 비교했을 때 false를 반환해야 한다.
참고
실무에서는 대부분 IDE가 만들어주는 equals()를 사용하므로, 이 규칙은 읽어 보고 넘어가도 된다.
참고
동등성 비교가 필요한 경우에만 equals()를 오버라이딩하면 된다.
Object의 나머지 메서드
clone()
- 객체를 복사할 때 사용된다. 잘 사용하지 않으므로 다루지 않는다.
hashCode()
- equals()와 hashCode()는 종종 함께 사용된다. hashCode()는 뒤에 컬렉션 프레임워크에서 자세히 설명한다.
getClass()
- 래퍼, Class 클래스 섹션에서 설명한다.
notify(), notifyAll(), wait()
- 멀티쓰레드용 메서드다.