Java/기초문법

[Java 기본 문법] 7. 상속과 다형성

robinjoon98 2021. 8. 17. 10:32

2021.08.16 - [Java] - [Java 기본 문법] 6. 접근제한자와 캡슐화

 

[Java 기본 문법] 6. 접근제한자와 캡슐화

2021.08.14 - [Java] - [Java 기본 문법] 5. 객체와 클래스 [Java 기본 문법] 5. 객체와 클래스 2021.08.14 - [Java] - [자바 기본 문법] 4. 참조타입 [자바 기본 문법] 4. 참조타입 2021.08.13 - [Java] - [Java..

blog.robinjoon.space

상속은 자바에서 객체지향의 핵심중 하나인 다형성을 구현하는 매커니즘이다. 그만큼 아주 중요한 내용이기 때문에 반드시 숙지해야한다.

상속

현실에서 상속은 부모가 자식에게 물려주는 것을 의미한다. 자식은 상속받은것을 원래부터 자기것인냥 자연스럽게 사용할 수 있다. 자바에서도 부모클래스의 맴버(필드와 매서드)를 자식클래스에 물려줄 수 있다.

이때 부모클래스 대신 슈퍼클래스, 상위클래스 라고 하기도 하고, 자식클래스대신에 서브클래스, 하위 클래스라고도 한다. 타입으로서 이야기 할때는, 슈퍼타입, 서브타입이라고 하는게 보통이다.

상속은 이미 만들어진 클래스를 활용해 새 클래스를 작성하는 것이므로, 코드의 중복을 줄여준다. 

예를 들면 field1, method1() 를 가진 클래스(A)가 있고, 여기에 field2, method2()를 추가로 갖는 클래스(B)가 필요하다면, 상속을 이용해 field2, method2() 만 추가하여 클래스를 생성할 수 있다. 이 경우, B 타입의 객체를 생성하여 사용하면, B에 method1()을 가지고있는 것처럼 보인다.

public class A{
	int field1;
	public void method1(){}
}
public class B extends A{
	int field2;
	public void method2(){}
}
// 다른 클래스에서
B b = new B();
/* A로부터 물려받은 필드와 매서드*/
b.field1 = 10;
b.method1();

/* B에 추가한 필드와 매서드*/
b.field2 = 11;
b.method2();

현실에서도 무엇을 상속할 지 부모가 정할 수 있는 것처럼, 자바에서도 상속되지 않는 것을 정할 수 있다.

접근제한자를 private으로 한 필드와 매서드는 상속되지 않는다. 만일, 부모와 자식 클래스가 서로 다른 패키지라면, 접근제한자가 default인 필드와 매서드 역시 상속되지 않는다.

상속하는 방법

현실과는 다르게 자바에선, 자식클래스가 상속받을 부모클래스를 결정한다. 클래스를 선언 할 때, 클래스 이름 뒤에 extends 키워드를 사용하여 어떤 클래스를 상속받을지 명시한다.

public class 자식클래스 extends 부모클래스{
	// 필드 , 매서드, 생성자 구현
}

예를 들어 Car 클래스를 상속하여 Bus 클래스를 만들고 싶다면 이렇게 하면 된다.

public class Bus extends Car{
	// 필드 , 매서드, 생성자 구현
}

자바에서는 여러 클래스로부터 상속받는 다중 상속을 지원하지 않는다. 즉, extends 키워드 뒤에는 클래스 하나만 허용된다.

public class 자식클래스 extends 부모클래스1, 부모클래스2{ // 불가능
	// 필드 , 매서드, 생성자 구현
}

이하 코드는 앞으로 예시로 사용할 Car 클래스와 Bus 클래스이다. 따로 언급이 없다면 이하 내용에서 Car 와 Bus 는 이 코드를 참고하자.

package robinjoon.api.vehicle;
public class Car{
    protected int speed;
    private Tire[] tires;
    private Engine engine;
    private Key key;
    private boolean engineStart;
    
    protected Car(Tire[] tires, Engine engine, Key key){
        this.tires = tires;
        this.engine = engine;
        this.key = key;
    }
    protected final void changeTires(Tire[] tires){
        this.tires = tires;
    }
    protected void openDoor(String where){
        System.out.println(where+"문이 열렸습니다.");
    }
    protected void openWindow(String where){
        System.out.println(where+"창문이 열렸습니다.");
    }
    protected final void startEngine(Key key){
        if(key.equals(this.key)){
            engineStart=true;
        }
    }
    protected final void stopEngine(){
        engineStart=false;
    }
    protected void stop(){
        while(speed!=0){
            speed-=10;
        }
    }
    protected final Key getKey(){
        return this.key;
    }
    protected final boolean isEngineStart(){
        return this.engineStart;
    }
}

package robinjoon.project.vehicle;

import robinjoon.api.vehicle.*;

public class Bus extends Car{
    private int number;
    private boolean needStop;
    public void pushStopBtn(){
        if(!needStop)needStop=true;
    }
    public void notice(String busStop, String nextStop){
        System.out.println("이번 정류장은 "+busStop+"입니다.\n 다음 정류장은 "+
        nextStop+"입니다.");
    }
    public boolean isNeedStop(){
        return this.needStop;
    }
    @Override
    public void stop(){
        while(speed!=0){
            speed-=5;
        }
    }
    public Bus(Tire[] tires, Engine engine, Key key, int number){
        super(tires,engine,key);
        this.number = number;
    }
    public int getNumber() {
        return number;
    }
}

자식과 부모의 관계

자식객체를 생성하면 내부적으로 부모객체가 먼저 생성되고 자식객체가 생성된다. 즉 아래 코드는 Bus 객체만 생성하는 것처럼 보이지만, 실제로는 Car 객체도 생성된다.

Bus bus = new Bus();

저번 포스팅에서 객체는 생성자를 이용해야만 생성할 수 있다고 말했다. 이건 부모객체라고 예외는 아니다.

자식클래스의 생성자에선 반드시 super(...); 을 이용해 부모클래스의 생성자를 호출해야 한다.

자식클래스에 생성자를 생략하여 기본 생성자가 추가되게 하였다면, 반드시 부모클래스에 기본생성자가 존재해야 된다. 컴파일러가 생성하는 기본 생성자에서 부모클래스의 기본생성자를 호출하기 때문이다. 즉, 자식클래스에 생성자가 선언되지 않았다면 아래 코드가 컴파일러에 의해 추가된다.

public 자식클래스(){
    super();
}

만일, 부모클래스에 기본생성자가 없다면 위 코드가 동작하지 않으므로 컴파일되지 않는다.

super(...) 키워드는 부모클래스의 생성자를 호출하는 키워드다. 따라서, 알맞은 매개변수를 ... 자리에 넣어야 한다.  

매서드 재정의(매서드 오버라이딩)

매서드 오버라이딩이란?

경우에 따라서, 부모클래스에서 정의된 매서드가 자식클래스에 알맞지 않은 경우가 있을 수 있다. 위에서 예로 든 Car와 Bus의 관계에서, 일반적인 자동차보다 훨씬 큰 버스의 경우 제동거리가 길고 관성이 크기때문에, 멈출때 속도를 더 느리게 줄여야 한다. 즉, Car에 정의된 매서드 자체는 알맞지만, 매서드의 구현은 알맞지 않다는 뜻이다. 이런 경우, 자식클래스에서 해당 매서드를 재정의 할 수 있다. 재정의된 매서드를 호출하면, 부모클래스가 아니라 자식클래스에서 재정의된 매서드가 호출된다.

매서드를 재정의 하는데는 몇가지 규칙이 있다.

  • 부모클래스에 정의된 매서드와 이름, 반환타입, 매개변수가 같아야 한다.
  • 접근제한을 부모클래스에 정의된 것보다 강하게 할 수 없다.
  • 새로운 예외를 throw 하지 못한다.(예외는 다음 포스팅에서 다룬다)

접근 제한을 강화할 수 없다는 말은, 접근제한자가 public < protected < default < private 순으로 접근제한이 강한데, 부모의 접근제한보다 오른쪽에 있는 접근제한자를 사용할 수 없음을 의미한다.

Bus 클래스의 경우 Car 클래스에 정의된 stop() 매서드를 접근제한을 완화하여 오버라이딩 했다.

package robinjoon.api.vehicle;
public class Car{
    protected int speed;
    ... 생략 ...
    protected void stop(){
        while(speed!=0){
            speed-=10;
        }
    }
    ... 생략 ...
}

package robinjoon.project.vehicle;

import robinjoon.api.vehicle.*;

public class Bus extends Car{
    ... 생략 ...
    @Override // 이 어노테이션을 사용하여 매서드 이름의 오탈자를 검사할 수 있다.
    public void stop(){
        while(speed!=0){
            speed-=5;
        }
    }
    ... 생략 ...
}

매서드 오버라이딩을 할 때, @Override 어노테이션을 사용하면 컴파일러가 이 매서드를 오버라이딩된 것으로 인지하여, 부모클래스에서 이 어노테이션을 사용한 매서드가 정의되어있지 않은 경우 컴파일이 되지 않는다. 이를 이용해 매서드 이름의 오타를 방지할 수 있다. 만약 이 어노테이션이 생략되고, 개발자의 실수로 매서드이름이 부모클래스의 그것과 다를 경우 오버라이딩이 아니라 새 매서드를 정의한 것으로 인식되어 의도한대로 코드가 동작하지 않을 수 있다.

부모 매서드 호출

가끔, 자식 매서드에서 부모의 매서드를 호출할 필요가 있을 수 있다. 만일, 매서드가 재정의되어있지 않으면 그냥 매서드 이름으로 호출하면 해당 매서드를 호출 할 수 있다. 하지만, 필요한 매서드가 이미 자신에 의해 재정의되었다면, 매서드 호출시 자신이 재정의한 매서드가 호출되게 된다. 이럴 경우, super 키워드를 사용하면 된다.

class A{
    void method1(){...};
    void method2(){...};
}
public class B extends A{
    @Override
    void method2(){...}
    void method3(){
        method2();        // B에서 재정의된 method2() 호출
        super.method2();  // A에서 정의된 method2() 호출
    }
}

this 키워드가 자기 자신을 의미한다면, super 키워드는 자신의 부모객체를 의미한다. 따라서, super.필드 로 부모객체의 필드에 접근할 수 있다.

final 매서드와 클래스

저번 포스팅에서, 필드에 final 키워드가 붙은경우 그 필드를 수정하지 못하는 것을 의미한다고 했다. 이와 비슷하게, 클래스와 매서드에도 final 키워드가 붙을 수 있는데, 그 의미는 상속과 관련이 있다.

만일, 클래스에 final 키워드가 붙은 경우, 그 클래스를 상속할 수 없음을 의미한다. 실제로, 문자열을 저장할때 쓰는 String 클래스에는 final 키워드가 붙어있다. 즉, String을 상속한 클래스는 존재할 수 없다.

public final class String { ... }

public class NewString extends String { ... } // 불가능하다

반면 매서드에 final 키워드가 붙은 경우, 이는 하위클래스에서 이를 오버라이딩 할 수 없음을 의미한다. 즉, 위의 Car 클래스의 startEngine(), stopEngine() 같은 매서드들은 Car 클래스를 상속받은 클래스에서 재정의 할 수 없다.

public class SuperCar extends Car{
    ... 생략 ...
    @Override 
    public void startEngine(Key key){ // 불가능
        if(key.equals(this.key)){
            engineStart=true;
        }
    }
    @Override
    public void stopEngine(){ // 불가능
        engineStart=false;
    }
}

다형성과 상속

다형성이란?

다형성은 같은 타입이지만 실행결과가 다양한 객체를 이용할 수 있는 성질을 의미한다. 코드측면에서, 다형성은 하나의 타입의 변수에 여러 객체를 대입하여 다양한 기능을 이용할 수 있게한다.

자바에서 다형성을 지원하기 위해 자식클래스 타입의 부모클래스타입으로의 자동타입변환을 지원한다.

Car bus = new Bus(); // Bus 타입은 Car 타입의 서브타입이므로 Car 타입 변수에 대입할 수 있다.

단, 이 경우 사용할 수 있는 매서드(필드)는 부모타입에 정의된 매서드(필드)로 한정된다.

Car bus = new Bus(tires, engine, key, busNumber); 
// Bus 타입은 Car 타입의 서브타입이므로 Car 타입 변수에 대입할 수 있다.
bus.startEngine(); // 가능
bus.stop(); // 가능
int number = bus.getNumber(); // 불가능

이 때, 자식클래스에서 오버라이딩된 매서드를 호출할 경우 오버라이딩된 매서드가 호출된다.

Car bus = new Bus(tires, engine, key, busNumber); 
// Bus 타입은 Car 타입의 서브타입이므로 Car 타입 변수에 대입할 수 있다.
bus.startEngine(); // 가능
bus.stop(); // 가능. 이 때 호출되는 매서드는 Bus에 오버라이딩된 매서드
int number = bus.getNumber(); // 불가능

다형성이 주는 이점

다형성이 주는 이점을 이해하기 위해 해외여행을 가는 상황을 생각해보자. 해외여행을 하는 방법에는 이동수단의 관점에서 비행기와 배가 있다. 어떤 것을 사용하던지 우리를 목적지로 이동시켜줄 것이다. 그러나, 이동 방법은 다르다. 비행기는 날아서 가고, 배는 물위에 떠서 간다. 이를 객체지향프로그래밍 세계로 추상화 해보자.

public class Vehcile{
    public void go(){};
}
public class AirPlane extends Vehcile{
    @Override
    public void go(){};
}
public class Ship extends Vehcile{
    @Override
    public void go(){};
}

public class Person{
    private Vehcile vehcile;
    public Person(Vehcile vehcile){
        this.vehcile = vehcile;
    }
    public void trip(){
        vehcile.go();
    }
    public void setVehcile(Vehcile vehcile){
        this.vehcile = vehcile;
    }
}

비행기와 배 모두 이동수단이므로 Vehcile 클래스를 상속했다. Vechile 클래스는 목적지로 이동하는 매서드인 go()가 있고, 이를 AirPlane과 Ship 에서 오버라이딩 했다. 또한, Vechile 을 Person 의 필드로 두어 사람이 이동수단을 타고 이동하는 것을 표현했다. 아래는 main 매서드이다.

public class Main {
    public static void main(String[] args) {
        Vehcile vehcile = new AirPlane();
        Person person = new Persion(vehcile);
        person.trip(); // 비행기를 타고 갔다.
        person.setVehcile(new Ship());
        person.trip(); // 배를 타고 갔다.
    }
}

이 코드를 보면, 단순히 setter 매서드를 이용해 다른 객체를 필드에 대입시켰을 뿐인데, 비행기를 타고 가던걸 배를 타고 가는 것으로 바꿨다. 즉, 객체를 부품화하여, 다른 객체를 이용하는 것으로 다양한 기능을 구현할 수 있고, 수정을 최소화하여 기능을 확장할 수 있도록 해준다.

추상클래스

사전적 의미로 추상(abstract)이란, 실체 간에 공통되는 특성을 추출한 것을 말한다. 예를들어, 새, 사람, 곤충, 물고기 등의 실체로부터 공통점을 추출해 동물 이라는 추상을 만들 수 있다.

클래스에서도 추상클래스가 존재한다. 추상클래스란 여러 클래스들의 공통적인 특성만 따로 추출하여 하나로 묶고, 다른 클래스들에선 이를 상속하고, 개별적인 특성만 따로 구현하기 위한 도구다. 일반적인 클래스로도 여기까지는 구현이 가능하지만, 추상클래스만의 차이가 있다. 바로, 추상클래스는 직접 객체를 생성하지 못하고, 특정 매서드를 오버라이딩 하는것을 강제할 수 있다는 것이다. 아래는 동물을 표현하는 Animal 추상클래스이다.

package test;

public abstract class Animal{
    private int age;
    private boolean male;
    
    public int getAge(){
        return this.age;
    }
    public boolean isMale(){
        return this.male;
    }
    public void grow() {
    	this.age++;
    }
    protected abstract void sound();
    
    protected Animal(boolean male) {
    	this.male = male;
    	this.age=0;
    }
}

아래는 Animal 클래스를 상속하여 만든 Pig 클래스이다.

public class Pig extends Animal {
    public Pig(boolean male) {
        super(male);
    }

    @Override
    protected void sound() {
        System.out.println("꿀꿀");
    }
}

추상클래스를 선언하기 위해선 abstract 키워드를 접근제한자와 class 키워드 사이에 넣는다.

public abstract class Animal{
... 생략 ...
}

추상클래스는 일반적인 필드와 매서드를 가질 수 있고, 생성자도 추가할 수 있다. 단, 생성자를 추가한다 해서 그 생성자로 추상클래스의 객체를 만들수는 없다. 추상클래스의 생성자는 자식클래스를 위한 것이다. 

추상클래스는 추상매서드를 가질 수 있다. 추상매서드는 자식클래스가 반드시 오버라이딩해야하는 매서드이다. 추상매서드는 일반적인 매서드 작성방식에서 매서드블록이 사라지고, abstract 키워드가 추가된다.

public abstract class Animal{
... 생략 ...
    protected abstract void sound();
}

추상매서드는 반드시 자식이 오버라이딩해야 한다. 단, 자식을 추상클래스로 선언하면 부모의 추상매서드를 구현하지 않아도 된다(단계적인 추상을 구현하기 위함이다. Animal - Mammalia(포유류) - Pig ). 추상매서드는 결국에는 자식클래스에의해 구현되어야 하므로 private 접근제한자를 가질 수 없다. 또한 default 접근제한자를 가지게 한다면, 다른 패키지에 자식클래스를 생성할 수 없다. 접근제한자에 의해 추상매서드를 구현할 수 없기 때문이다. 같은 이유로 추상클래스에서 private 접근제한을 가진 생성자는 만들 이유가 없다.