[ Java 기초 ] 상속

황승환·2021년 12월 29일
0

Java 기초

목록 보기
6/6
post-thumbnail

Goal

자바의 상속에 대해 학습하기

Study

상속 (Inheritance)

상속이란 기존의 클래스에 기능을 추가하거나 재정의하여 새로운 클래스를 정의하는 것을 의미한다. 상속은 캡슐화, 추상화와 더불어 객체 지향 프로그래밍을 구성하는 중요한 특징 중 하나이다.

상속을 받게 되면 기존에 정의되어 있는 클래스의 모든 필드와 모든 메소드를 물려받아 이를 이용한 새로운 클래스를 생성할 수 있게 된다. 이때 기존의 클래스를 부모 클래스(parent class) 또는 상위 클래스(super class), 기초 클래스(base class)라고 부른다. 상속을 받아 새롭게 생성된 클래스는 자식 클래스(child class) 또는 하위 클래스(sub class), 파생 클래스(derived class)라고 부른다.

상속의 장점

  • 기존의 클래스를 재활용할 수 있다.
  • 자식 클래스 설계 시 중복되는 멤버를 미리 부모 클래스에 선언하면 자식 클래스에서 해당 멤버를 선언하지 않아도 된다.
  • 클래스 간의 계층적 구조를 구성하여 다형성의 문법적 토대를 마련한다.

자식 클래스 (Child class)

자식 클래스는 부모 클래스의 모든 특성을 물려받아 새롭게 작성된 클래스를 의미한다.
class 자식클래스명 extend 부모클래스명 {...}
자식 클래스는 위와 같은 형식으로 작성된다.
이 그림을 보면 부모 클래스는 자식 클래스에 포함되는 것을 확인할 수 있다. 부모 클래스에 새로운 필드를 추가하면 이는 자식 클래스에서도 자동으로 필드가 추가된 것으로 볼 수 있다.

자식 클래스는 부모 클래스의 필드와 메소드만 상속받고, 생성자와 초기화 블록은 상속받지 않는다. 그리고 부모 클래스의 접근제어자가 private이거나 default인 멤버는 자식 클래스에서 상속 받지만 접근할 수 없다.

class Parent {
	private int a = 10; // private 필드
	public int b = 20; // public 필드
}

class Child extends Parent {
	public int c =30; // public 필드
    	void display() {
    	    // System.out.println(a); // 부모 클래스에서 private으로 선언되었기 때문에 접근 불가
            System.out.println(b); // 부모 클래스에서 public으로 선언 되었기 때문에 접근 가능. 20 출력
            System.out.println(c); // 현재 클래스에서 선언되었기 때문에 접근 가능. 30 출력
        }
}

public class Inheritance1 {
	public static void main(String[] args) {
    	Child ch = new Child();
        ch.display();
    }
}

Parent 클래스를 부모 클래스로 하는 Child 클래스를 예시로 작성하였다. a의 경우 부모 클래스에서 접근 제어자를 private로 지정하였기 때문에 자식 클래스에서 접근할 수 없는 것을 확인할 수 있다. 그러나 public으로 선언된 b의 경우 자식 클래스에서 접근할 수 있다.

자바에서 클래스는 단 한개의 클래스만을 상속받는 단일 상속만이 가능하다.

Object 클래스

Object 클래스는 모든 클래스의 부모 클래스가 되는 클래스이다. 자바의 모든 클래스는 자동으로 Object 클래스의 모든 필드와 메소드를 상속받는다.

때문에 자바의 모든 클래스는 별도로 extends 키워드를 사용하여 Object 클래스의 상속을 명시하지 않아도 Object 클래스의 모든 멤버를 자유롭게 사용할 수 있다.

모든 객체에서 toString()이나 clone()과 같은 메소드를 바로 사용할 수 있는 것이 바로 모든 클래스가 Object 클래스를 상속받는다는 사실에 대한 명백한 근거라고 할 수 있다.

super

super는 이전 포스트에서 알아 보았던 this와 비슷하게 생각하면 편하게 이해가 가능하다. super 역시 this와 마찬가지로 super 형태의 참조 변수와super() 형태의 메소드 이렇게 2가지로 존재한다.

super 참조 변수

super는 부모 클래스로부터 상속 받은 필드나 메소드를 자식 클래스에서 참조하는데 사용하는 참조 변수이다.

인스턴스 변수명과 지역 변수명이 같을 경우 인스턴스 변수 앞에 this를 붙여 구분하였던 것과 같은 개념이다. super는 부모 클래스의 멤버 변수명과 자식 클래스의 멤버 변수명이 같을 경우 super 키워드를 사용하여 구분하게 된다.

super.변수 or 메소드 이름
이러한 형식으로 사용이 가능하며 super 참조 변수를 통해 부모 클래스의 멤버에 접근할 수 있다. this와 마찬가지로 인스턴스 메소드에만 사용이 가능하며 클래스 메소드에는 사용이 불가능하다.

class Parent {
	int a = 10;
}
class Child extends Parent {
	void display() {
    		System.out.println(a); // 10 출력
	        System.out.println(this.a); // 10 출력
        	System.out.println(super.a); // 10 출력
        }
}
public class Inheritance2 {
	public static void main(String[] args) {
    		Child ch = new Child();
        	ch.display();
        }
}

이 코드에서 a라는 이름을 가지는 멤버 변수는 부모 클래스에서 10이라는 값을 가지는 변수로 하나만 존재하므로 자식 클래스에서 a, this.a, super.a 모두 같은 변수를 가리킨다.

class Parent {
	int a = 10;
}
class Child extends Parent {
	int a = 20;
    	void display() {
        	System.out.println(a); // 20 출력
            	System.out.println(this.a); // 20 출력
                System.out.println(super.a); // 10 출력
        }
}
public class Inheritance3 {
	public static void main(String[] args) {
    		Child ch = new child();
        	ch.display();
	}
}

이 코드에서는 a라는 이름을 가지는 멤버 변수가 부모 클래스에 10이라는 값을 가지는 변수와 자식 클래스에 20이라는 값을 가지는 변수 이렇게 두개가 존재한다. display()함수는 자식 클래스에 소속되기 때문에 a, this.a는 자식 클래스에서 정의한 20을 출력하게 되고 super.a는 부모 클래스에서 정의한 10을 출력하게 된다.

super() 메소드

super() 메소드 역시 this() 메소드와 같은 개념이다. this() 메소드가 같은 클래스의 다른 생성자를 호출할 때 사용된다면 super() 메소드는 부모 클래스의 생성자를 호출할 때에 사용된다.

자식 클래스의 인스턴스를 생성하면 해당 인스턴스에는 자식 클래스의 고유 멤버뿐만 아니라 부모 클래스의 모든 멤버까지 포함되어 있다. 그러므로 부모 클래스의 멤버를 초기화하기 위해서는 자식 클래스의 생성자에서 부모 클래스의 생성자까지 호출해야 한다. 부모 클래스의 생성자 호출은 모든 클래스의 부모 클래스인 Object 클래스까지 계속 올라가며 수행된다.

자바 컴파일러는 부모 클래스의 생성자를 명시적으로 호출하지 않는 모든 자식 클래스의 생성자 첫 줄에 자동으로 다음과 같은 명령문을 추가하여 부모 클래스의 멤버를 초기화하도록 한다.
super();

그러나 자바 컴파일러는 컴파일 시 클래스에 생성자가 하나도 정의되어있지 않아야만 자동으로 기본 생성자를 추가한다. 만약 부모 클래스에 매개변수를 가지는 생성자를 하나라도 선언했다면 부모 클래스에는 기본 생성자가 자동으로 추가되지 않는다.

class Parent {
	int a;
    	Parent(int n) {a=n;}
}

Parent 클래스를 상속 받은 자식 클래스에서 super() 메소드를 사용하여 부모 클래스의 기본 생성자를 호출하게 되면 오류가 발생한다.

class Parent {
	int a;
    	Parent(int n) {a=n;}
}
class Child extends Parent {
	int b;
    	Child() {
        	super(); // 부모 클래스의 기본 생성자 호출 but 부모 클래스는 기본 생성자를 가지고 있지 않음. -> error
            	b = 20;
        }
}

오류가 발생하는 이유는 부모 클래스에는 매개변수를 가지는 생성자가 선언되어 있기 때문에 기본 생성자가 생성되지 않았기 때문이다. 이러한 문제가 발생되지 않기 위해서는 다음과 같이 매개변수를 가지는 생성자를 선언해야 할 경우 기본 생성자까지 명시적으로 선언하는 것이 좋다.

class Parent {
	int a;
    	Parent() {a=10;}
        Parent(int n) {a=n;}
}
class Child extends Parent {
	int b;
    	Child() {
        	super(); // 부모 클래스의 기본 생성자 호출. 명시적으로 기본 생성자를 추가했기 때문에 수행 가능.
            	b = 20;
	}
}

Example

class Parent {
	int a;
    	Parent() {a=10;}
        Parent(int n) {a=n;}
}
class Child extends Parent {
	int b;
    	Child() {
     		// super(40);
    		b = 20;
	}
    	void display() {
    		System.out.println(a);
        	System.out.println(b);
    	}
}
public class Inheritance4 {
	public static void main(String[] args) {
    		Child ch = new Child();
        	ch.display();
        }
}

자식 클래스의 기본 생성자에 부모 생성자에 대한 호출이 없기 때문에 부모 클래스의 기본 생성자가 자동으로 호출된다. 결과적으로 10, 20이 출력된다.
자식 클래스의 기본 생성자에 주석처리 되어있는 super(40) 메소드가 활성화 될 경우 부모 클래스의 매개변수를 하나 가지는 생성자를 호출하게 되어 a가 40이 된다. 결과적으로 40, 20이 출력된다.

메소드 오버라이딩 (Method Overriding)

오버라이딩(overriding)을 설명하기 전에 오버로딩(overloaing)부터 간단하게 정리를 하려고 한다. 오버로딩이란 서로 다른 시그니처를 갖는 여러 메소드를 하나의 이름으로 정의하는 것이다.

오버라이딩이란 상속 관계에 있는 부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 같은 시그니쳐를 갖는 메소드로 다시 정의하는 것이다.

자식 클래스는 부모 클래스의 private 멤버를 제외한 모든 메소드를 상속받는다. 이렇게 상속 받은 메소드는 그대로 사용해도 되고, 필요한 동작을 위해 재정의하여 사용해도 된다.

간단하게 말하면 메소드 오버라이딩은 부모 클래스에서 정의된 메소드를 자식 클래스에서 재정의하여 사용하는 것이다.

오버라이딩의 조건

  • 오버라이딩이란 메소드의 동작만을 재정의하는 것으로 메소드의 선언부는 기존 메소드와 완전히 일치해야 한다.
  • 부모 클래스의 메소드보다 접근 제어자를 더 좁은 범위로 변경할 수 없다.
  • 부모 클래스의 메소드보다 더 큰 범위의 예외를 선언할 수 없다.

메소드 오버라이딩 (Method Overriding)

메소드 오버라이딩을 통해 부모 클래스의 메소드를 자식 클래스에서 재정의하여 사용할 수 있다.

class Parent {
	void display() {
    		System.out.println("parent class's display method");
        }
}
class Child extends Parent {
	void display() {
    		System.out.println("child class's display method");
        }
}
public class Inheritance5 {
	public static void main(String[] args) {
    		Parent p1 = new Parent();
        	p1.display(); // parent class's display method
        	Child ch = new Child();
        	ch.display(); // child class's display method
        	Parent p2 = new Parent();
        	p2.display(); // child class's display method
        }
}

3번째 인스턴스의 경우 자바의 다형성(polymorphism) 때문에 자식 클래스의 display() 메소드가 실행된다.

오버로딩 (Overloading) & 오버라이딩 (Overriding)

오버로딩과 오버라이딩의 헷갈리기 쉽지만 완전히 다르다.

  • 오버로딩
    새로운 메소드를 정의
  • 오버라이딩
    기존의 메소드를 재정의
class Parent {
	void display() {
    		System.out.println("parent class's display method");
        }
}
class Child extends Parent { 
	void display() { // 오버라이딩된 display() 메소드
    		System.out.println("child class's display method");
        void display(String str) { // 오버로딩된 display() 메소드
        	System.out.println(str);
        }
}
public class Inheritance6 {
	public static void main(String[] args) {
    		Child ch = new Child();
            	ch.display(); // child class's display method 출력
                ch.display("overloading display method"); // overloading display method 출력
        }
}

다이나믹 메소드 디스패치 (Dynamic Method Dispatch)

메서드 디스패치란 어떤 메소드를 호출할지 결정하여 실제로 실행시키는 과정을 의미한다. 자바는 런타임 과정에서 객체를 생성하고 컴파일 과정에서 생성할 객체 타입에 대한 정보만 보유한다. 메소드 디스패치는 정적(Static), 동적(Dynamic) 이렇게 두가지가 있다.

정적 메소드 디스패치 (Static Method Dispatch)

컴파일 시점에서 컴파일러가 특정 메소드를 호출할 것이라고 명확하게 알고 있는 경우를 의미한다. 컴파일 시 생성된 바이트 코드에도 이 정보가 남게 되고 런타임이 되지 않아도 미리 결정하는 개념의 메소드 디스패치이다. 함수를 오버로딩하여 사용하는 경우 인자의 타입이나 반환타입 등에 따라 어떤 메소드를 호출할 것인지 미리 알고 있는 경우이다.

class Parent {
	public void display() {
    		System.out.println("parent display method");
        }
}
class Child extends Parent {
	public void display(){
    		System.out.println("child display method");
        }
}
public class Inheritance7 {
	public static void main(String[] args) {
    		Child ch = new Child();
        	ch.display(); // child display method 출력
        }
}

동적(다이나믹) 메소드 디스패치 (Dynamic Method Dispatch)

정적 메소드 디스패치와 반대로 컴파일러가 어떤 메소드를 호출할지 모르는 경우이다. 동적 메소드 디스패치는 호출할 메소드를 런타임 시점에서 결정한다. 인터페이스나 추상 클래스에 정의된 추상 메소드를 호출하는 경우로 인터페이스 또는 추상 클래스로 선언하고 구현/상속 받은 자식 클래스의 인스턴스를 생성한다. 컴파일러가 알고 있는 타입에 대한 정보를 바탕으로 런타임 시 해당 타입의 객체를 생성하고 메소드를 호출한다.

public interface Family {
	void display();
}
public class Parent implements Family {
	@Override
    	public void display() {
        	System.out.println("parent display method");
        }
}
public class Child implements Family {
	@Override
    	public void display() {
        	System.out.println("child display method");
        }
}
public class Inheritance8 {
	public static void main(String[] args) {
    		Family family = new Parent();
        	System.out.println(family.display()); // parent display method 출력
        }
}

런타임 전에는 객체 생성이 되지 않기 때문에 Family family = new Parent()를 해도 컴파일러는 Parent가 생성됨을 알 수 없으므로 Family가 정의한 display() 메소드만 접근이 가능하다.

추상 클래스 (Abstract Class)

추상 메소드 (Abstract Method)

추상 메소드란 자식 클래스에서 반드시 오버라이딩해야만 사용할 수 있는 메소들르 의미한다. 추상 메소드를 선언하여 사용하는 목적은 추상 메소드가 포함된 클래스를 상속받는 자식 클래스가 반드시 추상 메소드를 구현하도록 하기 위함이다.

예로 모듈처럼 중복되는 부분이나 공통적인 부분은 미리 다 만들어진 것을 사용하고 이를 받아 사용하는 쪽에서는 자신에게 필요한 부분만 재정의하여 사용하여 생산성을 향상시키고 배포에 유리하게 한다.

추상 메소드는 선언부만 존재하고 구현부는 작성되지 않는다. 자식 클래스에서 구현부를 작성하여 오버라이딩 하여 사용하는 것이다.

abstract 반환타입 메소드명();

위와 같은 형식으로 선언부만 작성하게 된다.

추상 클래스 (Abstract Class)

추상 클래스란 하나 이상의 추상 메소드를 포함하는 클래스를 의미한다. 추상 클래슨느 객체 지향 프로그래밍에서 중요한 특징인 다형성을 가지는 메소드의 집합을 정의할 수 있도록 해준다. 반드시 사용되어야 하는 메소드를 추상 클래스에 추상 메소드로 선언하면 이 클래스를 상속받는 모든 클래스에서 이 추상 메소드를 반드시 재정의해야 한다.

abstract class 클래스명 {
	...
    	abstract 반환타입 메소드명();
    	...
}

위와 같은 형식으로 추상 클래스를 작성한다. 추상 클래스는 동작이 정의되지 않은 추상 메소드를 포함하고 있으므로 인스턴스를 생성할 수 없다. 추상 클래스는 먼저 상속을 통해 자식 클래스를 만들고 자식 클래스에서 추상 클래스의 모든 추상 메소드를 오버라이딩해야만 자식 클래스의 인스턴스를 생성할 수 있다.

추상 클래스는 추상 메소드를 포함하고 있다는 점을 제외하면 일반 클래스와 같다. 그러므로 생성자, 필드, 일반 메소드 모두 포함 가능하다.

abstract class Animal { 
	abstract void cry();
}
class Cat extends Animal {
	void cry() {
    		System.out.println("야옹");
        }
}
class Dog extends Animal {
	void cry() {
    		System.out.println("멍멍");
        }
}
public class Polymorphism1 {
	public static void main(String[] args) {
    		Cat c = new Cat();
            	Dog d =new Dog();
            	c.cry(); // 야옹 출력
                d.cry(); // 멍멍 출력
        }
}

위의 코드에서 추상 클래스인 Animal 클래스는 추상 메소드인 cry() 메소드를 가지고 있다. Animal 클래스를 상속받는 자식 클래스인 Cat, Dog 클래스는 cry() 메소드를 오버라이딩해야만 인스턴스를 생성할 수 있다.

추상 메소드 사용 목적

추상 메소드가 포함된 클래스를 상속받는 자식 클래스가 반드시 추상 메소드를 구현하도록 하기 위해 추상 메소드를 사용한다. 일반 메소드라면 사용자가 해당 메소드를 구현할 수도 있고 구현하지 않을 수도 있다. 하지만 추상 메소드가 포함된 추상 클래스를 상속받은 모든 자식 클래스는 추상 메소드를 구현해야만 인스턴스를 생성할 수 있으므로 반드시 구현해야 한다.

final 키워드

자바에서 클래스나 변수를 정의할 때 final 키워드를 사용할 수 있다. final 키워드를 클래스나 변수에 붙여 선언할 경우 시간이 지나도 처음 정의된 상태가 변하지 않는 것을 보장한다.

final은 변수, 인자, 클래스, 메소드에 사용이 가능하다.

final 변수

final 변수타입 변수명 = 정의하고자 하는 값;
위와 같은 형식으로 final 변수를 선언할 수 있고 final 변수로 선언이 되면 정의된 값을 변경할 수 없다.

final String myName = "Hwang Seunghwan";
myName = "xx0hn"; // complie error 발생

이미 final 변수 myName은 Hwang Seunghwan으로 정의되었기 때문에 이를 변경하면 에러가 발생한다.

if문에서의 초기화

final 변수를 선언할 때 반드시 바로 초기화해야되는 것은 아니다. if문을 사용하여 조건에 따라 다르게 정의할 수도 있다.

final String myName;
boolean condition = true;
if(condition) {
	myName = "Hwang Seunghwan";
}
else {
	myName = "xx0hn";
}

class 생성자에서의 초기화

클래스의 멤버 변수에 final을 적용할 때에는 클래스의 생성자에서 초기화할 수 있다.

class Final {
	final String myName;
    	Final() {
    		myName = "Hwang Seunghwan";
        }
}

static final 변수 초기화

static 변수도 final 변수로 만들고 초기화 할 수 있다. static final 변수도 일반적인 final 변수와 동일한 특징을 가진다.

static final String myName = "Hwang Seunghwan";
static final String myName;
static {
	myName = "Hwang Seunghwan";
}

final 인자

인자를 선언할 때에도 final 키워드를 사용할 수 있다. final로 선언된 인자는 메소드 내에서의 변경이 불가능하다.

public void display(final int num) {
	System.out.println(num);
    	num = 10; // complie error 발생
}

final 클래스

클래스를 정의할 때에도 final 키워드를 사용할 수 있다.

final class Final {
	final String myName;
    	Final() {
        	myName = "Hwang Seunghwan";
        }
}

변수가 아닌 클래스에 final 키워드를 붙이기 되면 다른 클래스가 상속할 수 없는 클래스가 된다.

final class Final {
	final String myName;
    	Final() {
        	myName = "Hwang Seunghwan";
        }
}
class Normal extends Final () {} // complie error 발생

final 메소드

메소드에도 final 키워드를 사용할 수 있다.

class Final {
	final String myName = "Hwang Seunghwan";
    	final String getMyName() {
        	return myName;
        }
}

final 메소드는 오버라이드를 할 수 없다. final 메소드를 재정의할 경우 에러가 발생하게 된다.

class Final {
	final String myName = "Hwang Seunghwan";
    	final String getMyName() {
        	return myName;
        }
}
class Normal extends Final {
	@Override
    	String getMyName() { // complie error 발생
    		return "xx0hn";
	}
}

Object 클래스

위에서 중간중간에 등장했던 Object 클래스에 대해 자세하게 알아보았다. Object 클래스는 모든 클래스의 부모 클래스로 생각하면 이해하기 쉽다. Object 클래스는 isString(), clone() 등의 메소드를 import문 없이 사용할 수 있게 해준다.

java.lang 패키지

java.lang 패키지는 자바에서 가장 기본적인 동작을 수행하는 클래스들의 집합이다. java.lang 패키지의 클래스들은 import문을 사용하지 않아도 클래스의 이름만으로 바로 사용할 수 있다.

java.lang.Object 클래스

java.lang 패키지 중에서 가장 많이 사용되는 클래스는 Object 클래스이다. Object 클래스는 모든 자바 클래스의 최상위 클래스이다. 따라서 자바의 모든 클래스는 Object 클래스의 모든 메소드를 바로 사용할 수 있다.

Object 클래스는 11개의 메소드로 구성되어있다.

toString() 메소드

toString() 메소드는 해당 인스턴스 정보를 문자열로 반환한다. 반환되는 문자열은 클래스명과 함께 구분자로 @가 사용되고 그 뒤로 16진수 해시 코드(hash code)가 추가된다. 16진수 해시 코드 값은 인스턴스의 주소를 나타내고 인스턴스마다 모두 다르다.

Car car1 = new Car();
Car car2 = new Car();
System.out.println(car1.toString()); // Car@15db9742 출력
System.out.println(car2.toString()); // Car@6d06d69c 출력

자바에서 toString() 메소드는 기본적으로 각 API 클래스마다 자체적으로 오버라이딩을 통해 재정의되어 있다.

equals() 메소드

equals() 메소드는 해당 인스턴스를 매개변수로 전달받는 참조 변수와 비교하여, 그 결과를 반환한다. 이때 참조 변수가 가리키는 값을 비교하기 때문에 서로 다른 객체는 언제나 false를 반환한다.

Car car1 = new Car();
Car car2 = new Car();
System.out.println(car1.equals(car2)); // false 출력
car1 = car2; // 두 참조 변수가 같은 주소를 가리키도록 함
System.out.println(car1.equals(car1)); // true 출력

자바에서 equals() 메소드는 기본적으로 각 API 클래스마다 자체적으로 오버라이딩을 통해 재정의되어 있다.

clone() 메소드

clone() 메소드는 해당 인스턴스를 복제하여 새로운 인스턴스를 생성해 반환한다. Object 클래스의 clone() 메소드는 단지 필드의 값만 복사하기 때문에 필드의 값이 배열이나 인스턴스라면 제대로 복사할 수 없다. 이러한 경우에는 해당 클래스에서 clone() 메소드를 오버라이딩하여 복제가 제대로 이뤄지도록 재정의해야 한다.

clone() 메소드는 데이터의 보호를 위해 Cloneable 인터페이스를 구현한 클래스의 인스턴스만 사용가능하다.

import java.util.*;

class Car implements Cloneable {
	private String modelName;
    	private ArrayList<String> owners = new ArrayList<String>();
        
        public String getModelName() {
        	return this.modelName;
        }
        public void setModelName(String modelName) {
        	this.modelName = modelName;
        }
        public ArrayList getOwners() {
        	return this.owners;
        }
        public void setOwners(String ownerName) {
        	this.owners.add(ownerName);
        }
        public Object clone() {
        	try{
            		Car clonedCar = (Car)super.clone();
                    	// clonedCar.owners = (ArrayList)owners.clone(); // owners 필드의 정확한 복제를 위해서는 이렇게 작성해야함
                    	return clonedCar;
               } catch (CloneNotSupportedException ex) {
               		ex.printStackTrace();
                   	return null;
               }
       }
}
public class Obeject1 {
	public static void main(String[] args) {
    		Car car1 = new Car();
            	car1.setModelName("그랜저");
                car1.setOwners("황승환");
                System.out.println("Car1" + car1.getModelName() + ", " + car1.getOwners() + "\n"); // Car1: 그랜저, [황승환] 출력
                Car car2 = (Car)car1.clone();

      		car2.setOwners("이순신");
		System.out.println("Car1 : " + car1.getModelName() + ", " + car1.getOwners()); // Car2: 그랜저, [황승환, 이순신] 출력
		System.out.println("Car2 : " + car2.getModelName() + ", " + car2.getOwners()); // Car2: 그랜저, [황승환, 이순신] 출력
        }
}

위의 코드는 부모 클래스의 clone() 메소드를 호출하여 오버라이딩한다. Car 클래스의 인스턴스인 car1을 생성한 뒤 Car 클래스의 다른 인스턴스 car2를 생성하여 car1을 복제한다.

그러나 위의 코드처럼 clone() 메소드를 재정의하게 되면 필드의 값이 인스턴스일 경우 제대로된 복사를 수행할 수 없게 된다.

복제된 인스턴스 car2의 owners 필드에 새로운 값을 추가하지만 실행 결과를 보면 원본 인스턴스인 car1의 owners 필드에도 새로운 값이 추가된 것을 볼 수 있다.

이렇게 단순히 부모 클래스의 clone() 메소드를 호출하여 clone() 메소드를 재정의하면 배열이나 인스턴스 필드는 복제되는 것이 아니라 해당 배열이나 인스턴스를 가리키는 주소값만 복제되는 것이다.

결과적으로 car1과 car2가 가지는 owners 필드는 같이 사용하는 것과 같아진다.

owners 필드를 정확하게 복제하기 위해서는 위의 코드에서 주석처리된 라인처럼 배열이나 인스턴스인 필드에 대해 별도로 clone() 메소드를 구현하여 호출해야 한다.

Object 메소드

profile
꾸준함을 꿈꾸는 SW 전공 학부생의 개발 일기

0개의 댓글