본문 바로가기

Backend/Java

[JAVA] 상속(extends)과 super, 오버라이딩(overriding)

 

 


클래스 간의 상속(extends)과 super, 오버라이딩(overriding) 알아보기


 

 

1. 클래스 간의 상속


클래스를 작성하다보면, 클래스 간 멤버 변수들이 겹치는 경우가 발생한다.

// 코드의 중복이 발생하고 있는 클래스들
class Student{
    String name;
    int age;
    String stdNum; // 학번
}

class OfficeWorker{
    String name;
    int age;
    String empNum; // 사번
}

 

위의 예제에서 이름과 나이를 나타내는 변수 String name; int age; 가 중복된다.

같은 내용을 반복하는 것은 코드를 지저분하게 보여 깔끔히 정리하는 것이 중요한데, 같은 메소드에 하다못해 같은 클래스도 아니고. 다른 클래스에 존재하는 두 변수를 어떻게 공유하도록 만들 수 있을까?

 

 

 

2. 상속의 구현


객체지향에는 '상속'이라는 개념이 있다. 상위클래스에 있는 변수를 하위클래스에서도 사용할 수 있도록 연결해주는 개념으로, 상위하위가 아닌 부모클래스 - 자식클래스의 관계라고도 이야기한다.

 

앞선 예제를 바탕으로 중복되는 멤버 변수나 메소드 등 생성 코드를 구성요소로한 상위 클래스를 생성한다.

class Person{
    String name;
    int age;
}

상위클래스 Person

 

이후 extends 키워드 상속받고자 하는 하위 클래스 이름 옆에 붙임으로써 상위클래스의 변수를 상속받는다.

class Student1 extends Person{
    String stdNum;
}

class OfficeWorker1 extends Person{
    String empNum;
}

하위클래스 Student1, OfficeWorker1

 

참고로, 다른 코드를 붙일 필요 없이 다른 클래스를 extends(상속)한 클래스는 별도의 언급이 없어도 상속받은 클래스의 모든 멤버를 상속한 것으로 인식한다. Student1과 OfficeWorker1은 Person 클래스의 name과 age를 상속 받은 상태다.

 

public static void main(String[] args) {
    Student1 std = new Student1();
    OfficeWorker1 ow = new OfficeWorker1();

    std.name = "홍길동";
    std.age = 18;

    ow.name = "홍길남";
    ow.age = 30;

    System.out.println(std.name);
    System.out.println(std.age);
    System.out.println(ow.name);
    System.out.println(ow.age);
}

 

Student1과 OfficeWorker1 클래스에 name, age 변수는 따로 선언되어있지 않지만, Person 클래스를 상속한 덕분에 각각의 클래스 객체를 선언하여 멤버 변수로 활용할 수 있다.

 

 

 

3. 상속과 접근 지정자 private


 

 

[JAVA] 클래스(Class) 생성과 사용

클래스 (Class) 생성과 사용 알아보기국비지원수업 12일차    1. 클래스(class)란?클래스 (Class)프로그램으로 처리하고자 하는 대상의 요소값들을 자료화하여 하나의 자료형으로 정의한 사용자 정

hanadoescoding.tistory.com

접근지정자란? 위의 포스트에 '3. 접근지정자'를 참고하자

 

상속 받은 멤버 변수를 사용할 수 있다... 그런데 만약 해당 변수 private로 보호되어있다면 어떨까?

접근지정자 private는 현재 클래스 내에 있는 멤버 메소드를 통해서만 접근이 가능하다. 따라서 상속을 받았다할지라도 외부 클래스인 자식 클래스는 접근이 불가능하다.

 

이렇게 private로 보호되어 있는 변수는 getter, setter 메소드를 통해서 접근이 가능하다.

 

class SuperA {
    // 외부에 완전 은닉, 자식 클래스에게도 은닉
    private int n1 = 100;

    // private 멤버를 위한 getter/setter는 반드시 같은 클래스내부에 정의해야 이를 상속받은 자식 클래스에서도 사용이 가능
    public int getN1() {
        return n1;
}

 

public으로 선언값을 반환하는 getter, 값을 수정하는 setter 메소드를 부모클래스에 작성해준다면 무리없이 사용가능하다.

 

class SubA extends SuperA {	
    public void prnInfo() {
        // System.out.pritnln("n1변수 값 :" + n1); // 에러  The field SuperA.n1 is not visible

        // public으로 생성된 멤버 메소드를 이용하여 부모의 private 멤버값을 리턴 받음
        int temp = getN1();
        System.out.println("부모의 public 메소드로 얻은 n1 접근 : " + temp);
    }
}

SubA 클래스

public static void main(String[] args) {
    SubA s1 = new SubA();
    s1.prnInfo();
}

main 메소드

 

 

외부에서 접근 가능한 public, 다른 패키지에서 private로 작용하는 default, 자식 클래스에서는 public, 외부로부터는 private로 작동하는 protected자유롭게 접근 가능하다.

 

class SuperA {
    public int n2 = 200;
    int n3 = 300;
    protected int n4 = 400;
}

SuperA 클래스

class SubA extends SuperA {
    public void prnInfo() {
        // 부모의 public, default 멤버는 자유롭게 접근 가능
        System.out.println("부모의 public 멤버변수 n2 접근 : " + this.n2);
        System.out.println("부모의 default 멤버변수 n3 접근 : " + this.n3);
        // 부모 클래스의 protected는 자식클래스에게 public과 같음
        System.out.println("부모의 protected 멤버변수 n4 접근 : " + this.n4);
    }
}

SuperA를 상속하는 SubA 클래스

public static void main(String[] args) {
    SubA s1 = new SubA();
    s1.prnInfo();
}

main 메소드

 

 

 

4. 상속관계에 따른 객체 생성 과정


super를 살펴보기 전, 간단하게 상속관계에 따른 객체의 생성 과정을 알아보자.

 

먼저, 상속관계가 없는 객체

  1. 멤버변수들을 메모리에 확보하고, 주소를 참조변수에 전달한다 (new를 통한 객체 생성 시 대입한 변수)
  2. 생상자 메소드를 호출하고 그 안에 추가 코드들을 실행한다

상속관계가 있는 객체

  1. 멤버변수들을 메모리에 확보하되, 상위-하위클래스의 모든 멤버변수를 한번에 생성한다.
  2. 하위 클래스의 객체가 생성되는 명령일 때, 하위클래스의 생성자가 먼저 호출된다.
  3. 하위 클래스의 생성자 명령 중 맨 첫번째 명령으로 상위클래스의 생성자를 호출한다. 
    1. 별도의 호출 명령이 없어도 호출된다.
    2. 별도로 호출 명령을 super(); 작성하기도 한다. (디폴트 생성자처럼 직접 꺼내쓰기 전까지 숨어있는 명령)
  4. 호출된 상위클래스의 생성자 코드들이 모두 실행된 후, 하위 클래스 생성자로 돌아와 나머지 코드를 실행한다.

하위클래스의 객체를 생성해도 상위클래스의 생성자가 먼저 호출되는 이유는 하위클래스에서 초기화하지 못하는 private 멤버가 상위클래스에 존재할 수 있기 때문이다. 이러한 구조는 하위클래스가 접근하지 못하는 priavate 멤버를 상위클래스 생성자가 값을 초기화할 수 있도록 한다.

 

class SuperB{
    SuperB(){ // 생성자
        System.out.println("상위클래스의 생성자 호출");
    }
}

class SubB extends SuperB{
    SubB(){
        // super(); // 현재는 생략되어도 무방한 명령
        // 사용하려면 다른 명령들보다 항상 위쪽에 써야함
        System.out.println("하위클래스의 생성자 호출");
    }
}

 

다음과 같은 상속관계를 갖는 클래스가 존재할 때, 하위클래스에는 상위클래스의 생성자를 호출하는 super() 메소드가 최상단에 생략되어 있다. 눈에 보이지는 않지만 main에서 하위클래스인 SubB 객체를 생성하고 호출했을 때 상위클래스의 생성자 속 내용이 먼저 출력됨을 알 수 있다.

 

 

 

5. super()


앞서 언급했듯 super() 메소드는 상위클래스의 생성자 중 디폴트 생성자를 호출하는 명령이다.

 

class SuperC{
    SuperC(int n){

    }

    SuperC(){

    }
}

 

다음과 같은 상위클래스 SuperC가 있다고 할 때, 하위클래스에서 SuperC의 생성자를 불러오려면 두가지 방법 중 하나를 사용하면 된다.

  1. Super(); // SuperC의 디폴트 생성자
  2. Super(n); // SuperC의 매개변수 n을 갖는 생성자

 

class SubC extends SuperC{
    SubC(){
        super();
        // super(n);
        // this();
    }
}

 

참고로 한 메소드안에서 super(); / super(n); / this(); 키워드 중 단 하나만 사용가능하다. 두 개 이상 사용시 에러를 발생시킨다. 아무것도 쓰지 않는다면 super(); 가 사용된 것으로 인식한다. (this(); 는 자기자신의 생성자를 호출하는 명령)

 

만약 상위클래스의 생성자가 매개변수 있는 생성자로 대체되었다면 super();는 에러를 발생하게 된다.

상위 클래스의 생성자가 매개변수 있는 생성자로 대체되었다면

  1. super() 호출에 전달인수 넣기 // super(10);
  2. 상위클래스의 매개변수가 있는 생성자의 매개변수를 삭제 // SuperC(){   } 로 수정
  3. 상위클래스의 디폴트 생성자를 추가해서 오버로딩 // SuperC(){   } 추가 생성

하는 방법으로 에러를 해결할 수 있다.

 

 

또한 오버로딩된 생성자가 두개 이상이라면 그 중 하나는 반드시 super() 를 써야한다.

 

class SuperD{
    SuperD(){
        System.out.println("부모클래스의 디폴트 생성자");
    }
    SuperD(int n){
        System.out.println("부모클래스의 매개변수가 있는 생성자");
    }
}

class SubD extends SuperD{
    SubD(){
        // this(100); // 형제 생성자 호출
        // this(); // 자기 자신은 호출 불가능
        // super(); // 상위 클래스의 디폴트 생성자 호출
        super(100); // 상위 클래스의 매개변수가 있는 생성자 호출
    }

    SubD(int n){
        this(); // 형제 생성자 호출
        // this(100); // 자기 자신은 호출 불가능
        // super(); // 상위 클래스의 디폴트 생성자 호출
        // super(100); // 상위 클래스의 매개변수가 있는 생성자 호출
    }
}

 

최소한 한개는 super(); (생략가능)  이면서 나머지 생성자가 this나 super를 선택 호출하는 것은 가능하다. 

그리고 오버로딩된 하위클래스의 생성자는 각각 자기자신을 호출하는 this(); / this(100); 를 호출하지 못하지만, super(); 는 사용할 수 있다. 

 

public static void main(String[] args) {
    SubD d1 = new SubD();
    SubD d2 = new SubD(10);
}

main 메소드

 

 

 

헷갈리는 상속관계의 생성자 호출 순서는 아래의 예시를 통해 복습하자.

class SuperE{
    SuperE(){
        this(5);
        System.out.println("SuperE() #3");
    }

    SuperE(int i){
        this(10.12);
        System.out.println("SuperE(int) #2");
    }

    SuperE(double d){
        System.out.println("SuperE(double) #1");
    }

    SuperE(String s){
        this();
        System.out.println("SuperE(String) #4");
    }
}

class SubE extends SuperE{
    SubE(){
        this("");
        System.out.println("SubE() #8");
    }

    SubE(int i){
        super("");
        System.out.println("SubE(int) #5");
    }

    SubE(double d){
        this(3);
        System.out.println("SubE(double) #6");
    }

    SubE(String s){
        this(123.12);
        System.out.println("SubE(String) #7");
    }
}

상위클래스 SuperE, SuperE를 상속하는 하위클래스 SubE

public static void main(String[] args) {
    SubE e = new SubE();
}

main 메소드

 

 

 

6. 오버라이딩 (Overriding)


상속 과정에서 상위클래스의 변수와 메소드들이 하위클래스에서 유효하게 사용되지 못하는 경우가 있다.

 

아래 예시를 통해 살펴보자.

// * 어울리지 않을 가능성 -> Dog, Cat 모두 "소리를 냅니다" 출력
class Animal{
    int age;
    int name;
    public void crying() {
        System.out.println("소리를 냅니다");
    }
}

class Dog extends Animal{ }
class Cat extends Animal{ }

 

다음과 같은 클래스가 존재할 때, Dog와 Cat 클래스에서 crying 메소드를 호출할 때, 부모클래스의 "소리를 냅니다"를 출력하게 된다. 강아지와 고양이의 울음소리를 각각 "왈왈~"과 "야옹~"으로 호출하기 위해서 새로운 메소드를 작성해야할까?

 

이때 하위클래스는 상위클래스에서 물려 받은 메소드를 자신의 용도에 맞게 재정의. 즉, 오버라이딩 할 수 있다.

오버라이딩이란 하위클래스에서 상위클래스의 메소드 내용을 다시 정의하는 문법으로, 하위클래스의 객체가 생성되고 해당 메소드가 호출되면 재정의된 메소드가 우선적으로 실행된다.

 

class Dog extends Animal{
    // 부모클래스의 메소드와 같은 형태라도 오버라이딩으로 인식
    public void crying() {
        super.crying();
        System.out.println("왈왈~");
    }
}

class Cat extends Animal{
    public void crying() {
        System.out.println("야옹~");
    }
}

Animal 클래스를 상속받은 Dog, Cat 클래스

public static void main(String[] args) {
        Dog d = new Dog();
        Cat c = new Cat();

        d.crying();
        c.crying();

        // c.super.crying(); // 에러  생성자는 클래스 내부에서 사용할 수 있음
    }

main 메소드

 

참고로 클래스 외부에서는 super를 쓸 수 없으므로 c.super.crying()처럼 생성된 객체에 super를 붙여서 사용할 수는 없다.

 

 

메소드 오버라이딩의 특징을 정리하면

  1. 상위클래스 메소드의 이름과 리턴형, 매개변수가 동일해야 한다. 매개변수 타입, 갯수, 순서, 리턴값 등이 다를 경우 다른 메소드로 인식한다.
  2. 상속 관계에서만 사용이 가능하다.
  3. 접근지정자는 축소될 수 없다. (public -> private)
  4. 상위클래스에 정의된 메소드 중 final로 정의된 메소드는 오버라이딩 불가능하다.
  5. 오버라이딩 된 메소드 안에서 super 키워드를 이용하여 오버라이딩 되기 전 상위클래스의 메소드를 호출할 수 있다.