[Java] 김영한의 실전 자바 - 기본편 섹션9,10 상속,다형성
○ 섹션 9 상속
◼️상속 관계
- 상속을 사용하려면 extends 키워드를 사용하면 된다. 그리고 extends 대상은 하나만 선택할 수 있다.
- 부모 클래스 (슈퍼 클래스): 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
- 자식 클래스 (서브 클래스): 부모 클래스로부터 필드와 메서드를 상속받는 클래스
◼️상속과 메모리 구조
ElectricCar electricCar = new ElectricCar();
new ElectricCar() 를 호출하면 상속 관계에 있는 Car 까지 함께 포함해서 인스턴스를 생성한다.
외부에서 볼때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 구분된다.
메소드를 호출할 때는 호출하는 변수의 타입(클래스)을 기준으로 선택한다. electricCar 변수의 타입이 ElectricCar 이므로 인스턴스 내부에 같은 타입인 ElectricCar 를 통해서 charge() 를 호출한다.
만약 부모에서도 해당 기능을 찾지 못하면 더 상위 부모에서 필요한 기능을 찾아본다. 부모에 부모로 계속 올라가면서 필드나 메서드를 찾는 것이다. 물론 계속 찾아도 없으면 컴파일 오류가 발생한다.
🟢 상속과 메모리 구조 정리
- 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성된다.
- 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
- 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류 가 발생한다.
◼️상속과 메서드 오버라이딩
- 메서드 오버라이딩: 부모에게서 상속 받은 기능을 자식이 재정의 하는 것
🟢 @Override
- 상위 클래스의 메서드를 오버라이드하는 것
- 컴파일러는 이 애노테이션을 보고 메서드가 정확히 오버라이드 되었는지 확인한다. 오버라이딩 조건을 만족시키지 않으
면 컴파일 에러를 발생시킨다. 따라서 실수로 오버라이딩을 못하는 경우를 방지해준다.
🟢 오버로딩(Overloading)과 오버라이딩(Overriding)
메서드 오버로딩: 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것. 오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻이다. 따라서 같은 이름 의 메서드를 여러개 정의했다고 이해하면 된다.
메서드 오버라이딩: 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정. 따라서 상속 관계에서 사용한다. 부모의 기능을 자식이 다시 정의하는 것이다. 오버라이딩을 단순히 해석하면 무 언가를 넘어서 타는 것을 말한다. 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으 로 덮어버린다고 이해하면 된다. 오버라이딩을 우리말로 번역하면 무언가를 다시 정의한다고 해서 재정의라 한다. 상속 관계에서는 기존 기능을 다시 정의한다고 이해하면 된다. 실무에서는 메서드 오버라이딩, 메서드 재정의 둘 다 사용한다.
🟢 메서드 오버라이딩 조건
- 메서드 이름: 메서드 이름이 같아야 한다.
- 메서드 매개변수(파라미터): 매개변수(파라미터) 타입, 순서, 개수가 같아야 한다.
- 반환 타입: 반환 타입이 같아야 한다. 단 반환 타입이 하위 클래스 타입일 수 있다.
- 접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다. 예를 들어, 상위 클래스의 메서드가 protected 로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected 로 오버라이드할 수 있지만, private 또는 default 로 오버라이드 할 수 없다.
- 예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws 로 선언할 수 없다. 하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다. 예외를 학습해야 이해할 수 있다. 예외는 뒤 에서 다룬다.
- static , final , private : 키워드가 붙은 메서드는 오버라이딩 될 수 없다.
- 생성자 오버라이딩: 생성자는 오버라이딩 할 수 없다.
◼️상속과 접근 제어
본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이때 접근 제어자가 영향을 준다. 왜냐하면 객체 내부에서는 자식과 부모가 구분되어 있기 때문이다. 결국 자식 타입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출 한 것과 같다.
◼️super - 부모 참조
super 키워드를 사용하면 부모를 참조할 수 있다. super 는 이름 그대로 부모 클래스에 대한 참조를 나타낸다.
◼️super - 생성자
- 상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다.(규칙)
- 예외로 생성자 첫줄에 this(...) 를 사용할 수는 있다. 하지만 super(...) 는 자식의 생성자 안에서 언젠가는 반드시 호출해야 한다.
- 상속을 받으면 생성자의 첫줄에 super(...) 를 사용해서 부모 클래스의 생성자를 호출해야 한다.
- 부모 클래스의 생성자가 기본 생성자(파라미터가 없는 생성자)인 경우에는 super() 를 생략할 수 있다.
package extends1.super2;
public class ClassA {
public ClassA() {
System.out.println("ClassA 생성자");
}
}
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);
}
}
package extends1.super2;
public class ClassC extends ClassB {
public ClassC() {
super(10, 20);
System.out.println("ClassC 생성자");
}
}
ClassA 생성자
ClassB 생성자 a=10 b=20
ClassC 생성자
초기화는 최상위 부모부터 이루어진다. 왜냐하면 자식 생성자의 첫 줄에서 부모의 생성자를 호출해야 하기 때문이다.
🟢 정리
- 상속 관계의 생성자 호출은 결과적으로 부모에서 자식 순서로 실행된다. 따라서 부모의 데이터를 먼저 초기화하고
그 다음에 자식의 데이터를 초기화한다. - 상속 관계에서 자식 클래스의 생성자 첫줄에 반드시 super(...) 를 호출해야 한다. 단 기본 생성자 super() 인 경우 생략할 수 있다.
◼️ 클래스와 메서드에 사용되는 final
클래스에 final
- 상속 끝! final 로 선언된 클래스는 확장될 수 없다. 다른 클래스가 final 로 선언된 클래스를 상속받을 수 없다.
- 예: public final class MyFinalClass {...}
메서드에 final
- 오버라이딩 끝! final 로 선언된 메서드는 오버라이드 될 수 없다. 상속받은 서브 클래스에서 메서드를 변경할 수 없다.
- 예: public final void myFinalMethod() {...}
○ 섹션 10 다형성1
◼️다형성 핵심 이론
- 다형적 참조
- 메서드 오버라이딩
🟢 다형적 참조
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
이제 차근차근 코드를 살펴보자.
//부모 변수가 부모 인스턴스 참조
System.out.println("Parent -> Parent");
Parent parent = new Parent();
parent.parentMethod();
Parent -> Parent
Parent.parentMethod
이 경우 부모타입 Parent 를 생성했기 때문에, 메모리 상에 Parent만 생성된다. 자식은 생성되지 않는다.
//부모 변수가 자식 인스턴스 참조(다형적 참조)
System.out.println("Parent -> Child");
Parent poly = new Child();
poly.parentMethod();
Parent -> Child
Parent.parentMethod
Child 인스턴스를 만들었다. 이 경우 자식 타입인 Child 를 생성했기 때문에 메모리 상에 Child 와 Parent 가 모두 생성된다. 그리고 생성된 참조값을 Parent 타입의 변수인 poly 에 담아둔다.
부모는 자식을 담을 수 있다. 하지만 자식은 부모를 담을 수 없다!
//Child child1 = new Parent(); 자식은 부모를 담을 수 없다.
//자식의 기능은 호출할 수 없다. 컴파일 오류 발생
//poly.childMethod();
🟢 다형적 참조
자바에서 부모 타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이것이 바로 다양한 형태를 참조할 수 있다고 해서 다형적 참조라 한다.
여기서 핵심은, 부모는 자식을 품을 수 있다는 것이다.
자식을 참조한 상황에서 poly 가 자식 타입인 Child 에 있는 childMethod() 를 호출하면 어떻게 될까?
상속 관계는 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수는 없다.
Parent 는 부모 타입이고 상위에 부모가 없다. 따라서 childMethod() 를 찾을 수 없으므로 컴파일 오류가 발생한다.
◼️다형성과 캐스팅
🟢 캐스팅
업캐스팅(upcasting): 부모 타입으로 변경
다운캐스팅(downcasting): 자식 타입으로 변경
//부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
//단 자식의 기능은 호출할 수 없다. 컴파일 오류 발생
//poly.childMethod();
- 다형성 참조시 자식 타입에 있는 기능은 호출 할 수 없다.
- 인스턴스 안에서 사용할 타입을 찾아야 한다.
🟢 다운캐스팅
//다운캐스팅(부모 타입 -> 자식 타입)
Child child = (Child) poly;
child.childMethod();
위와같이 부모 타입을 사용하는 변수를 자식 타입에 대입하려고 하면 컴파일 오류가 발생한다. 자식은 부모를 담을 수 없다. 이때는 다운캐스팅이라는 기능을 사용해서 부모 타입을 잠깐 자식 타입으로 변경하면 된다.
캐스팅을 한다고 해서 Parent poly 의 타입이 변하는 것은 아니다. 해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이다. 따라서 poly 의 타입은 Parent 로 기존과 같이 유지된다.
🟢 일시적 다운 캐스팅
//일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod();
🟢 업캐스팅
업캐스팅은 생략할 수 있다. 다운캐스팅은 생략할 수 없다. 참고로 업캐스팅은 매우 자주 사용하기 때문에 생략을 권장한다.
Child child = new Child();
Parent parent1 = (Parent) child; //업캐스팅은 생략 가능, 생략 권장
Parent parent2 = child; //업캐스팅 생략
◼️다운캐스팅과 주의점
//다운캐스팅을 자동으로 하지 않는 이유
public class CastingMain4 {
public static void main(String[] args) {
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); //문제 없음
Parent parent2 = new Parent();
Child child2 = (Child) parent2; //런타임 오류 - ClassCastException
child2.childMethod(); //실행 불가
}
}
첫번째 경우는 다운캐스팅을 해도 문제가 되지 않는다.
두번째 경우는 메모리 상에 자식 타입이 전혀 존재하지 않는다. 그런데 Child 타입으로 다운 캐스팅하면, 메모리상에 Child가 존재하지 않기때문에 Child 자체를 사용할 수 없다.
업캐스팅은 객체 생성 시 해당 객체의 모든 상위 부모 타입도 함께 생성되기 때문에 메모리 상에 이미 존재하는 상위 타입으로의 타입 변환은 안전하다.
반면, 다운캐스팅은 생성된 객체의 자식 타입이 메모리 상에 존재하지 않을 수 있다. 따라서 다운캐스팅은 명시적 캐스팅을 해주어야 한다.
◼️instanceof
Parent parent1 = new Parent();
Parent parent2 = new Child();
부모는 자식을 참조할 수 있기때문에, 위 두가지 경우 모두 가능하다. 각 변수가 실제 참조하는 인스턴스 타입을 확인해야할때 instanceof를 사용한다.
쉽게 이야기해서 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 대입해보면 된다.
대입이 가능 하면 true , 불가능하면 false 가 된다.
new Parent() instanceof Parent
Parent p = new Parent() //같은 타입 true
new Child() instanceof Parent
Parent p = new Child() //부모는 자식을 담을 수 있다. true
new Parent() instanceof Child
Child c = new Parent() //자식은 부모를 담을 수 없다. false
new Child() instanceof Child
Child c = new Child() //같은 타입 true
자바 16부터는 instanceof 를 사용하면서 동시에 변수를 선언할 수 있다.
private static void call(Parent parent) {
parent.parentMethod();
//Child 인스턴스인 경우 childMethod() 실행
if (parent instanceof Child child) {
System.out.println("Child 인스턴스 맞음");
child.childMethod();
}
}
◼️다형성과 메서드 오버라이딩
메서드 오버라이딩에서 꼭! 기억해야 할 점은 오버라이딩 된 메서드가 항상 우선권을 가진다는 점이다.
//부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
System.out.println("Parent -> Child");
System.out.println("value = " + poly.value); //변수는 오버라이딩X
poly.method(); //메서드 오버라이딩!
Parent -> Child
value = parent
Child.method
예를들어 poly 변수는 Parent 타입이다. 따라서 poly.value , poly.method() 를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아서 실행한다.
poly.value : Parent 타입에 있는 value 값을 읽는다.
poly.method() :오버라이딩 된 메서드는 항상 우선권을 가진다. 따라서 Parent.method() 가 아니라 Child.method() 가 실행된다.
🟢 정리
다형적 참조: 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
메서드 오버라이딩: 기존 기능을 하위 타입에서 새로운 기능으로 재정의
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 중급편 섹션1 Object 클래스 (0) | 2024.05.05 |
---|---|
[Java의 정석] chapter09 java.lang패키지와 유용한 클래스 - 요약정리 (0) | 2024.05.05 |
[Java] 김영한의 실전 자바 - 기본편 섹션7,8 자바 메모리 구조와 static, final (0) | 2024.04.18 |
[Java의 정석] chapter06 객체지향 프로그래밍 II - 요약정리 (0) | 2024.04.15 |
[Java] 김영한의 실전 자바 - 기본편 섹션5,6 패키지, 접근 제어자 (0) | 2024.04.15 |