JAVA - 상속

흑이·2022년 5월 15일
0

상속

상속이란 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 자바의 문법

두 클래스를 상위 클래스하위 클래스로 나누어 상위 클래스의 멤버(필드, 메서드, 이너 클래스)를 하위 클래스에게 내려주는 것을 의미한다.

따라서 하위 클래스의 멤버 개수는 언제나 상위 클래스와 비교했을 때 같거나 많다.



왜 상속을 사용할까

상속을 통해 클래스를 작성하면 코드를 재사용하여 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있어 코드의 중복을 제거할 수 있습니다.
더 나아가, 상속은 다형적 표현이 가능하다는 장점이 있다.

  • 예를 들면, ‘프로그래머는 프로그래머이다'라는 문장은 참입니다.
    그와 동시에 ‘프로그래머는 사람이다' 또한 참입니다.

  • 즉 하나의 객체가 여러 모양으로 표현될 수 있다는 것을 우리는 다형성이라 말합니다.



사용 방법

클래스를 상속할 때에는 extends 키워드를 사용하며, 클래스명 다음에 extends 상위 클래스명을 사용하여 정의



class Animal {

  // field and method of the parent class
  String name;
  public void eat() {
    System.out.println("I can eat");
  }
}

// inherit from Animal
class Dog extends Animal {

  // new method in subclass
  public void display() {
    System.out.println("My name is " + name);
  }
}

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

    // create an object of the subclass
    Dog labrador = new Dog();

    // access field of superclass
    labrador.name = "Rohu";
    labrador.display();

    // call method of superclass
    // using object of subclass
    labrador.eat();

  }
}

출력

My name is Rohu
I can eat

Animal 클래스로부터 Dog 클래스가 확장되어 Animal 클래스에 있는 속성과 기능들을 사용할 수 있는 것을 확인할 수 있다.

또한 클래스의 개별적인 속성과 기능들은 객체 생성 이후 개별적으로 정의해주고 있습니다.

만약 상속이 없었더라면 객체 하나하나 속성과 기능들을 모두 선언해주어야 했을 것이고, 그 경우 계속해서 같은 코드를 중복해야하는 번거로운 상황을 초래할 것입니다.


자바의 객체지향 프로그래밍에서는 단일 상속(single inheritance)만을 허용한다.

또 다른 객체지향언어인 C++에서는 여러 상위 클래스로부터 상속이 가능한 다중 상속을 허용하지만

자바에서는 다중 상속이 허용되지 않는다. 하지만 자바에서는 인터페이스라는 문법 요소를 통해 다중 상속과 비슷한 효과를 낼 수 있는 방법이 존재한다.



포함 관계

포함(composite)은 상속처럼 클래스를 재사용할 수 있는 방법으로, 클래스의 멤버로 다른 클래스 타입의 참조변수를 선언하는 것을 의미합니다.

public class Employee {
    int id;
    String name;
    Address address;

    public Employee(int id, String name, Address address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    void showInfo() {
        System.out.println(id + " " + name);
        System.out.println(address.city+ " " + address.country);
    }

    public static void main(String[] args) {
        Address address1 = new Address("서울", "한국");
        Address address2 = new Address("도쿄", "일본");

        Employee e = new Employee(1, "김코딩", address1);
        Employee e2 = new Employee(2, "박해커", address2);

        e.showInfo();
        e2.showInfo();
    }
}

class Address {
    String city, country;

    public Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}

//Output
1 김코딩
서울 한국
2 박해커
도쿄 일본

한 회사의 근로자(Employee)를 표현하기 위한 Employee 클래스의 멤버 변수로 근로자가 사는 주소를 나타내는 Address 클래스가 정의되어 있습니다.

원래라면 Address 클래스에 포함되어 있는 인스턴스 변수 city와 country를 각각 Employee 클래스의 변수로 정의해주어야 하지만

Address 클래스로 해당 변수들을 묶어준다음 Employee 클래스 안에 참조변수를 선언하는 방법으로 코드의 중복을 없애고 포함관계로 재사용하고 있습니다.



어떤 기준으로 상속? 포함? 관계 설정

가장 손쉬운 방법은 클래스 간의 관계가 ‘~은 ~이다(IS-A)’ 관계인지 ~은 ~을 가지고 있다(HAS-A) 관계인지 문장을 만들어 생각해보는 것입니다.

위의 예시로 예를 들어보면, Employee는 Address이다. 라는 문장은 성립하지 않는 반면, Employee는 Address를 가지고 있다. 는 어색하지 않은 올바른 문장임을 알 수 있다.

따라서 이 두 클래스는 포함 관계를 맺어주는 것이 적합하다.

반면 Car 클래스와 SportCar라는 클래스가 있다고 할 때, SportsCars는 Car를 가지고 있다. 라는 문장보다 SportsCar는 Car이다. 라는 문장이 훨씬 더 자연스럽게 들리고, 따라서 이 두 클래스는 Car를 상위클래스로 하는 상속관계를 맺어주는 것이 더 적합하다고 할 수 있다.



메서드 오버라이딩

매서드 오버라이딩(Method Overriding)은 상위 클래스로부터 상속받은 메서드와 동일한 이름의 메서드를 재정의하는 것을 의미한다.


예시

class Vehicle {
    void run() {
        System.out.println("Vehicle is running");
    }
}

public class Bike extends Vehicle { // Vehicle 클래스 상속
    void run() {
        System.out.println("Bike is running"); // 메서드 오버라이딩
    }

    public static void main(String[] args) {
        Bike bike = new Bike();
        bike.run();
    }
}

//Output
"Bike is running"

메서드 오버라이딩은 Bike 메서드가 Vehicle 클래스로 부터 상속받은 run() 메서드를 자신에 맞게 변경하는 일

  1. 메서드의 선언부(메서드 이름, 매개변수, 반환타입)이 상위클래스의 그것과 완전히 일치해야한다.
  2. 접근 제어자의 범위가 상위 클래스의 메서드보다 같거나 넓어야 한다.
  3. 예외는 상위 클래스의 메서드보다 많이 선언할 수 없다.

메소드 오버라이딩을 왜 사용할까?

예시

public class Main {
    public static void main(String[] args) {
        Bike bike = new Bike(); // 각각의 타입으로 선언 + 각각의 타입으로 객체 생성
        Car car = new Car();
        MotorBike motorBike = new MotorBike();
        
	    bike.run();
        car.run();
        motorBike.run();

	    Vehicle bike2 = new Bike(); // 상위 클래스 타입으로 선언 + 각각 타입으로 객체 생성
        Vehicle car2 = new Car();
        Vehicle motorBike2 = new MotorBike();

        bike2.run();
        car2.run();
        motorBike2.run();
    }
}

class Vehicle {
    void run() {
        System.out.println("Vehicle is running");
    }
}

class Bike extends Vehicle {
    void run() {
        System.out.println("Bike is running");
    }
}

class Car extends Vehicle {
    void run() {
        System.out.println("Car is running");
    }
}

class MotorBike extends Vehicle {
    void run() {
        System.out.println("MotorBike is running");
    }
}

//Output
Bike is running
Car is running
MotorBike is running

예시를 보면 Bike, Car, MotorBike라는 세 개의 클래스가 각각 Vehicle 클래스로부터 상속을 받아 run()메서드를 자신에게 맞는 방식으로 오버라이딩하고 있다.

따라서 각각의 객체의 run() 메서드를 실행하면 각 객체에 맞는 run() 메서드가 실행되어 결과값을 반환하고 있는 것을 확인할 수 있다.

다형적 표현을 사용하여 참조 변수 bike2, car2, motorBike2는 모두 Vehicle 타입이지만 메서드 오버라이딩을 통해 각각의 run() 메서드가 다른 출력값을 보여준다는 사실을 확인할 수 있다.

모든 객체를 상위 클래스 타입 하나로 선언하면 다음과 같이 간편하게 배열로 선언하여 관리할 수 있다는 편리성이 있다.


배열로 선언하여 관리

// 배열로 한번에 관리하기

Vehicle[] vehicles = new Vehicle[] { new Bike(), new Car(), new MotorBike()};
for (Vehicle vehicle : vehicles) {
		vehicle.run();
}

//Output
Bike is running
Car is running
MotorBike is running


super 키워드와 super()

super 키워드는 상위 클래스의 객체, super()는 상위 클래스의 생성자를 의미한다.


public class Super {
    public static void main(String[] args) {
        Lower l = new Lower();
        l.callNum();
    }
}

class Upper {
    int count = 20; // super.count
}

class Lower extends Upper {
    int count = 15; // this.count

    void callNum() {
        System.out.println("count = " + count);
        System.out.println("this.count = " + this.count);
        System.out.println("super.count = " + super.count);
    }
}

//Output
count = 15
count = 15
count = 20

Lower 클래스는 Upper 클래스로부터 count를 상속받는데, 자신의 인스턴스 변수 count와 이름이 같아 둘을 구분할 방법이 필요합니다.

두 개의 같은 이름의 변수를 구분하기 위한 방법이 super 키워드입니다.

만약 super 키워드를 붙이지 않는다면, 자바 컴파일러는 해당 객체는 자신이 속한 인스턴스 객체의 멤버를 먼저 참조합니다.

super 키워드를 사용하면 부모의 객체의 멤버 값을 참고할 수 있습니다.

위의 예시에서 첫 번째 count는 자기에게서 가장 가까운 count 변수, 즉 15를 가리킵니다.

두 번째 카운트 또한 자신이 호출된 객체의 인스턴스 변수를 가리키기 때문에 15를 출력합니다.

마지막으로 super 키워드를 사용하여 호출한 count는 상위 클래스의 변수를 참조하여 숫자 20을 출력합니다.


super()

super()도 this() 메서드처럼 생성자입니다. this()와의 차이는 this()가 같은 클래스의 다른 생성자를 호출하는데 사용하는 반면, super()는 상위 클래스의 생성자를 호출하는데 사용된다는 것


public class Test {
    public static void main(String[] args) {
        Student s = new Student();
    }
}

class Human {
    Human() {
        System.out.println("휴먼 클래스 생성자");
    }
}

class Student extends Human { // Human 클래스로부터 상속
    Student() {    
        super(); // Human 클래스의 생성자 호출
        System.out.println("학생 클래스 생성자");
    }
}

//Output
휴먼 클래스 생성자
학생 클래스 생성자

Human 클래스를 확장하여 Student 클래스를 생성하고, Student 생성자를 통해 상위 클래스 Human 클래스의 생성자를 호출하고 있습니다.

super() 메서드 또한 this()와 마찬가지로 생성자 안에서만 사용가능하고, 반드시 첫 줄에 와야 합니다.

예제에서 super() 메서드는 Student 클래스 내부에서 호출되고 있고 상위 클래스 Human의 생성자를 호출하고 있습니다. 따라서 출력값으로 “휴먼 클래스 생성자”와 "학생 클래스 생성자"가 순차적으로 출력됩니다.

모든 생성자의 첫 줄에는 반드시 this() 또는 super()가 선언되어야 한다.

만약 super()가 없는 경우에는 컴파일러가 생성자의 첫 줄에 자동으로 super()를 삽입합니다.

이때 상위클래스에 기본생성자가 없으면 에러가 발생하게 됩니다.

따라서 클래스를 만들 때는 자동으로 기본 생성자를 생성해야 한다.



Object 클래스

Object 클래스는 자바의 클래스 상속계층도에서 최상위에 위치한 상위클래스이다.

따라서 자바의 모든 클래스는 Object 클래스를 상속받는다는 명제는 참이 된다.

실제로 자바 컴파일러는 다른 클래스로부터 아무런 상속을 받지 않는 클래스에 자동적으로 extends Object를 추가하여 Object 클래스를 상속받도록 합니다.


class ParentEx { // extends Object 자동 추가 

}

class ChildEx extends ParentEx {

}

ParentEx 클래스를 상속받아 ChildEx 클래스를 만들었을 때 상위클래스 ParentEx는 아무것도 상속하고 있지 않기에 컴파일러는 extends Object를 삽입하는 것입니다.

Object 클래스는 자바 클래스의 상속계층도에 가장 위에 위치하기 때문에 Object 클래스의 멤버들을 자동으로 상속받아 사용할 수 있습니다.

아래와 같은 메서드들을 따로 정의하지 않고도 사용가능

https://www.programiz.com/java-programming/inheritance

0개의 댓글