Back-End (Web)/JAVA

[JAVA] 클래스 간의 관계와 상속

JABHACK 2024. 11. 12. 17:44

상속

📌 상속의 사전적 정의는 부모가 자식에게 물려주는 행위를 말합니다.

  • 객체 지향 프로그램에서도 부모 클래스의 필드와 메서드를 자식 클래스에게 물려줄 수 있습니다.
  • 상속을 사용하면 적은 양의 코드로 새로운 클래스를 작성할 수도 있고 공통적인 코드를 관리하여 코드의 추가와 변경이 쉬워질 수도 있습니다.
  • 이러한 특성 때문에 상속을 사용하면 코드의 중복이 제거되고 재사용성이 크게 증가하여 생산성과 유지 보수성에 매우 유리해집니다. 

= 자바 객체 지향의 중요한 기능 중 하나로, 한번 변수나 메서드 선언하고 그걸 다른 이들이 빌려쓰는 형태로 만들어, 매번 변수, 메서드를 반복해서 선언하는 것을 방지한 방식이다.

 

https://wonkang.tistory.com/88

 

  • 위의 그림처럼 개발자, 댄서, 가수 모두 인간의 특징인 이름,나이,학습,일등의 특징을 가질 수 밖에 없다. 이 인간으로서의 특징을 개발자, 댄서, 가수에 각각 변수로 선언하면 같은 내용이 3배가 될 뿐이니, 상위 클래스(부모)를 생성하여 부모에서 인간의 특징을 정하고 다른 하위 클래스(자식)에게 정보를 공유하는게 효과적이다.

 

public class 자식클래스 extends 부모클래스 {

}

 

  • 상속은 위와 같이 선언하게 되는데, 잘 보면 extends라는 키워드가 존재한다.
  • 말 그대로 확장이란 의미인데, 상속이 이 확장의 개념과 동일하다

 

부모의 확장형 자식

 

  1. 부모 클래스에 새로운 필드와 메서드가 추가되면 자식 클래스는 이를 상속받아 사용할 수 있다.
  2. 자식 클래스에 새로운 필드와 메서드가 추가되어도 부모 클래스는 어떠한 영향도 받지 않는다.
  3. 따라서 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.

‘부모가 자식보다 큰 사람이니까 부모 클래스도 마찬가지로 자식 클래스 보다 큰 범위겠지?’ 라고 생각하면 망한다. 여기서는 '청출어람'이라는 느낌으로 자식이 부모보다 더 많은 데이터를 가지고 있다. 단 위의 설명처럼 자식은 부모에게 영향을 주지 못하지만 부모는 자식에게 영향을 줄 수 있다.

 

< 1980년대 일반적인 가정을 생각하면 편하다 >

  • 1. 자식이 인터넷 세상에 태어나면서 자연스럽게 부모보다 많은 데이터를 가지게 되었다.
  • 2. 보통 부모님이 자식에게 끼치는 영향이 크다.
  • 3. 부모의 수보다 자식 수가 보통 많다.

 

<예제>

public class Car {

    String company; // 자동차 회사
    private String model; // 자동차 모델
    private String color; // 자동차 색상
    private double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public char changeGear(char type) {
        gear = type;
        return gear;
    }

    public boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    public void horn() {
        System.out.println("빵빵");
    }

}

public class SportsCar extends Car{
    String engine;
    public void booster() {
        System.out.println("엔진 " + engine + " 부앙~\n");
    }
}


public class Main {
    public static void main(String[] args) {
        // 부모 클래스 객체에서 자식 클래스 멤버 사용
        Car car = new Car();
        // car.engine = "Orion"; // 오류
        // car.booster(); // 오류

        // 자식 클래스 객체 생성
        SportsCar sportsCar = new SportsCar();
        sportsCar.engine = "Orion";
        sportsCar.booster();

        // 자식 클래스 객체에서 부모 클래스 멤버 사용
        sportsCar.company = "GENESIS";
        sportsCar.setModel("GV80");
        System.out.println("sportsCar.company = " + sportsCar.company);
        System.out.println("sportsCar.getModel() = " + sportsCar.getModel());
        System.out.println();

        sportsCar.horn();
        System.out.println(sportsCar.changeGear('D'));
    }
}

 

 

클래스 간의 관계

📌 클래스 간의 관계를 분석하여 관계 설정을 해줄 수 있다.

  • 상속관계 : is - a (”~은 ~(이)다”)
  • 포함관계 : has - a (”~은 ~을(를) 가지고 있다”) 
상속관계  포함관계
  • 상속관계 : 고래는 포유류다 👍
  • 포함관계 : 고래는 포유류를 가지고 있다…? 🤔
  • 자동차는 타이어를 가지고 있다. 👍
  • 자동차는 차 문을 가지고 있다. 👍
  • 자동차는 핸들을 가지고 있다. 👍

1. 포함(Composition)

포함은 "has-a" 관계를 표현합니다.
한 클래스가 다른 클래스를 속성(멤버 변수)으로 가지는 형태입니다.

특징

  • 유연성: 포함된 객체는 독립적이며, 필요에 따라 다른 객체로 교체할 수 있습니다.
  • 코드 재사용: 여러 클래스에서 포함된 클래스를 재사용할 수 있습니다.
  • 의존성 낮음: 포함된 객체의 내부 구현은 외부 클래스에 영향을 주지 않습니다.
class Circle{
	int x;
    int y;
    int r;
}

----------------------------

class Circle{
	Poiont c = new Point();
    int r;
}

class Point {
	int x;
    int y;
}

2. 상속(Inheritance)

상속은 "is-a" 관계를 표현합니다.
하위 클래스(Subclass)가 상위 클래스(Superclass)의 속성과 메서드물려받는 구조입니다.

특징

  • 코드 재사용: 공통 기능을 상위 클래스에서 정의하고, 하위 클래스에서 상속받아 사용.
  • 유연성 낮음: 상속받은 클래스는 상위 클래스와 강한 결합을 가지며, 부모 클래스의 변경이 하위 클래스에 영향을 줌.
  • 다형성 지원: 부모 타입의 참조로 자식 객체를 사용할 수 있음.

 

3. 포함 vs 상속의 차이점

구분 포함(Composition) 상속(Inheritance)
관계 "has-a" 관계 "is-a" 관계
결합도 낮음 (유연성 높음) 높음 (부모 클래스에 강하게 의존)
코드 재사용성 객체를 포함하여 재사용 가능 공통된 동작을 부모 클래스에서 정의
확장성 포함된 객체를 교체하여 기능 확장 가능 부모 클래스 설계 변경 시 제한적
다형성 지원 다형성 제공하지 않음 다형성(Polymorphism) 지원
목적 객체의 조립, 독립적인 관계 유지 객체의 계층 구조 정의, 공통 기능 상속

 

4. 포함과 상속의 사용 시점

포함(Composition)이 적합한 경우:

  1. 두 클래스가 명확한 "has-a" 관계일 때.
  2. 한 클래스가 다른 클래스의 특정 기능만 필요할 때.
  3. 코드 재사용과 독립성을 유지하면서 기능 확장이 필요할 때.
  4. 설계 변경 가능성이 높아 유연성이 중요한 경우.

상속(Inheritance)이 적합한 경우:

  1. 두 클래스가 명확한 "is-a" 관계일 때.
  2. 하위 클래스가 상위 클래스의 대체 가능성을 가져야 할 때(다형성).
  3. 공통 기능을 상위 클래스에 정의하고, 하위 클래스에서 추가적으로 확장할 때.

5. 실무에서의 선호

  • 포함이 더 자주 사용됩니다.
    이유: 유연성, 낮은 결합도, 유지보수 용이성 때문.
  • 상속은 사용이 간단하지만, 잘못된 설계로 인해 강한 결합이나 불필요한 상속 계층 구조를 초래할 수 있습니다.
    따라서, 상속보다는 포함을 우선적으로 고려하고, 정말 필요한 경우에만 상속을 사용합니다.

 

단일 상속과 다중 상속

📌 Java는 클래스의 다중 상속을 허용하지 않습니다…

  • 다중 상속을 허용하면 복잡한 소프트웨어의 기능을 구현할 때 여러 개의 클래스를 상속받아 쉽게 구현할 수 있다는 장점이 있는데.. 왜? 허용하지 않을까요?
  • 왜냐하면 다중 상속을 허용하면 클래스 간의 관계가 복잡해지는 문제가 생기기 때문입니다.
  • 만약 자식 클래스에서 상속받는 서로 다른 부모 클래스들이 같은 이름의 멤버를 가지고 있다면?
  • 자식 클래스에서는 이 멤버를 구별할 수 있는 방법이 없다는 문제가 생깁니다. 

= 자식이 부모를 여럿 골라도 되는데(?), 부모 입장에서 자식 2명이 이름이 같으면 구분을 못한다.

 

 

final 클래스와 final 메서드

📌 클래스에 final 키워드를 지정하여 선언하면 최종적인 클래스가 됨으로 더 이상 상속할 수 없는 클래스가 됩니다.

public class Car {
    public final void horn() {
        System.out.println("빵빵");
    }
}

...

public class SportsCar extends Car{
    public void horn() { // 오류가 발생합니다.
        super.horn();
    }
}

 

  • final은 마치 채권처럼 모든 것을 처음 상태로 동결한다. 채권이 구매 시의 세금,가격,이자로 모든 변수를 상수로 지정해버리는 것과 같다 = 절대로 무엇도 바꿀 수 없다. 그저 그 모양그대로 가져와서 써야한다

 

Object

📌 Object는 말 그대로 “객체”를 의미하는 단어이며 보통, Object 클래스를 의미합니다.

  • Object 클래스는 Java 내 모든 클래스들의 최상위 부모 클래스입니다.
  • 따라서, 모든 클래스는 Object의 메서드를 사용할 수 있습니다.
  • 또한 부모 클래스가 없는 자식 클래스는 컴파일러에 의해 자동으로 Object 클래스를 상속받게 됩니다.
  • Object 클래스는 프로그래머가 만드는 클래스가 아니라 자체적으로 제공되는 최상위 클래스이다.

object 클래스의 메서드

 

 


 

 

오버라이딩

 🐳 부모 클래스로부터 상속받은 메서드의 내용을 재정의 하는 것을 오버라이딩이라고 합니다.

  • 부모 클래스의 메서드를 그대로 사용 가능하지만 자식 클래스의 상황에 맞게 변경을 해야 하는 경우 오버라이딩을 사용합니다.
  • 오버라이딩을 하기 위해서는 아래 조건들을 만족해야 합니다.
  1. 선언부가 부모 클래스의 메서드와 일치해야 합니다.
  2. 접근 제어자를 부모 클래스의 메서드 보다 좁은 범위로 변경할 수 없습니다.
  3. 예외는 부모 클래스의 메서드 보다 많이 선언할 수 없습니다
public class Car {

    String company; // 자동차 회사
    private String model; // 자동차 모델
    private String color; // 자동차 색상
    private double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public char changeGear(char type) {
        gear = type;
        return gear;
    }

    public boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    public void horn() {
        System.out.println("빵빵");
    }

}

public class SportsCar extends Car{
    String engine;
    public void booster() {
        System.out.println("엔진 " + engine + " 부앙~\n");
    }

    public SportsCar(String engine) {
        this.engine = engine;
    }

    @Override
    public double brakePedal() {
        speed = 100;
        System.out.println("스포츠카에 브레이크란 없다");
        return speed;
    }

    @Override
    public void horn() {
        booster();
    }
}

public class Main {
    public static void main(String[] args) {
        // 부모 클래스 자동차 객체 생성
        Car car = new Car();
        car.horn(); // 경적

        System.out.println();
        // 자식 클래스 스포츠카 객체 생성
        SportsCar sportsCar = new SportsCar("Orion");

        // 오버라이딩한 brakePedal(), horn() 메서드 호출
        sportsCar.brakePedal();
        sportsCar.horn();

    }
}
  • 오버라이딩에서 자식 클래스에서 부모 클래스의 메서드를 바꾼다고 해서 부모 클래스의 메서드가 바뀌는 것은 아닙니다.
  • 자식 클래스에서 그 메서드를 새로운 방식으로 재정의하는 것입니다.  =  "엄마"를 자식이 "착한 엄마"라고 자신이 원하는(필요한) 대로 재해석한다는 말. 부모는 "엄마"라는건 바뀌지 않지만 자식에 한해서는 "착한 엄마"가 됨
  • 그래서 아래와 같은 답이 나온다.
더보기

빵빵

 

스포츠카에 브레이크란 없다

엔진 Orion 부앙~

 

  • 이름 비슷하지만 엄연히 오버로드와 오버라이딩은 다른 개념이다.
오버로드 같은 메서드 이름을 다른 매개변수를 사용하여 여러 번 정의하는 것입니다. 반환 타입은 동일하거나 달라질 수 있습니다.
오버라이딩 상속 관계에서 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것입니다. 메서드 이름, 매개변수, 반환 타입이 부모 클래스와 정확히 일치해야 합니다.

 

 

 

super 와 super()

 🐳 super는 부모 클래스의 멤버를 참조할 수 있는 키워드입니다.

  • 객체 내부 생성자 및 메서드에서 부모 클래스의 멤버에 접근하기 위해 사용될 수 있습니다.
  • 자식 클래스 내부에서 선언한 멤버와 부모 클래스에서 상속받은 멤버와 이름이 같을 경우 이를 구분하기 위해 사용됩니다.
public void setCarInfo(String model, String color, double price) {
    super.model = model; // model은 부모 필드에 set
    super.color = color; // color는 부모 필드에 set
    this.price = price; // price는 자식 필드에 set
}
  • 말 그대로 자식 클래스에서 부모 클래스의 맴버에 접근하기 위해 필요한 기능
  • super = 부모 , this = 만들어질 객체
  • model, color는 부모 클래스의 매개변수가 바뀐다. price는 생성될 객체의 price값이 바뀐다.

 

🐳 super()는 부모 클래스의 생성자를 호출할 수 있는 키워드입니다.

  • 객체 내부 생성자 및 메서드에서 해당 객체의 부모 클래스의 생성자를 호출하기 위해 사용될 수 있습니다.
  • 자식 클래스의 객체가 생성될 때 부모 클래스들이 모두 합쳐져서 하나의 인스턴스가 생성됩니다.
  • 이때 부모 클래스의 멤버들의 초기화 작업이 먼저 수행이 되어야 합니다.
    • 따라서 자식 클래스의 생성자에서는 부모 클래스의 생성자가 호출됩니다.
    • 또한 부모 클래스의 생성자는 가장 첫 줄에서 호출이 되어야 합니다.
// 자식 클래스 SportsCar 생성자
public SportsCar(String model, String color, double price, String engine) {
     // this.engine = engine; // 오류 발생
    super(model, color, price);
    this.engine = engine;
}
  • 자식 클래스 객체를 생성할 때 생성자 매개변수에 매개값을 받아와 super(…)를 사용해 부모 생성자의 매개변수에 매개값을 전달하여 호출하면서 부모 클래스의 멤버를 먼저 초기화합니다.
  • 오버로딩된 부모 클래스의 생성자가 없다고 하더라도 부모 클래스의 기본 생성자를 호출해야 합니다.
    • 따라서 눈에 보이지는 않지만 컴파일러가 super();를 자식 클래스 생성자 첫 줄에 자동으로 추가해 줍니다.
  • 간단히 말해, 자식 클래스에서 객체를 생성하면
  • 위 처럼 자식이 부모 클래스의 필드, 메서드를 포함함으로 자식의 객체에도 부모의 필드, 메서드가 포함되어야한다.
  • 당연히 객체가 새로 생성되는 만큼 부모 필드도 초기화를 해줘야하는데
  • 위의 코드에서 super(model, color, price);로 초기화를 따로 해줬지만, 안써도 자동으로 컴파일러가 초기화 해준다는 말
  • 쉽게 말하면, 자식 클래스라는 붕어빵 틀(자식 클래스) 안에 부모 클래스 호두과자 틀(부모 클래스)이 또 있다면 당연히 붕어빵을 만들기 전에 붕어빵 틀만이 아니라 호두과자 틀도 청소해야한다.(초기화)

 


 

 

참조 변수의 타입 변환

자동 타입 변환

🧩 부모 타입 변수 = 자식 타입 객체; 는 자동으로 부모 타입으로 변환이 일어납니다.

  • 자식 객체는 부모 객체의 멤버를 상속받기 때문에 부모와 동일하게 취급될 수 있습니다.
    • 예를 들어 포유류 클래스를 상속받은 고래 클래스가 있다면 포유류 고래 = 고래 객체; 가 성립될 수 있습니다.
    • 왜냐하면 고래 객체는 포유류의 특징인 모유 수유 행위를 가지고 있기 때문입니다.
    • 다만 주의할 점은 부모 타입 변수로 자식 객체의 멤버에 접근할 때는 부모 클래스에 선언된 즉, 상속받은 멤버만 접근할 수 있습니다. 
class Mammal {
    // 포유류는 새끼를 낳고 모유수유를 한다.
    public void feeding() {
        System.out.println("모유수유를 합니다.");
    }
}

class Whale extends Mammal {
    // 고래는 포유류 이면서 바다에 살며 수영이 가능하다.
    public void swimming() {
        System.out.println("수영하다.");
    }

    @Override
    public void feeding() {
        System.out.println("고래는 모유수유를 합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        // 고래는 포유류이기 때문에 포유류 타입으로 변환될 수 있습니다.
        Mammal mammal = new Whale();

        // 하지만 포유류 전부가 바다에 살고 수영을 할 수 있는 것은 아니기 때문에
        // 수영 하다 메서드는 실행 불가
        // 즉, 부모 클래스에 swimming이 선언되어있지 않아서 사용 불가능합니다.
        // mammal.swimming(); // 오류 발생

        // 반대로 모든 포유류가 전부 고래 처럼 수영이 가능한 것이 아니기 때문에 타입변환이 불가능합니다.
        // 즉, 부모타입의 객체는 자식타입의 변수로 변환될 수 없습니다.
        // Whale whale = new Mammal(); // 오류 발생

        mammal.feeding();
    }
}

 

  • Mammal mammal = new Whale();은 자식 클래스인 Whale의 객체를 만들었지만, 이를 부모 클래스 타입 변수인 mammal에 할당했다. 
  • 원래 안될 것 같은 이 행위가 되는 이유는, 자식이 부모의 일부분의 데이터를 공유하기 때문이다.
  • 즉 위 처럼 부모 클래스 Mammal이 가지고 있는 메서드나 필드가 아닌 요청은 오류가 나고 나머지는 가능한것

 

강제 타입 변환

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

  • 부모 타입 객체는 자식 타입 변수로 자동으로 타입 변환되지 않습니다.
  • 이럴 때는 (자식 타입) 즉, 타입 변환 연산자를 사용하여 강제로 자식 타입으로 변환할 수 있습니다.
class Animal {
    void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Woof Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();  // 부모 타입 변수에 자식 타입 객체 할당
        animal.makeSound();  // Animal 클래스의 메서드 호출

        // 강제 타입 변환
        Dog dog = (Dog) animal;  // Animal 타입을 Dog 타입으로 강제 변환
        dog.bark();  // Dog 클래스의 메서드 호출
    }
}
더보기

설명:

  • Animal animal = new Dog();에서 자식 객체인 Dog를 부모 타입인 Animal 변수에 할당합니다.
  • animal.makeSound();는 부모 클래스인 Animal의 메서드를 호출합니다.
  • 그 후, animal 변수는 Dog 객체를 참조하고 있기 때문에, 강제 타입 변환을 통해 Animal 타입 변수 animal을 Dog 타입 변수 dog로 변환합니다.
  • 강제 타입 변환 (Dog) animal을 통해 이제 dog 변수는 Dog 클래스에서 정의한 bark() 메서드를 호출할 수 있게 됩니다.
  • "Woof Woof!"가 결과로 나옵니다.
  • 다만 무조건 강제 타입 변환을 할 수 있는 것은 아닙니다.
    • 자식 타입 객체가 부모 타입으로 자동 타입 변환된 후 다시 자식 타입으로 변환될 때만 강제 타입 변환이 가능합니다.
    • 부모 타입 변수로는 자식 타입 객체의 고유한 멤버를 사용할 수 없기 때문에 사용이 필요한 경우가 생겼을 때 강제 타입 변환을 사용합니다.
Mammal newMammal = new Mammal();
Whale newWhale = (Whale) newMammal; // ClassCastException 발생


public class Main {
    public static void main(String[] args) {
        // 자동 타입변환된 부모타입의 변수의 자식 객체
        Mammal mammal = new Whale();
        mammal.feeding();

        // 자식객체 고래의 수영 기능을 사용하고 싶다면
        // 다시 자식타입으로 강제 타입변환을 하면된다.
        Whale whale = (Whale) mammal;
        whale.swimming();

        Mammal newMammal = new Mammal();
        // Whale newWhale = (Whale) newMammal;
    }
}

 

  • Mammal mammal = new Whale();이걸로 자동 타입 변환 한 상태면, Whale whale = (Whale) mammal;로 다시 자식타입으로 강제로 되돌릴 수 있다.
  • 되돌리는게 된다는 것이기 때문에, 부모를 자식의 변수에 할당하는 것은 불가능하다.
  • 용암을 5칸(자식)짜리 틀을 들고왔는데, 3칸(부모)에 부워버릴 수 없다. 다 넘친ㄷ;;;
  • 그러니까 저 말은 3칸을 5칸으로 늘렸던 걸 다시 3칸으로 줄인다는 말이다.

 

다형성

🧩 다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미합니다.

  • 예를 들어 자동차의 핸들을 교체하면 핸들링이 부드러워지고 바퀴를 교체하면 승차감이 좋아집니다.
  • 소프트웨어 또한 구성하고 있는 객체를 바꿨을 때 소프트웨어의 실행 성능 및 결과물이 다르게 나올 수 있습니다.
  • 일전에 배운 참조 변수 타입 변환을 활용해서 다형성을 구현할 수 있습니다. 
public Car(Tire tire) {
    this.tire = tire;
}

...

Car car1 = new Car(new KiaTire("KIA"));
Car car2 = new Car(new HankookTire("HANKOOK"));
  • 위에서 본것 처럼 객체도 된다. 지금 위 예시는 자식 객체를 선언하고 그 자식 객체를 부모 타입 객체로 다시 재 선언한 코드이다.

 

Tire getHankookTire() {
    return new HankookTire("HANKOOK");
}

Tire getKiaTire() {
    return new KiaTire("KIA");
}

...

Tire hankookTire = car1.getHankookTire();
KiaTire kiaTire = (KiaTire) car2.getKiaTire();
  • 심지어는 매개변수도 된다... 여기서도 마찬가지로 반환 타입이 부모 타이어이기 때문에 자식 타이어 객체들을 반환값으로 지정할 수 있다. 당연히 이 자동 타입 변환 후라면 다시 강제 타입 변환도 된다......

 

  • 아마 위의 설명만으로는 이걸 왜 하는지 이해가 안될거다. 간단히 설명하면
// Tire 클래스 (부모 클래스)
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// KiaTire와 HankookTire는 Tire를 상속받는 자식 클래스들
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }
}

class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }
}

// Car 클래스
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;  // Tire 타입 객체를 받는다
    }

    public void showTireInfo() {
        tire.getTireInfo();  // Tire 객체의 메서드 호출
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 Tire 타입을 받아 Car 객체 생성 가능
        Car car1 = new Car(new KiaTire("KIA"));
        Car car2 = new Car(new HankookTire("HANKOOK"));

        car1.showTireInfo();  // Tire Brand: KIA
        car2.showTireInfo();  // Tire Brand: HANKOOK
    }
}

왜 이렇게 해야 할까?

  • 자동차의 타이어는 여러 종류일 수 있습니다. KiaTire, HankookTire 등 다양한 브랜드의 타이어가 있을 수 있는데, Car 클래스는 이 타이어를 구체적인 종류와 관계없이 받아들일 수 있어야 합니다.
  • 타이어 브랜드가 늘어난다고 해서 Car 클래스를 매번 수정하는 것보다는, 부모 클래스인 Tire 타입으로 받아들여 자식 클래스에서 Tire 객체를 사용하게 하면, 확장성이 좋아지고 변경에 강한 코드가 됩니다.

  • 말이 어려운데, 자 여기서 Car를 만들고자 한다. 자동차는 타이어라는 자식 클래스를 포함한다.
  • 그런데 타이어에 A,B 종류가 있다면?
  • Car가 tire의 종류를 파악하려면 , car -> 요청 -> tire -> 요청 -> A의 과정을 거친다
  • 만약 tire가 A의 정보를 바로 접속해서 이미 들고 있다면? car -> 요청 -> tire(자식 클래스 A에서 정보 이미 받음)
  • 더 빠르다.
변형 X car -> 요청 -> tire -> 요청 -> A
변형 O  car -> 요청 -> tire(자식 클래스 A에서 정보 다이렉트로 받음)

 

(변형 없이 접근)

// 부모 클래스 : Tire
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// 자식 클래스 : KiaTire
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }

    public void getKiaTireInfo() {
        System.out.println("Kia Tire Information: " + brand);
    }
}

// 자식 클래스 : HankookTire
class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }

    public void getHankookTireInfo() {
        System.out.println("Hankook Tire Information: " + brand);
    }
}

// 자동차 클래스 : Car
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;
    }

    // 변형 없이 타이어 종류에 맞는 메서드 직접 호출
    public void showTireInfo() {
        if (tire instanceof KiaTire) {
            KiaTire kiaTire = (KiaTire) tire;
            kiaTire.getKiaTireInfo();
        } else if (tire instanceof HankookTire) {
            HankookTire hankookTire = (HankookTire) tire;
            hankookTire.getHankookTireInfo();
        } else {
            tire.getTireInfo();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 타이어 종류를 Car에 할당
        Car car1 = new Car(new KiaTire("KIA"));
        car1.showTireInfo();  // Kia Tire Information: KIA

        Car car2 = new Car(new HankookTire("HANKOOK"));
        car2.showTireInfo();  // Hankook Tire Information: HANKOOK
    }
}

 

(다형성 접근)

// 부모 클래스 : Tire
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// 자식 클래스 : KiaTire
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }

    @Override
    public void getTireInfo() {
        System.out.println("Kia Tire Information: " + brand);
    }
}

// 자식 클래스 : HankookTire
class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }

    @Override
    public void getTireInfo() {
        System.out.println("Hankook Tire Information: " + brand);
    }
}

// 자동차 클래스 : Car
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;
    }

    // 다형성 활용, 부모 타입으로 자식 클래스 메서드를 호출
    public void showTireInfo() {
        tire.getTireInfo();  // Tire 타입으로 메서드 호출
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 타이어 종류를 Car에 할당
        Car car1 = new Car(new KiaTire("KIA"));
        car1.showTireInfo();  // Kia Tire Information: KIA

        Car car2 = new Car(new HankookTire("HANKOOK"));
        car2.showTireInfo();  // Hankook Tire Information: HANKOOK
    }
}

 

  • 이게 더 효율적이다.... 정말 자바는 말 그대로 효율의 극한, 중복을 정말 정말 정말 극혐한다.....
  • 위 방식으로 굳이 새로운 정보가 필요할 때마다 클래스를 수정할 필요 없이 Main에서 다형성 코딩만 진행하면되고 관리 유지, 오류 방지에도 탁월하다.... (만든 사람이 진짜 변태인가 보다...)

 

instanceof

🧩 다형성 기능으로 인해 해당 클래스 객체의 원래 클래스명을 체크하는 것이 필요한데 이때 사용할 수 있는 명령어가 instance of입니다.

  • 이 명령어를 통해서 해당 객체가 내가 의도하는 클래스의 객체인지 확인할 수 있습니다.
  • {대상 객체} instance of {클래스 이름} 와 같은 형태로 사용하면 응답값은 boolean입니다. 
  • 다형성 설명 보면 알겠지만, 상당이 코드가 복잡해진다. 
  • 이로 인해 프로그래머가 코드를 짜면서도 이 객체(붕어빵)가 어디 클래스(설계도)를 바탕으로 만들어 진건지 헷갈리는 경우가 있는데
  • 이때 해당 객체가 어느 클래스의 객체인지 확인하기 위해 있는 기능이다.
// 다형성

class Parent { }
class Child extends Parent { }
class Brother extends Parent { }


public class Main {
    public static void main(String[] args) {

				Parent pc = new Child();  // 다형성 허용 (자식 -> 부모)

        Parent p = new Parent();

        System.out.println(p instanceof Object); // true 출력
        System.out.println(p instanceof Parent); // true 출력
        System.out.println(p instanceof Child);  // false 출력

        Parent c = new Child();

        System.out.println(c instanceof Object); // true 출력
        System.out.println(c instanceof Parent); // true 출력
        System.out.println(c instanceof Child);  // true 출력

    }
}

 

 

 


 

 

추상 클래스

🎵 클래스가 설계도라면 추상 클래스는 미완성된 설계도입니다.

  • abstract 키워드를 사용하여 추상 클래스를 선언할 수 있습니다.
public abstract class 추상클래스명 {
		abstract 리턴타입 메서드이름(매개변수, ...);
}
  • 추상 클래스는 추상 메서드를 포함할 수 있습니다.
    • 추상 메서드가 없어도 추상 클래스로 선언할 수 있습니다.
  • 추상 클래스는 자식 클래스에 상속되어 자식 클래스에 의해서만 완성될 수 있습니다.
  • 추상 클래스는 여러 개의 자식 클래스들에서 공통적인 필드나 메서드를 추출해서 만들 수 있습니다.
  • 추상 메서드는 일반적인 메서드와는 다르게 블록{ }이 없습니다. 즉, 정의만 할 뿐, 실행 내용은 가지고 있지 않습니다.

 

추상 클래스는 왜 쓰는가?

  1. 공통된 기능을 구현: 추상 클래스는 여러 자식 클래스가 공통적으로 가지는 기능을 제공하는데 유용합니다. 예를 들어, 여러 종류의 자동차가 있지만, 모든 자동차는 움직이는 기능이 필요하고, 이 기능을 추상 클래스에 구현할 수 있습니다.
  2. 구체적인 구현과 공통의 계약을 결합: 추상 클래스는 구체적인 메서드 구현을 제공하고, 일부는 추상 메서드로 남겨두어 자식 클래스가 구현하도록 할 수 있습니다. 이를 통해, 자식 클래스는 기본 구현을 상속받고, 필요한 부분만 구체화합니다.
  3. 상태(필드) 공유: 추상 클래스는 **상태(필드)**를 가질 수 있기 때문에, 자식 클래스에서 공통적으로 사용하는 값을 선언하고, 이를 자식 클래스에서 상속받아 사용할 수 있습니다.
  4. 일관성 유지: 추상 클래스를 사용함으로써, 모든 자식 클래스가 공통적으로 특정 메서드를 구현하도록 강제할 수 있습니다. 이를 통해 일관된 동작을 보장할 수 있습니다.

 

추상 클래스 상속

public class 클래스명 extends 추상클래스명 {
		@Override
    public 리턴타입 메서드이름(매개변수, ...) {
		       // 실행문
    }
}

 

  • 상속받은 클래스에서 추상 클래스의 추상 메서드는 반드시 오버라이딩 되어야 합니다.

< 예제 >

public abstract class Car {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public abstract void horn();
}

public class BenzCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Benz 빵빵");
    }

}

public class AudiCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Audi 빵빵");
    }

}

public class ZenesisCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h
    
    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Zenesis 빵빵");
    }

}

public class Main {
    public static void main(String[] args) {
        Car car1 = new BenzCar();
        car1.horn();
        System.out.println();

        Car car2 = new AudiCar();
        car2.horn();
        System.out.println();

        Car car3 = new ZenesisCar();
        car3.horn();
    }
}
  • BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에 차이가 존재합니다.
  • 따라서 horn() 메서드를 추상 메서드로 선언하여 자식 클래스에서 재정의 될 수 있도록 합니다.
  • BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에 차이가 존재합니다.
  • 즉 위의 자식 클래스에서 horn()메서드만 내용이 제각각이라는거
  • 각자 메서드 선언을 하면 3번 해야하니
  • 추상 클래스를 선언해서 메서드 선언을 1번으로 줄이고
  • 자식들이 내용만 채우라는것....
  • 아래 참고...
public class BenzCar extends Car {

    @Override
    public void horn() {
        System.out.println("Benz 빵빵");
    }
}

public class AudiCar extends Car {

    @Override
    public void horn() {
        System.out.println("Audi 빵빵");
    }
}

public class AudiCar extends Car {

    @Override
    public void horn() {
        System.out.println("Audi 빵빵");
    }
}

public abstract class Car {
    String company; // 자동차 회사
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public abstract void horn(); // 추상클래스~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new BenzCar();
        car1.horn();
        System.out.println();

        Car car2 = new AudiCar();
        car2.horn();
        System.out.println();

        Car car3 = new ZenesisCar();
        car3.horn();
    }
}

 

 


<  일단 정리 >

 

  • 다형성은 데이터 접근이 많을 때 그를 단순화 하기 위해 사용 ( 부모가 자식에게 손자의 정보를 달라고 할때)
  • 추상 클래스는 여러 자식들 중 메서드가 다 다른 애가 있으면 그걸 추상 클래스의 메서드로 만들어서 자식들이 내용만 채우게 (추상 : 그리기 / 자식들 : 원, 삼각형 동그라미 등)
  • 다형성은 다양한 데이터나 객체를 하나의 부모 타입으로 처리해야 할 때 유용하게 사용됩니다. 예를 들어, 여러 자식 클래스의 객체들을 부모 클래스 타입으로 다루면서, 각 자식 클래스의 고유한 기능을 호출하는 상황에서 사용됩니다.
  • 추상 클래스는 여러 자식 클래스가 공통된 기능을 가지고 있어야 할 때 유용합니다. 부모 클래스에서 추상 메서드를 정의해 두고, 각 자식 클래스가 그 메서드를 어떻게 구현할지 결정하게 만들 수 있습니다. 즉, 자식들이 부모 클래스의 특정 기능을 "구현"하게 강제하는 경우입니다.

< 요약 >

  • 추상 클래스는 공통적인 기능을 정의하지만, 세부 구현은 자식 클래스에서 하도록 강제합니다.
  • 다형성은 부모 타입으로 여러 자식 객체를 다룰 수 있게 해주며, 각 자식 클래스의 고유 메서드를 호출할 수 있도록 합니다.
  • 오버라이딩은 부모 클래스에서 정의한 메서드를 자식 클래스에서 구체적으로 구현하여 각 자식 클래스에 맞는 동작을 하도록 만듭니다.

 

 

 

오늘의 문제

다음은 자동차와 관련된 클래스 구조입니다. Car 클래스를 부모 클래스로 하고, 이를 상속받은 Sedan과 SUV 클래스를 만들어보세요. 각 차종은 drive라는 메서드를 오버라이딩하여 다르게 동작해야 합니다. 이때 Car는 추상 클래스이며, drive 메서드는 추상 메서드로 정의되어야 합니다. 또한, 다형성을 활용하여 Car 타입의 배열을 만들어 다양한 차들을 처리하는 방법을 구현하세요.

요구사항

  1. Car 클래스는 추상 클래스이며, drive()라는 추상 메서드를 가지고 있습니다.
  2. Sedan 클래스와 SUV 클래스는 Car 클래스를 상속받으며, 각자의 drive() 메서드를 구현해야 합니다.
  3. Main 클래스에서 Car 타입의 배열을 이용해 여러 종류의 자동차 객체를 생성하고, 다형성을 이용해 각 차종의 drive() 메서드를 호출하는 코드 작성.
// 1. 부모 클래스 Car
abstract class Car {
    String model;
    
    public Car(String model) {
        this.model = model;
    }
    
    // 2. 추상 메서드 drive()
    public abstract void drive();
}

// 3. 자식 클래스 Sedan
class Sedan extends Car {
    
    public Sedan(String model) {
        super(model);
    }
    
    // 4. 오버라이딩된 drive() 메서드
    @Override
    public void drive() {
        System.out.println(model + " 세단은 부드럽게 달립니다.");
    }
}

// 5. 자식 클래스 SUV
class SUV extends Car {
    
    public SUV(String model) {
        super(model);
    }
    
    // 6. 오버라이딩된 drive() 메서드
    @Override
    public void drive() {
        System.out.println(model + " SUV는 험한 길도 잘 달립니다.");
    }
}

// 7. Main 클래스
public class Main {
    public static void main(String[] args) {
        // 8. Car 타입 배열 선언
        Car[] cars = new Car[2];
        
        // 9. 다양한 차종 생성
        cars[0] = new Sedan("아반떼");
        cars[1] = new SUV("투싼");
        
        // 10. 다형성 활용하여 각 차의 drive 메서드 호출
        for (Car car : cars) {
            car.drive(); // 각 차종의 구체적인 drive 메서드가 호출됨
        }
    }
}

 

 

더보기

해설

  1. 추상 클래스 Car: Car 클래스는 abstract로 선언되어 있으며, drive()라는 추상 메서드를 정의하고 있습니다. 추상 메서드는 자식 클래스에서 반드시 구현해야 하므로, Car 클래스는 직접 drive()를 구현하지 않고, 자식 클래스들이 이 메서드를 구현하도록 강제합니다.
  2. 자식 클래스 Sedan과 SUV: Sedan과 SUV는 Car 클래스를 상속받고, 각자의 drive() 메서드를 구현합니다. 이때 drive() 메서드는 Car 클래스에서 정의된 추상 메서드를 오버라이딩하여 각자의 방식으로 자동차가 달리는 방식을 다르게 구현합니다.
  3. 다형성: Main 클래스에서 Car 타입의 배열을 만들고, 그 배열에 Sedan과 SUV 객체를 각각 저장합니다. 이후 Car 타입으로 선언된 변수 car를 통해 Sedan과 SUV 객체를 다룰 수 있습니다. 이때 각 객체는 다형성에 의해 자신의 drive() 메서드를 호출합니다. 즉, Sedan과 SUV는 부모 클래스 Car의 타입을 가지지만, 각자 다른 구현을 가진 drive() 메서드를 호출하게 됩니다.
  4. 오버라이딩: Sedan과 SUV 클래스에서 Car 클래스의 drive() 메서드를 오버라이딩하여, 각자 다른 방식으로 자동차의 운전 기능을 구현합니다. 이 메서드는 부모 클래스의 메서드를 자식 클래스에서 수정한 것으로, 해당 자식 객체에 맞는 동작을 하게 됩니다.

 

<결과>

아반떼 세단은 부드럽게 달립니다.
투싼 SUV는 험한 길도 잘 달립니다.

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 오류 및 예외에 대한 이해  (1) 2024.11.12
[JAVA] 인터페이스  (2) 2024.11.12
[JAVA] package와 import  (0) 2024.11.12
[JAVA] 접근 제어자  (1) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12