[Java] Object 클래스
자바가 기본으로 제공하는 라이브러리 중에 가장 기본은 lang 패키지이다.
java.lang package의 대표적인 클래스들은 Object, String, 래퍼 클래스들, Class 메타 정보, System 등이 있다.
1. import 생략 가능한 lang 패키지
java.lang은 모든 자바 애플리케이션에서 자동으로 임포트 해준다. 즉, import java.lang을 안 해도 된다.
2. 모든 객체의 최상위 부모 Object 클래스
자바에서 모든 클래스의 최상위 클래스는 항상 Object 클래스이다.
Parent class는 묵시적으로 Object 클래스를 상속받는다. 그래서 extends Object를 생략할 수 있다.
//묵시적으로 Object 클래스를 상속 받음
class Parent /*extends Object*/ {
private int value;
}
묵시적(Implicit) vs 명시적(Explicit)
묵시적은 개발자가 코드에 직접 기술하지 않아도 컴파일러 또는 시스템이 자동으로 기술해주는 것
명시적은 개발자가 코드에 직접 기술해야 작동하는 의미이다.
public class Main {
public static void main(String[] args) {
Parent p = new Parent();
System.out.println(p.toString());
}
}
//묵시적으로 Object 클래스를 상속 받음
class Parent{
private int value;
}
해당 코드를 보면 Parent 클래스에 toString() 이라는 메서드는 안 보인다. toString 메서드는 묵시적으로 상속받은 Object 클래스에 존재하는 메서드이다. 상속받았기에 호출이 가능하고 사용 가능하다. 오버라이딩(재정의)도 가능하다.
위 코드를 자세히 설명하자면
p.toString() 을 호출하면 먼저 본인의 타입인 Parent에서 toString()을 찾는다. 없으면 부모 타입으로 올라가서 찾는다.
여기서 Parent의 부모는 Object이니 Object까지 올라가서 toString()을 찾는다. Object에는 toString()이 존재하므로 존재한 메서드를 호출한다.
자바에서는 모든 객체의 최종 부모는 Object이다.
3. Object 클래스의 장점
모든 클래스는 Object 클래스를 상속받는다. Object 클래스를 상속받는다는 의미는 다음과 같다.
- 모든 클래스는 Object 클래스의 기능을 수행할 수 있다.
- Object 클래스는 모든 클래스를 다형적 참조가 가능하다.
공통 기능
객체의 정보를 제공하고, 객체의 메타 정보 또는 객체들 간의 연산기능 다른지 같은지를 비교할 수 기능은 모든 객체에게 필요한 기능들이다. 하지만 위 기능들이 개발자들이 구현하는 것은 번거롭고 막상 만든다고 해도 개발자마다 메서드들의 이름이 다 다를 수 있어 일관성이 없어진다.
예를 들어 equal() 메서드를 구현하고자 하면 개발자A는 same()으로 개발자 B는 equals()로 정의했다면 일관성이 없어진다.
즉, 세상에 있는 모든 객체는 Object라는 클래스로 인해 공통 기능을 편리하게 상속 받을 수 있다.
Object의 다형성
부모 클래스는 자식 클래스를 참조할 수 있다. 다형적 참조 덕분인데 Object 클래스는 모든 클래스의 부모 클래스이다. 따라서 Object클래스는 모든 객체를 참조할 수 있게 된다.
public class Main {
public static void main(String[] args) {
Car car = new Car();
Airplane airplane = new Airplane();
run(car);
run(airplane);
}
private static void run(Object o){
if (o instanceof Car car){
car.drive();
}else if (o instanceof Airplane airplane){
airplane.fly();
}
}
}
class Car{
public void drive(){
System.out.println("붕붕");
}
}
class Airplane{
public void fly(){
System.out.println("슈웅");
}
}
해당 코드의 run 메서드 매개변수를 보면 Object o이다. 인수로 car가 보내진다고 가정하면
Object o = car; 이다. 해당 코드는 문제없는 코드이다. 부모가 자식 클래스를 참조할 수 있기 때문이다.
Object car = new Car();
Object airplane = new Airplane();
다형적 참조가 가능하다.
하지만 Obejct 클래스는 자식 클래스들의 메서드들을 직접적으로 호출할 수 없다.
o.drive() 호출 과정
- o.drive()를 호출
- o는 Object 타입이므로 Object 타입에서 drive()를 찾는다.
- Object에는 drive()가 존재하지 않는다. 하지만 Object는 최종 부모이므로 drive()를 찾기 위해 더 올라갈 수 없다.
이로 인해 drive() 호출하기 위해서 다운캐스팅을 해야 한다.
Object o = new Car();
//o.drive() 불가능
Car c = (Car)o; //다운캐스팅
c.drive();
Object 클래스 다형성의 한계
Object는 모든 객체를 참조할 수 있다. 하지만 자식 클래스의 메서드를 호출하기 위해서는 다운캐스팅을 해야 한다.
Object 배열
Object는 모든 타입의 객체를 담을 수 있다. 따라서 Object []을 만들면 세상에 있는 모든 객체를 배열 안에 담을 수 있다.
Object[] objects = {new Car(), new Airplane(), new Object()};
System.out.println(Arrays.toString(objects));
toString()
Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공하는 메서드이다. Object 클래스에 존재하는 메서드이며 모든 객체는 Obeject를 상속받기에 toString()을 사용할 수 있다.
public class Main {
public static void main(String[] args) {
Object object = new Object();
String string = object.toString();
System.out.println(string); //string을 출력했는데 왜 주소가 나올까?
System.out.println(object); //object를 직접 출력하면
}
}
참조변수를 출력하면 당연히 객체주소가 나온다. 그런데 string을 출력하면 왜 객체주소가 나올까? toString() 메서드를 호출해서 객체주소가 나온 것 같다.
toString() 메서드를 자세히 살펴봐야겠다.
toString method이다. toString 메서드는 객체의 정보를 return 하는 메서드이다.
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
자세히 보면 "@"+Integer.toHexString(hashCode()); 는 객체의 참조값을 출력한다. 16진수로 반환한다.
toString method는 원하는 대로 오버라이딩도 가능하다.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Object o = new Dog();
System.out.println(o.toString());
System.out.println(o);
}
}
class Dog{
@Override
public String toString(){
return "멍멍";
}
}
println()
println() 메서드 안에 인수로 object를 넘겼다. println() 결과 객체주소 값이 나온다. 이번에는 println() 메서드를 더 상세히 살펴보자.
public void println(Object x) {
String s = String.valueOf(x);
if (getClass() == PrintStream.class) {
// need to apply String.valueOf again since first invocation
// might return null
writeln(String.valueOf(s));
} else {
synchronized (this) {
print(s);
newLine();
}
}
}
println(Object x)는 어떤 객체든 인수로 넘길 수 있다. println() 호출 후 String.valueOf(x)를 호출한다. 여기서 valueOf는 객체 주소라는 것을 확인할 수 있다.
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
valueOf()는 결국 null이면 "null"을 리턴하고 아니면 obj.toString() 메서드를 호출해서 return 한다.
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
결국 println(Object x)를 호출하면 결국 toString()을 호출하게 되며 객체주소값을 return 하여 println() = 객체주소 출력인 것이다.
결론은 toString() method는 객체의 주소값과 정보를 return 하는 함수이며 println(Object o) println()에 인수로 객체를 보내면 toString method와 같은 결과가 나온다. 그 이유는 println() 메서드는 Object 클래스의 toString()를 호출하기 때문이다.
equals 동일성 동등성
동일성과 동등성의 차이가 뭘까?
동일성: == 연산자를 사용해 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
동등성: equals() 메서드를 사용해 두 객체가 논리적으로 동등한지 확인
동일은 완전히 같음을 의미하고 동등은 같은 가치나 수준을 의미한다.
즉, 동일성은 물리적으로 같은 메모리에 있는 객체 참조값을 확인하는 것이다. 동등성은 논리적으로 같은지 확인하는 것
동일성은 JVM 기준이고 동등성은 사람이 생각하는 논리적 기준이다.
소스 코드로 예를 들자.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Address address = new Address("서울");
Address address1 = new Address("서울");
System.out.println(address == address1); //동일성
System.out.println(address.equals(address1)); //동등성
}
}
class Address {
private String address;
public Address(String address) {
this.address = address;
}
}
결과는 둘 다 false이다. 이상하다 동등성 부분은 논리적으로 같은 서울에 사니깐 true여야 하지 않나?
equals() 메서드를 뜯어보자
public boolean equals(Object obj) {
return (this == obj);
}
equals() 안을 보니 결국 동일성 체크다. 그럼 equals()는 왜 있는 걸까?
동등성으로 비교하고 싶으면 개발자 입맛에 맞게 오버라이딩해야 한다.
@Override
public boolean equals(Object object) {
Address address1 = (Address) object;
return address.equals(address1.address);
}
이렇게 오버라이딩하면 Address 클래스 멤버변수 address의 값을 비교한다.
하지만 사실 equals() 동등성 비교는 더 복잡하다.
@Override
public boolean equals(Object object) {
if (object == null || getClass() != object.getClass()) return false;
Address address1 = (Address) object;
return Objects.equals(address, address1.address);
}
✅정리
1. 모든 객체의 부모는 Object이다.
2. Object class 덕분에 다형적 참조, Object 객체 배열 생성 가능
3. Object.toString()은 객체 정보(주소)를 반환한다.
4. System.out.println(Object o) 내부적으로 toString 호출
5. 동일성은 == 동등성은 equals()
6. 동등성을 위해서는 equals() 오버라이딩해야함.