10. 자바는 상속이라는 것이 있어요
상속
- 부모 클래스에 선언되어 있는 public / protected로 선언된 모든 변수 / 메소드를 내가 갖고 있는 것 처럼 사용할 수 있다.
- 접근 제어자가 없거나 (package-private) private으로 선언된 변수 / 메소드들은 자식 클래스에서 사용할 수 없다.
- extends 키워드 다음에 클래스 이름을 지정하면 그 클래스를 상속 받는다는 의미
- 클래스 다이어그램에서 상속 관계는 속이 빈 삼각형 화살표로 자식 ㅡ▷ 부모 클래스를 가리킨다.
- 참조 관계는 그냥 직선으로 나타낸다.
- 확장을 한 클래스 (자식 클래스)가 생성자를 호출하면 자동으로 부모 클래스의 기본 생성자가 실행된다.
- 이후 자식 클래스의 생성자가 실행된다.
ex) Inheritance Example
public class Parent {
public Parent(){} // 부모 클래스 생성자
}
==========================
public class Child extends Parent {
public Child(){} // 자식 클래스 생성자
public static void main(String [] args) {
// 자식 클래스의 생성자 호출 -> 부모 클래스 기본 생성자 실행 -> 자식 클래스 생성자 실행
Child child = new Child(); // 자식 클래스의 객체 생성
}
}
- 상속을 위한 작업
- 부모 클래스
- 기본 생성자 만들기
- 기본 생성자가 없고 매개 변수가 있는 생성자만 있는 경우 컴파일 에러 발생
- 기본 생성자 만들기
- 자식 클래스
- 클래스 선언 시 extends 부모 클래스 이름 작성
- 부모 클래스의 public / protected로 선언된 모든 인스턴스 및 클래스 변수 / 메소드 사용.
- 부모 클래스
- 하나의 클래스를 잘 만들어 놓으면 그 클래스를 상속받아 추가적인 기능을 넣을 수 있다는 장점.
- 자바에서는 단일 상속만 가능하다. (다중 상속 불가능)
- extends 뒤에 클래스는 하나만(두 개이상 작성 시 컴파일 에러 발생)
- 여러 자식 클래스에서 한 부모 클래스를 상속받는 것은 문제가 되지 않는다.
- 부모 클래스에 매개 변수가 있는 생성자만 있는 경우 해결 방법
- 부모 클래스에 기본 생성자를 별도로 생성한다.
- 자식 클래스에서 부모 클래스의 생성자를 명시적으로 지정하는 super()를 사용한다.
- super()는 부모 클래스의 생성자를 호출한다는 것을 의미한다.
- ex) super("hi")는 String을 매개 변수로 받는 생성자를 찾아 호출한다
super(1)은 int형을 매개 변수로 받는 생성자를 찾아서 호출한다. - super()를 사용하여 생성자를 호출할 때는 모호하게 null을 넘기는 것보다 호출하고자 하는 생성자의 기본 타입을 넘겨주는 것이 좋다.
- 부모 클래스에 매개 변수가 있는 생성자만 있을 경우 super()를 이용해 부모 생성자를 명시적으로 호출해야만 한다.
- 자식 클래스에서 super()를 명시적으로 지정하지 않으면 컴파일 시 자동으로 super()가 추가된다.
- super()는 기본 생성자를 호출
- 부모 클래스의 생성자를 호출하는 super()는 자식 클래스의 생성자에서 첫 줄에 선언되어야 한다!!
- ex) super("hi")는 String을 매개 변수로 받는 생성자를 찾아 호출한다
메소드 오버라이딩 (overriding)
- 자식 클래스에서 부모 클래스에 있는 메소드와 동일하게 선언하는 것
- 동일하게 선언되어 있다 = 동일한 시그니처를 가진다.
- 접근 제어자, 리턴 타입, 메소드 이름, 매개 변수 타입 및 개수가 모두 동일해야 한다.
- 오버라이딩 된 메소드를 호출 시 자식 클래스의 메소드만 실행된다.
시그니처 = 메소드 이름, 매개 변수의 타입 및 개수
- 메소드 오버라이딩 시 부모 클래스에 같은 이름의 메소드가 존재하는 이상 자식 클래스에서 리턴 타입을 변경할 수 없다.
- 부모 클래스 메소드의 접근 제어자보다 확대되는 것은 문제가 되지 않지만 축소되는 것은 문제가 된다.
- public > protected > package-private > private (왼쪽으로 가는건 괜찮지만 오른쪽으로 가면 문제가 된다.)
- 축소되면 컴파일 에러가 발생, ex) public(부모) ㅡ> private(자식) 접근 제어자 축소로 인한 컴파일 에러 발생.
- 오버로딩 vs 오버라이딩
- 오버로딩은 같은 메소드 이름 / 다른 매개 변수 개수, 타입, 리턴 타입 (확장 개념)
- 오버라이딩은 같은 메소드 이름, 매개 변수 개수, 타입, 리턴 타입 (복제 개념)
참조 자료형의 형 변환
public class Parent{
// 내용 생략
}
public class Child extends Parent {
// 내용 생략
}
// 상속 관계 성립으로 인한 객체 생성 방식
Parent parent = new Child();
Child child = new Parent(); // 컴파일 에러 발생
// 명시적 형 변환
Parent parent2 = new Parent();
Child child2 = new Child();
Parent parent3 = child; // 자식 객체는 부모 클래스의 모든 것을 사용할 수 있기에 아무런 문제 없음.
// 명시적 형 변환으로 컴파일 에러는 피하지만
// 부모 객체는 자식 클래스의 모든 것을 사용할 수 없기에 결국 실행 시 예외 발생
Child child3 = (Child)parent2;
- 참조 자료형도 형 변환이 가능하다!
- 상속 관계가 성립이 되기 때문에 예시와 같이 객체를 생성할 수 있다.
- 자식 클래스의 객체를 생성할 때 부모 클래스의 생성자를 사용할 수 없다.
- = 자식 클래스의 객체가 부모 클래스의 타입을 사용할 수 없다.
- 자식 클래스에 추가된 메소드나 변수가 있을 수 있기 때문 (부모 클래스를 확장시킨 개념이 자식 클래스이기에)
- 명시적 형 변환을 시켜줘야 컴파일 에러가 발생하지 않는다.
- 명시적 형 변환으로 컴파일 에러는 피하지만 부모 객체는 자식 클래스의 모든 것을 사용할 수 없기에 결국 실행 시 예외 발생
- 부모 타입의 객체를 자식 타입으로 형 변환할 때 명시적으로 형 변환을 해줘야 할 뿐더러 부모 타입의 실제 객체는 자식 타입이어야 한다.
- 명시적 형 변환으로 컴파일 에러는 피하지만 부모 객체는 자식 클래스의 모든 것을 사용할 수 없기에 결국 실행 시 예외 발생
- 큰 범위 ㅡ> 작은 범위로 형 변환 시 명시적 형 변환 필요, long ㅡ> int 형 변환처럼
- 부모 클래스의 객체를 생성할 때 자식 클래스의 생성자를 사용할 수 있다.
- = 부모 클래스의 객체는 자식 클래스의 타입을 사용할 수 있다.
- 명시적 형 변환이 필요 없다. (자동으로 형 변환)
- 자식 클래스에서는 부모 클래스의 모든 변수, 메소드들을 사용할 수 있기 때문.
그렇다면 명시적 형 변환을 어떻게 사용해야 할까?
public class Parent { }
=======================
public class Child extends { public void printAge(){} }
=======================
public class Casting {
public static void main(String[] args) {
Casting casting = new Casting();
casting.castArray();
}
public void castArray() {
Parent[] parent = new Parent[3];
parent[0] = new Child();
parent[1] = new Parent();
parent[2] = new Child();
typeCheck(parent);
}
private void typeCheck(Parent[] parent) {
for (Parent parents : parent) {
if (parents instanceof Child) {
Child tmpChild = (Child) parents;
tmpChild.printAge();
} else if (parents instanceof Parent) {
// 내용 생략
}
}
}
}
- Parent 객체 배열 공간을 할당했는데 일반적으로 여러 개의 값을 처리하거나, 매개 변수로 값을 전달할 때 보통 부모 클래스의 타입으로 보낸다.
- 그렇지 않으면 배열과 같이 여러 값을 한 번에 보낼 때 각 타입별로 구분해 메소드를 만들어야하는 문제가 생길 수 있기 때문에.
- if (parents instanceof Child) Child tmpChild = (Child) parents; 의 경우 예외가 발생하지 않는 이유는?
- instanceof 키워드는 객체 instanceof 클래스 로 사용하는 형태
- 클래스의 객체인지 확인해 boolean 타입으로 리턴해준다.
- 자기 자신의 타입뿐만 아니라 부모 타입도 true.
- 먼저 instanceof 키워드로 타입만 부모 클래스인 자식 클래스의 객체를 찾아서 다시 자식 클래스의 타입으로 형 변환을 시켜준다.
- 매개 변수로 전달하기 위해 부모 클래스의 타입으로 선언 후 자식 클래스의 객체를 참조한 것이기에(parent[0], parent[2]가 참조하는 값은 원래 자식 클래스의 객체이기 때문에) 명시적 형 변환을 하더라도 예외가 발생하지 않는다.
- 무작정 형 변환 후 printAge()메소드 호출 시 예외가 분명 발생하지만 instanceof로 정확하게 타입을 확인 후 형 변환을 통해 Parent에는 없고 Child에만 선언되어 있는 printAge() 메소드를 호출할 수 있다.
- instanceof 키워드는 객체 instanceof 클래스 로 사용하는 형태
- (자식 클래스의 객체) instanceof (부모 클래스)의 결과는?
- ㅡ> true
- 그렇기 때문에 자식 클래스인지 부모 클래스의 객체인지 instanceof로 점검하는 경우 가장 하위 자식 클래스부터 먼저 확인해야 정상적인 타입 점검이 가능하다.
다형성 (Polymorphism)
- 자식 클래스는 자신만의 "행위"를 가질 수 있지만, 부모 클래스에 선언된 메소드들도 공유 가능하다는 것을 의미한다.
- 형 변환을 하더라도 실제 호출되는 것은 원래 객체에 있는 메소드가 호출된다.
public class Parent {
public void printName() {
System.out.println("Parent");
}
}
====================================
public class Child extends Parent {
public void printName() {
System.out.println("Child");
}
}
=====================================
public class ChildOther extends Parent{
public void printName() {
System.out.println("ChildOther");
}
}
=====================================
public class Polymorphism {
public static void main(String[] args) {
Polymorphism polymorphism = new Polymorphism();
polymorphism.callPrintNames();
}
public void callPrintNames() {
Parent parent1 = new Parent();
Parent parent2 = new Child();
Parent parent3 = new ChildOther();
parent1.printName();
parent2.printName();
parent3.printName();
}
}
결과 값:
Parent
Child
ChildOther
- 선언 시 모두 Parent 타입으로 선언했지만, 실제로 호출된 메소드는 생성자를 사용한 클래스에 있는 메소드가 호출되었다.
- 각 객체의 실제 타입이 서로 다르기 때문
간단 내용 정리
1. 상속을 받는 클래스의 선언문에 사용하는 예약어는 무엇인가요?
ㅡ> extends
2. 상속을 받은 클래스의 생성자를 수행하면 부모의 생성자도 자동으로 수행된다?
ㅡ> O, 부모의 기본 생성자가 호출된다.
3. 부모 클래스의 생성자를 자식 클래스에서 직접 선택하려고 할 때 사용하는 예약어는 무엇인가요?
ㅡ> super(), 매개 변수를 넣으면 매개 변수타입과 일치하는 생성자를 찾아 호출한다.
4. 메소드 Overriding과 Overloading을 정확하게 설명해 보세요.
ㅡ> 오버라이딩은 부모 클래스의 메소드의 이름, 리턴 타입, 매개변수 타입과 개수까지 일치한 메소드를 자식 클래스에서 선언하는 것이다.
오버로딩은 메소드의 이름만 같고 매개변수의 타입 및 개수가 다른 메소드를 선언하는것을 의미한다.
오버라이딩은 복제의 개념 오버로딩은 확장의 개념
5. A가 부모, B가 자식 클래스라면 A a=new B(); 의 형태로 객체 생성이 가능한가요?
ㅡ> O, 역은 불가능하다.
6. 명시적으로 형변환을 하기 전에 타입을 확인하려면 어떤 예약어를 사용해야 하나요?
ㅡ> instanceof
7. 위의 문제에서 사용한 예약어의 좌측에는 어떤 값이, 우측에는 어떤 값이 들어가나요?
ㅡ> 좌측에는 확인하고자 하는 변수 (객체), 우측에는 클래스 명이 위치한다.
8. instanceof 예약어의 수행 결과는 어떤 타입으로 제공되나요?
ㅡ> boolean 타입
9. Polymorphism이라는 것은 뭔가요?
ㅡ> 다형성을 의미하며 자식 클래스는 자신만의 "행위"를 가질 수 있지만, 부모 클래스에 선언된 메소드들도 공유 가능하다는 것을 의미한다.
형 변환을 하더라도 실제 호출되는 것은 원래 객체에 있는 메소드가 호출된다.