Untitled_Blue

[JAVA] 상속 - super(), super 키워드, 오버라이딩, 형변환 본문

Programming Language/JAVA

[JAVA] 상속 - super(), super 키워드, 오버라이딩, 형변환

Untitled_Blue 2023. 5. 16. 14:10
반응형

안녕하세요. 이번 글은 상속에 대한 설명입니다.

- 상속

상속 : 부모 클래스의 멤버를 물려받아서 자식 클래스에서 해당 멤버를 그대로 사용할 수 있도록 구현하는 요소

상속 이전

상속 하기 전에는 사람과 강아지라는 두 클래스에서 중복되는 3개의 동작이 있다는 점을 확인할 수 있다. 이를 소스코드로 구현하게 된다면 중복되는 멤버를 두 클래스에 따로 선언해줘야 한다. 이렇게 되면 중복되는 코드가 존재해서 작업의 효율성이 떨어지고 코드의 길이가 상대적으로 길어져서 가독성이 떨어질 수 있다. 또한 해당 멤버를 유지보수할 때 각각에 선언되어있는 장소로 찾아가 일일이 일관성 있게 수정해줘야 하는 번거러움도 있다.

상속 이후

이러한 번거로움을 해결하기 위해 우리는 상속이라는 점을 활용할 줄 알아야 한다. 클래스에서 기존의 기능과 속성을 그대로 이어받기 위해서 extends 키워드를 사용하여 일일이 입력할 필요없이 간단하게 그러나 정확하게 자식 클래스에서 그대로 사용할 수 있도록 구현할 수 있다. 이를 통해 중복된 코드를 일일이 입력할 필요도 없어서 중복성이 제거되고 부모 클래스를 기반으로 각각의 특성과 목적에 맞게 다방면으로 클래스를 구현할 수 있다는 점에서 다형성을 보장받을 수 있다. 그리고 부모 클래스의 멤버를 수정할 때도 한 곳에만 있는 멤버만 수정해주면 다른 상속받은 클래스들에서도 변경 사항을 똑같이 적용할 수 있다는 점에서 유지보수와 효율성이 개선된다는 점도 확인할 수 있다. 

 

+ 다형성 : 하나의 객체를 여러 가지 모양으로 표현가능한 특성

package classes;

class Animal {
    void Sleep() {
        System.out.println("잔다");
    }

    void walk() {
        System.out.println("걷는다");
    }

    void eat() {
        System.out.println("먹는다");
    }
}
class Human extends Animal {
    void speak() {
        System.out.println("말한다");
    }

    void work() {
        System.out.println("일한다");
    }

    void economic_Activity() {
        System.out.println("경제활동을 한다");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("짖는다");
    }

    void shake_Tail() {
        System.out.println("꼬리를 흔든다");
    }
}

public class Inherice_Pract {
    public static void main(String[] args) {
        Human hm = new Human();

        hm.Sleep();
        hm.eat();
        hm.walk();
        hm.work();
        hm.speak();
        hm.economic_Activity();

        System.out.println("======================= \n");

        Dog dog = new Dog();

        dog.Sleep();
        dog.eat();
        dog.walk();
        dog.bark();
        dog.shake_Tail();
    }
}

상단 소스코드의 실행결과

상단 소스코드와 실행결과를 통해 상속의 특징을 파악할 수 있다. 이때 상속하는 클래스를 부모 클래스, 상속받는 클래스를 자식 클래스라고 칭한다. 그리고 하나의 클래스가 다른 클래스의 기능을 상속받으려면 클래스명 뒤에 extends를 쓰고 바로 뒤에 상속받을 클래스(부모 클래스)의 이름을 작성한다.

해당 소스코드에서는 Animal이 부모 클래스이고 Human과 Dog가 자식 클래스이다. 이는 곧 자식 클래스 뒤에 extends 부모 클래스를 사용하여 부모로부터 멤버들을 상속받아 자신들의 클래스에서 해당 기능을 그대로 물려받았다는 뜻이다.

그러므로 자식 클래스에서는 별도로 부모 클래스의 멤버를 언급하지 않아도 main 메서드에서 자식 클래스의 객체를 활용해서 물려받은 멤버를 사용할 수 있는 점을 확인할 수 있다. 물론 따로 클래스의 용도에 맞게 언급해도 상관없다. 

- 객체의 다양한 표현 - 다형성

다형성이란 하나의 객체를 여러 가지 모양으로 표현가능한 특성을 의미한다. 이는 상속 구조를 기반으로 한다.

package other_pack;

class A {}

class B extends A {}

class C extends B {}

class D extends B {}

public class Casting_Example {
    A a1 = new A();
    A a2 = new B();
    A a3 = new C();
    A a4 = new D();

    //B b1 = new A();
    B b2 = new B();
    B b3 = new C();
    B b4 = new D();

    //C c1 = new A();
    //C c2 = new B();
    C c3 = new C();
    //C c4 = new D(); // C와 D 클래스 서로 상속 관계가 아니다.

    //D d1 = new A();
    //D d2 = new B();
    //D d3 = new C();
    D d4 = new D();
}

좌측의 이미지는 상단 소스코드 내 클래스들의 상속 관계이다.

다형성 내지 업캐스팅, 다운캐스팅은 상속 관계가 어떻게 구성되어있는지에 따라 클래스끼리 객체 생성이 되는 경우도 있고 안되는 경우도 존재한다.

A a1 = new B(), B b4 = new D()처럼 상속 관계 내 화살표 방향을 위배하지 않으면 오류 표시없이 정상적으로 객체 생성되는 점을 확인할 수 있다. 이때 B b4 = new D()가 오류 표시가 없다는 부분에서 의문이 생길 수 있는데 이는 D의 부모 클래스인 B가 A 클래스를 상속받은 자식 클래스이기 때문이다. 이는 곧 D 클래스의 부모인 B 클래스에게 A의 정보가 그대로 상속된 상태이기 때문에 D는 곧 A 클래스를 상속받은 것이라고 볼 수 있다.

이를 실제 가족 관계로 보면 D는 A의 조부모라고 볼 수 있다.

 

- 객체의 다양한 표현 - 업캐스팅, 다운캐스팅

  • 업캐스팅 : 자식 클래스에서 부모 클래스 방향으로 변환되는 것
  • 다운캐스팅 : 부모 클래스에서 자식 클래스 방향으로 변환되는 것

업캐스팅은 명시적으로 언급하지 않아도 되나 다운캐스팅은 직접 변환형을 언급해야 한다. 그러나 변환형을 언급하는 방식으로 다운캐스팅을 진행해도 전부 다 된다는 것은 아니다. 

예를 들어 A a = new A();와 A b = new B();가 있다. 이때 A는 부모 클래스 B는 A의 자식 클래스이다. 전자의 경우 A는 A이다라고 해석할 수 있으며 후자도 B는 A이다라고 읽을 수 있다. 그리고 A를 동물, B는 사람이라고 가정하자.

여기서 A a = new A();를 a라는 객체는 A(동물) 역할을 위한 A(동물)이다. 라고 해석 할 수 있으며 후자인 A a = new B();의 경우에는 b라는 객체는 B(사람) 역할을 위한 A(동물)이라고 해석할 수 있다. (인간의 사회적 동물이라고도 하니까..)

이 부분은 지난 게시물(하단 링크)에서 객체 생성과정 파트와 같이 보는 것을 추천한다.

https://untitedblue.tistory.com/14

 

[JAVA] 클래스, 필드, 메서드, 생성자, this()

안녕하세요. 이번 글은 객체지향의 기본이자 핵심에 대한 설명입니다. - 객체지향 프로그래밍이란? 객체지향 프로그래밍이란 객체를 중심으로 하는 프로그래밍이며 순서를 중요시하기보다 객

untitedblue.tistory.com

  • A a = new A(); "a는 동물의 역할을 하기 위한 동물이다."
  • A b = new B(); "b는 사람의 역할을 하기 위한 동물이다."

이 부분을 자세히 보면 자식 클래스에서 부모 클래스 방향으로 진행되며 업캐스팅에 해당되는 것을 확인할 수 있다.

반면 이 부분을 반대로 하면 B b = new A();이다. 이는 b는 동물의 역할을 하는 사람이라고 해석할 수 있다. 부모 클래스에서 자식클래스 방향으로 진행되는 것이며 이를 다운 캐스팅이라고 한다. 그리고 다운캐스팅을 하는 행위는 기존에 업캐스팅을 통한 확장(상속)된 부분을 모두 상실되며 기본 부분만 있는 상태로 돌아간다는 뜻이다. 이럴 때는 B b = (B) new A();처럼 타입을 직접 명시해줘야 한다. 사람과 동물의 관계가 아니더라도 이러한 다운캐스팅은 가능한 경우도 있고 안되는 경우도 존재한다. 

package other_pack;

class First {
    int a = 10;
}

class Second extends First {
    int a = 600;
    int b = 60;
}
public class Casting_Example02 {
    public static void main(String[] args) {
        First fs = new First();
        Second sc = new Second();

        System.out.println("First fs = new First()");
        System.out.println(fs.a);

        System.out.println("\nSecond sc = new Second()");
        System.out.println(sc.a);
        System.out.println(sc.b);

        System.out.println("\nFirst fs1 = new Second()");
        First fs1 = new Second(); // 업캐스팅 (자식 -> 부모)
        System.out.println(fs1.a);

        /*
        Second sc1 = (Second)new First(); // 다운캐스팅 (부모 -> 자식) ClassCastException
        System.out.println(sc1.a);
        System.out.println(sc1.b);
        */

        /*
        System.out.println("\nSecond sc2 = (Second)fs"); // ClassCastException
        Second sc2 = (Second)fs;
        System.out.println(sc2.a);
        System.out.println(sc2.b);
         */

        System.out.println("\nSecond sc3 = (Second)fs1");
        Second sc3 = (Second)fs1;
        System.out.println(sc3.a);
        System.out.println(sc3.b);
    }
}

상단 소스코드에 대한 결과

다음과 같이 Second sc2 = (Second) fs(); 처럼 캐스팅 예외처리가 발생하는 것을 볼 수 있으며 다운 캐스팅이 아무리 상속관계가 명확하고 문법적인 오류가 없다고 해도 다운캐스팅이 가능한 것이 아니라는 점을 확인할 수 있다.

코드로만 보면 부모 클래스에서 자식 클래스로 가는 것이니 다운 캐스팅이 아닌가? 라는 의문이 생길 수 있다. 이는 fs라는 객체가 부모 타입으로 만들어졌기 때문이다. fs는 First fs = new First();이다. 다시 말하면 부모 클래스의 생성자로 만든 것이다. sc2의 자료형인 Second와 fs의 생성자인 First()인데 일치하지 않다. 반면 Second sc3 (Second)fs1;은 다운캐스팅이 가능한 것으로 확인된다. 이때 fs1는 First fs1 = new Second();이다. sc3의 자료형인 Second와 fs1의 생성자인 Second()와 일치한다. 이러한 점에서 다운 캐스팅에서 중요한 점이자 가능 여부는 좌측의 자료형과 우측의 생성자가 일치해야 하며 우측 부분이 어떤 생성자로부터 생성되었는지가 중요한 점을 확인할 수 있다.

 

First fs1 = new Second()는 자식의 역할을 하기 위한 부모이며 Second sc3 = (Second)fs1는 자식의 역할을 담당하는 부모의 역할을 자식이 다시 담당하는 것이라고 볼 수 있다. 즉, 자신의 역할(속성 및 기능)을 다시 되찾아오는 것이다.

- instanceof 키워드 - 캐스팅 가능 여부를 미리보기!

  • instanceof : 자식클래스와 부모클래스 사이의 업캐스팅, 다운캐스팅의 가능 여부 반환하는 키워드

사용 방법은 다음과 같다. 참조변수 instanceof 타입(클래스명) 해석은 우측 방향으로 진행되며 참조변수(생성자 기준)가 타입이 될 수 있느냐 이다.

package other_pack;

class First {}

class Second extends First {}
public class Casting_Example02 {
    public static void main(String[] args) {
        First fs = new First();
        Second sc = new Second();

        First fs1 = new Second();
        Second sc2 = (Second)fs1;

        System.out.println(fs instanceof First); // true --> 부모가 부모의 물건을 소유하려는 것 (본래 본인 소유이니까)
        System.out.println(fs instanceof Second); // false --> 부모가 자식의 물건을 소유하려는 것 (이미 상속받았는데...?)

        System.out.println(sc instanceof First); // true --> 자식이 부모의 물건을 소유하려는 것 (상속)
        System.out.println(sc instanceof Second); // true --> 자식이 자식의 물건을 소유하려는 것 (본래 본인 소유이니까)

        System.out.println(fs1 instanceof First); // true --> 생성자 기준 자식이 부모의 물건을 상속
        System.out.println(fs1 instanceof Second); // true --> 생성자 기준 자식이 자식의 물건을 상속

        System.out.println(sc2 instanceof First); // true --> 생성자 기준 자식이 부모의 물건을 상속
        System.out.println(sc2 instanceof Second); // true --> 생성자 기준 자식이 자식의 물건을 상속
    }
}

이를 자식-부모의 관계로 보면 전자(자식 또는 부모)가 후자(자식 또는 부모의 물건)을 소유하려는 것으로 생각하면 된다.

이렇게 instanceof 키워드를 통해 미리 업캐스팅과 다운캐스팅의 가능 여부를 좀 더 빠르게 파악할 수 있다.

- 오버라이딩과 오버로딩

  • 오버라이딩 : 상속받은 부모 클래스의 멤버를 동일한 이름으로 재정의하는 행위
  • 오버로딩 : 같은 이름의 생성자 또는 메서드를 매개변수만 다르게 한 채로 여러 개 생성하는 행위

이 둘의 차이는 상속의 유무이다. 오버라이딩은 일명 덮어쓰기로 오버로딩은 과적이라고 생각하면 될 것이다.

package other_pack;

class First {
    void mFirstprint() {
        System.out.println("First");
    }

    void mFirstprint(int k) {
        System.out.println("int k's First Class's mFirstprint Method");
    }
}

class Second extends First {
    @Override
    void mFirstprint() {
        System.out.println("Second");
    }
}
public class Casting_Example02 {
    public static void main(String[] args) {
        First fs = new First();
        Second sc = new Second();

        fs.mFirstprint();
        sc.mFirstprint();

        fs.mFirstprint(6);
    }
}

다음 소스코드는 오버라이딩과 오버로딩을 모두 확인할 수 있는 코드이다. 또한 실행 결과에 대한 이미지도 있다.

오버로딩과 오버라이딩에 대한 설명

먼저 부모 클래스 First에 있는 메서드 mFirstprint()가 있고 이를 자식 클래스인 Second에서 다시 선언해서 내부 코드를 재정의한 것을 확인할 수 있다. 이를 오버라이딩이라고 한다. 그 다음 First 클래스에서 같은 이름의 메서드가 존재하는 점을 확인할 수 있다. 하지만 자세히 보면 소괄호 안이 다르다. 하나는 ()으로 매개변수가 없으며 다른 하나는 (int k)와 같이 매개변수가 존재하는 점을 확인할 수 있다. 이와 같이 같은 이름의 메서드를 과적하는 방식으로 생성하는 것을 오버로딩이라고 한다. 이 외에도 void, int, String 등과 같이 리턴 타입을 다르게 한 메서드도 오버로딩에 포함된다.

이와 같이 오버라이딩을 사용하는 이유는 다형성과 각 객체의 용도에 맞게 커스텀마이징할 수 있기 때문이다. 객체가 각자의 목적과 특성에 맞게 만들어서 사용하는 만큼 오버라이딩과 오버로딩 또한 상속 여부만 다를 뿐 실질적인 목적 및 활용도는 같다고 볼 수 있다.

추가로 메서드를 오버라이딩할 때 접근 지정자를 첨가하는 부분에서 주의가 필요하다. 부모 클래스에서 자식 클래스 방향으로 상속이 진행될 때는 반드시 부모 클래스보다 접근 가능 범위가 같거나 넓어야 한다. 좁아지는 일이 없어야 한다.

참고로 접근 지정자 범위가 넓은 순에서 좁은 순서는 public -> protected -> default -> private이다.

다음과 같이 접근 제어자는 부모 클래스 멤버의 접근 제어자 ≤ 자식 클래스 멤버의 접근 제어자 규칙이 성립해야 한다.

- super 키워드와 super() 메서드

  • super : 부모 클래스의 객체를 호출하는 키워드
  • super() : 부모 클래스의 생성자 호출하는 메서드
package classes;

class First {
    First() {
        System.out.println("First 클래스의 생성자");
    }
    void abc() {
        System.out.println("A Class's abc Method()");
    }
}

class Second extends First {
    Second() {
        super();
        System.out.println("Second 클래스의 생성자");
    }
    @Override
    void abc() {
        System.out.println("B Class's abc Method() - Override");
    }

    void super_call() {
        super.abc();
    }
}

public class super_Practice {
    public static void main(String[] args) {
        First fr = new First();
        fr.abc();

        Second sc = new Second();
        sc.abc();
        sc.super_call();
    }
}

상단 소스코드는 super()와 super 키워드에 대한 예시이며 바로 하단에 실행 결과 이미지가 있는 것을 확인할 수 있다.

먼저 super()는 부모 클래스의 생성자를 호출할 때 사용하는 메서드인데 이는 반드시 생성자 내 맨 첫 줄에 작성해야 되며 자식 클래스의 생성자 내부에만 작성 가능하다. 실행하면 자식 클래스의 객체를 선언 및 생성하는 동시에 자식 클래스의 생성자 내부에 적은 코드가 실행되는 점을 확인할 수 있다. 또한 super()를 통해 부모 클래스의 생성자 내 코드도 같이 출력되는 점도 확인할 수 있다. 다음은 super 키워드인데 이는 자식 클래스 내에서 부모 클래스의 메서드를 호출할 때 사용하는 키워드다. 자식 클래스 내 void 형식의 super_call()이라는 메서드 안에 super.abc()라고 있는데 이는 부모 클래스 내 abc라는 메서드를 그대로 호출할 것이라는 뜻이다. 그리고 메인 메서드 안에 객체를 통해 해당 메서드를 호출하면 부모 메서드에 있는 코드가 그대로 출력되는 점을 확인할 수 있다.

 

다음 글은 final 제어자에 대한 설명입니다.

반응형