윤성우 저자의 '윤성우의 열할 Java 프로그래밍'을 읽으면서, 상속에 대한 개념을 정리하고자 한다.
내 자신이 중요하다고 생각한 부분을 정리하였기에, 책의 내용과 상이할 수도 있고, 모든 내용이 담겨있지도 않다.
chatGPT의 의견도 반영되어 있다.
1. 왜 상속을 사용해야 하는가
보통 상속의 개념을 사용하는 이유를 '코드의 재활용'에서 찾지만, 실제로 이는 옳은 답이 아니다.
상속은 '연관된 클래스들에 대해 공통적인 규칙을 정의할 수 있기' 때문이다.
즉, 기존에 정의된 클래스에, 메서드와 변수를 추가하여 새로운 클라스를 정의할 수 있다.
class Man{
String name;
public void tellName(){
System.out.println("My Name: " +name);
}
}
과 같은 클래스가 정의되어 있다고 하자.
class BusinessMan extends Man{
String company;
public BusinessMan(String company){
this.company = company;
}
public void tellInfo(){
System.out.println("My company: " + company);
tellName();
}
}
와 같은 BusinessMan class를 선언하여, Man 클래스를 상속하였다. (물려받았다.)
즉, BusinessMan 클래스에서는 Man 클래스를 자유롭게 사용할 수 있다. 또한, Businessman 클래스에서의 메서드와 변수를 사용할 수 있다.
이를 UML 기호와 같은 표시를 이용하면, BusinessMan -> Man 처럼 Man에게 화살표를 보이는 것 처럼, 하위클래스에서 상위 클래스로 향하게 그리면 된다.
2. 생성자
또한 클래스에 생성자를 정의할 수 있다.
상위 클래스에서 정의한 변수를 바탕으로, 상속을 받은 클래스가 초기화를 할 수도 있다.
또한, 하위 클래스의 인스턴스 생성 시 상위 클래스와 하위 클래스의 생성자가 모두 호출된다. 이 과정에서, 상위 클래스의 생성자가 먼저 호출되고, 하위클래스의 인스턴스 생성이 이루어진다.
다양한 생성자가 있는 경우가 있다. 예시를 보자.
class SuperCLS {
public SuperCLS() {
System.out.println("Con: SuperCLS()");
}
public SuperCLS(int i, int j) {
System.out.println("Con: SuperCLS(int i, int j)");
}
}
class SubCLS extends SuperCLS {
public SubCLS() {
System.out.println("Con: SubCLS()");
}
public SubCLS(int i, int j) {
super(i, j);
System.out.println("Con: SubCLS(int i, int j)");
}
}
class SuperSubCon {
public static void main(String[] args) {
System.out.println("1. ");
new SubCLS();
System.out.println("3. ");
new SubCLS(1, 2);
}
}
SubCLS가 SuperCLS를 상속받았다. SubCLS의 생성자는 argument로 아무 것도 받지 않은 경우와, 2개의 정수를 받는 경우가 있다.
해당 경우에는, super()을 이용하여 두 정수를 인자로 전달 받을 수 잇는 생성자를 호출할 수 있다.
여기서, super를 이용한 생성자 호출문은 생성자의 첫 문장으로 들어가야 한다.
(만약 argument가 없는 경우에는, super(); 가 있다고 가정한다.)
기본적으로 IS - A 관계로 상속을 맺는다. 하위 클래스는 상위 클래스의 모든 특성을 지니고, 거기에 자신의 추가적인 특성을 더한다.
즉, 스마트폰은 모바일 폰의 일부이고, 스마트폰은 모바일폰이며, 스마트폰은 일종의 모바일폰이기 때문이기에,
class 스마트폰 extends 모바일폰 과 같이 나타낼 수 있다.
(HAS - A 관계도 상속으로 쓰일 수 있지만, 잘 쓰이지는 않는다.)
3.클래스의 상속과 참조 가능성
class Cake {
public void sweet()
}
class CheeseCake extends Cake{
public void milky()
}
class StrawberryCheeseCake extends CheeseCake{
public void sour()
}
와 같은 형태의 클래스 상속이 있다고 하자.
StrawberryChesseCake인스턴스는 CheeseCake 인스턴스이면서, Cake 인스턴스이다.
따라서,
Cake cake1= new StrawberryCheeseCake();
CheeseCake cake2= new StrawberryCheeseCake();
와 같이 참조가 가능하다.
하지만, cake1은 오로지 cake1.sweet(); 의 메서드만 호출할 수 있고,
cake2는 cake2.sweet(), cake2.milky()의 메서드를 호출 할 수 있다.
이는 배열을 생성하여 참조할 수 있다.
super 키워드 정리
super 키워드는 상속 관계에서 자식 클래스가 부모 클래스의 멤버 ( 필드,메서드,생성자)에 접근할 때 사용됨.
- 생성자 호출
class 부모 {
부모() { // 부모 생성자
}
}
class 자식 extends 부모 {
자식() {
super(); // 부모 클래스의 생성자 호출
}
}
- 메서드 호출
class 부모 {
void 메서드() {
System.out.println("부모 메서드");
}
}
class 자식 extends 부모 {
void 메서드() {
super.메서드(); // 부모 클래스의 메서드 호출
System.out.println("자식 메서드");
}
}
- 부모 클래스 필드
class 부모 {
int 숫자 = 10;
}
class 자식 extends 부모 {
int 숫자 = 20;
void 출력() {
System.out.println(super.숫자); // 부모 클래스의 숫자 (10)
System.out.println(this.숫자); // 자식 클래스의 숫자 (20)
}
}
4. instanceof 연산자
특정 객체가 특정 클래스거나 인터페이스의 인스턴스인지 사용되는 연산자
객체 instanceof 클래스명 와 같은 방식으로 사용되어, if문에 자주 사용됨.
5. 클래스와 메서드의 final 선언
public final class MyClass
와 같이, final 키워드가 부여되면,
다른 클래스가 해당 클래스를 상속할 수 없다.
이와 같이,
class myClass{
public final void func(int n){}
}
처럼 final이 메서드에 사용되면 이 메서드는 다른 클래스에서 오버라이딩 할 수 없다.
6. @Override 애너테이션
메서드가 부모 클래스나 인터페이스의 메서드를 override (오버라이드, 재정의)하고 있음을 명시한다.
만약 메서드 이름을 잘못 적거나, 부모 클래스와 메서드 이름이 일치하지 않으면 컴파일러가 오류를 발생시킨다.
class 부모 {
void 인사() {
System.out.println("안녕하세요, 부모입니다.");
}
}
class 자식 extends 부모 {
@Override
void 인사() {
System.out.println("안녕하세요, 자식입니다.");
}
}
7. 인터페이스
인터페이스는 클래스가 구현해야 하는 메서드의 계약서 역할을 한다고 생각할 수 있다.
interface 비행기 {
void 이륙();
void 착륙();
}
class 여객기 implements 비행기 {
@Override
public void 이륙() {
System.out.println("여객기가 이륙합니다.");
}
@Override
public void 착륙() {
System.out.println("여객기가 착륙합니다.");
}
}
다음 코드처럼, 구현의 세부 사항을 구현하지 않고, 공통적인 기능을 추상화할 때 사용할 수 있다.
인터페이스는 다중 상속을 지원하기에, 한 클래스가 여러 인터페이스의 기능을 사용할 수 있다.
이를 이용하여 코드의 유연성을 확보할 수 있고, 코드의 결합도 또한 낮출 수 있다.