자바에서 모든 클래스의 최상위 부모 클래스는 항상 Object
클래스이다.
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
Object
타입으로 처리될 수 있다.Object 클래스는 다양한 메서드를 가지고 있다.
toString()은 객체의 정보를 제공하는 메서드로, 문자열 형태로 제공한다. 그래서 디버깅과 로깅에 유용하게 사용된다.
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public class ToStringMain {
public static void main(String[] args) {
Object obj = new Object();
String str = object.toString();
System.out.println(str);
System.out.println(obj);
}
}
java.lang.Object@a09ee92
java.lang.Object@a09ee92
toStirng()
의 결과를 출력한 결과와 printlin()
에 객체를 직접한 출력한 것이 같았다.
왜 그랬던 것일까? println()
은 내부에서 toString()
을 호출하기 때문이다.
println()
메서드를 살펴보면 다음과 같은 코드를 수행하는 것을 알 수 있다. 여기서 valueOf()
메서드를 자세히 살펴보면 toString() 메서드를 호출하는 것을 확인할 수 있다.
public void println(String x) {
if (getClass() == PrintStream.class) {
writeln(String.valueOf(x));
} else {
synchronized (this) {
print(x);
newLine();
}
}
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
toString()
메서드가 클래스 정보와 참조값을 제공한다. 하지만, 이 정보만으로는 객체의 상태를 나타내지 못하는 경우도 있기 때문에 보통 toString()을 재정의하여 사용한다.
- 단축키: command
+ N
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 +
'}';
}
}
toStirng()
은 기본으로 객체의 참조값을 출력한다. 하지만, toString()
이나 hashCode()
를 재정의하면 객체의 참조값을 출력할 수 없기 때문에 다음의 코드를 활용하여 참조값을 출력할 수 있다.
String refValue = Integer.toHexString(System.identityHashCode(obj));
System.out.println("refValue = " + refValue);
equals()
는 객체의 같음을 비교하는 메서드로, 동등성을 비교햔다.
동등성이란 무엇일까?
자바는 두 객체가 같다는 표현을 2가지로 분리해서 제공한다.
==
연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인, 완전히 같음, 물리적 같음equals()
메서드를 사용하여 두 객체가 논리적으로 동등한지 확인, 같은 가치나 수준을 의미하지만 그 형태나 외관 등이 완전히 같지는 않음, 논리적 같음이렇게 의미를 보면 와닿지 않는다. 동일성은 같은 참조값인지 확인하는 것이고, 동등성은 논리적으로 값은 값인지 확인하는 것이다. 예시로 살펴보자.
Member member1 = new Member("Kim"); //x001
Member member2 = new Member("Kim"); //x002
String str1 = "Kim";
String str2 = "Kim";
여기서 이상함을 느껴야 한다. 왜 두 경우 모두 같은 결과가 나오는 것일까?
equals()
메서드를 살펴보자.
Object
가 기본으로 제공하는 equals()
는 ==
으로 동일성 비교를 제공한다. 즉, 동등성 비교를 사용하고 싶으면 equals()
메서드를 재정의해야 한다.
public boolean equals(Object obj) {
return (this == obj);
}
equals()
재정의해서 사용해보기- Member의 동등성은 `id`(고객번호)로 비교
- `equals()`는 `Object` 타입을 매개변수로 사용해서 다운캐스팅
- 현재 인스턴스의 `id` 문자열과 비교 대상 객체의 `id` 문자열 비교
- `id`는 `String`으로 선언했기에 `equals()`를 사용하여 비교
public class Member {
private String id;
public Member(String id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
Member member = (Member) obj;
return Objects.equals(id, member.id);
}
}
public class EqualsMain {
public static void main(String[] args) {
Member member1 = new Member("Kim");
Member member2 = new Member("Kim");
System.out.println("identity = " + (member1 == member2));
System.out.println("equality = " + member1.equals(member2));
}
}
실행하면 결과는 다음과 같이 나온다.
identity = false
equality = true
equals()
구현해보기command
+N
을 통해 equals() 코드를 자동으로 만들 수 있다.
@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);
}
equals()
메서드를 구현할 때 지켜야 할 규칙equals()
메소드는 항상 동일한 값을 반환해야 한다.null
과 비교했을 때 false
를 반환해야 한다.getClass()
는 자신이 속한 클래스의 객체를 반환하는 메서드로, 객체의 클래스 정보 제공한다.
@IntrinsicCandidate
public final native Class<?> getClass();
Card card = new Card().getClass();
Cat
과 Car
는 아무 관련이 없는 클래스이지만, 둘 다 부모가 없기 때문에 Object
를 자동으로 상속받는다.
class Cat {
public void sound() {
System.out.println("야옹"); }
}
class Car {
public void move() {
System.out.println("부릉"); }
}
action()
에서 각각의 인스턴스의 메서드를 호출하려면 다운캐스팅이 필요하다.
public class ObjectPolyEx {
public static void main(String[] args) {
Cat cat = new Cat();
Car car = new Car();
action(cat);
action(car);
}
private static void action(Object obj) {
if (obj instanceof Cat cat) {
cat.sound();
} else if (obj instanceof Car car) {
car.move();
}
}
}
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
ObjectPolyEx
클래스의 action()
메서드action()
메서드 안에서 obj.sound()
를 호출하면 오류가 발생한다. 매개변수인 obj
는 Object
타입이므로, Object
에는 sound()
메서드가 없기 때문이다.obj
는 Object
타입에서 sound()
찾기 → Object
에서 sound()
찾을 수 없으므로 오류 발생obj.sound()
호출 action(cat)
private static void action(Object obj) {
obj.sound(); //컴파일 오류, Object는 sound()가 없다.
}
Object[]
를 만들면 모든 객체를 담을 수 있는 배열을 만들 수 있다.
public class ObjectPolyExample2 {
public static void main(String[] args) {
Cat cat = new Cat();
Car car = new Car();
Object object = new Object(); //Object 인스턴스도 만들 수 있다.
Object[] objects = {cat, car, object};
/**
Object[] objects = new Object[3];
objects[0] = cat;
objects[1] = car;
objects[2] = object;
**/
size(objects);
}
private static void size(Object[] objects) {
System.out.println("전달된 객체의 수는: " + objects.length);
}
}
전달된 객체 수는: 3
void action(Object obj)
과 같이 모든 객체를 받을 수 있는 메서드를 만들 수 없다.Object[] objects
처럼 모든 객체를 저장할 수 있는 배열을 만들 수 없다.구체적인 것에 의존하는 것을 추상적인 것에 의존함으로써 해결할 수 있다.
부모타입으로 올라갈수록 개념은 더 추상적이게 되고, 하위 타입으로 내려갈수록 개념은 더 구체적이게 된다.
출력해야 할 클래스가 늘어나면 그 수에 맞춰 메서드도 계속 늘어나게 된다.
public class BadObjectPrinter {
public static void print(Car car) { //Car 전용 메서드
String string = "객체 정보 출력: " + car.carInfo();
}
public static void print(Dog dog) { //Dog 전용 메서드
String string = "객체 정보 출력: " + dog.dogInfo();
}
}
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
,
Object` 타입을 매개변수로 사용해서 다형적 참조를 사용Object
가 가지고 있는 toString()
메서드를 오버라이딩 할 수 있음Object
타입에 의존하면서 각 인스턴스의 toString()
을 호출할 수 있음toString()
을 오버라이딩해서 기능을 확장할 수 있다.Object
와 toString()
을 사용하는 클라이언트 코드인 ObjectPrinter
는 변경하지 않아도 된다.참고
Class Object
Package java.lang
Java의 정석
김영한의 실전 자바 - 중급 1편