9-1. 상속 시작
ElectricCar.java
package extends1.ex1;
public class ElectricCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
GasCar.java
package extends1.ex1;
public class GasCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
CarMain.java
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();
}
}
- 전기차와 가솔린차는 자동차(Car)의 좀 더 구체적인 개념
- 자동차(Car)는 전기차와 가솔린차를 포함하는 추상적인 개념
- 전기차와 가솔린차는 공통 기능 move()가 있어 상속 관계를 사용하는 것이 효과적
9-2. 상속 관계
상속
- 객체 지향 프로그래밍의 핵심 요소 중 하나
- 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용 가능
=> 기존 클래스의 속성과 기능을 그대로 물려받는 것 - extends 키워드로 상속을 사용하고, 대상은 하나만 선택 가능
용어
- 부모 클래스 (슈퍼 클래스): 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
- 자식 클래스 (서브 클래스): 부모 클래스로부터 필드와 메서드를 상속받는 클래스
Car.java
package extends1.ex2;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
}
ElectricCar.java
package extends1.ex2;
public class ElectricCar extends Car {
public void charge() {
System.out.println("충전합니다.");
}
}
GasCar.java
package extends1.ex2;
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
CarMain.java
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();
}
}
- 전기차와 가솔린차가 Car를 상속받은 덕분에 electricCar.move(), gasCar.move()를 사용
- 부모는 자식에 대한 정보가 없고, 자식은 extends Car를 통해 부모를 안다.
단일 상속
- 자바는 다중 상속을 지원하지 않음 => extend 대상은 하나만 선택
- 부모가 또 다른 부모를 가질 수 있다.
다이아몬드 문제
- 비행기와 자동차를 상속 받아서 하늘을 나는 자동차를 만든다고 가정
- AirplaneCar입장에서 move()를 호출할 때,
- 어떤 부모의 move()를 사용해야 할지 애매해지는 문제
- 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해 짐
※ 인터페이스의 다중 구현을 허용해서 위 와 같은 문제를 피한다.
9-3. 상속과 메모리 구조 (중요❗)
ElectricCar electricCar = new ElectricCar(); // new ElectricCar() 호출
- new ElectricCar()를 호출하면,
- 상속관계에 있는 Car까지 포함해 인스턴스를 생성
(부모 클래스도 함께 포함해서 생성) - 외부에서는 하나의 인스턴스처럼 보임
- 내부에서는 부모와 자식 모두 생성, 공간 구분
electricCar.charge() // 호출
- 참조값을 확인해서 x001.charge() 를 호출
- 내부에 부모와 자식이 모두 존재
- 호출하는 변수의 타입(클래스)을 기준으로 선택
- 같은 타입인 ElectricCar 를 통해서 charge() 를 호출
electricCar.move() // 호출
- 호출하는 변수 electricCar의 타입이 ElectricCar이므로 이 타입을 선택
- 자식 타입에 해당 기능이 없으면,
부모 타입으로 이동 - 부모에서도 해당 기능 없으면 상위 부모로 이동
정리
- 상속 관계의 객체를 생성하면, 내부에 부모와 자식 모두 생성
- 상속 관계의 객체를 호출할 때, 호출자의 타입을 통해 대상 타입 물색
- 현재 타입에서 기능을 못 찾으면, 상위 부모 타입으로 이동해 기능을 찾아서 실행
- 기능을 찾지 못하면 컴파일 오류 발생
9-4. 상속과 기능 추가
상속 관계의 장점 알아보기
- 모든 차량에 문열기 기능 추가 => openDoor()
- 새로운 수소차 추가 => HydrogenCar
- 수소차는 수소 충전 기능 보유 => fillHydrogen()
ex3을 만들어, ex2의 모든 클래스 복붙
ex3.Car.java (openDoor( ) 추가)
package extends1.ex3;
public class Car {
public void move() { System.out.println("차를 이동합니다."); }
// 추가
public void openDoor() { System.out.println("문을 엽니다."); }
}
HydrogenCar.java
package extends1.ex3;
// 추가
public class HydrogenCar {
public void fillHydrogen() {
System.out.println("수소를 충전합니다.");
}
}
CarMain.java
package extends1.ex3;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
electricCar.openDoor();
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
gasCar.openDoor();
HydrogenCar hydrogenCar = new HydrogenCar();
hydrogenCar.move();
hydrogenCar.fillHydrogen();
hydrogenCar.openDoor();
}
}
기능 추가와 클래스 확장
- 상속 관계 덕분에 중복은 줄어들고,
- 새로운 수소차를 편리하게 확장(extend)
9-5. 상속과 메서드 오버라이딩
메서드 오버라이딩(Overriding)
부모에게서 상속 받은 기능을 자식이 재정의 하는 것
- 자동차의 Car.move() 라는 기능을 사용하면, "차를 이동합니다."라고 출력
- 전기차가 move() 를 호출한 경우에는 "전기차를 빠르게 이동합니다."라고 출력을 변경
overriding을 만들어, ex3의 모든 클래스 복붙
overriding.ElectricCar.java (나머지 클래스는 기존과 동일)
package extends1.overriding;
public class ElectricCar extends Car {
@Override // 메서드 오버라이딩
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
CarMain.java
package extends1.overriding;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
GasCar gasCar = new GasCar();
gasCar.move();
}
}
@Override
- @이 있는 부분을 애노테이션이라 하고, 주석과 비슷한데 프로그램이 읽을 수 있는 특별한 주석
- 이 @Override 애노테이션은 상위 클래스의 메서드를 오버라이드하는 것
- 컴파일러는 이 애노테이션을 보고 메서드가 정확히 오버라이드 되었는지 확인
- 오버라이딩 조건을 만족시키지 않으면 컴파일 에러 발생
=> 이 경우에 만약 부모에 move() 메서드가 없다면 컴파일 오류가 발생 - 이 기능은 필수는 아니지만(생략 가능) 코드의 명확성을 위해 붙여주는 것이 좋다.
오버로딩(Overloading)과 오버라이딩(Overriding)
- 메서드 오버로딩
- 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것
- 오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻
- 같은 이름의 메서드를 여러개 정의
- 메서드 오버라이딩
- 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정 = 부모의 기능을 자식이 다시 정의하는 것
- 따라서 상속 관계에서 사용 => 기존 기능을 다시 정의
- 오버라이딩을 단순히 해석하면 무언가를 넘어서 타는 것
=> 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다 - 우리말로 번역하면 무언가를 다시 정의한다고 해서 재정의
- 실무에서는 메서드 오버라이딩, 메서드 재정의 둘 다 사용
메서드 오버라이딩 조건 (참고)
- 메서드 이름: 메서드 이름이 같아야 한다.
- 메서드 매개변수(파라미터): 매개변수(파라미터) 타입, 순서, 개수가 같아야 한다.
- 반환 타입: 반환 타입이 같아야 한다. 단, 반환 타입이 하위 클래스 타입일 수 있다.
- 접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다.
예를 들어, 상위 클래스의 메서드가 protected 로 선언되어 있으면
하위 클래스에서 이를 public 또는 protected 로 오버라이드할 수 있지만, (가능)
private 또는 default 로 오버라이드 할 수 없다. (불가능) - 예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws 로 선언할 수 없다.
하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다. 예외는 뒤에서 다룬다. - static , final , private : 키워드가 붙은 메서드는 오버라이딩 될 수 없다
- static은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없다.
그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 된다. - final메서드는 재정의를 금지한다. (final은 메서드를 변경 안되게 고정한다는 의미)
- private메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않는다.
- static은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없다.
- 생성자 오버라이딩: 생성자는 오버라이딩 할 수 없다.
9-6. 상속과 접근 제어
- 접근 제어 설명을 위해 부모, 자식 패키지를 따로 분리
- 접근 제어자 표현을 위해 UML 표기법 일부 사용
+ : public => 모든 외부 호출을 허용
# : protected => (package-private) 같은 패키지 안에서 호출은 허용
~ : default => 같은 패키지 호출은 허용, 다른 패키지의 상속 관계 호출은 허용
- : private => 모든 외부 호출을 불가
parent.Parent.java
package extends1.access.parent;
public class Parent {
public int publicValue;
protected int protectedValue;
int defaultValue;
private int privateValue;
public void publicMethod() {
System.out.println("Parent.publicMethod");
}
protected void protectedMethod() {
System.out.println("Parent.protectedMethod");
}
void defaultMethod() {
System.out.println("Parent.defaultMethod");
}
private void privateMethod() {
System.out.println("Parent.privateMethod");
}
public void printParent() { // 자기자신 호출
System.out.println("==Parent 메서드 안==");
System.out.println("publicValue = " + publicValue);
System.out.println("protectedValue = " + protectedValue);
System.out.println("defaultValue = " + defaultValue); // 부모 메서드 안에서 접근 가능
System.out.println("privateValue = " + privateValue); // 부모 메서드 안에서 접근 가능
// 부모 메서드 안에서 모두 접근 가능
defaultMethod();
privateMethod();
}
}
child.Child.java
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; // 상속 관계 or 같은 패키지
//defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
//privateValue = 1; // 접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
//defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
//privateMethod(); // 접근 불가, 컴파일 오류
printParent(); // 이 메서드는 public 메서드
}
}
둘의 패키지가 다르다는 부분의 유의
자식 클래스인 Child에서 부모 클래스인 Parent에 접근 가능 범위 확인
- publicValue = 1 : 부모의 public 필드에 접근
- protectedValue = 1 : 부모의 protected 필드에 접근. 다른 패키지이지만, 상속 관계이므로 접근
- defaultValue = 1 : 자식과 부모가 다른 패키지이므로 접근할 수 없다.
- privateValue = 1 : private은 모든 외부 접근을 막으므로 자식이라도 호출할 수 없다.
access.ExtendsAccessMain.java
package extends1.access;
import extends1.access.child.Child;
public class ExtendsAccessMain {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
접근 제어와 메모리 구조
- 본인 타입에 없으면 부모 타입에서 기능을 찾는데,
이때 접근 제어자가 영향을 준다. - 객체 내부에서는 자식과 부모가 구분되어 있기 때문
- 결국 자식 타입에서 부모 타입의 기능을 호출할 때,
부모 입장에서 보면 외부에서 호출한 것과 같다.
9-7. super - 부모 참조
super
- 부모와 자식의 필드명이 같거나, 메서드가 오버라이딩 되어 있을 때,
- 자식에서 부모의 필드나 메서드를 호출 못함
- 이때, super 키워드를 사용하면 부모 클래스 참조 가능
super1.Parent.java
package extends1.super1;
public class Parent {
public String value = "parent";
public void hello() {
System.out.println("Parent.hello");
}
}
super1.Child.java
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 생략 가능 => 자식에서 hello()찾고 없으면 부모로 이동
super.hello();
}
}
- this 는 자기 자신의 참조 => this 생략 가능
- super 는 부모 클래스 참조
Super1Main.java
package extends1.super1;
public class Super1Main {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
9-8. super - 생성자
상속 관계의 인스턴스를 생성하면 => 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어진다.
Child 를 만들면 부모인 Parent 까지 함께 만들어지는 것 (위 그림 참고)
따라서 각각의 생성자도 모두 호출 되어야 한다.
상속 관계를 사용하면, 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다. (규칙)
상속 관계에서 부모의 생성자를 호출할 때는 super(...) 를 사용하면 된다.
ClassA.java (최상위 부모 클래스)
package extends1.super2;
public class ClassA {
public ClassA() {
System.out.println("ClassA '기본'생성자");
}
}
ClassB.java
package extends1.super2;
public class ClassB extends ClassA {
public ClassB(int a) {
super(); // 기본 생성자는 생략 가능 => java에서 자동 생성해 줌
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()를 생략할 수 있다.
- 상속 관계에서 첫줄에 super(...) 를 생략하면 자바는 부모의 기본 생성자를 호출하는 super() 를 자동 생성
참고로 기본 생성자를 많이 사용하기 때문에 편의상 이런 기능을 제공
ClassC.java
package extends1.super2;
public class ClassC extends ClassB {
public ClassC() {
super(10, 20); // ClassB에는 기본생성자가 없어서 생략 불가능
System.out.println("ClassC 생성자");
}
}
- ClassC 의 부모인 ClassB 에는 기본 생성자가 없다. super()를 사용하거나 생략할 수 없다.
- 사용자 정의 생성자를 만들면, 자바에서는 기본생성자를 만들지 않는다.
- ClassC 는 ClassB 를 상속 받았다. ClassB 다음 두 생성자가 있다.
- ClassB(int a)
- ClassB(int a, int b)
- 생성자는 하나만 호출할 수 있다. 두 생성자 중에 하나를 선택하면 된다.
- super(10, 20) 를 통해 부모 클래스의 ClassB(int a, int b) 생성자를 선택
Super2Main.java
package extends1.super2;
public class Super2Main {
public static void main(String[] args) {
ClassC classC = new ClassC();
}
}
- ClassA ▶ ClassB ▶ ClassC 순서로 실행
- 생성자의 실행 순서가 결과적으로 최상위 부모부터 실행되어서 하나씩 아래로 내려오는 것
- 초기화는 최상위 부모부터 => 자식 생성자의 첫 줄에서 부모의 생성자를 호출해야 하기 때문
- new ClassC()를 통해 ClassC 인스턴스 생성
- ClassC()의 생성자 먼저 호출
- ClassC() 생성자의 super(..)를 통해 ClassB(..)
생성자 호출 - ClassB(..) 생성자의 super(..)를 통해 ClassA()
생성자 호출 - ClassA()가 생성자 코드 실행 후 호출 종료
- ClassA()를 호출한 ClassB(..) 생성자로 제어권이
돌아감 - ClassB(..)가 생성자 코드 실행 후 호출 종료
- ClassB(..)를 호출한 ClassC() 생성자로 제어권 돌아감
- ClassC()가 마지막으로 생성자 코드 실행
정리
- 상속 관계의 생성자 호출은 부모에서 자식 순으로 실행
- 부모의 데이터를 먼저 초기화하고, 그 다음에 자식의 데이터를 초기화
- 상속 관계에서 자식 클래스의 생성자 첫줄에 반드시 super(..) 호출
- 단, 기본 생성자 super()인 경우 생략 가능
this(...)와 함께 사용
코드의 첫줄에 this(...)를 사용하더라도 반드시 한번은 super(...)를 호출해야 한다.
package extends1.super2;
public class ClassB extends ClassA {
public ClassB(int a) {
this(a, 0); // 코드 변경
System.out.println("ClassB 생성자 a="+a);
}
public ClassB(int a, int b) {
super(); // 기본 생성자는 생략 가능
System.out.println("ClassB 생성자 a="+a + " b=" + b);
}
}
package extends1.super2;
public class Super2Main {
public static void main(String[] args) {
//ClassC classC = new ClassC();
ClassB classB = new ClassB(100); // 코드 변경
}
}
9-9. 문제와 풀이
Item.java
package extends1.ex;
public class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public void print() {
System.out.println("이름:" + name + ", 가격:" + price);
}
public int getPrice() {
return price;
}
}
Book.java
package extends1.ex;
public class Book extends Item {
private String author;
private String isbn;
public Book(String name, int price, String author, String isbn) {
super(name, price);
this.author = author;
this.isbn = isbn;
}
@Override
public void print() {
super.print();
System.out.println("- 저자:" + author + ", isbn:" + isbn);
}
}
Album.java
package extends1.ex;
public class Album extends Item {
private String artist;
public Album(String name, int price, String artist) {
super(name, price);
this.artist = artist;
}
@Override
public void print() {
super.print();
System.out.println("- 아티스트:" + artist);
}
}
Movie.java
package extends1.ex;
public class Movie extends Item{
private String director;
private String actor;
public Movie(String name, int price, String director, String actor) {
super(name, price);
this.director = director;
this.actor = actor;
}
@Override
public void print() {
super.print();
System.out.println("- 감독:" + director + ", 배우:" + actor);
}
}
9-10. 클래스와 메서드에 사용되는 final
클래스에 final
- 상속 끝!
- final 로 선언된 클래스는 확장될 수 없다. 다른 클래스가 final 로 선언된 클래스를 상속받을 수 없다.
- 예: public final class MyFinalClass {...}
메서드에 final
- 오버라이딩 끝!
- final 로 선언된 메서드는 오버라이드 될 수 없다. 상속받은 서브 클래스에서 이 메서드를 변경할 수 없다.
- 예: public final void myFinalMethod() {...}