티스토리 뷰

JAVA

[Java Study 06] 상속

DevBee 2021. 4. 27. 11:15

상속

상속이란, 부모 클래스의 필드와 메소드를 자식 클래스에서 그대로 물려받아 사용할 수 있는 것을 의미합니다. 간단하게 Animal 클래스와 Animal 클래스를 상속 받는 Dog 클래스를 구현해보겠습니다.

 

// 부모 클래스 Animal
public class Animal {
    String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    // 모든 동물의 짖는 소리가 멍멍은 아니기 때문에 추후 수정이 진행될 예정입니다!
    public void bark() {
       System.out.println(name + "이(가) 짖는 소리는 멍멍");
    }
}

// 자식 클래스 Dog
public class Dog extends Animal {
    public Dog() {
        super("Dog");  // 부모 클래스의 생성자 호출
    }
}

 

상속 관계를 나타내기 위해 extends 키워드를 사용합니다.

 

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

 

자식 클래스인 Dog는 name이라는 필드와 bark()라는 메소드가 없지만 Animal 클래스를 상속 받았기 때문에 부모 클래스의 필드인 name과 메소드인 bark()를 그대로 사용할 수 있습니다.

 

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.println(dog.name);  // 출력: Dog
        dog.bark();  // 출력: Dog이(가) 짖는 소리는 멍멍
    }
}

 

또한, 자식 클래스에는 자식 클래스만의 필드나 메소드를 가질 수 있습니다. 

 

public class Dog extends Animal {
    public Dog() {
        super("Dog");
    }
    
    public void sleep() {
        System.out.println(this.name + "...zzz😴");
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.println(dog.name);  // 출력: Dog
        dog.bark();  // 출력: Dog이(가) 짖는 소리는 멍멍
        dog.sleep();  // 출력: Dog...zzz😴
    }
}

 

상속은 다음과 같은 특징을 가집니다.

  • 자바는 다중 상속을 지원하지 않습니다. 즉, extends 키워드 뒤에는 하나의 부모 클래스만이 올 수 있습니다.
  • 상속의 횟수에는 제한이 없습니다.
  • 부모 클래스에서 private 접근 제한을 가지는 필드와 메소드는 상속 대상이 아닙니다. 부모 클래스와 자식 클래스가 다른 패키지에 존재하여 default 접근 제한을 가지는 필드와 메소드도 상속 대상이 아닙니다.
  • 자바 클래스들은 모두 Object 클래스를 상속 받고 있습니다. 즉, Object 클래스는 모든 자바 클래스의 부모 클래스입니다.

super 키워드와 super() 메소드

super 키워드는 부모 클래스로부터 상속받은 필드나 메소드를 자식 클래스에서 참조하는 데 사용하는 참조 변수입니다. 부모 클래스의 멤버와 자식 클래스의 멤버 이름이 같은 경우 this처럼 super를 통해 구분할 수 있습니다.

 

class Parent {
    int a = 10;
}

class Child extends Parent {
    int a = 20;
    
    void display() {
        System.out.println(a);
        System.out.println(this.a);
        System.out.println(super.a);
    }
}

public class Main {
    public static void main(String[] args) {
        Child ch = new Child();
        ch.display();
    }
}

// 출력
// 20
// 20
// 10

 

this 키워드처럼 super 키워드도 인스턴스 메소드에서만 사용이 가능합니다.

 

this() 메소드가 같은 클래스의 다른 생성자를 호출할 때 사용된다면, super() 메소드는 부모 클래스의 생성자를 호출할 때 사용됩니다.

 

자식 클래스가 생성될 때 자식클래스보다 먼저 부모 클래스가 생성됩니다. 자식 클래스에서 별도로 부모 클래스 생성자를 호출하지 않으면 컴파일러에서 자동적으로 super() 메소드를 통해 부모 클래스의 생성자를 호출하게 됩니다. (단, 부모 클래스에 다른 생성자가 선언되어 있지 않은 경우에만 자동적으로 default 생성자를 추가하기 때문에 만약 default 생성자 없이 다른 생성자만 등록되어 있는 경우라면 super() 메소드는 에러가 발생합니다...)

 

메소드 오버라이딩

부모 클래스에 정의된 메소드를 자식 클래스에서 동일한 형태로 다시 재정의하는 것을 메소드 오버라이딩이라고 합니다.

 

오버라이딩은 다음과 같은 규칙을 가집니다.

  • 부모의 메소드와 동일한 시그니처(리턴 타입, 메소드 이름, 매개변수 리스트)를 가져야 합니다.
  • 접근 제한을 더 강하게 오버라이딩 할 수 없습니다. 즉, 부모가 public인데 자식이 private일 수 없습니다. 반대의 경우는 가능합니다.
  • 새로운 예외를 throws 할 수 없습니다.
public class Cat extends Animal {
    public Cat() {
        super("Cat");
    }
    
    // Animal 클래스의 bark() 메소드를 재정의
    @Override
    public void bark() {
        System.out.println(this.name + "이(가) 짖는 소리는 냐옹~");
    }
}

 

@Override 애노테이션의 경우 생략이 가능하지만 이것을 붙여주면 해당 메소드가 정확히 오버라이딩된 것인지 컴파일러가 체크하기 때문에 개발자 실수를 줄일 수 있습니다.

 

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

런타임 시에 어떤 구현 클래스의 메소드를 호출할지 결정하는 방식을 다이나믹 메소드 디스패치라고 합니다. 이는 upcasting과 overriding을 통해 구현할 수 있습니다. 예를 살펴보겠습니다.

 

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

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

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

class Main {
    public static void main(String[] args) {
        Super ref = new Super();
        ref.print();  // 출력: Super class print
        ref = new Sub1();
        ref.print();  // 출력: Sub1 class print
        ref = new Sub2();
        ref.print();  // 출력: Sub2 class print
    }
}

 

위 예제의 경우 Super 타입의 ref에 Sub1, Sub2를 대입하면 upcasting이 일어나고 그때마다 ref는 Sub1, Sub2 인스턴스의 주소를 가리키기 때문에 현재 가리키는 객체의 overriding된 메소드를 호출하게 됩니다.

 

final 키워드

final 키워드는 클래스, 필드, 메소드 선언 시 사용하며 해당 선언이 최종 상태로 변경될 일이 없다는 것을 의미합니다. 클래스, 필드, 메소드에 따라 final 키워드에 대한 해석이 조금씩 달라집니다.

 

✔️final 클래스

클래스를 선언할 때 class 앞에 final 키워드를 붙이면 이 클래스는 최종 클래스라는 의미로 더 이상 상속할 수 없습니다. 즉, final 클래스의 자식 클래스를 더 이상 만들 수 없습니다.

 

public final class 클래스명 {}

 

✔️final 필드

필드 선언 시 final 키워드를 붙이면 해당 필드는 초기화된 후 값을 변경할 수 없습니다. 따라서 선언과 동시에 초기화가 진행되어야 합니다.

 

public class Korean {
    // final 필드는 생성과 동시에 초기화 진행
    final String nationality = "Korea";
    final List<String> residence = new ArrayList<>();
    
    public void setNationality(String nationality) {
        this.nationality = nationality;  // final 필드 값 변경 시 에러 발생!!!
    }
    
    public void setResidence(List<String> residence) {
        this.residence = residence;  // final 필드 값 변경 시 에러 발생!!!
    }
    
    public void addResidence(String residence) {
        this.residence.add(residence);  // residence 객체 자체를 변경한 것이 아니라 내부의 값은 변경 가능!!!
    }
}

 

✔️final 메소드

메소드 선언 시 final 키워드를 붙이면 이 메소드는 최종 메소드이므로 더 이상 이 메소드를 오버라이딩할 수 없습니다.

 

public final 리턴타입 메소드명(매개변수, ...) {}

 

추상 클래스(Abstract Class)

추상(Abstract)란, 실체 간의 공통된 특성을 추출한 것입니다. 객체를 직접 생성할 수 있는 클래스를 실체 클래스라고 하고 이런 실체 클래스들의 공통적인 특성을 추출해 선언한 클래스를 추상 클래스라고 합니다.

 

추상 클래스는 abstract 키워드를 통해 선언합니다.

 

public abstract class 추상클래스명 {
    // 필드
    // 생성자
    // 메소드
}

 

추상 클래스도 필드, 생성자, 메소드를 모두 선언할 수 있지만, new를 통해 직접 객체(인스턴스)를 생성할 수 없습니다. 추상 클래스의 생성자는 추상 클래스를 상속 받은 자식 클래스에서 super() 키워드를 통해 접근할 수 있습니다. 따라서 반드시 추상 클래스에도 생성자가 필요합니다.

 

추상 클래스를 사용하는 용도는 다음과 같습니다.

  • 공통된 필드와 메소드를 통일할 목적으로 사용합니다. 각 구현 클래스마다 동일한 기능이지만 다른 이름을 가지는 필드와 메소드를 생성하는 것을 방지하고 유지보수를 쉽게 하기 위해 사용합니다.
  • 실체 클래스 구현 시 드는 시간을 절약할 수 있습니다. 이미 생성되어 있는 추상 클래스를 상속 받음으로써 현재 필요한 필드와 메소드 구현에만 집중할 수 있습니다.
  • 규격에 맞는 실체 클래스를 구현할 수 있습니다. abstract 메소드의 경우 반드시 오버라이딩 해야한다는 강제성이 있기 때문에 꼭 필요한 기능을 빠트리지 않고 구현할 수 있습니다.

추상 메소드와 오버라이딩

추상 클래스를 상속하는 자식 클래스들이 모두 동일한 메소드 구현 내용을 가지고 있다면 추상 메소드에서 해당 메소드를 구현해도 되지만 경우에 따라 각 자식 클래스에서 구현 내용을 다르게 해야 하는 경우가 있습니다. 이때는 추상 메소드를 선언하고 구현은 이를 상속받은 자식 클래스에서 하도록 만들 수 있습니다.

 

[public | protected] abstract 리턴타입 메소드명(매개변수, ...);

 

일반 메소드와의 차이점은 abstract 키워드가 붙어있고 {} 구현부가 없다는 점입니다.

 

// Animal이라는 추상 클래스 선언
public abstract class Animal {
    String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    // 추상 메소드
    public abstract void bark();
}

// Animal 추상 클래스를 상속 받은 Dog 클래스
public class Dog extends Animal {
    public Dog() {
        super("Dog");
    }
    
    @Override
    public void bark() {
        System.out.println(this.name + "의 짖는 소리는 멍멍!");
    }
}

// Animal 추상 클래스를 상속 받은 Cat 클래스
public class Cat extends Animal {
    public Cat() {
        super("Cat");
    }
    
    @Override
    public void bark() {
        System.out.println(this.name + "의 짖는 소리는 냐옹~");
    }
}

 

추상 메소드와 오버라이딩을 통해 다형성을 구현할 수 있습니다.

 

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.bark();  // 출력: Dog의 짖는 소리는 멍멍!
        
        animal = new Cat();
        animal.bark();  // 출력: Cat의 짖는 소리는 냐옹~
        
        animalBark(new Dog());
        animalBark(new Cat());
    }
    
    // 메소드 파라미터의 다형성 구현
    private static void animalBark(Animal animal) {
        animal.bark();
    }
}

 

참고

- 상속: wikidocs.net/280

- super 키워드: tcpschool.com/java/java_inheritance_super

- 다이나믹 메소드 디스패치: velog.io/@maigumi/Dynamic-Method-Dispatch

- 추상 클래스: limkydev.tistory.com/188

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함