Java - Object 클래스

Yuri Lee·2020년 10월 7일
0

상속

자바에서 상속이란 필수적이다. 여러분이 상속하건 하지 않았건 기본적인 상속을 하게 된다.

package com.yuri.javatutorials.progenitor;

class O {
}

위의 코드는 아래와 코드가 같다.

package com.yuri.javatutorials.progenitor;

class O extends Object {
}

자바에서 모든 클래스는 사실 Object를 암시적으로 상속받고 있는 것이다. 그런 점에서 Object는 모든 클래스의 조상이라고 할 수 있다. 그 이유는 모든 클래스가 공통으로 포함하고 있어야 하는 기능을 제공하기 위해서다.

API 문서: http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html

위의 그림은 Object 클래스가 가지고 있는 메소드를 보여준다. 자바의 객체는 위의 메소드들을 반드시 가지고 있다고 할 수 있다.

toString

toString은 객체를 문자로 표현하는 메소드이다.

package com.yuri.javatutorials.progenitor;

class Calculator {
	int left, right;

	public void setOprands(int left, int right) {
		this.left = left;
		this.right = right;
	}

	public void sum() {
		System.out.println(this.left + this.right);
	}

	public void avg() {
		System.out.println((this.left + this.right) / 2);
	}
}

public class CalculatorDemo {

	public static void main(String[] args) {

		Calculator c1 = new Calculator();
		c1.setOprands(10, 20);
		System.out.println(c1);
	}

}

/*
com.yuri.javatutorials.progenitor.Calculator@15db9742
*/

c1 이라는 변수에 담겨있는 객체, 인스턴스의 패키지는 이것이고, Calculator 클래스라는 것이다. @을 중심으로 앞은 어떤 클래스의 소속인가, 뒤의 숫자는 인스턴스를 식별하는 숫자이다.

public class CalculatorDemo {

	public static void main(String[] args) {

		Calculator c1 = new Calculator();
		c1.setOprands(10, 20);
		System.out.println(c1.toString());
	}

}

/*
com.yuri.javatutorials.progenitor.Calculator@15db9742
*/

c1.toString() 이것도 마찬가지의 결과가 나온다. 이 메소드의 호출을 명시적으로 하지 않고 인스턴스를 담고 있는 변수만을 println의 인자로 전달하게 되면 자바는 내부적으로 toString이라는 메소드를 호출하도록 약속이 되어있기 때문이다.

Calculator라는 클래스가 사실 뒤에 extends object를 생략하고 있기 때문이다. object 클래스 안에는 toString 메소드가 있는 것이다.

toString이라는 것은 어떠한 객체가 있을 때 그 객체를 문자화시키는 메소드이다.

toString() 메소드는 object 의 멤버이다. 그 이야기는 지금 만든 Calculator 클래스는 object의 자식이다. 자식이기 때문에 부모 클래스가 가지고 있는 어떠한 메소드를 다시 정의하면 (오버라이딩을 통해) toString이 출력하는 내용을 바꿀 수 있다.

toString()에다가 ctrl+ open implementation 한 결과 ..

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
package com.yuri.javatutorials.progenitor;

class Calculator {
	int left, right;

	public void setOprands(int left, int right) {
		this.left = left;
		this.right = right;
	}

	public void sum() {
		System.out.println(this.left + this.right);
	}

	public void avg() {
		System.out.println((this.left + this.right) / 2);
	}
	
    public String toString() {
    	return "left : " + this.left + ", right : "+ this.right;
    }
}

public class CalculatorDemo {

	public static void main(String[] args) {

		Calculator c1 = new Calculator();
		c1.setOprands(10, 20);
		System.out.println(c1.toString());
	}

}

/*
left : 10, right : 20
*/

클래스 Calculator에 toString을 재정의(overiding)했다. 그리고 인스턴스를 System.out.println의 인자로 전달하니까 toString을 명시적으로 호출하지 않았음에도 동일한 효과가 나고 있다. toString 메소드는 자바에서 특별히 취급하는 메소드다. toString을 직접 호출하지 않아도 어떤 객체를 System.out.print로 호출하면 자동으로 toString이 호출되도록 약속되어 있다.

이를 통해서 인스턴스 c1의 상태를 쉽게 파악할 수 있게 되었다.

equals

package com.yuri.javatutorials.progenitor;

class Student {
	String name;

	Student(String name) {
		this.name = name;
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
		Student s2 = new Student("egoing");
		System.out.println(s1 == s2);
		System.out.println(s1.equals(s2));

	}

}

결과

false
false

결과는 false다. s1과 s2가 서로 다른 객체이기 때문이다. 어찌 보면 당연한 결과다

💀💀 여기서 잠깐! 왜 ? 결과값이 다르게 나왔을까 ? 💀💀

(1) 형태의 차이
equals()는 메소드. 객체끼리 내용을 비교할 수 잇도록 한다.
== 는 비교를 위한 연산자이다.

(2) 주소값 비교와 내용 비교
equals() 메소드는 비교하고자 하는 대상의 내용 자체를 비교
== 연산자는 비교하고자 하는 대상의 주소값을 비교

주소값이란?
확실하게 집주소나 이메일 주소처럼 확정적으로 정해져 보여지는 것은 아니지만 대상을 구별할 수 있게 하는 값이다.

Call By Reference : 대상을 선언했을 때, 주소값이 부여된다. 그래서 어떠한 객체를 불러왔을 때 그 주소값을 불러온다고 본다.

Call By Value : 기본적으로 대상에 주소값을 가지지 않는 것으로 값을 할당받는 형태. int, float, double, byte 등 primitive type에 해당

String a = "aaa";
String b = a;
String c = new String("aaa");

a, b, c 모두 aaa라는 문자열 내용을 가지고 있지만, a,b는 300이라는 임의의 주소값을 할당 받았으며, c는 400이라는 임의의 주소값을 할당받았다.

내용은 같지만 c가 다른 주소값을 할당받은 이유는 "aaa"라는 문자열을 대입한 것이 아니라 new String("aaa")을 통해서 새로운 문자열을 선언하였기 때문!

System.out.println(a.equals(b));
System.out.println(a==b);
System.out.println(a==c);
System.out.println(a.equals(c));

결과

true // a, b 가 가지고 있는 내용을 비교하였으므로
true //a, b 가 가지고 있는 주소값을 비교하였으므로
false //a, c가 가지고 있는 주소값을 비교하였으므로
true // a, c가 가지고 있는 내용을 비교하였으므로


두 개의 객체가 논리적으로는 egoing이라는 값을 가지고 있기 때문에 저 두 개의 객체가 같은 객체로 간주 되었으면 좋겠다. 이럴 때 클래스 Object의 메소드 equals를 overiding하면 된다.

데이터 타입이 Object이다. 그리고 Object 안으로 들어오는 s2의 데이터 타입은 Student이다. 그럼 s2의 데이터 타입인 student와 그 s2가 들어갈 변수인 object의 데이터타입인 Object는 어떤 관계가 있을까?

Student의 부모가 Object이다. 이것을 코드로 풀어서 표현해보면

Object obj = s2

Object라는 데이터타입의 obj 라는 변수에 s2를 대입하려고 한다.
자식 데이터 타입을 부모 데이터 타입을 갖고 있는 것에 할당하려고 한다. 자식 데이터 타입은 부모 데이터 타입에 할당될 수 있다. (다형성) 그 일이 매개변수에서 일어나고 있는 것이다.

obj에 들어있는 값은 s2가 된다. 그런데 s2를 object 데이터타입으로 하게 되면 이 s2가 갖고 있는 변수인 name이라는 값에 접근할 수 없다. object 데이터 타입에는 name이라고 하는 멤버가 존재하지 않기 때문이다.

즉 s2라는 녀석이 object 타입으로 변환되어서 안으로 들어오게 되는데, 우리가 필요한 것은 s2, student안의 name 변수에 접근하는 것을 필요로 한다. 따라서 obj 변수를 Student라는 데이터 타입으로 다시 변환시켜야 한다!!!! (복잡하네^^)

package com.yuri.javatutorials.progenitor;

class Student {
	String name;

	Student(String name) {
		this.name = name;
	}
	
	public boolean equals(Object obj) {
		Student s = obj;
		return true;
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
		Student s2 = new Student("egoing");
		System.out.println(s1 == s2);
		System.out.println(s1.equals(s2));

	}

}

에러발생~!

false
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Type mismatch: cannot convert from Object to Student
at com.yuri.javatutorials.progenitor.Student.equals(ObjectDemo.java:11)
at com.yuri.javatutorials.progenitor.ObjectDemo.main(ObjectDemo.java:23)

Object obj = s2 는 가능하다. 왜냐? s2는 object의 자식이기 때문이다. 자식은 부모 행세를 하는 게 가능하다. 하지만 부모가 자식의 데이터 타입으로 할당되는 것은 불가능하다. 그냥은 불가능하다. 자식의 입장에서 부모행세를 하면 부모가 갖고 있지 않은 자식만 갖고있는 여러가지 특성들을 없는 척하면 된다. 감추면 그만이다.

그런데 부모 입장에서는 자식이 갖고 있는 특성들이 어떤 것인지 알 수 없는 것이다. 즉 부모 입장에서는 없었던 것을 만드는 것이고, 자식 입장에서는 있었던 것을 감추는 것이다. 감추는 것은 가능하지만 없던 것을 만드는 것은 불가능하다. 그래서 이것을 가능하게 하려면

Student s = (Student)obj

(Student)는 obj를 형변환 하려는 데이터타입이다. obj라는 변수를 Student 타입으로 강제로 명시적으로 형변환을 하는 것이다. 데이터 형식을 변환하는 것!

🏳‍🌈 자식이 부모가 될 때는 자동으로 형변환이 이루어지지만, 부모가 자식이 될 때는 명시적으로 형변환을 해줘야 한다. 다형성과 연결되는 부분이다.

어떠한 매개변수를 받을 때 그 매개변수로 들어올 데이터가 어떤 게 들어올지 알 수 없을 때 매개변수의 데이터타입으로 Object를 쓰는 것은 매우 일반적인 용법이다. 꼭 알고 있어야 함!

package com.yuri.javatutorials.progenitor;

class Student {
	String name;

	Student(String name) {
		this.name = name;
	}
	
	public boolean equals(Object obj) {
		Student s = (Student) obj;
		return this.name == s.name;
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
		Student s2 = new Student("egoing");
		System.out.println(s1 == s2);
		System.out.println(s1.equals(s2));

	}

}

false
true

equals는 우리가 오버라이딩을 해서 (재정의) 서로가 동등하다는 내용을 true로 바꿔줬기 때문에 다음과 같은 결과가 나온다.

다시 정리!
(Student)obj 는 메소드 equals로 전달된 obj의 데이터 타입이 Object이기 때문에 이를 Student 타입으로 형 변환하는 코드다. 아래 코드를 통해서 현재 객체의 변수 name과 equals의 인자로 전달된 객체의 변수 name을 비교한 결과를 Boolean 값으로 리턴하고 있다. 이 값에 따라서 두 개의 객체는 같거나 다른 것이 된다.

eqauls를 제대로 사용하기 위해서는 hashCode라는 클래스도 함께 구현해야 한다. 하지만 이에 대한 이야기는 우리 수업의 범위를 넘어서고 그 효용(사용 빈도)도 높지 않기 때문에 더 이상 설명을 하지 않겠다. 하지만 이 메소드의 취지를 이해하는 것은 또한 중요하기 때문에 언급을 하지 않을 수는 없었다. 메소드 equals에 대해서 필자가 권고하는 입장은 아래와 같다.

  1. 객체 간에 동일성을 비교하고 싶을 때는 ==를 사용하지 말고 equals를 이용하자.

  2. equals를 직접 구현해야 한다면 hashCode도 함께 구현해야 함을 알고 이에 대한 분명한 학습을 한 후에 구현하자.

  3. equals를 직접 구현해야 한다면 eclipse와 같은 개발도구들은 equals와 hashCode를 자동으로 생성해주는 기능을 가지고 있다.

  1. 그 이유가 분명하지 않다면 비교 연산자 == 은 원시 데이터형을 비교할 때만 사용하자.

원시 데이터 형(Primitive Data Type)이란 자바에서 기본적으로 제공하는 데이터 타입으로 byte, short, int, long, float, double, boolean, char가 있다. 이러한 데이터 타입들은 new 연산자를 이용해서 생성하지 않아도 사용될 수 있다는 특징이 있다.

finalize

finalize는 객체가 소멸될 때 호출되기로 약속된 메소드이다. 여기서는 이 메소드의 취지만 이해하면 된다. 많은 자바의 전문가들이 이 메소드의 사용을 만류하고 있다.

이 메소드 보다는 가비지 컬렉션(garbage collection)에 대해서 알아보자. 인스턴스를 만드는 것은 내부적으로는 컴퓨터의 메모리를 사용하는 것이다. 여기서 말하는 메모리는 RAM을 의미한다. 램은 가장 빠른 저장 장치이기 때문에 컴퓨터 프로그램들은 이 램에 저장된 후에 동작하게 된다. 하지만 램은 가격이 비싸고 용량이 적기 때문에 램은 컴퓨터에서 가장 소중한 저장 장치라고 할 수 있다. 그러므로 램의 적게 사용하는 프로그램이 좋은 프로그램이다. 그런 이유로 많은 프로그래밍 언어들이 램을 효율적으로 사용하기 위해서 더 이상 사용하지 않는 데이터를 램에서 제거할 수 있는 방법들을 제공한다.

하지만 자바에서는 이러한 방법이 제한적으로 제공되고 있는데 그것은 자동으로 해주기 때문이다. 이 작업을 자동화한 것을 가비지 컬렉션이라고 한다. 이를테면 어떤 인스턴스를 만들었고, 그것을 변수에 담았다. 그런데 그 변수를 사용하는 곳이 더 이상 없다면 이 변수와 변수에 담겨있는 인스턴스는 더 이상 메모리에 머물고 있을 필요가 없는 것이다. 자바는 이를 감지하고 자동으로 쓰지 않은 데이터를 삭제한다. 따라서 개발자가 사용하지 않는 데이터를 직접 삭제하는 작업을 하지 않아도 되는 것이다. 이것은 어려운 메모리 관리로부터 개발자들의 부담을 경감시킨 도약이라고 할 수 있다. 좋은 에플리케이션을 만들기 위해서는 가비지 컬렉션에 대한 이해는 필요하다.

https://d2.naver.com/helloworld/1329

clone

clone은 복제라는 뜻이다. 어떤 객체가 있을 때 그 객체와 똑같은 객체를 복제해주는 기능이 clone 메소드의 역할이다.

package com.yuri.javatutorials.progenitor;

class Student {
	String name;

	Student(String name) {
		this.name = name;
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
	}
}

여기서 s1을 복제하고 싶다!

package com.yuri.javatutorials.progenitor;

class Student {
	String name;

	Student(String name) {
		this.name = name;
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
		s1.clone();
	}
}

s1.clone(); 에러가 발생한다. 왜?! 복제 가능한 객체라는 것을 vm에게 알려줘야 한다. cloneable라고 하는 인터페이스를 구현해주면 된다.

cloneable에 들어가면

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

비어있음을 확인할 수 있다. 비어있는 인터페이스를 왜 구현해야 하는가?! 단지 Student라는 클래스가 복제 가능하다는 사실을 알려주는 구분자의 역할을 한다고 생각하면 된다!

class Student implements Cloneable {
	String name;

	Student(String name) {
		this.name = name;
	}

}

하지만 여전히 에러가 발생한다 ㅠㅠ 왜? Object 클래스에 있는 clone()라고 하는 메소드의 특징 때문이다.

접근 제어자는 protected이다. 서로 다른 패키지에서는 호출할 수 없다. 서로 다른 패키지라고 할지라도 상속은 가능하다.

package com.yuri.javatutorials.progenitor;

class Human {
	protected String test() {
		return "test";
	}
}

class Student extends Human implements Cloneable {
	String name;

	Student(String name) {
		this.name = name;
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
		s1.test();
	}
}

Student가 Human 라는 클래스를 상속받고 있다. Human 클래스 안에는 protected 접근제어자를 갖고 있는 test() 메소드가 있다.
s1이라는 인스턴스에 test를 호출하면 Student 클래스는 갖고 있지 않기 때문에 부모인 Human 클래스에 갖고 있는 test() 메소드가 호출된다.

Q. 왜 호출이 될까?
Human에서 제공하는 test 메소드는 protected이지만 같은 패키지 소속이기 때문이다.

그런데 Object는 우리와 다른 패키지에 있다. Object가 소속되어있는 패키지는 java.lang이라는 패키지이다. 그렇기 때문이 우리는 clone이라는 메소드를 직접적으로 호출할 수 없다. 반면에 toString(), 등의 다른 메소드는 접근 제어자가 public 이기 때문에 호출할 수 있었던 것이다.

package com.yuri.javatutorials.progenitor;

class Student implements Cloneable {
	String name;

	Student(String name) {
		this.name = name;
	}

	public Object clone() {
		return super.clone();
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");

	}
}

에러 발생! Unhandled exception type CloneNotSupportedException

CloneNotSupportedException 예외는 runtimeException 이 아니라 그냥 Exception라는 의미이다. 그냥 Exception은 반드시 처리를 하도록 강제되어있다. 따라서 이를 꼭 처리해줘야 한다.

	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

클론하는 과정에서 Exception 이 발생하면 사용자에게 위임하도록 하였음! 그리고 여기 있는 접근제어자를 public으로 한 것은 ..
clone()메소드는 접근 제어자가 protected였는데 더 개방적인 접근 제어자로 바꿀 수 있으므로 바꿔주었다.

package com.yuri.javatutorials.progenitor;

class Student implements Cloneable {
	String name;

	Student(String name) {
		this.name = name;
	}

	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
		s1.clone();
	}
}

에러 발생! Unhandled exception type CloneNotSupportedException

suber clone(object clone)에서 익셉션이 발생했을 때 그것을 여기서 처리하지 않고 사용자에게 처리하고 있는 것이기 때문에 s1.clone(); 사용하는 쪽에서 이것을 다시 throws할 것인지 자기가 해결할 것인지 결정해야 한다. 지금 한번 여기서 해결해보도록 하겠다 🤗

package com.yuri.javatutorials.progenitor;

class Student implements Cloneable {
	String name;

	Student(String name) {
		this.name = name;
	}

	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
		try {
			Student s2 = s1.clone();
		} catch (CloneNotSupportedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

surround with try catch 를 통해 여기서 해결해주고, 그 것을 Student s2에 담아보자. 근데 또 에러 발생! Type mismatch: cannot convert from Object to Student

s1.clone(); 은 리턴값이 object이다. 데이터 타입이 Object s2 = s1.clone(); 였다면 에러가 없었을 것이다. 하지만 우리는 Student로 하고 싶기 때문에.. 명시적으로 형변환을 해줘야 한다.

package com.yuri.javatutorials.progenitor;

class Student implements Cloneable {
	String name;

	Student(String name) {
		this.name = name;
	}

	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

}

class ObjectDemo {

	public static void main(String[] args) {
		Student s1 = new Student("egoing");
		try {
			Student s2 = (Student) s1.clone();
            System.out.println(s1.name);
            System.out.println(s2.name);
		} catch (CloneNotSupportedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

결과

egoing
egoing

이해해야 하는 것과 알아야 하는 것

모든 클래스들은 오브젝트가 갖고 있는 내용을 갖고 있고, 필요에 의해서 오버라이딩해서 기본적인 동작방법을 바꿀 수 있다. 오브젝트 클래스가 모든 클래스의 부모 클래스이기 때문에 모든 클래스는 오브젝트 클래스의 데이터 타입을 가지고 있는 변수에 인스턴스화 되어서 변수에 담길 수 있다. 반대로 오브젝트 데이터 타입이 된, 데이터 타입에 담겨있는 인스턴스는 원래의 클래스가 되려면 명시적으로 형변환을 해야 한다.

이 클래스가 모든 클래스의 부모라는 사실은 이해의 영역이 아니라 약속의 영역이다. 즉 자바를 만든 측과 자바를 사용하는 측의 약속이다. 그리고 이 클래스가 clone이나 toString과 같은 메소드를 가지고 있다는 것 또한 이해의 영역이 아니라 숙지해야 하는 영역이다. 한편 모든 클래스가 toString을 사용할 수 있고 또한 이 메소드를 새롭게 재정의 할 수 있다는 점은 이해의 영역이다. 어떤 지식을 배울 때는 이해해야 하는 것과 그냥 알아야 하는 것을 잘 분별하는 것이 중요하다.


이 글은 생활코딩의 자바 강좌를 바탕으로 정리한 내용입니다.
https://ojava.tistory.com/15

profile
Step by step goes a long way ✨

0개의 댓글