JAVA

[JAVA] 상속, 다형성, 추상 클래스

sagecode 2024. 11. 26. 20:16

1. 상속이란?

객체 지향 프로그래밍에서 부모 클래스의 멤버를 자식 클래스에게 물려주는 것을 상속이라고 한다.

상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여준다.

  • 클래스 상속 : 현실에서 상속은 부모가 자식을 선택해서 물려주지만, 프로그램에서는 자식이 부모를 선택합니다.
    • 자식 클래스를 선언할 때 어떤 부모 클래스를 상속받을 것인지 결정하고, 선택된 부모 클래스는 extends 뒤에 기술한다.
    • 자식 클래스는 여러 개의 부모 클래스를 상속할 수 없다.
    • 부모 클래스에서 private를 가진 필드와 메소드는 상속 대상에서 제외된다.
class 자식클래스 extends 부모클래스 {
	...
}
  • 부모 생성자 호출 : Java에서 자식 객체를 생성하면, 부모 객체가 먼저 형성되고 그다음에 자식 객체가 생성된다.
    • 내부적으로 자식 객체를 생성할 때, 부모 객체가 먼저 생성되고 자식 객체가 그 이후에 생성된다.
    • 모든 객체는 클래스의 생성자를 호출해야만 생성된다. 그렇기 위해선 부모 생성자를 호출해야 하는데 부모 생성자는 자식 생성자의 맨 첫 줄에서 호출된다.
class Child extends Parent {
	public Child() {
            super();
        }
}

위 코드와 같이 super()는 부모의 기본 생성자를 호출한다. Parent의 생성자가 선언되지 않더라도 컴파일러에 의해 기본 생성자가 만들어지므로 문제없이 실행된다.

  • 메소드 재정의 : 모든 하위 클래스가 상위 클래스의 메소드를 사용하기에 항상 적합하지 않을 수 있다. 그래서 상속된 일부 메소드는 하위 클래스에서 다시 수정해야 하는데, 이런 경우를 메소드 재정의(Overriding)이라고 한다.
    • 부모의 메소드와 동일한 리턴타입, 메소드 이름, 매개 변수 목록을 가져야 한다.
    • 접근 제한을 더 강하게 재정의 할 수 없다.(ex. 부모 메소드가 default를 가지면 자식 메소드는 private 또는 protected로 가질 수 없다.)
    • 새로운 예외를 throws 할 수 없다.

2. 타입 변환과 다형성

다형성은 사용 방법은 동일하지만 다양한 객체를 이용해서 다양한 실행결과가 나오도록 하는 성질이다. 예를 들어 타이어를 사용하는 방법은 동일하지만 어떤 타이어를 사용하느냐에 따라 성능, 기능, 용도가 달라질 수 있다.

  • 자동 타입 변환 : 프로그램 실행 도중에 자동적으로 타입 변환이 일어나는 것을 말한다.
    • 클래스의 변환은 상속 관계에 있는 클래스 사이에서 발행한다. 자식은 부모타입으로 자동 타입 변환이 가능하다.

Cat 클래스로부터 Cat 객체를 생성하고 이것을 Animal 변수에 대입하면 자동 타입 변환이 일어난다.

Cat cat = new Cat();
Animal animal = cat;

// Animal animal = new Cat(); 도 가능한다.

animal 변수는 Animal 타입이지만, Cat() 객체를 참조한다.

 

  • 필드의 다형성 : 결국 우리는 상속을 통해서 자동타입변환을 이용하는 이유는 다형성을 구현하고 싶기 때문이다. 동일한 부모타입의 객체를 사용하지만 자식타입의 객체를 통해 메소드들을 재정의함으로써 다양한 결과를 내는것이 다형성이다.
class Car {
	Tire frontLeftTire = new Tire();
	Tire frontRightTire = new Tire();
    	Tire backLeftTire = new Tire();
    	Tire backRightTire = new Tire();
    
    // 메소드
    void run() {...}
}

Car 클래스에는 4개의 Tire 필드를 가지고 있다. Car 클래스로부터 Car 객체를 생성하면 4개의 Tire 필드에 각각 하나씩 Tire 객체가 들어가게 된다. 그런데 frontLeftTire와 backRightTire를 HankookTire와 KumhoTire로 교체할 이유가 생겼다.

Car myCar = new Car();

mycar.frontRightTire = new HankookTire();
mycar.backLeftTire = new KumhoTire();
myCar.run();

class HankookTire extends Tire {
...
}

class KumhoTire extends Tire {
...
}

Tire 클래스 타입인 frontRightTire와 backLeftTire는 원래 Tire 객체가 저장되어야 하지만 Tire의 자식 객체가 저장되어도 부모타입으로 자동 타입 변환되기 때문에 문제가 없다. 또한, Car 객체는 Tire 클래스의 선언된 필드와 메소드만 사용하므로 전혀 문제가 되지 않는다. Tire의 필드와 메소드를 자식 클래스들도 갖고 있기 때문이다.

 

  • 강제 타입 변환 : 부모 타입을 자식 타입으로 변환하는 것을 말한다.
    • 부모 클래스에 없고 자식 클래스에는 있는 메소드 또는 필드를 사용할 때 사용한다.
public class Parent {
	public String field1;
    
    public void method1() {
    	system.out.println("parent-method1");
    }
}

public class Child extends Parent {
	public String field2;
    
    public void method2() {
    	system.out.println("parent-method2");
    }
}

public class ClassCastEx {
	public static void main(String[] args) {
    	Parent p1 = new Child(); // 자동 타입 변환

        p1.field1 = "data1"; // 가능
        p1.method1(); // 가능
        
        p1.filed2 = "data2"; // 불가능
        p1.method2(); // 불가능
        
        Child c2 = (Child) p1; // 강제 타입 변환
        
        c2.field2 = "data3"; // 가능
        c2.method2(); / 가능
    }
}

 

  • 객체 타입 확인 : 어떤 객체가 어떤 클래스의 인스턴스인지 확인하기 위해 instanceof 연산자를 사용한다.
boolean result = 좌항(객체) instanceof 우항(타입);

 

좌항의 객체가 우항의 타입으로 생성되었다면 true, 그렇지 않다면 false 값을 리턴한다

 

public void method(Parent parent) {
	if(parent instanceof Child)
    	Child child = (Child) parent;
    }
}

메소드 내에서 강제 타입변환이 필요한 경우 반드시 매개값이 자식 클래스 타입인지 확인한 후 안전하게 강제 타입변환을 해야 한다.

 

3. 추상클래스

객체를 직접 생성할 수 있는 클래스를 실체 클래스라고 한다면 이 클래스들의 공통적인 특성을 추춘해서 선언한 클래스를 추상 클래스라고 한다. 추상 클래스와 실체 클래스는 상속의 관계를 갖고 있으며, 실체 클래스는 추상 클래스의 모든 특성을 물려받고, 추가적인 특성을 가질 수 있다.

 

추상클래스는 공통적인 특성(필드, 메소드)를 미리 선언할 수 있기 때문에 여러개의 세부적인 실체 클래스를 만드는데 시간을 절약하고 관리하는데 편리하다.

  • 추상 클래스 선언 : abstract를 붙여서 선언한다. abstract를 붙일 경우 new 연산자를 통해서 객체를 만들지 못하고 상속을 통해 자식 클래스를 활용해 객체를 생성해야 한다.
public abstract class 부모클래스 {
    // 필드
    // 메소드
    // 생성자
}

 

  • 추상 메소드와 재정의 : 추상 클래스는 실체 클래스가 공통적으로 가져야 할 필드와 메소드들을 미리 정의해둔다. 하지만 메소드의 선언만 통일하고, 실행 내용은 실체 클래스마다 달라야 하는 경우가 있다. 그럴때는 실체 클래스에서 재정의한다.
public abstract class Animal {
	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 class AnimalExample {
	public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        dog.sound(); // 멍멍 출력
        cat.sound(); // 냐옹 출력

        Animal animal = null;
        animal = new Dog(); // 자동 타입 변환
        animal.sound(); // 멍멍 출력
        animal = new Cat(); // 자동 타입 변환
        animal.sound(); // 냐옹 출력

        animalSound(new Dog()); // 멍멍 출력
        animalSound(new Cat()); // 냐옹 출력
    }
    
    public static void animalsound(Animal animal) {
    	animal.sound(); // 재정의된 메소드 호출
    }
}

따라서 자식 클래스에서 재정의 할 경우 추상 클래스에 있는 메소드를 사용할 경우 자식 클래스에 있는 메소드를 호출하게 된다. 이에 따라 메소드의 다형성을 적용할 수 있다.