상속과 다형성

박정훈·2021년 1월 22일
1

java

목록 보기
10/15

상속(inheritance)

class A{
}
class B extends A{
}

A 클래스가 B클래스에게 상속한다.
= B 클래스가 A클래스를 상속받는다.

상위 클래스는 super class, base class
하위 클래스는 subvlass, derived class로 표현하기도 한다.
부모 클래스(parent class)를 상위 클래스, 자식 클래스(child class)를 하위 클래스라고 부른다.

A<-B
클래스 간 상속을 표현할 때는 위와 같이 상속받는 클래스에서 상속하는 클래스로 화살표가 간다.

자바 문법으로 상속을 구현할 때는 extends 예약어를 사용한다.
상위 클래스에 구현한 기능을 하위 클래스에서 확장한다는 의미로 extends 예약어를 사용한다.

class B extends A{
}

위의 코드는 'B 클래스가 A 클래스를 상속받는다'라고 한다.

class Mammal{
}
class Human extends Mammal{
}

포유류는 사람보다 일반적인 개념이다. 즉 사람은 포유류의 특징과 긴응을 기본으로 더 많거나 다른 특징과 기능을 가지고 있다. 이렇게 상속 관계에서는 상위 클래스가 하위 클래스보다 일반적인 개념이고, 하위 클래스는 상위 클래스보다 구체적인 구체적인 클래스가 된다.

하위 클래스에서는 상위 클래스에서 private으로 선언한 멤버 변수에 접근할 수 없다.
상위 클래스에 작성한 변수나 메서드 중 외부 클래스에서 사용할 수 없지만 하위 클래스에서는 사용할 수 있도록 지정하는 예약어가 protected이다.

하위클래스 생성자에서 상위 클래스 생성자 호출

하위 클래스가 생성될 때는 상위 클래스의 생성자가 먼저 호출된다.
상속을 받은 하위 클래스는 상위 클래스의 변수와 메서드를 사용할 수 있다. 변수를 사용할 수 있다는 것은 그 변수를 저장하고 있는 메모리가 존재한다는 뜻이다. 그러나 하위 클래스에는 상위 클래스의 변수가 없다. 여기서 상속받은 하위 클래스가 생성되는 과정을 다시 생각할 필요가 있다.
상위 클래스를 상속 받은 하위 클래스가 생성될 때는 반드시 상위 클래스의 생성자가 먼저 호출된다. 그리고 상위 클래스의 생성자가 호출될 때 상위 클래스의 멤버 변수가 메모리(힙 메모리)에 생성되는 것이다.
상위 클래스에서 private으로 선언한 변수도 물론 메모리에 올라간다. 하지만 하위 클래스에서 접근할 수 없을 뿐이다.

super 예약어는 하위 클래스에서 상위 클래스로 접근할 때 사용한다. 하위 클래스는 상위 클래스의 주소, 즉 참조 값을 알고 있다. 이 참조 값을 가지고 있는 예약어가 super다. 또한 super는 상위 클래스의 생성자를 호출하는 데도 사용한다. 하위 클래스 생성자만 호출했는데 상위 클래스 생성자가 호출되는 이유는 하위 클래스 생성자에서 super()를 자동으로 호출하기 때문이다. super()를 호출하면 상위 클래스의 디폴트 생성자가 호출된다. 하위 클래스의 디폴트 생성자는 바이트 코드로 변환되기 전에 다음과 같이 코드가 자동으로 변경된다.(컴파일러가 자동으로 super();코드를 추가한다.)

public VIPCustomer() {
	super();
    ...
}

하위 클래스가 생성될 때는 상위 클래스의 디폴트 생성자를 호출하는 super()가 자동으로 생성된다. 부모 클래스에서 디폴트 생성자를 없애고 자식 클래스의 생성자에서 명시적으로 부모 클래스의 생성자를 호출하지 않는다면 오류가 생긴다. 자식 클래스의 생성자에서는 먼저 부모 클래스의 생성자를 호출해야 하기 때문이다. 원래 컴파일러가 자식 클래스의 생성자에서 부모 클래스의 디폴트 생성자를 호출하도록 자동으로 코딩하지만 부모 클래스의 디폴트 생성자가 없기에 오류가 생기는 것이다. 이런 경우는 자식 클래스에서 super를 이용해 생성자를 명시적으로 호출해야 한다.(코드로 써주어야 한다.)
하위 클래스의 인스턴스를 만드는 경우
즉, 하위 클래스의 생성자가 호출되면 먼저 상위 클래스 생성자가 호출되고 상위 클래스 생성자가 호출이 끝나면 하위 클래스 생성자의 내부 코드 수행이 마무리된다.

하위 클래스가 생성될 때는 상위 클래스의 생성자가 먼저 호출된다.
상위 클래스에 디폴트 생성자가 없고 매개변수가 있는 생성자만 있을 경우 super()에 매개변수를 추가하여, 매개변수가 있는 상위 클래스의 생성자를 직접 호출해야 한다.

상위 클래스에 선언한 멤버 변수나 메서드를 하위 클래스에서 참조할 때도 super를 사용한다. this를 사용하여 자신의 멤버에 접근했던 것과 비슷하다.
super()예약어는 상위 클래스의 참조 값을 가지고 있다.
하위 클래스가 상위 클래스와 동일한 이름의 메서드를 구현하는 경우도 있다. 이러한 경우 하위 클래스에서 동일한 이름의 상위 클래스 메서드를 가리킬 때 super().메서드()라고 써야한다.

상위 클래스로 묵시적 클래스 형 변환

VIPCustomer는 VIPCustomer형이면서 동시에 Customer형이기도 하다. 즉 VIPCustomer클래스로 인스턴스를 생성할 때 이 인스턴스의 자료형을 Customer형으로 클래스 형 변환하여 선언할 수 있다. 왜냐하면 VIPCustomer 클래스는 Customer 클래스를 상속받았기 때문이다.
이러한 클래스 형 변환을 업캐스팅(upcasting)이라고 한다.

Customer vc = new VIPCustomer();

반대로 Customer로 인스턴스를 생성할 때 VIPCustomer형으로 선언할 수는 없다. 상위 클래스인 Customer가 VIPCustomer 클래스의 기능을 다 가지고 있는 것은 아니기 때문이다.
모든 하위 클래스는 상위 클래스 자료형으로 묵시적 형 변환될 수 있지만 그 역은 성립하지 않는다.
VIPCustomer는 상위 클래스 Customer형을 내포하고 있기 때문에 Customer형으로 묵시적 형 변환될 수 있지만 그 역은 성립되지 않는다.

형변환된 vc가 가리키는 것

Customer vc = new VIPCustomer();

위의 코드에서 참조 변수 vc가 가리킬 수 있는 변수와 메서드는 Customer 클래스의 멤버변수와 메서드 뿐이다.
클래스가 형 변환이 되었을 때는 선언한 클래스형에 기반하여 멤법 ㅕㄴ수와 메서드에 접근할 수 있다.

하위 클래스의 인스턴스가 상위 클래스로 형 변환되는 과정이 묵시적으로 이루어진다.
하위 클래스의 인스턴스를 생성할 때 상위 클래스 형으로 선언할 때 묵시적 형 변환, 업캐스팅이 일어난다.

클래스의 상속 계층이 여러 단계일 경우도 상위 클래스로의 형 변환은 묵시적으로 이루어진다.

// Mammal <- Primate -<- Human
Primate aHuman = new Human();
Mammal mHuman = new Human();

메서드 오버라이딩

상위 클래스의 메서드를 하위 클래스에서 재정의하는 것
오버라이딩을 하려면 반환형, 메서드 이름, 매개변수 개수, 매개변수 자료형이 반드시 같아야 한다.
그렇지 않으면 자바 컴파일러는 재정의한 메서드를 기준 메서드와 다른 메서드로 인식한다.

@Override

위의 애노테이션은 '이 메서드는 재정의된 메서드입니다.'라고 컴파일러에 명확히 알려주는 역할을 한다.

애노테이션(annotation 주석)

애노테이션은 영어로는 주석이라는 의미이다. @ 기호와 함께 사용하며 '@애노테이션 이름'으로 표현한다. 자바에서 제공하는 에노테이션은 컴파일러에게 특정한 정보를 제공해 주는 역할을 한다. 예를 들어 @Override는 이 메서드가 재정의된 메서드임을 컴파일러에게 알려준다. 만약 메서드의 선언부가 다르다면 컴파일 오류가 발생하여 프로그래머의 실수를 막아 준다. 이렇게 미리 정의되어 있는 애노테이션을 표준 애노테이션이라고 한다.

@Override: 재정의된 메서드라는 정보 제공
@FuntionalInterface: 함수형 인터페이스라는 정보 제공
@Deprecated: 이후 버전에서 사용하지 않을 수 있는 변수, 메서드에 사용함
@SuppressWarnings: 특정 경고가 나타자니 않도록 함

묵시적 클래스 형변환과 메서드 재정의

멤버 변수와 메서드는 선언한 클래스형에 따라 호출된다.
상속에서 상위 클래스와 하위 클래스에 같은 이름의 메서드가 존재할 때 호출되는 메서드는 인스턴스에 따라 결정된다.
다시 말해 선언한 클래스형이 아닌 생성된 인스턴스의 메서드를 호출하는 것이다. 이렇게 인스턴스의 메서드가 호출되는 기술을 '가상 메서드(virtual method)'라고 한다.

가상 메서드

클래스를 생성하여 인스턴스가 만들어지면 멤버 변수는 힙 메모리에 위치한다. 변수가 사용하는 메모리와 메서드가 사용하는 메모리가 다르다. 변수는 인스턴스가 생성될 때마다 새로 생성되지만, 메서드는 실행해야 할 명령 집합이기 때문에 인스턴스가 달라도 같은 로직을 수행한다. 즉 같은 클래스의 인스턴스를 여러 개 생성한다고 해서 메서드도 여러 개 생성되지 않는다.

public class TestA {
	int	num;
    	void aaa() {
        	System.out.println("aaa() 출력");
        }
        public static void main(String[] args){
        	TestA a1 = new TestA();
            	a1.aaa();
            	TestA a2 = new TestA();
                a2.aaa();
        }
}

main()함수가 실행되면 지역 변수는 스택 메모리에 위치한다. 그리고 참조 변수 a1과 a2가 가리키는 인스턴스는 힙 메모리에 생성된다. 메서드의 명령 집합은 메서드 영역(코드 영역)에 위치한다. 우리가 메서드를 호출하면 메서드 영역의 주소를 참조하여 명령이 실행된다.
따라서 인스턴스가 달라도 동일한 메서드가 호출된다.

일반적으로 프로그램에서 메서드를 호출한다는 것은 그 메서드의 명령 집합이 있는 메모리 위치를 참조하여 명령을 실행하는 것이다.
그런데 가상 메서드의 경우에는 '가상 메서드 테이블'이 만들어진다. 가상 메서드 테이블은 각 메서드 이름과 실제 메모리 주소가 짝을 이루고 있다.
어떤 메서드가 호출되면 이 테이블에서 주소 값을 찾아서 해당 메서드의 명령을 수행한다.

상위 클래스(Customer)에서 선언한 calcPrice() 메서드가 있고 이를 하위 클래스(VIPCustomer)에서 재정의한 상태에서 하위 클래스 인스턴스가 상위 클래스로 형변환이 된 경우를 보자.

Customer vc = new VIPCustomer();

이 때 vc.calcPrice()가 호출되면, vc변수를 선언할 때 사용한 자료형(Customer)의 메서드가 호출되는 것이 아니라 생성된 인스턴스(VIPCustomer)의 메서드가 호출된다. 이것을 가상 메서드라고 한다. 자바의 모든 메서드는 가상 메서드이다.

다형성(polymorphism)

하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 말한다. 쉽게 말해 같은 코드에서 여러 실행 결과가 나오는 것이다.
유연하면서도 구조화된 코드를 구현하여 확장성 있고 유지보수하기 좋은 프로그램을 만들 수 있다.

상속 관계에 있는 상위 클래스와 하위 클래스는 같은 상위 클래스 자료형으로 선언되어 생성할 수 있지만 재정의된 메서드는 각각 호출될 뿐만 아니라 이름이 같은 메서드가 서로 다른 역할을 구현하고 있음을 알 수 있다.

is-a관계(is a relationship, inheritance)

is-a 관계란 일반적인 개념과 구체적인 개념의 관계이다. 상속은 is-a 관계에서 사용하는 것이 가장 효율적이다. '사람은 포유류이다'와 같은 관계.
일반 클래스를 점차 구체화하는 상황에서 상속을 사용하는 것이다. 상속을 사용하면 많은 장점이 있지만, 하위 클래스가 상위 클래스에 종속되기 때문에 이질적인 클래스 간에는 상속을 사용하지 않는 것이 좋다.

has-a 관계(has a relationship, association)

한 클래스가 다른 클래스를 소유한 관계
Student와 Subject관계
Subject는 Student에 포함되어 Student의 멤버 변수로 사용하는 것이 적절하다.
이런 경우에는 상속을 사용하지 않는 게 좋다. 왜냐하면 Subject가 Student를 포괄하는 개념의 클래스가 아니기 때문이다.
또한 Student 클래스를 상속받는 다른 클래스가 있을 수도 있다.

상속을 사용하면 클래스간의 결합도가 높아져서 상위 클래스의 변화가 하위 클래스에 미치는 영향이 크다.
따라서 상속은 '일반적인 클래스'와 '구체적인(확장되는)클래스'의 관계에서 구현하는 것이 맞다.

자바는 다중 상속을 지원하지 않는다.

다중 상속으로 인한 모호성 때문이다. 예를 들어 두 개 이상의 상위 클래스에 같은 이름의 메서드가 정의되어 있다면, 다중 상속을 받는 하위 클래스는 어떤 메서드를 상속받을지 모호해진다. 객체 지향에서 다중 상속의 모호성에 대한 예가 다이아몬드 문제이다.

다운캐스팅

Animal ani = new Human();

이때 생성된 인스턴스 Human은 Animal형이다. 이렇게 Animal형으로 형 변환이 이루어진 경우에는 Animal 클래스에서 선언한 메서드와 멤버 변수만 사용할 수 있다. 다시 말해 Human 클래스에 더 많은 메서드가 구현되어 있고 다양한 멤버 변수가 있다고 하더라도 자료형이 Animal 형인 상태에서는 사용할 수가 없다. 따라서 필요에 따라 다시 원래 인스턴스의 자료형(여기에서는 Human형)으로 되돌아가야 하는 경우가 있다. 이렇게 상위 클래스로 형 변환되었던 하위 클래스를 다시 원래 자료형으로 형 변환하는 것을 다운 캐스팅(down casting)이라고 한다.

instanceof

모든 인간은 동물이지만 모든 동물이 인간은 아니다. 따라서 다운 캐스팅을 하기 전에 상위 클래스로 형 변환된 인스턴스의 원래 자료형을 확인해야 변환할 때 오류를 막을 수 있다. 이를 확인하는 예약어가 instanceof이다.

Animal hAnimal = new Human();
if(hAnimal instanceof Human){		// hAnimal 인스턴스 자료형이 Human 자료형이라면
	Human human = (Human)hAnimal;	// 인스턴스 hAnimal을 Human자료형으로 다운 캐스팅
}

위 코드에서 hAnimal은 원래 Human형으로 생성되었는데, Animal형으로 형 변환되었다. instanceof 예약어는 왼쪽에 있는 변수의 원래 인스턴스형이 오른쪽 클래스 자료형인가를 확인한다. 상위 클래스로는 묵시적으로 형 변환이 되지만, 하위 클래스로 형변환을 할 때는 명시적으로 해야한다. 다운캐스팅을 할 때는 항상 확인해야 한다.

Animal ani = new Tiger();
Human h = (Human)ani;

위와 같이 코딩해도 컴파일 오류는 나지 않는다. 일단 Tiger 인스턴스는 Animal 형으로 자동 형 변환이 된다. 변수 h의 자료형 Human과 강제 형 변환되는 ani의 형이 동일하므로 컴파일 오류는 나지 않는다. 그러나 코드를 실행하면 실행 오류가 발생한다.
따라서 참조 변수의 원래 인스턴스형을 정확히 확인하고 다운 캐스팅을 해야 안전하며 이때 instanceof를 사용한다.

정리

자바에서 어떤 클래스의 기능을 확장하여 새로운 클래스를 만들기 위해 상속을 한다. 이때 사용하는 예약어는 extends이다.
하위 클래스가 상위 클래스의 생성자를 호출하거나 상위 클래스의 멤버 변수, 메서드를 호출하기 위해 사용하는 예약어로 상위 클래스의 주소, 즉 참조 값을 나타내는 예약어는 super이다.

클래스를 상속받은 상태에서 상위 클래스에 이미 정의되어 있는 메서드를 하위 클래스에 사용하기에 적절하지 않은 경우에 해당 메서드를 재정의할 수 있다. 이것을 오버라이딩이라고 한다.

상속을 받은 하위 클래스 Engineer에는 생성자가 없다면 디폴트 생성자를 지원받는다.
Engineer 디폴트 생성자안에서는 상위 클래스의 디폴트 생성자를 호출한다. 하지만 상위 클래스에서 디폴트생성자가 없는 경우라면 오류가 발생한다.
하위 클래스의 생성자에서는 상위 클래스의 생성자를 먼저 호출해야하는데 하위 클래스의 생성자안에서 상위 생성자를 호출하지 않았다면 디폴트 생성자를 호출하기 때문이다.
따라서 하위 클래스의 생성자에서 상위 클래스의 생성자를 명시적으로 호출해 주어야 한다.

출처

  • Do it 자바 프로그래밍 입문
profile
정팔입니다.

0개의 댓글