본문 바로가기
코딩

[ Java ] 실전자바-기본편 9.상속

by 모두의 아카이브 2024. 2. 19.
반응형

상속

상속 관계가 필요한 이유

 

package extends1.ex1;
public class ElectricCar {
 public void move() {
 	System.out.println("차를 이동합니다.");
 }
 public void charge() {
 	System.out.println("충전합니다.");
 }
}
package extends1.ex1;
public class GasCar {
 public void move() {
 	System.out.println("차를 이동합니다.");
 }
 public void fillUp() {
 	System.out.println("기름을 주유합니다.");
 }
}
package extends1.ex1;
public class CarMain {
 public static void main(String[] args) {
     ElectricCar electricCar = new ElectricCar();
     electricCar.move();
     electricCar.charge();
     GasCar gasCar = new GasCar();
     gasCar.move();
     gasCar.fillUp();
 }
}

 

실행 결과

차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.

전기차와 가솔린 차를 만들었다.

전기차와 가솔린차는 자동차의 좀 더 구체적인 개념이다. 반대로 자동차는 전기차와 가솔린차를 포함하는 추상적인 개념이다. 그래서 잘 보면 둘의 공통점이 보이는데 이동( move() )이다.

전기차든 가솔린차든 주유방식이 다를 뿐 이동하는 것은 똑같기 때문에, 이런 경우 상속관계를 사용하는 것이 효과적이다.

 

상속관계

상속은 객체 지향 프로그래밍의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해준다. 이름 그대로 기존 클래스의 속성과 기능을 그대로 물려받는것이다.

상속을 사용하려면  extends 키워드를 사용하면 되고, extends 대상은 하나만 선택할 수 있다.

 

용어정리

  • 부모 클래스(슈퍼 클래스) : 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
  • 자식 클래스(서브 클래스) : 부모 클래스로부터 필드와 메서드를 상속받는 클래스

 

package extends1.ex2;
public class Car {
 public void move() {
 System.out.println("차를 이동합니다.");
 }
}

Car라는 부모클래스를 만들고 공통 기능은 move() 가 포함

package extends1.ex2;
public class ElectricCar extends Car {
 public void charge() {
 System.out.println("충전합니다.");
 }
}

전기차는 extends Car를 통해 부모클래스인 Car를 상속받는다 덕분에 ElectricCar에서도 move()를 사용할 수 있다.

package extends1.ex2;
public class GasCar extends Car {
 public void fillUp() {
 System.out.println("기름을 주유합니다.");
 }
}

가솔린 차도 Car를 상속받아 move() 사용가능

package extends1.ex2;
public class CarMain {
 public static void main(String[] args) {
     ElectricCar electricCar = new ElectricCar();
     electricCar.move();
     electricCar.charge();
     GasCar gasCar = new GasCar();
     gasCar.move();
     gasCar.fillUp();
 }
}
차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.

// 실행 결과는 기존꺼와 완전히 동일하다.

 

상속 구조도

여기서 주의할점은 상속은 부모의 기능을 자식이 물려받는거지, 반대로 부모클래스는 자식 클래스에 접근할 수 없다.

이유는 부모코드를 보면 자식에 대한 정보가 없지만 자식코드는 extends Car를 통해 부모를 알고 있다.

단일 상속

자바에서는 다중 상속을 지원하지 않는다.

예를 들어 해당 그림처럼 다중 상속을 사용하게 되면 AirplaneCar 입장에서 move()를 호출할 때 어떤 부모의 move()를 사용해야 할지 애매한 문제가 발생한다. 이후에 인터페이스 다중 구현을 허용해 이러한 문제를 피한다.

 

상속과 메모리 구조

상속 관계를 객체로 생성할 때 메모리 구조를 확인해보자!

ElectricCar electricCar = new ElectricCar();

ElectricCar()를 호출하면 ElectricCar뿐만 아니라 상속 관계에 있는 Car까지 함께 포함해서 인스턴스를 생성한다.

참조값은 x001 하나지만, 실제로 그 안에서는 Car,ElectricCar라는 두가지 클래스 정보가 공전하는 것이다.

 

electricCar.charge() 호출

ElectricCar.charge()를 호출할때 부모인 Car를 통해서 charge()를 찾을지 아니면 ElectricCar를 통해서 charge()를 찾을지 선택해야 한다. 이때는 호출하는 변수의 타입(클래스)을 기준으로 선택한다. electricCar 변수의 타입이 ElectricCar이므로

인스턴스 내부에 같은 타입인 ElectricCar를 통해서 charge()를 호출한다.

electricCar.move() 호출

electricCar.move()를 호출하면 호출하는 변수는 electricCar의 타입이 ElectricCar 이므로 이 타입을 선택한다.

그런데 ElectricCar에는 move() 메서드가 없기 때문에 상속관계에서는 자식타입에 해당 기능이 없으면 부모타입으로 올라가서 찾는다. 부모인 Car에 move()가 있으므로 부모에 있는 move() 메서드를 호출한다.


만약 부모에서도 해당 기능을 찾지 못하면 더 상위 부모에서 필요한 기능을 찾아본다. 계속 올라가면서 찾아도 없으면 컴파일 오류 발생!

 

정리

  • 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성된다.
  • 상속 관계의 객체를 호출할 떄, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
  • 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류가 발생한다.

상속과 기능 추가 

상속의 장점을 알아보기 위해 상속관계에 다음 기능을 추가해보자!

1. 모든 차량에 문열기(openDoor() ) 기능을 추가

2. 새로운 수소차를 추가

package extends1.ex3;
public class Car {
 public void move() {
 	System.out.println("차를 이동합니다.");
 }
 //추가
 public void openDoor() {
 	System.out.println("문을 엽니다.");
 }

이렇게 하면 Car의 자식들은 해당 기능을 모두 물려받게 된다. 만약 상속관계가 아니었다면 각각의 차량에 해당 기능을
모두 추가해야 한다.

 

수소차 추가

package extends1.ex3;
//추가
public class HydrogenCar extends Car {
 public void fillHydrogen() {
	 System.out.println("수소를 충전합니다.");
 }
}

 

상속관계 덕분에 중복은 줄고, 새로운 수소차를 편리하게 확장할 수 있다.

 

상속과 메서드 오버라이딩

상속 받은 기능을 자식이 재정의 하는 것을 메서드 오버라이딩이라 한다.

package extends1.overriding;
public class Car {
 public void move() {
 	System.out.println("차를 이동합니다.");
 }
 public void openDoor() {
	 System.out.println("문을 엽니다.");
 }
}
package extends1.overriding;
public class ElectricCar extends Car {
 @Override
 public void move() {
	 System.out.println("전기차를 빠르게 이동합니다.");
 }
 public void charge() {
	 System.out.println("충전합니다.");
 }
}

 

ElectricCar는 부모인 Car의 move() 기능을 그대로 사용하고 싶지 않다.메서드 이름은 같지만 새로운 기능을 사용하고 싶어서  ElectricCar의 move() 매서드를 새로 만들었다.

 

@Override

@이 붙은 부분은  애노테이션이라 한다.

해당 애노테이션은 상위 클래스의 메서드를 오버라이드 하는 것임을 나타낸다.

해당 경우에 부모에 move() 메서드가 없다면 컴파일 오류가 발생한다.

 

오버라이딩과 클래스

 

오버라이딩과 메모리 구조

  • 1. electricCar.move() 를 호출한다.
  • 2. 호출한 electricCar 의 타입은 ElectricCar 이다. 따라서 인스턴스 내부의 ElectricCar 타입에서 시작 한다.
  • 3. ElectricCar 타입에 move() 메서드가 있다. 해당 메서드를 실행한다. 이때 실행할 메서드를 이미 찾았으므 로 부모 타입을 찾지 않는다

메서드 오버라이딩 조건

  • 메서드 이름: 메서드 이름이 같아야 한다.
  • 메서드 매개변수(파라미터): 매개변수(파라미터)타입, 순서, 개수가 같아야 한다.
  • 반환 타입: 반환 타입이 같아야 한다. 단 반환 타입이 하위 클래스 타입일 수 있다.
  • 접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다. 예를 들어, 상위 클래스의 메서드가 protected로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected로 오버라이드 할 수 있지만, prviate 또는 default로 오버라이드 할 수 없다.
  • 예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws 로 선언할 수 없다. 하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다. 예외를 학습해야 이해할 수 있다. 예외는 뒤 에서 다룬다.
  • static , final , private : 키워드가 붙은 메서드는 오버라이딩 될 수 없다.
    • static 은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없다. 쉽게 이 야기해서 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 된다.
    • final 메서드는 재정의를 금지한다.
    • private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않는다. 따라서 오 버라이딩 할 수 없다.
  •  생성자 오버라이딩: 생성자는 오버라이딩 할 수 없다.

super - 부모 참조

부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어 있으면, 자식에서 부모의 필드나 메서드를 호출할 수 없다.

이때 super 키워드를 사용하면 부모를 참조할 수 있다. super는 부모 클래스의 참조를 나타낸다.

 

해당 사진처럼 부모와 자식의 필드명이 value와 hello()가 같을때 호출하고 싶다면 super 키워드 사용

package extends1.super1;
public class Parent {
 public String value = "parent";
 public void hello() {
 	System.out.println("Parent.hello");
 }
}
package extends1.super1;
public class Child extends Parent {
 public String value = "child";
 @Override
 public void hello() {
	 System.out.println("Child.hello");
 }
 public void call() {
     System.out.println("this value = " + this.value); //this 생략 가능
     System.out.println("super value = " + super.value);
     this.hello(); //this 생략 가능
     super.hello();
 }
}

 

call() 메서드를 보면 필드 이름과 메서드 이름이 같지만 super를 사용해서 부모 클래스에 있는 기능을 사용할 수 있다.

package extends1.super1;
public class Super1Main {
 public static void main(String[] args) {
     Child child = new Child();
     child.call();
 }
}

 

실행결과

this value = child
super value = parent
Child.hello
Parent.hello

 

super 메모리 그림

 

super - 생성자

상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야한다.(규칙)

 

package extends1.super2;
public class ClassA {
 public ClassA() {
 	System.out.println("ClassA 생성자");
 }
}

class A는 최상위 부모 클래스이다.

package extends1.super2;
public class ClassB extends ClassA {
 public ClassB(int a) {
     super(); //기본 생성자 생략 가능
     System.out.println("ClassB 생성자 a="+a);
 }
 public ClassB(int a, int b) {
     super(); //기본 생성자 생략 가능
     System.out.println("ClassB 생성자 a="+a + " b=" + b);
 }
}
  • classB는 classA를 상속 받았다. 상속을 받으면 생성자의 첫줄에 super()를 사용해서 부모 클래스의 생성자를 호출해야 한다.
    • 예외로 생성자 첫줄에 this()를 사용할 수는 있다. 하지만 super()는 자식의 생성자 안에서 언젠가는 반드시 호출해야한다.
  • 부모 클래스의 생성자가 기본생성자인 경우에는 super() 생략가능
package extends1.super2;
public class ClassC extends ClassB {
 public ClassC() {
     super(10, 20);
     System.out.println("ClassC 생성자");
 }
}
  • ClassC 는 ClassB 를 상속 받았다. ClassB 다음 두 생성자가 있다.
    • ClassB(int a)
    • ClassB(int a, int b)
  •  생성자는 하나만 호출할 수 있다. 두 생성자 중에 하나를 선택하면 된다.
    • super(10, 20) 를 통해 부모 클래스의 ClassB(int a, int b) 생성자를 선택했다.
  • 참고로 ClassC 의 부모인 ClassB 에는 기본 생성자가 없다. 따라서 부모의 기본 생성자를 호출하는 super() 를 사용하거나 생략할 수 없다
  •  
package extends1.super2;
public class Super2Main {
 public static void main(String[] args) {
 	ClassC classC = new ClassC();
 }
}

 

실행결과

ClassA 생성자
ClassB 생성자 a=10 b=20
ClassC 생성자

 

실행해보면 ClassA ClassB ClassC 순서로 실행된다. 생성자의 실행 순서가 결과적으로 최상위 부모부터 실행되어서 하나씩 아래로 내려오는 것이다. 따라서 초기화는 최상위 부모부터 이루어진다. 왜냐하면 자식 생성자의 첫 줄에서 부모의 생성자를 호출해야 하기 때문이다.

반응형