본문 바로가기

Java/Java SE, EE

[Java/Java SE, EE] 클래스 상속(Inheritance)

상속(Inheritance)

상속은 어떤 클래스의 필드(Field)와 메소드(Method)를 다른 클래스에게 물러주는 것을 의미합니다. 이때 필드와 메소드를 물러주는 클래스를 부모 클래스라고 부르며, 물러받는 클래스를 자식 클래스라고 부릅니다. 

부모 클래스(Parent class)

부모 클래스는 필드와 메소드를 물러주는 주체입니다. 부모 클래스는 상위 클래스(Super class)라고도 부릅니다.

[접근 제한자 public|protected|default|private] class [클래스 이름] extends [부모 클래스 이름] { }

하나의 부모 클래스는 여러 개의 자식 클래스를 가질 수 있습니다. 예를 들면, 음식(Food) 클래스는 사과(Apple)와 고기(Meat) 클래스의 부모 클래스가 될 수 있습니다. 사실, 문법 상으로는 자식 클래스가 상속 받고자 하는 부모 클래스를 선택하는 것에 가깝습니다.

public class Food { } 
public class Apple extends Food { }
public class Meat extends Food { }

자식 클래스(Child class)

자식 클래스는 필드와 메소드를 물러받는 객체입니다. 자식 클래스는 하위 클래스(Sub class) 또는 파생 클래스(Derived class)라고도 부릅니다. 자바에서 하나의 자식 클래스는 하나의 부모 클래스를 가질 수 있습니다. 따라서 다음과 같은 문법(C++의 다중 상속과 같은)은 자바에서 허용하지 않습니다.

public class Food { }
public class Fruit { }
public class Apple extends Food, Fruit { }	// 다중 상속 불가능

부모 클래스의 접근 제한자(Access modifier)

자식 클래스는 상속 관계에 있는 부모 클래스의 모든 필드와 메소드를 물러받는 것은 아닙니다. 자식 클래스가 물러받을 수 있는 필드와 메소드는 접근 제한자(Access modifier)에 따라서 달라집니다.

접근 제한자 비고
public 자식 클래스를 포함하여 모든 코드에서 부모 클래스가 갖는 public 필드 또는 public 메소드에 접근 가능
protected 자식 클래스는 부모 클래스가 갖는 public 필드 또는 public 메소드에 접근 가능. 외부에서의 접근은 불가능
default 두 클래스가 서로 동일한 패키지에 위치하고 있다면, 자식 클래스는 부모 클래스가 갖는 public 필드 또는 public 메소드에 접근 가능. 그 외의 접근은 불가능
private 자식 클래스는 부모 클래스가 갖는 public 필드 또는 public 메소드에 접근 불가능

일단 접근 제한자로 인해 부모 클래스의 멤버(필드 또는 메소드)에 접근이 허용되면, 멤버의 이름을 사용하거나 this 또는 super 키워드를 사용하여 멤버에 접근 할 수 있습니다.

public class Food {
   public int abc = 10;
   public int ABC() {
      return abc;
   }
}

public class Apple extends Food {
   public void CDE() {
      System.out.println(abc);
      System.out.println(this.abc);
      System.out.println(super.abc);
      System.out.println(ABC());
      System.out.println(super.ABC());
   }
}

부모 클래스의 생성자 호출

어떤 클래스가 객체화-인스턴싱(Instantiate)되기 위해서, 클래스는 생성자를 제공합니다. 상속 관계에서 자식 클래스는 부모 클래스의 인스턴싱을 위해 부모 클래스의 생성자를 호출 할 수 있습니다.

부모 클래스의 생성자는 자식 클래스의 생성자에서 super 키워드를 사용하여 호출 할 수 있습니다.

public class Food {
   public Food(int _a) {
   
   }
}

public class Apple extends Food {
   public Apple() {
      super(10);
   }
}

부모 클래스의 생성자는 반드시 자식 클래스의 첫 번째 코드에서 호출되어야 합니다. 예를 들면, 다음과 같은 문법은 자바에서 허용하지 않습니다. 자식 클래스의 생성자가 반드시 부모 클래스의 생성자를 호출해야 하는 것은 아니지만, 일단 호출하게 되면 생성자의 가장 앞부분에서 호출해야 합니다.

public Apple() {
   int a = 123;		// 부모 클래스 호출 이전에 다른 코드 삽입 불가능
   super(10);
}

메소드 재정의(Override)

부모 클래스가 갖는 메소드를 자식 클래스가 재정의 할 수 있습니다. 메소드의 재정의를 위해서 @Override 어노테이션을 사용합니다. 자식 클래스가 메소드를 재정의하면, 해당 메소드를 내/외부에서 호출하게 되면 재정의된 메소드가 호출됩니다.

public class Food {
   public void ABC() { System.out.println("super class ABC()"); }
}

public class Apple extends Food {
   @Override
   public void ABC() { System.out.println("child class ABC()"); }
}

메소드가 재정의되었다고 하더라도 부모 클래스의 원래 메소드를 호출하는 방법이 없는 것은 아닙니다. 자식 클래스 내부에서 super 키워드를 사용하여 메소드를 호출하게 되면, 부모 클래스의 원래 메소드가 호출됩니다.

ABC();			// child class ABC()
super.ABC();		// super class ABC()

메소드를 오버라이딩 할 때, 부모 클래스가 갖는 시그니쳐(리턴 타입, 메소드 이름, 파라미터)와 접근 제한자를 변경 할 수 없습니다. 또한 부모 클래스가 처리하는 예외(Exception) 외에 새로운 예외를 throws 할 수 없습니다.

final 클래스와 final 메소드

클래스에서 final 키워드를 사용하면 클래스에 대한 불변성(Immutable)을 부여 할 수 있습니다. final 키워드를 사용하여 클래스를 선언하면, 다른 클래스가 final 클래스를 상속 받을 수 없습니다.

public final class Food { }
public class Apple extends Food { }       // Food를 상속 불가능

만약 클래스에 대한 상속은 허용하면서, 어떤 메소드에 대해서 자식 클래스가 오버라이딩 할 수 없도록 하려면 메소드에 final 키워드를 사용 할 수 있습니다.

public class Food { 
   public final void ABC() { }
}

public class Apple extends Food {
   @Override
   public void ABC() { }     // ABC()를 오버라이딩 불가능    
}

다형성(Polymorphism)과 자동 타입 변환(Promotion)

클래스에서 다형성은 자식 클래스 객체를 부모 클래스 변수에 대입 할 수 있는 것을 의미합니다. 예를 들어, 다음과 같이 3개의 클래스가 있습니다.

public class Food {
   public void ABC() { }
}

public class Apple extends Food {
   public void DEF() { }
}

public class Meat extends Food {
   public void GHI() { }
}

이때 Apple과 Meat는 Food 클래스의 자식 클래스입니다. 예시에서는 Food 변수에는 Apple과 Meat 객체를 저장 할 수 있습니다. Apple 객체(또는 Meat 객체)의 경우 자신을 정의하는 Apple 클래스와 부모 클래스인 Food 클래스 두 개의 타입으로 모두 사용되고 있습니다. 이처럼 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 다형성이라고 부릅니다.

Apple apple = new Apple();
Meat meat = new Meat();

Food food = apple;	// 다형성에 의한 허용
food = meat;		// 다형성에 의한 허용

다형성을 사용했을 때의 장점은, 객체가 실제 어떤 타입인지에 관계 없이 부모 클래스가 정의하고 있는 필드와 메소드를 사용 할 수 있다는 것입니다. 

Food food = apple;
food.ABC();

예를 들어, 다음과 같이 Food[] 배열 타입의 엘리먼트가 Apple 클래스인지 또는 Meat 클래스인지 관계 없이 부모 클래스가 정의하고 있는 ABC() 메소드를 호출 할 수 있습니다.

Food[] foods = new Food[] { apple, meat };
for (Food food : foods) {
   food.ABC();
}

만약, 두 자식 클래스가 부모 클래스의 메소드 ABC()를 오버라이딩하고 있다면, Food[] 배열의 엘리먼트의 실제 타입을 신경쓰지 않더라도 객체가 정의하는 실제 메소드를 호출합니다. 이는 하나의 타입으로 다양한 결과를 나타내는 다형성의 특징이기도 합니다.

public static class Apple extends Food {
   @Override
   public void ABC() { }
}

public static class Meat extends Food {
   @Override
   public void ABC() { }
}

앞서 다형성의 예시를 보여주면서, Apple 객체 또는 Meat 객체를 Food 변수 타입에 저장하고 있습니다. 실제 객체를 정의하는 클래스가 서로 다름에도 불구하고 컴파일은 이를 허용합니다. 이와 같은 타입 변환을 자동 타입 변환(Promotion) 또는 자동 형 변환이라고 부릅니다.

Promotion은 Primitive 타입에서는 형 변환으로 인한 손실이 발생하지 않을 때 허용됩니다.

int a = 10;
long b = a;

반면, 클래스에서의 Promotion은 자식 클래스가 부모 클래스로의 형 변환에 대해서 허용됩니다. 실제로는, 자식 클래스가 부모 클래스의 필드와 메소드를 모두 갖고 있으며(접근 가능 여부와 관계 없이), 추가적인 멤버를 구성 할 수 있음에도 말이죠.

[부모 클래스 타입] [변수 이름] = [자식 클래스 이름]

instanceof

객체의 타입을 확인하고 싶다면, instanceof 연산자를 사용 할 수 있습니다. 좌항에는 객체를, 우항에는 확인하고자 하는 타입을 입력하면 boolean(true=일치)을 리턴합니다.

boolean result = [객체] instanceof [타입]

예를 들면, 다형성에 의해서 어떤 변수에 저장된 객체의 실제 타입을 확인 할 수 있습니다.

Food[] foods = new Food[] { apple, meat };
for (Food food : foods) {
   if (food instanceof Apple) {
      System.out.println("This food is apple");
   } else if (food instanceof Meat) {
      System.out.println("This food is meat");
   }
}
This food is apple
This food is meat

이때 주의할 점은, 객체가 상속 관계이 있다면 부모 클래스 또한 instanceof의 결과가 true라는 점입니다. Apple 객체는 Apple 타입이면서 동시에 Food 타입이기 때문입니다. 앞서 설명한것 처럼 객체가 여러 가지 타입을 갖을 수 있는 다형성의 특성 때문입니다.

Food[] foods = new Food[] { apple, meat };
for (Food food : foods) {
   if (food instanceof Apple) {
      System.out.println("This food is apple");
   } else if (food instanceof Meat) {
      System.out.println("This food is meat");
   }
   
   if (food instanceof Food) {
      System.out.println("This is also food");
   }
}
This food is apple
This is also food
This food is meat
This is also food