도도한 개발자

[Java] #10. 상속 본문

Backend/Java

[Java] #10. 상속

Kiara Kim 2022. 3. 14. 22:16
public class Parent {

	public Parent() {
		System.out.println("Parent Constructor");
	}
	
	public void printName() {
		System.out.println("Parent printName()");
	}
}

 

Parent 클래스에는 생성자와 printName()이라는 메소드가 있다. 

 

public class Child extends Parent {
	public Child() {
		System.out.println("Child Constructor");
	}
}

 

Child 클래스에는 그냥 생성자만 있다. 그런데 클래스를 선언문을 보면 extends Parent가 뒤에 붙을 걸 볼 수 있다. extends라는 것은 '확장하다'로, 자바의 예약어이며, 그 다음에 클래스 이름을 지정하면 그 클래스를 상속받는다는 말이다. 즉, Parent 클래스를 확장한다는 말이다. 이렇게 확장을 하면 부모 클래스에 선언되어 있는 public 및 protected로 선언되어 있는 모든 변수와 메소드를 내가 갖고 있는 것처럼 사용할 수 있다. 

 

새로운 클래스를 만들어 main() 메소드에서 Child 클래스에 잇는 printName() 메소드만 호출해보자.

 

public class Inheritance {

	public static void main(String[] args) {
		Child child = new Child();
		child.printName();
	}
}

 

위 클래스를 컴파일하고 실행하면 컴파일은 정상적으로 될 것이며 실행 결과는 다음과 같이 나온다.

 

Parent Constructor
Child Constructor
Parent printName()

 

Parent 클래스의 메소드를 호출하지도 않았는데 확장을 한 클래스가 생성자를 호출하면 자동으로 부모 클래스의 기본 생성자가 호출된다. 그래서 "Parent Constructor"라는 문장이 출력된 것이다. 그리고 부모 클래스에만 printName() 메소드아 있지만 상속을 했기 때문에 실행된 것을 볼 수 있다. 다시 정리하면

 

- 부모 클래스에서는 기본 생성자를 만들어 놓는 것 외에는 상속을 위해 아무런 작업을 할 필요가 없다

- 자식 클래스는 클래스 선언시 extends 다음에 부모 클래스 이름을 적어준다.

- 자식 클래스의 생성자가 호출되면, 자동으로 부모 클래스의 매개 변수 없는 생성자가 실행된다.

- 자식 클래스에서는 부모 클래스에 있는 public, protected로 선언된 모든 인스턴스 및 클래스 변수와 메소드를 사용할 수 있다. 

 

추가로, 자바는 다중 상속이 안된다. 즉, extends 뒤에 클래스 하나만 써야지 두 개 이상 클래스를 나열하면 컴파일이 되지 않는다. 

 

 

* 메소드 overriding

 

자식 클래스에서 부모 클래스에 있는 메소드와 동일하게 선언하는 것을 메소드 overriding이라고 한다. 접근 제어자, 리턴 타입, 메소드 이름, 매개 변수 타입 및 개수가 모두 동일해야만 메소드 Overriding이라고 부른다. 자식 클래스인 B 클래스에 부모 클래스 A 클래스에 선언된 것과 동일한 abc() 메소드를 만들어 실행하면 어떻게 될까? A의 abc() 메소드가 수행되는 것이 아니라 B의 abc() 메소드가 호출될 것이다. 다시 말해 부모 클래스에 선언되어 있는 메소드와 동일하게 선언되어 있는 메소드를 자식 클래스에 선언하면 자식 클래스의 메소드만 실행된다.

 

생성자의 경우 부모 클래스에 있는 생성자를 호출하는 super() 가 추가되지만, 메소드는 그러지 않다. 참고로, '동일하게 선언되어 있다'는 말을 이 분야에선 '동일한 시그니처(signature)를 가진다'라고 표현한다. 여기서 시그니처는 메소드 이름과 매개 변수의 타입 및 개수를 의미한다. 

 

 

* 참조 자료형의 형 변환

 

부모 클래스를 A, 자식 클래스를 B라고 가정했을 때 두 클래스를 작성하면,

 

public class A {
	public A() {}
    public A(String name) {}
    public void printName() {
    	System.out.println("printName() -A");
    }
}

 

public class B {
	public B() {}
    public B (String name) {}
    public void printName() {
    	System.out.println("printName() - B");
    }
    public void printAge() {
    	System.out.println("printAge() - B");
    }
}

 

그리고 지금까지 객체를 생성하려면

A a = new A();
B b = new B();

 

이렇게 만듷었다. 그런데 상속관계가 성립되면, 위의 방법관 다르게 생성할 수도 있다.

 

A obj1 = new B();

 

이는 자바에서 상속 관계가 성립되기 때문에 가능한 것이지만 다음과 같은 객체 생성은 안된다.

 

B obj2 = new A();

 

자식클래스인 B 클래스에선 부모 클래스인 A 클래스의 메소드와 변수들을 사용할 수 있지만 그 반대는 사용할 수 없다.

5장에서 기본 자료형의 형 변환에 대해 알아보았는데, int에서 long으로 형 변환은 별도의 작업없이 가능했지만 그 반대는 갑시 바뀔 확률이 있기 때문에 명시적으로 (long)이라고 형 변환을 해야 했다. 참조 자료형도 마찬가지로 자식 클래스의 타입을 부모 클래스의 타입으로 형 변환하면 부모 클래스에서 호출할 수 있는 메소드들은 자식 클래스에서도 호출할 수 있으므로 문제가 되지 않는다. 

 

예제를 통해 알아보자.

 

public class C {
	public static void main(String[] args) {
    	C c = new C();
        c.casting();
    }
    
    public void casting() {
    	A a = new A();
        B b = new B();
        
        A a2 = b;
        B b2 = a;	// @
    }
}

 

C 클래스를 컴파일해보면 @에서 "incompatible types"라는 에러가 발생한다.  왜냐하면 a 객체는 부모 클래스인 A 클래스의 객체이며 B 클래스에 선언되어 있는 메소드나 변수를 완전히 사용할 수 없기 때문이다. 따라서 컴파일 오류를 피하려면 @는 다음과 같이 형 변환을 해야 한다.

 

B b2 = (B)a;		// @

 

이렇게 하면 정상적으로 실행될까? 컴파일은 되겠지만 예외가 발생한다. 왜냐하면 a 객체는 실제로 A 클래스의 객체이므로 컴파일 오류는 없지만 실행 시엔 "a는 원래 A 클래스의 객체라서 사용할 수 없어요" 라고 예외가 발생한다. 

 

그렇다면 어떻게 명시적으로 형 변환을 해도 문제가 없는 것일까?

 

public class C {
	// 중간 생략
    public void casting2() {
    	B b = new B();
        A a2 = b;
        B b2 = (b)a2;
    }
}

 

a2는 b를 대입한 것이다. 그리고 b는 B 클래스의 객체이다. a2로 겉모습은 A 클래스의 객체인 것처럼 보이나 실제로는 B 클래스의 객체이기 때문에 a2를 B 클래스로 형 변환해도 전혀 문제가 없는 것이다.

 

 

그런게 왜 이렇게 복잡하게 형 변환을 사용해야 할까? 다음의 메소드를 보자.

 

public class C {
	public static void main(String[] args) {
    	C c = new C();
        c.objArr();
    }
    public void objArr() {
    	A[] a = new A[3];
        a[0] = new B();
        a[1] = new A();
        a[2] = new B();
    }
}

 

A 배열은 3개의 값을 저장할 수 있는 공간을 가진다. 그런데 0번째 배열과 2번째 배열은 B 클래스의 객체를 할당한 것을 볼 수 있다. 이처럼 일반적으로 여러 개의 값을 처리하거나, 매개 변수로 값을 전달할 때에는 보통 부모 클래스의 타입으로 보낸다. 그렇지 않으면 여러 값을 보낼 때 각 타입별로 구분해서 메소드를 만들어야 하는 번거로움이 생길 수 있기 때문이다. 

 

 

* instanceof

 

그러면 a라는 배열의 타입이 B인지 A인지 어떻게 구분할 수 있을까? 그럴 때 사용하는 예약어가 바로 instanceof이다.

 

private void checkType(A[] a) {
	for(A temp : a) {
    	if(temp instanceof B) {
        	System.out.println("B");
        } else if(temp instanceof A) {
        	System.out.println("A");
        }
    }
}

 

instanceof 앞에는 객체를, 뒤에는 클래스(타입)을 지정해주면 된다. 그런데, 이 예약어를 사용할 때 주의해야 할 점이 있다. a의 0번째 값은 B 타입이다. 그런데, A 타입이기도 하다. 그렇기 때문에 instanceof 로 타입을 구분할 때 A 의 인스턴스인지 여부를 먼저 점검하면 전부 A 타입이라고 나올 것이다. 따라서 B 타입인지 여부를 먼저 작성해야 제대로 점검이 가능하다.

 

 

* 자식 클래스에서 할 수 있는 것들

 

** 생성자에 대하여

- 자식 클래스의 생성자가 호출되면, 자동으로 부모 클래스의 매개 변수가 없는 기본 생성자가 호출된다. 명시적으로 super() 라고 지정할 수도 있다.

- 부모 클래스의 생성자를 명시적으로 호출하려면 super()를 사용하면 된다.

 

** 변수에 대하여

- 부모 클래스에 private로 선언된 변수를 제외한 모든 변수가 자신의 클래스에서 선언된 것처럼 사용할 수 있다. 

- 부모 클래스에 선언된 변수와 동일한 이름을 가지는 변수를 선언할 수도 있다. 하지만, 이렇게 엎어 쓰는 것은 권장하지 않는다.

- 부모 클래스에 선언되어 있지 않는 이름의 변수를 선언할 수 있다.

 

** 메소드에 대하여

- 변수처럼 부모 클래스에 선언된 메소드들이 자신의 클래스에 선언된 것처럼 사용할 수 있다.

- 부모 클래스에 선언된 메소드와 동일한 시그니처를 사용함으로써 메소드를 overriding할 수 있다.

- 부모 클래스에 선언되어 있지 않은 이름의 새로운 메소드를 선언할 수 있다.

 

 

* 정리

 

상속을 받는 클래스의 선언문에 사용하는 예약어는 무엇인가요?
어떤 클래스를 상속을 받아 확장할 때에는 extends라는 예약어를 사용한다.

상속을 받은 클래스의 생성자를 수행하면 부모의 생성자도 자동으로 수행된다.
확장을 한 클래스가 생성자를 호출하면, 자동으로 부모 클래스의 "기본 생성자"가 호출된다.

부모 클래스의 생성자를 자식 클래스에서 직접 선택하려고 할 때 사용하는 예약어는 무엇인가요?
 super라는 예약어를 사용하면 부모 클래스를 의미한다. 이 super를 메소드처럼 super()로 사용하면 부모 클래스의 생성자를 호출한다.

메소드 Overriding과 Overloading을 정확하게 설명해 보세요.
Overriding은 자식 클래스에서 부모 클래스에 선언된 메소드의 선언 구문을 동일하게 선언하여 사용하는 것을 의미한다.
즉, public void printName()이라는 메소드가 부모클래스에 있고, 자식 클래스에도 동일한 메소드를 선언한다는 것이다.
혼동되기 쉬운 Overloading은 상속관계와 거의 상관 없이 메소드의 이름을 동일하게 하고, 매개변수만 다르게 하는 것을 의미한다.

A가 부모, B가 자식 클래스라면 A a=new B(); 의 형태로 객체 생성이 가능한가요?
 "부모 변수명=new 자식();" 과 같이 선언하는 것은 가능하다. 
하지만 "자식 변수명=new 부모();"와 같이 선언하는 것은 불가능하다. 
왜냐하면 자식 클래스는 부모 클래스의 모든 내용을 상속 받으므로 전자와 같이 사용하는 것이 가능하지만,
후자의 경우와 같이 부모는 자식이 갖고 있는 모든 것을 갖지 못하기 때문에 불가능하다.

명시적으로 형변환을 하기 전에 타입을 확인하려면 어떤 예약어를 사용해야 하나요? 
실행시 형변환 관련 예외가 발생하지 않도록 하려면 "instanceof"예약어를 사용하여 검증 작업을 해야만 한다.

위의 문제에서 사용한 예약어의 좌측에는 어떤 값이, 우측에는 어떤 값이 들어가나요
instanceof의 좌측에는 확인하고자 하는 변수를, 우측에는 클래스 이름이 위치한다.

instanceof 예약어의 수행 결과는 어떤 타입으로 제공되나요? 
instanceof 예약어를 통해 검증한 결과는 boolean 타입으로 제공된다. 

Polymorphism이라는 것은 뭔가요?

Polymorphism은 다형성을 의미한다. 
자식 클래스는 자신만의 "행위"를 가질 수 있지만, 부모 클래스에 선언된 메소드들도 공유 가능하다는 것을 의미한다. 
다시 말해, 부모 클래스의 타입으로 변수를 선언하고, 자식 클래스의 생성자를 사용할 경우 overriding된 메소드를 호출하면 자식 클래스에 선언된 메소드가 호출되고, 부모 클래스의 메소드도 공유 가능하다는 것을 의미한다. 

 

 

그럼 이만.
 

'Backend > Java' 카테고리의 다른 글

[Java] #12. Object. 모든 클래스의 부모 클래스  (0) 2022.03.15
[Java] #11. API  (0) 2022.03.15
[Java] #09. 패키지와 접근 제어자  (0) 2022.03.14
[Java] #08. 참조 자료형  (0) 2022.03.14
[Java] #07. 배열의 모든 것  (0) 2022.03.13