📝 이것이 자바다

Chapter 07. 상속

j_estory 2022. 12. 15. 19:29

🏷 상속 개념

 

상속은 부모가 자식에게 물려주는 행위를 말한다.

객체 지향 프로그램에서도 부모 클래스의 필드와 메소드를 자식 클래스에게 물려줄 수 있다.

 

상속의 장점 !

상속은 이미 잘 개발된 클래스를 재사용하여 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여 시간을 단축할 수 있다.

 

public class A {
	int field1;
    void method1() { ... }
}

public class B extends A { // A 클래스 상속
	String field1;
    void method2() { ... }
}

// A로부터 물려받은 필드와 메소드
B b = new B();
b.field1 = 10;
b.methdo1(); 

// B가 추가한 필드와 메소드
b.filed2 = "홍길동";
method2();

상속의 또 다른 장점은 수정을 최소화 할 수 있다는 것이다.

  • 부모 클래스를 수정하면 모든 자식 클래스의 수정 효과를 가져온다.

 

🏷 클래스 상속

 

현실에서는 부모가 자식을 선택해서 물려주지만, 

프로그램에서는 자식이 부모를 선택하게 된다.

자식 클래스를 선언할 때 어떤 부모로부터 상속받을 것인지 결정하고 extends 키워드를 통해 부모 클래스를 명시한다.

 

또한, 다른 언어와 달리 자바는 다중 상속을 허용하지 않는다. 

 

 

🏷 부모 생성자 호출

 

 

자식 객체를 생성하면 부모 객체가 먼저 생성된 다음에 자식 객체가 생성된다. 

자식 클래스 변수 = new 자식 클래스();

위의 코드를 보면 자식 객체만 생성된 것으로 보이지만, 사실은 부모 객체가 먼저 생성되고 자식 객체가 생성된 것이다.

 

모든 객체는 생성자를 호출해야만 생성되는데 부모 객체의 생성자는 어디서 어떻게 호출된 것일까?

이 비밀은 !! 자식 생성자에 숨어 있다. 

부모 생성자는 자식 생성자의 맨 첫줄에 숨겨져 있는 super()에 의해 호출된다.

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

super()는 컴파일 과정에서 자동 추가되는데, 이것은 부모의 기본 생성자를 호출하게 된다. 

만약 부모 클래스에 기본 생성자가 없다면 자식 생성자 선언에서 컴파일 에러가 발생된다.

 

부모 클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있다면 개발자는 그에 맞게 매개변수를 추가한 super(매개값 ... ) 넣어야 한다. 

 

 

🏷 메소드 재정의

 

부모 클래스의 모든 메소드가 자식 클래스에 맞게 설계되어 있다면 가장 이상적인 상속이지만, 자식 클래스가 사용하기에 적합하지 않을 수 있다.

이러한 메소드는 자식 클래스에서 재정의해서 사용해야 한다.

이것을 메소드 오버라이딩 이라고 한다.

 

✔️ 메소드 오버라이딩

 

상속된 메소드를 자식 클래스에서 재정의하는 것을 의미한다.

메소드가 오버라이딩 되었다면 해당 부모 메소드는 숨겨지고, 자식 메소드가 우선적으로 사용된다.

 

- 메소드 오버라이딩 시 주의점

  • 부모 메소드의 선언부(리턴 타입, 메소드 이름, 매개변수)와 동일해야 한다.
  • 접근 제한자를 더 강하게 오버라이딩 할 수는 없다. 
  • 새로운 예외를 throws 할 수 없다.

자바는 개발자의 실수를 줄여주기 위해서 정확히 오버라이딩이 되었는지 체크해주는 @Override 어노테이션을 제공해준다.

 

✔️ 부모 메소드 호출

 

메소드를 재정의하면, 부모 네소드는 숨겨지고 자식 메소드만 사용되기 때문에 비록 부모 메소드의 일부만 변경된다 하더라도,
중복된 내용을 자식 메소드도 가지고 있어야 한다.

 

예를들어, 부모 메소드가 100줄의 코드를 가지고 있을 경우, 자식 메소드에서 1줄만 추가하고 싶더라도
100줄의 코드를 자식 메소드에서 다시 작성해야 한다.

 

이 문제는 !! 공동 작업 처리 기법을 이용하면 해결된다.

// 부모 메소드
class Parent {
	public void method() {
    	// 작업 처리1
    }
}

// 자식 메소드
class Child extends Parent {
	@Override
    void method() {
    	super.method();
        // 작업 처리2
    }
}

자식 메소드 내에서 부모 메소드를 호출하는 것인데,

위와 같이 super 키워드와 도트(.) 연산자를 사용하면 숨겨진 부모 메소드를 호출할 수 있다.

 

super.method()의 위치는 작업 처리2 전,후 어디든 올 수 있다.

이 방법은 부모 메소드를 재사용함으로써 자식 메소드의 중복 작업을 없애는 효과를 가져온다.

 

 

🏷 final 클래스와 final 메소드

 

✔️ final 클래스

 

클래스를 선언할 때 final 키워드를 class 앞에 붙이게 되면 최종적인 클래스이므로 더이상 상속할 수 없는 클래스가 된다.

즉, final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다.

 

대표적인 예가 String 클래스이다. 

public final class String { ... }

따라서 String 클래스는 자식 클래스를 만들 수 없다. 

 

✔️ final 메소드

 

메소드 선언 시, final 키워드를 붙이게 되면 이 메소드는 최종적인 메소드이므로, 

오버라이딩 할 수 없는 메소드가 된다.

 

즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때, 부모 클래스에 선언된 final 메소드는 재정의 할 수 없다.

 

🏷 protected 접근 제한자

 

protected는 상속과 관련이 있고, public과 default의 중간쯤에 해당하는 접근 제한을 한다. 

 

protected는 같은 패키지에서는 default 처럼 접근이 가능하나, 

다른 패키지에서는 자식 클래스만 접근을 허용한다.

 

protected는 필드, 생성자, 메소드 선언에 사용될 수 있다.

public class A {
    protected String field;
    
    protected A() {}
    
    protected void method() {}
}


// 클래스 A와 다른 패키지에 있는 클래스 D

public class D extends A {
	public D() {
    	super();
    }
    
    public void method1() {
    	this.field = "value";
        this.method();
    }
    
    public void method2() {
        // method1() 처럼 상속을 통해서만 사용이 가능하고 직접 객체 생성으로는 사용하면 안됨!!
    	A a = new A();
        a.field = "value";
        a.method()
    }
}

 

🏷 타입 변환

 

기본 타입 변환에 대해서는 앞장에서 학습한 바가 있다.

클래스도 마찬가지로 타입 변환이 있는데, 클래스의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생한다.

 

✔️ 자동 타입 변환

 

자동적으로 타입 변환이 일어나는 것을 말한다.

자동 타입 변환은 다음과 같은 조건에서 일어난다.

부모타입 변수 = 자식 타입 객체;

 

자식은 부모의 특징과 기능을 상속받기 때문에 부모와 동일하게 취급될 수 있다. 

또한 바로 위의 부모가 아니더라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있다.

 

부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다.

비록 변수는 자식 객체를 참조하지면 변수로 접근 가능한 멤버는 부모 클래스 멤버로 한정된다.

 

그러나, 자식 클래스에서 오버라이딩된 메소드가 있다면 부모 메소드 대신 오버라이딩된 메소드가 호출된다.

이것은 다형성과 관련 있기 때문에 잘 알아두어야 한다.

 

✔️ 강제 타입 변환

 

자식 타입은 부모 타입으로 자동 변환되지만, 반대로 부모 타입은 자식 타입으로 자동 변환되지 않는다.

대신, 다음과 같이 캐스팅 연산자로 강제 타입 변환을 할 수 있다.

자식타입 변수 = (자식 타입) 부모 타입 객체;

그렇다고 해서 부모 타입 객체를 자식 타입으로 무조건 강제 변환할 수 있는 것은 아니다.

자식 객체가 부모 타입으로 자동 변환된 후 다시 자식 타입으로 변환될 때 강제 타입 변환을 사용할 수 있다.

 

어떤 사항일 때 사용될까?

자식 객체가 부모 타입으로 자동 변환하면 부모 타입에 선언된 필드와 메소드만 사용 가능한데, 

다시 자식 타입에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해서 다시 자식 타입으로 변환해야 한다.

 

🏷 다형성

 

다형성이란 사용방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말한다.

 

예시.

한국 타이어와 금호 타이어는 모두 타이어를 상속하고 있다.

그렇다면 한국 타이어와 금호 타이어는 타이어의 메소드를 동일하게 가지고 있다고 말할 수 있다.

만약, 한국 타이어와 금호 타이어가 타이어의 메소드를 오버라이딩 하고 있다면,

타이어 메소드 호출 시 오버라이딩 된 메소드가 호출된다.

오버라이딩된 내용은 두 타이어가 다르기 때문에 실행 결과가 다르게 나온다.

이것이 바로 다형성이다!!

 

다형성을 구현하기 위해서는 자동 타입 변환과 메소드 재정의가 필요하다.

한국 타이어와 금호 타이어는 타이어 타입으로 자동 타입 변환이 될 수 있고,

타이어의 메소드를 재정의하고 있다.

자동 타입 변환 + 메소드 오버라이딩 -> 다형성

 

✔️ 필드 다형성

 

필드 다형성은 필드 타입은 동일하지만, 대입되는 객체가 달라져서 실행 결과가 다양하게 나올 수 있는것을 말한다.

public class Tire {
	public void roll() {
    	System.out.println("회전합니다.");
    }
}

public class HankookTire extends Tire {
	@Override
    public void roll() {
    	System.out.println("한국 타이어가 회전합니다.");
    }
}


public class KumhoTire extends Tire {
	@Override
    public void roll() {
    	System.out.println("금호 타이어가 회전합니다.");
    }
}

public class Car {
	public Tire tire;
    
    public void run() {
    	tire.roll();
    }
}

public class CarExample {
	public static void main(String[] args) {
    	Car myCar = new Car();
        
        myCar.tire = new Tire();
        myCar.run();
        
        myCar.tire = new HankookTire();
        myCar.run();
        
        myCar.tire = new KumhoTire();
        myCar.run();
    }
}

Car 클래스에는 Tire 필드가 선언되어 있다.

먼저 Car 객체를 생성한 후 타이어를 장착하기 위해서는 다음과 같이 HankookTire 또는 KumhoTire 객체를 Tire 필드에 대입할 수 있다.

왜냐하면 자동 타입 변환이 되기 때문이다. 

 

Car 클래스의 run() 메소드는 tire 필드에 대입된 객체의 roll() 메소드를 호출한다.

만약 HankookTire와 KumhoTire가 roll() 메소드를 재정의하고 있다면,

재정의된 메소드가 호출된다.

 

따라서! 어떤 타이어를 장착했는지에 따라 roll() 메소드의 실행 결과는 달라지게 된다.

이것이 바로 필드의 다형성이다.

 

✔️ 매개변수 다형성

 

다형성은 필보다는 메소드를 호출할 때 많이 발생한다.

메소드가 클래스 타입의 매개변수를 가지고 있을 경우, 호출할 때 동일한 타입의 객체를 제공하는 것이 정석이지만
자식 객체를 제공할 수도 있다. 

여기서 다형성이 발생한다. 

Vehicle 클래스와 Bus 클래스는 상속관계의 클래스로 생성해두었다.

 

즉, dirve() 메소드는 매개변수 vehicle이 참조한느 객체의 run() 메소드를 호출하는데, 

자식 객체가 run() 메소드를 재정의하고 있다면 재정의된 run() 메소드가 호출된다. 

 

그러므로 !! 어떤 자식 객체가 제공되느냐에 따라서 drive()의 실행 결과는 달라진다.

이것이 매개변수의 다형성이다.

 

🏷 객체 타입 확인

 

매개변수의 다형성에서 실제로 어떤 객체가 매개값으로 제공되었는지 확인하는 방법이다. 

꼭 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인하고자 할 때, instanceof 연산자를 사용한다.

boolean result = 객체 instanceof 타입;

instanceof 연산자의 좌항에는 객체가 오고 우항에는 타입이 오는데,

좌항의 객체가 우항의 타입이면 true 그렇지 않으면 false를 산출한다.

 

예제 코딩 ↓

public class Person {
    public String name;

    // 생성자 선언
    public Person(String name) {
        this.name = name;
    }

    // 메소드 선언
    public void walk() {
        System.out.println("걷습니다.");
    }
}

public class Student extends Person{

    public int studentNo;

    public Student(String name, int studentNo) {
        super(name);
        this.studentNo = studentNo;
    }

    public void study() {
        System.out.println("공부를 합니다.");
    }

}

public void instanceofExample(Person person) {
        System.out.println("name: " + person.name);
        person.walk();

        if (person instanceof Student) {
            // Student 객체일 경우 강제 타입 변환
            Student student = (Student) person;
            // Student 객체만 가지고 있는 필드 및 메소드 사용
            System.out.println("studentNo: " + student.studentNo);
            student.study();
        }
}

@Test
public void instanceOf_테스트() {
    Person p1 = new Person("홍길동");
    exService.instanceofExample(p1);

    Person p2 = new Student("김길동", 10);
    exService.instanceofExample(p2);
}

// 결과
/**
name: 홍길동
걷습니다.
name: 김길동
걷습니다.
studentNo: 10
공부를 합니다.
**/

 

🏷 추상 클래스

 

사전적인 의미로 추상은 실체 간에 공통되는 특성을 추출한 것을 말한다. 

 

✔️ 추상 클래스란 ?

 

객체를 생성할 수 있는 클래스를 실체 클래스라고 하면, 

이 클래스들의 공통적인 필드나 메소드를 추출해서 선언한 클래스를 추상 클래스라고 한다.

 

추상 클래스는 실체 클래스의 부모 역할을 한다. 

따라서, 실체 클래스는 추상 클래스를 상속해서 공통적인 필드나 메소드를 물려받을 수 있다. 

 

추상 클래스는 실체 클래스의 공통되는 필드와 메소드를 추출해서 만들었기 때문에 

new 연산자를 사용해서 객체를 직접 생성할 수 없다.

Animal animal = new Animal(); // XXX

class Fish extends Animal {

}

추상 클래스는 새로운 실체 클래스를 만들기 위한 부모 클래스로만 사용된다.

즉, 추상 클래스는 extends 뒤에만 올 수 있다.

 

✔️ 추상 클래스 선언

 

클래스 선언에 abstract 키워드를 붙이면 추상 클래스 선언이 된다.

추상 클래스는 new 연산자를 이용해서 객체를 직접 만들지는 못하고 상속을 통해 자식 클래스만 만들 수 있다.

public abstract class 클래스명 {
    // 필드
    // 생성자
    // 메소드
}

추상 클래스도 필드, 메소드를 선언할 수 있다. 

그리고 자식 객체가 생성될 때 super()로 추상 클래스의 생성자가 호출되기 때문에 생성자도 반드시 있어야 한다.

 

/**
 * 전화기의 공통 필드 메소드만 뽑아내어 추상 클래스로 선언
 */
public abstract class Phone {
    // 필드 선언
    String owner;

    // 생성자 선언
    public Phone(String owner) {
        this.owner = owner;
    }

    // 메소드 선언
    void trunOn() {
        System.out.println("폰 전원을 켭니다.");
    }

    void trunOff() {
        System.out.println("폰 전원을 끕니다. ");
    }
}



public class SmartPhone extends Phone{

    // 생성자 선언
    public SmartPhone(String owner) {
        // phone의 생성자 호출
        super(owner);
    }

    // 메소드 선언
    void internetSearch() {
        System.out.println("인터넷 검색을 합니다.");
    }
}


@Test
    public void 추상클래스_테스트() {
        SmartPhone smartPhone = new SmartPhone("홍길동");
        smartPhone.trunOn();
        smartPhone.internetSearch();
        smartPhone.trunOff();
    }

Phone 객체는 new 연산자로 직접 생성할 수는 없지만, 자식 객체인 SmartPhone은 new 연산자로 객체 생성이 가능하고,

phone으로 부터 물려받은 trunOn()과 turnOff() 메소드를 호출할 수 있다.

 

✔️ 추상 메소드와 재정의

 

자식 클래스들이 가지고 있는 공통 메소드를 뽑아내어 추상 클래스로 작성할 때,

메소드 선언부만 동일하고 실행 내용은 자식 클래스마다 달라야 하는 경우가 많다. 

 

이런 경우 추상 메소드를 선언할 수 있는데

일반 메소드와의 차이점은 abstract 키워드가 붙고, 메소드 실행 내용인 중괄호 {} 가 없다.

abstract 리턴타입 메소드명(매개변수, ...);

 

추상 메소드는 자식 클래스의 공통 메소드라는 것만 정의할 뿐, 실행 내용은 가지지 않는다.

또한, 자식 클래스에서 반드시 재정의해서 실행 내용을 채워야 한다. 

 

ex.

public abstract class Animal {
    public void breathe() {
        System.out.println("숨을 쉽니다.");
    }

    public abstract void sound();
}

public class Dog extends Animal{
    // 추상 메소드 재정의
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

public class Cat extends Animal{
    // 추상 메소드 재정의
    @Override
    public void sound() {
        System.out.println("야옹");
    }
}

public void abstractMethod(Animal animal) {
        animal.sound();
}

@Test
public void 추상메소드_테스트() {
    Dog dog = new Dog();
    dog.sound();

    Cat cat = new Cat();
    cat.sound();

    // 자동 타입 변환도 가능
    exService.abstractMethod(new Dog());
    exService.abstractMethod(new Cat());
}

 

🏷 봉인된 클래스

 

기본적으로 final 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 

그러나 java 15 부터는 무분별한 자식 클래스 생성을 방지하기 위해서 봉인된 클래스가 도입되었다.

 

public sealed class Person permits Employee, Manager {
}

이런식으로 sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 클래스를 지정해야 한다.

 

자식 클래스인 Employee, Manger 클래스는 어떻게 정의하면 될까 ?

//final은 더이상 상속할 수 없다는 뜻이다.
public final class Employee extends Person {...}

// non-sealed는 봉인을 해제한다는 뜻이다.
public non-sealed class Manager extends Person {...}

// 따라서 Employee는 더이상 자식 클래스를 만들 수 없지만 Manager는 다음과 같이 자식 클래스를 만들 수 있다.
public class Director extends Manager {...}