본문 바로가기
Java

[자바 라이브 스터디] 06. 상속

by 매트(Mat) 2021. 10. 12.

6주차 과제: 상속

목표

자바의 상속에 대해 학습하세요.

 

학습할 것

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스




자바 상속의 특징

상속이란, 부모가 자식에게 물려준다는 의미입니다. 자바에서 상속도 마찬가지입니다. 자바에서 상속이란 부모 클래스에서 정의된 필드와 메소드를 자식 클래스가 물려받는 것입니다.

 

상속이 필요한 이유

객체지향 프로그래밍에서 상속은 필수적입니다. 그 이유는 다음과 같습니다.

  • 공통적인 코드는 상속을 통해 부모 클래스의 필드나 메소드를 가져와서 사용할 수 있습니다. 이러면 중복된 코드를 줄여서 코드가 더욱 간결해집니다.
  • OOP란 결국 코드의 확장성과 재활용을 용이하게 만들기 위한 기법입니다. 상속을 사용함으로써 코드를 확장시킬 수 있으며 유지보수가 쉬워집니다.

 

상속 특징

  • 명칭은 보통 부모 클래스: super class, 자식 클래스: sub class 라고 부릅니다.
  • 자바에서는 다중 상속을 지원하지 않습니다. 따라서 extends 뒤에는 하나의 부모 클래스만 올 수 있습니다.
  • 자바에서는 상속의 횟수에 제한을 두지 않습니다.
  • 최상위 클래스는 Object 클래스입니다.

상속의 단점도 존재합니다. 하지만 이는 다루지 않겠습니다. 좀 더 깊이 들어가야하기 때문에 자세히 알고 싶으시다면 Effective Java나 오브젝트 책을 한번 보는 것이 좋을 것 같습니다. (저도 얼른 봐야되는데 말이죠..ㅎㅎ;)

 

상속 사용하는 방법

상속은 어떻게 사용하는지 간단히 예제를 보겠습니다.

Animal Class

package com.azurealstn.sociallogin.inherit;

public class Animal {

    public String name;
    public String gender;
    public int age;
    public String species;

    public void eat() {
        System.out.println(name + "(이)는 " + species + " 사료를 먹습니다.");
    }
}

 

Dog Class

package com.azurealstn.sociallogin.inherit;

public class Dog extends Animal {

    public void namePrint() {
        name = "피피";
        System.out.println("개의 이름은 " + name + " 입니다.");
    }
}

코드를 보면 자식 클래스인 Dog가 부모 클래스인 Animal을 상속받아서 Dog 클래스에서 굳이 name 필드를 생성하지 않아도 사용할 수 있는 것을 확인할 수 있습니다.




super 키워드

super는 자식 클래스가 부모 클래스로부터 상속 받은 멤버를 참조할 때 사용하는 참조 변수입니다. 쉽게 말해서 클래스 내의 인스턴스 변수와 지역 변수의 이름이 서로 같을 때는 this 키워드를 통해 구별했었습니다. 이와 마찬가지로 부모 클래스와 자식 클래스의 변수명이 같은 때는 super 키워드를 사용합니다.

 

바로 예제를 보도록 하죠.

package com.azurealstn.sociallogin.inherit;

public class Dog extends Animal {

    String name = "쿠쿠"; //Dog의 인스턴스 변수

    public void namePrint() {
        super.name = "피피"; //Animal의 인스턴스 변수
        System.out.println("개의 이름은 " + super.name + " 입니다.");
    }

    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.namePrint(); //개의 이름은 피피 입니다.
    }
}
  • 출력된 결과를 확인해 보면 피피 라는 이름으로 출력된 것을 확인할 수 있습니다.

 

super()

super()this()와 유사합니다. this()는 같은 클래스 내의 오버로딩된 다른 생성자를 호출할 때 사용한다면 super()는 부모 클래스의 생성자를 호출할 때 사용합니다.

 

바로 예제를 보도록 하겠습니다.

Animal Class

package com.azurealstn.sociallogin.inherit;

public class Animal {

    public String name;
    public String gender;
    public int age;
    public String species;

    public Animal(String name, String gender, int age, String species) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.species = species;
    }

    public void eat() {
        System.out.println(name + "(이)는 " + species + " 사료를 먹습니다.");
    }
}

 

Dog Class

package com.azurealstn.sociallogin.inherit;

public class Dog extends Animal {

    String name = "쿠쿠";

    public Dog(String name, String gender, int age, String species) {
        super(name, gender, age, species); //초기화
    }

    public void namePrint() {
        super.name = "피피";
        System.out.println("개의 이름은 " + super.name + " 입니다.");
    }

    public static void main(String[] args) {
        Dog dog = new Dog("피피", "Male", 3, "개");
        dog.namePrint();
    }
}

코드를 보시면 부모 클래스의 생성자를 호출하기 위해서는 super()를 사용합니다.
그렇다면 왜 super()를 사용해야 할까요?

 

자식 클래스의 인스턴스를 생성하면 자식의 멤버와 부모의 멤버가 모두 합쳐진 하나의 인스턴스가 생성됩니다. 그래서 자식 클래스가 부모 클래스의 멤버들을 사용할 수 있는 것이죠.
이 때 부모 클래스 멤버의 초기화 작업이 수행되어야 하기 때문에 자식 클래스의 생성자에서 부모 클래스의 생성자가 호출되어야 합니다. 만약 생성자 호출을 하지 않으면 컴파일 에러가 발생합니다.




메소드 오버라이딩

오버라이딩(Overriding)이란 상속 관계에 있는 부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 재정의하는 것을 말합니다.

 

오버라이딩은 왜 사용하는 것일까요?

 

이유는 간단합니다. 부모 클래스로부터 상속받은 메소드를 그대로 사용해도 되지만 만약에 가져온 메소드를 커스터마이징 하고 싶다면 어떻게 해야할까요? 다시 비슷한 메소드를 정의해야 할까요?
바로 오버라이딩해서 부모 클래스로부터 받은 메소드는 그대로 쓰고 나머지 필요한 로직을 작성해주시면 됩니다. 바로 아래 코드처럼요.

@Override
public void eat() {
    super.eat();
    System.out.println("먹은 후에 산책시킬 것을 권장합니다."); //로직 추가
}
  • 메소드 오버라이딩을 할 경우 메소드 위에 @Override를 붙여주세요! 필수는 아니지만 개발은 혼자하는 것이 아니기 때문에 쓰는 것을 권장합니다.

 

오버라이딩 조건

  • 오버라이딩이란 메소드의 동작만을 재정의하는 것이므로, 메소드 선언부는 기존 메소드와 완전히 동일해야 합니다.
  • 부모 클래스의 메소드보다 접근 제어자를 더 좁은 범위로 변경할 수 없습니다.

 

오버로딩 & 오버라이딩

자바를 처음 배울 때 오버로딩 & 오버라이딩의 차이가 굉장히 헷갈렸습니다.
간단히 정리하면 오버로딩(Overloading)은 새로운 메소드를 정의하는 것이고,
오버라이딩(Overriding)은 상속 받은 기존의 메소드를 재정의하는 것입니다.
바로 예제를 보겠습니다.

public class Dog extends Animal {

    String name = "쿠쿠";

    public Dog(String name, String gender, int age, String species) {
        super(name, gender, age, species);
    }

    @Override
    public void eat() {
        super.eat();
        System.out.println("먹은 후에 산책시킬 것을 권장합니다.");
    }
    //Overloading
    public void eat(int time) {
        System.out.println(time + "분 후에 " + super.name + "(이)는 " + super.species + " 사료를 먹습니다.");
    }

    public void namePrint() {
        super.name = "피피";
        System.out.println("개의 이름은 " + super.name + " 입니다.");
    }

    public static void main(String[] args) {
        Dog dog = new Dog("피피", "Male", 3, "개");
        dog.namePrint(); //개의 이름은 피피 입니다.
        dog.eat(30); //30분 후에 피피(이)는 개 사료를 먹습니다.
    }
}

위 코드를 보시면 오버라이딩은 부모의 것을 그대로 사용하되 자식 클래스에서 재정의하여 사용하고 있고 오버로딩은 완전히 새로운 메소드를 정의하고 있습니다.

 

오버로딩을 사용하는 이유는 비슷한 메소드명을 매번 짓기가 어렵기 때문에 사용합니다.
예를 들어, 두 수를 더하는 메소드와 세 수를 더하는 메소드가 있다면 서로 이름을 다르게 해야한다면 조금 이름을 생각하는데 시간을 쏟게 될 것입니다. 하지만 오버로딩을 사용하면 간단합니다.

public int add(int x, int y) {
    return x + y;
}

public int add(int x, int y, int z) { 
    return x + y + z; 
}

오버로딩의 조건은 다음과 같습니다.

 

  • 메소드명이 같아야 합니다.
  • 파라미터의 개수나 타입이 달라야 합니다.
  • 파라미터의 개수와 타입은 같고 리턴 타입이 다른 경우에는 오버로딩이 성립되지 않습니다.




다이나믹 메소드 디스패치 (Dynamic Method Dispatch)

자바 공부하면서 Dynamic Method Dispatch란 말을 처음 들어보았습니다. 좀 생소했지만 역시 구글에 검색하니 많은 자료가 있더군요.

 

Dynamic Method Dispatch란 오버라이딩된 메소드에 대한 호출이 컴파일타임이 아닌 런타임에 확인되는 메커니즘이라 합니다.

  • 자바는 호출이 발생할 때 참조되는 객체의 유형에 따라 해당 메소드를 실행합니다. 따라서 이 결정은 런타임에 이루어집니다. -> 컴파일타임에는 객체의 유형을 알 수가 없는 걸로 해석이 되는군요.

Dynamic Method Dispatch를 다른 말로 실행시간 다형성(runtime polyporphism)이라고 합니다. runtime polyporphism은 컴파일타임이 아닌 런타임에 upcasting된 자식 클래스의 오버라이딩된 메소드를 호출하는 것을 말합니다. 이러면 실행시간에 어떤 자식 클래스의 오버라이딩된 메소드를 호출할지가 명확해집니다.

 

예제를 한번 보도록 하겠습니다.

package com.azurealstn.sociallogin.inherit;

public class Super {
    void print() {
        System.out.println("Super.print");
    }
}

class Sub1 extends Super {
    @Override
    void print() {
        System.out.println("Sub1.print");
    }
}

class Sub2 extends Super {
    @Override
    void print() {
        System.out.println("Sub2.print");
    }

    public static void main(String[] args) {
        Super ref = new Super();
        ref.print();
        ref = new Sub1();
        ref.print();
        ref = new Sub2();
        ref.print();
    }
}

부모 클래스인 Super 객체 타입인 ref에 자식 객체인 Sub1Sub2를 대입하면 upcasting이 이루어지고, ref는 대입될 때마다 자식 객체의 주소를 가리키게 됩니다.

 

결론

upcasting과 overriding을 통해 rumtime polymorphism을 구현할 수 있습니다.
자세한 설명은 https://velog.io/@maigumi/Dynamic-Method-Dispatch 이 블로그를 보시면 좋을 것 같습니다.




추상 클래스

추상 클래스는 각각 클래스의 공통적인 부분(필드, 메소드)를 추출해서 선언한 클래스입니다.
이를 이해하기 위해서는 다형성에 대해 알면 이해가 쉽습니다.
바로 코드를 보면서 이해해보죠. 그 전에 추상 클래스에 대한 간단한 특징을 설명하겠습니다.

추상 클래스 특징

  1. 추상 클래스는 객체를 생성할 수 없습니다. 따라서 객체 생성을 막는 용도로도 사용됩니다.
  2. 추상 클래스를 사용하려면 반드시 상속 관계여야 합니다.
  3. 추상 클래스를 상속받을 경우에는 추상 메소드는 반드시 오버라이딩하여 구현해주어야 합니다.

Animal Class

public abstract class Animal {
    public int legs;
    public String kind;

    public void breath() {
        System.out.println("숨 쉬다.");
    }

    public abstract void cry();
}

 

Dog Class

public class Dog extends Animal {

    public Dog() {
        this.kind = "포유류";
        this.legs = 4;
    }

    @Override
    public void cry() {
        System.out.println("멍멍!!");
    }
}

 

Cat Class

public class Cat extends Animal {

    public Cat() {
        this.kind = "포유류";
        this.legs = 4;
    }

    @Override
    public void cry() {
        System.out.println("야옹~~");
    }
}

코드를 보시면 DogCat 클래스는 Animal 추상 클래스를 상속 받았습니다.
DogCat 클래스는 서로 공통된 필드와 메소드를 가지고 있습니다. 그래서 Animal 클래서에서 이 공통된 메소드를 추상 메소드로 선언하여 각각 DogCat 클래스에서 구현하도록 강제하고 있습니다. 물론 추상 메소드가 아닌 일반 메소드도 사용할 수 있습니다.

 

추가로 추상 클래스에서 추상 메소드만 선언이 가능하며, 추상 필드는 선언이 안됩니다. 그래서 공통된 메소드가 있다면 추상 메소드로 선언하여 각각 오버라이딩해서 구현하시면 됩니다. 이런식으로 추상 클래스를 사용하게되면 확장성이 용이해지겠죠.

 

요즘은 인터페이스의 기능이 너무 좋아서 추상 클래스를 안써도 될 정도라고 합니다. 그래서 인터페이스를 더 잘 아는 것이 좋을 것 같습니다. 또한 위의 개념에서 다형성이 포함되어 있습니다. 다형성이란, 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 말합니다. 위 예제에서 Animal이란 하나의 객체가 DogCat 타입을 가질 수 있는 것이죠.




final 키워드

자바에서 final 키워드는 여러 컨텍스트에서 단 한 번만 할당될 수 있는 entity를 정의할 때 사용됩니다.

 

final은 총 가지에 적용할 수 있습니다.

  • final 변수
    • 프리미티브 타입
    • 객체 타입
    • 메소드 파라미터
    • 멤버 변수
  • final 메소드
  • final 클래스

final 변수

  • 프리미티브 타입
    • 로컬 프리미티브 타입 변수에 final로 선언하면 한번 초기화된 변수는 변경할 수 없는 상수값이 됩니다.
public class FinalKeyword {

    public void finalPrint() {
        final int x = 10;
    }
}
  • 객체 타입
    • 객체 변수에 final로 선언하면 그 변수에 참조 값을 지정할 수 없습니다. 다만 객체 자체가 immutable하다는 것이 아니라 객체의 속성은 변경이 가능합니다.
public class FinalKeyword {

    public int age;
    public String name;

    public static void main(String[] args) {
        final FinalKeyword finalKeyword = new FinalKeyword();
        //finalKeyword = new FinalKeyword(); //다른 객체로 변경 불가

        finalKeyword.age = 3; //객체 필드는 변경 가능
    }
}
  • 메소드 파라미터
    • 메소드 파라미터에 final 키워드를 붙이면 , 메소드 안에서는 변수값을 변경할 수 없습니다.
public void agePrint(final int age) {
    age = 1; //컴파일 에러 발생
} 
  • 멤버 변수
    • 클래스의 멤버 변수에 final로 선언하며 상수값이 되거나 write-once 필드로 한 번만 쓰이게 됩니다. 또한 static이 있냐 없냐에 따라서 초기화 시점이 달라집니다.
    • static final 멤버 변수
      • 값과 함께 선언시
      • 정적 초기화 블록에서
    • instance final 멤버 변수
      • 값과 함께 선언시
      • 인스턴스 초기화 블록헤서
      • 생성자 메소드에서

final 메소드

메소드에 final로 선언하면 상속받은 클래스에서 오버라이딩을 할 수없게 됩니다.

public class FinalKeyword extends FinalMethod {

    @Override
    public void impossible() {
        System.out.println("컴파일 에러");
    }
    @Override
    public void possible() {
        System.out.println("정상 동작");
    }
}

final 클래스

클래스에 final을 선언하면 상속 자체가 불가능해집니다. Util 형식의 클래스나 여러 상수 값을 모아둔 Constants 클래스를 final로 선언합니다.

public class FinalKeyword extends FinalMethod {
    //상속 자체가 불가능
}

final을 공부하면서 단순히 변경불가능한 값이라고만 생각했는데 이렇게 많은 종류가 있을 줄은 몰랐습니다. https://advenoh.tistory.com/13 이 블로그 공부하시면 도움이 많이 될 듯 하네요.




Object 클래스

자바에서 모든 클래스는 사실 Object를 암시적으로 상속받고 있습니다.

public class FinalKeyword {

}
//-----------------------------
public class FinalKeyword extends Object {

}

위 코드를 보시면 두 클래스는 서로 같습니다. 즉, 흔히 hascodetoString, equals 같은 메소드를 따로 객체를 생성하지 않아도 사용할 수 있는 이유는 바로 Object 클래스를 암시적으로 상속받고 있기 때문이죠.

결론,
모든 클래스는 Object 클래스를 암시적으로 상속받고 있다.
즉, 모든 클래스들의 최고 조상 클래스이다.

 

더 많은 메소드를 알고 싶으시면 공식 문서를 참고하세요.
https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html




References

댓글