간단 지식/Java

12. 상속

납작한돌맹이 2020. 6. 25. 23:52
반응형

어떤 클래스 A의 field와 method를 어떤 클래스 B에게 상속해주면 클래스 B에서는 아무 제약없이 클래스 A에 있는 field와 method를 가져다 쓸 수 있다. 이런 관계를 상속관계라 부르며 A를 상위 클래스, B를 하위 클래스라고 부른다. 상속의 장점은 2가지가 있다. 하나는 코드의 중복이 감소한다는 것이고, 또 하나는 부모 클래스의 수정으로 모든 자식 클래스의 수정 효과를 볼 수 있다는 것이다. 사용 키워드는 extends로, 다음과 같다. public class B extends A{ ... }

 

단, 접근제한자가 private인 field와 method는 상속 대상에서 제외된다. 또한 부모, 자식 클래스가 서로 다른 패키지에 있다면 접근제한자가 default인 field와 method도 제외된다. 참고로 자바에서는 다중 상속이 허용되지 않는다.

 

부모 클래스, 자식 클래스가 있고 자식 클래스의 객체를 생성하기 위해 아래 코드를 실행시켰다고 가정해보자.

자식클래스명 필드 = new 자식클래스();

이렇게 하면 자식 클래스의 객체의 주소가 필드에 저장이 될 것이다. 그런데 내부적으로는 부모 클래스의 객체가 먼저 생성된 후에 자식 클래스의 객체가 생성된다. 바로 아래 그림처럼 상속을 해줘야하기 때문이다. 

그렇다면 부모 클래스의 객체는 언제 생성된걸까? 자바에서 모든 객체는 클래스의 생성자를 호출함으로써 생성된다. 자식 클래스의 생성자 첫 줄에는 super(매개변수); 가 항상 존재한다. 자식 클래스의 생성자가 명시되지 않은 기본생성자일지라도 컴파일러가 알아서 바이트코드에 넣어준다. super(매개변수)는 매개변수와 일치하는 부모 클래스의 생성자를 호출하는 역할을 한다. 따라서 부모 클래스의 객체는 자식 클래스의 객체가 생성되기 직전에 생성된다.


일전에 메소드 오버로딩을 배웠다면, 상속에서는 메소드 오버라이드(Override)을 알게된다. 부모 클래스의 메소드를 상속 받아왔는데 막상 보니 자식 클래스에서 쓸만하지 않을 수 있다. 이럴 때 메소드를 약간 수정해 사용하는 것을 메소드 오버라이드라고 부른다. 단 규칙 3가지를 지켜야 한다.

1. 상속하는 메소드와 return type, 이름, 매개변수가 동일해야한다.

2. 상속하는 메소드보다 더 강한 접근제한자로 바꿀 수 없다.

3. 새로운 Exception을 throw할 수 없다.

그렇다면 자식 클래스에서 오버라이드 된 메소드를 사용한다면, 부모 클래스의 원래 메소드는 사용할 수 없을까? 정답은 '사용할 수 있다' 이다. super.부모메소드(); 이렇게 앞에 super만 붙여주면 된다. super가 부모 객체를 참조하기 때문에 직접적으로 접근이 가능하기 때문이다. 아래 예제가 바로 상속관계의 클래스이다.

public class InheritP {
    public String card, money;

    public void pay(){
        System.out.println("결제 도와드리겠습니다");
    }
    public void receipt(){
        System.out.println("영수증 드리겠습니다");
    }

}
public class InheritC extends InheritP{
    public String payment = card;

    InheritC(){
        super();
    }

    @Override
    public void pay(){
        if(payment == card){
            System.out.println("카드결제 도와드리겠습니다");
            receipt();
        }
        else if(payment == money){
            System.out.println("현금결제 도와드리겠습니다");
            receipt();
        }
        else{
            super.pay();
            receipt();
        }
    }
}

final 키워드는 최종 선언 상태임을 알려준다. 따라서 final 클래스는 최종적인 클래스라는 의미로 다른 클래스가 상속할 수 없다. 가장 대표적인 final 클래스는 public final class String{ ... } 으로, 우리가 흔히 아는 String 클래스이다. 그럼 이어서 final 메소드에도 어떠한 제약이 있다고 생각된다. 바로 자식클래스에서 메소드 오버라이딩을 할 수 없다는 것이다. 


 

OOP의 특징 중 하나인 다형성은 상속과 밀접한 관계가 있다. 다형성은 같은 타입지만 실행결과가 다양한 객체를 사용할 수 있는 성질이다. 이 성질을 위해 자바는 자식객체의 타입을 부모클래스로 자동변환하는 것을 지원한다. 이렇게 하면 부모클래스 타입의 변수가 모든 자식클래스의 객체를 참조할 수 있게 된다. 이것이 바로 객체의 부품화이다. 이런 성질이 성립하는 것은 자식클래스가 부모클래스의 특징(field)과 기능(method)를 상속받기 때문에 가능한 일이다.

ex) 부모 클래스: Animal, 자식 클래스: Cat

     Animal animal = new Cat();  -> Animal타입의 필드 animal에는 Cat 클래스의 객체의 주소가 저장

이렇게 타입변환이 일어난 객체는 해당 부모클래스의 필드&메소드에만 접근할 수 있다. 자식클래스에만 존재하는 필드&메소드에는 접근할 수 없다는 뜻이다.

좀 더 깊게 알아보기 위해 아래 코드를 보자.

//부모클래스
public class Tire {
    public int maxRotation;         //타이어 최대 회전 수
    public int countRotation;       //타이어 누적 회전 수
    public String position;         //타이어 위치

    public Tire(int maxRotation, String position){
        this.maxRotation = maxRotation;
        this.position = position;
    }

    public boolean roll(){
        countRotation++;
        if(maxRotation < countRotation){
            System.out.println(position + " 타이어가 펑크났습니다");
            return false;
        }
        else{
            System.out.println(position + " 타이어의 수명은 " + (maxRotation-countRotation) + "바퀴입니다");
            return true;
        }
    }
}
public class Atire extends Tire {
    public Atire(int maxRotation, String position) {
        super(maxRotation, position);
    }

    @Override
    public boolean roll(){
        countRotation++;
        if(maxRotation<countRotation){
            System.out.println(position + " 의 A타이어가 펑크났습니다");
            return false;
        }
        else{
            System.out.println(position + " 의 A타이어의 수명은 " + (maxRotation-countRotation) + "바퀴입니다");
            return true;
        }
    }
}

public class Btire extends Tire{
    public Btire(int maxRotation, String position){
        super(maxRotation, position);
    }

    @Override
    public boolean roll(){
        countRotation++;
        if(maxRotation<countRotation){
            System.out.println(position + " 의 B타이어가 펑크났습니다");
            return false;
        }
        else{
            System.out.println(position + " 의 B타이어의 수명은 " + (maxRotation-countRotation) + "바퀴입니다");
            return true;
        }
    }
}
public class Car {
    Tire FR = new Tire(10, "FR");
    Tire FL = new Tire(10, "FL");
    Tire BR = new Tire(5, "BR");
    Tire BL = new Tire(10, "BL");

    void stop(){
        System.out.println("자동차 주행이 종료되었습니다");
    }

    int run(){
        System.out.println("자동차 주행이 시작되었습니다");
        if(FR.roll() == false){            
            stop();
            return 1;
        }
        if(FL.roll() == false){
            stop();
            return 2;
        }
        if(BR.roll() == false){
            stop();
            return 3;
        }
        if(BL.roll() == false){
            stop();
            return 4;
        }
        return 0;
    }
}

 

public class Test {
    public static void main(String[] args){
        Car car = new Car();
        for(int i = 0; i<10; i++) {
            int problemLocation = car.run();

            switch (problemLocation) {
                case 1:
                    System.out.println("FR 타이어를 A타이어로 교체");
                    car.FR = new Atire(6, "FR");        
                    break;
                case 2:
                    System.out.println("FL 타이어를 B타이어로 교체");
                    car.FL = new Btire(3, "FL");
                    break;
                case 3:
                    System.out.println("BR 타이어를 A타이어로 교체");
                    car.BR = new Atire(6, "BR");
                    break;
                case 4:
                    System.out.println("BL 타이어를 B타이어로 교체");
                    car.BL = new Btire(3, "BL");
                    break;
                default:
                    break;
            }
            System.out.println("------------------------------");
        }
    }
}

위 main에서 Car클래스의 객체를 생성함으로써 해당 클래스에 있는 Tire 필드 FR, FL, BR, BL을 사용할 수 있게 되었다. 그리고 switch문에서 해당 필드에 각각 자식클래스의 객체를 저장함으로써 자동타입변환이 일어나게 했다.

이 과정을 제대로 이해했다면 이 질문에 답할 수 있어야한다. FR타이어가 펑크가 난 후, switch문의 case1을 거쳤다고 가정해보자. 그 후에는 for문을 한바퀴 돌아 실행되는 run() 메소드의 첫번째 if문에서 roll() 메소드가 호출될 것이다. 이때 호출되는 roll() 메소드는 부모클래스의 roll()일까 아니면 자식클래스의 오버라이드된 roll()일까?

정답은 오버라이드된 roll()이다. Tire 필드인 FR에 자식클래스의 객체가 저장되었기 때문이다. 즉, FR.roll()은 자식객체.roll()라는 의미이므로 자식 클래스의 오버라이드된 roll()이 실행되야 마땅하다. 


위와 같이 자동타입변환은 필드에 값을 대입할 때 발생하지만, 이보다 더 자주 발생하는 때가 있다. 바로 메소드 호출이다. 아래 부모클래스 School과 자식클래스 Student, Teacher와 부모클래스를 이용하는 Attendance클래스를 살펴보자.

public class School{
    public void attend(){
    	System.out.println("교문을 통과했습니다.");
    }
}
public class Attendance{
    public void check(School sc){
    	sc.attend();
    }
}
public class Student extends School{
     @Override
     public void attend(){
     	System.out.println("학생이 교문을 통과합니다.");
     }
}

public class Teacher extends School{
     @Override
     public void attend(){
     	System.out.println("교사가 교문을 통과합니다.");
     }
}

Attendance 클래스의 check 메소드는 School 타입의 매개변수를 매개로 하여 attend()메소드를 호출한다. 아래 main을 보면, 자동타입변환이 2가지 타입으로 이뤄지고 있다.

public class MainTest{
    public static void main(String[] args){
        School sch = new School();
        Attendance att = new Attendance();
        
        att.check(sch);            -----------(1)
       
        sch = new Student();       -----------(ㄱ)
        att.check(sch);            -----------(2)
        
        Teacher tch = new Teacher();
        att.check(tch);            -----------(3)
    }
}

하나는 이미 언급한 자식클래스의 객체를 부모타입의 필드로 변환, 다른 하나는 이제 알아야할 메소드 매개변수의 타입변환이다.  (1)을 실행하면 출력되는 결과는 '교문을 통과했습니다.' 일 것이다. 그렇다면 (2)와 (3)은 어떨까? 먼저 (2)를 보자. (ㄱ)에 의해 부모 타입의 필드에 자식클래스의 객체가 저장되는 자동타입변환이 일어난다. 따라서 매개변수에는 자식객체가 저장되므로 check함수는 Student클래스의 오버라이드된 attend()메소드가 호출한다. 즉, '학생이 교문을 통과합니다' 가 출력된다. (3)은 부모 클래스 타입을 매개값으로 줘야하는데 자식 클래스의 타입이 왔다. 이때 바로 메소드 매개변수의 자동타입변환이 일어난다. 따라서 출력되는 결과는 '교사가 교문을 통과합니다' 이다.

 

이로써 매개변수의 타입이 클래스라면, 해당 클래스 뿐만 아니라 자식 클래스까지 매개값으로 사용가능하다는 매개변수의 다형성에 대해 알 수 있게 되었다.


자동타입변환을 적용해봤다면, 강제타입변환은 상속과 어떤 관련이 있는지 궁금해진다. 그러나 강제타입변환은 부모타입으로 자동변환된 자식객체를 다시 자식클래스타입으로 변환할때만 사용할 수 있다. 예를 들어 Parent 클래스에는 field1, method2, method3가 있고 Child 클래스에는 field2, method3가 있다고 가정해보자. Parent pa = new Child()를 통해 자동타입변환이 일어나면 필드 pa로는 field2와 method3에 접근할 수 없다. 그럴 때 Child ch = (Child)pa 처럼 다시 강제타입변환을 이용하는 것이다.


이렇게 복잡하게 객체를 다루다보면 부모타입의 필드가 참조하는 객체가 부모클래스인지 자식클래스의 것인지 헷갈린다. 이때 instanceof라는 연산자를 이용하면 쉽게 해결할 수 있다.

boolean result = 객체 instanceof 타입;

간혹 위 조건을 걸지 않고 타입변환을 사용하면 ClassCastException이라는 예외가 발생할 수 도 있다.

 

 

(이 글이 도움이 됐다면 광고 한번씩만 클릭 해주시면 감사드립니다, 더 좋은 정보글 작성하도록 노력하겠습니다 :) )

반응형