부모 클래스의 모든 필드
와 메소드
를
자식 클래스가 재정의 하는 것
[ 클래스의 부모-자식 상속 관계 표현 ]
상속에 대한 매우 치명적인 오해
상속의 이유와 목적을 물어보면 다음과 같이 답을 하는 경우를 매우 흔히 본다.
" 상속은 코드의 재활용을 위한 문법이다 "
그러나 객체지향 기반의 개발 경험이 풍부한 개발자나 대학원 전공자에게 물어보면
다음과 같이 대답을 한다.
" 상속은 코드의 재활용을 목적으로 사용하는 문법이 아니며
연관된 일련의 클래스들에 대해 공통적인 규약을 정의할 수 있습니다. "
만약 재활용 목적으로 상속을 사용할 경우 무의미하게 코드가 복잡해지고,
기대와 달리 코드를 재활용하지 못하는 상황을 쉽게 경험하게 될 것
코드의 재활용은 프로그래머라면 누구나 바라는 일이다.
// 부모 클래스: Animal
// 모두 공통적인 특성을 정의
class Animal {
// name필드와 makeSound 메서드를 중복 작성하지 않고 여러 종류의
// 동물을 모델링할 수 있다.
// 각 하위 클래스에서는 자체적인 동작을 추가로 정의하거나
// 부모 클래스에서 상속받은 동작을 재정의할 수 있다.
// 이것이 객체 지향 프로그래밍의 상속을 통한 코드 재사용과 일반화의
// 예시이다.
protected String name;
public Animal(String name) {
this.name = name;
}
public void makeSound() {
System.out.println(name + " makes a sound");
}
public void eat() {
System.out.println(name + " is eating");
}
}
// 하위 클래스: Dog
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " barks");
}
public void fetch() {
System.out.println(name + " fetches a ball");
}
}
// 하위 클래스: Cat
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " meows");
}
public void scratch() {
System.out.println(name + " is scratching");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Buddy");
Cat cat = new Cat("Whiskers");
// Dog 클래스의 메서드 사용
dog.makeSound(); // 오버라이드된 메서드 호출
dog.fetch(); // Dog 클래스에만 있는 메서드 호출
// Cat 클래스의 메서드 사용
cat.makeSound(); // 오버라이드된 메서드 호출
cat.scratch(); // Cat 클래스에만 있는 메서드 호출
}
}
상속과 생성자의 호출 코딩해보기
위 예제를 활용하여 상속에 대한 이해를 하기 위해 다음 클래스 각각에 대한 생정자를 삽입해보자
물론 상속 관계를 고려하여 각 클래스 별로 필요한 생성자를 삽입해야 한다.
class Car{
int gasolineGauge;
public Car(int gasolineGauge) {
this.gasolineGauge = gasolineGauge;
}
}
class HybridCar extends Car {
int electricGauge;
public HybridCar(int gasolineGauge, int electricGauge) {
super(gasolineGauge);
this.electricGauge = electricGauge;
}
}
class HybridWaterCar extends HybridCar {
int waterGauge;
public HybridWaterCar(int gasolineGauge, int electricGauge, int waterGauge) {
super( gasolineGauge , electricGauge );
this.waterGauge = waterGauge;
}
public void showCurrentGauge(){
System.out.println("잔여 가솔린 : " + gasolineGauge);
System.out.println("잔여 전기량 : " + electricGauge);
System.out.println("잔여 워터랑 : " + waterGauge);
}
}
public class 상속과생성자의호출 {
public static void main(String[] args) {
HybridWaterCar hwc = new HybridWaterCar(10, 20 , 30 );
hwc.showCurrentGauge();
}
}
[ 출력 값 ]
잔여 가솔린 : 10
잔여 전기량 : 20
잔여 워터랑 : 30
클래스 변수, 클래스 메소드와 상속 그리고 접근 지시자
다음 예제 코드를 보자
class SuperCLS{
// 문제 풀이 후 접근 지시자 설명 예정
static int count = 0;
public SuperCLS() {
count++;
}
}
class SubCLS extends SuperCLS {
public void showCount(){
System.out.println(count);
}
}
public class Main {
public static void main(String[] args) {
SuperCls sc = new SuperCls();
SubCls sbc = new SubCls();
SuperCls dsc = new SubCls();
sbc.showCount();
}
}
[ 출력 값 ]
3
상속 관계를 맺은 참조변수 간 대입과 형 변환
다음과 같이 상속 관계를 맺은 두 클래스가 있다고 가정했을때
몇가지 문제를 보며 대입 가능 여부를 가늠해보자.
class A{
...
}
class B extends A{
...
}
class Main{
public static void main( String args[] ){
A ab = new B();
B bb = new B();
A a1 = ab;
}
}
가능하다 !
그 이유는 다형성은 상위 클래스
타입의 참조 변수가 -> 하위 클래스
의 객체를
가리킬 수 있는 성질을 나타낸다.
ab
는 B 클래스의 객체이며 a
는 B 클래스의 객체를 가리킬 수 있지만
a
의 타입은 A 클래스로 선언되었기 때문에 A 클래스에 정의된 멤버 변수와 메서드에만 접근 가능하다.
이러한 다형성은 코드의 유연성을 제공하며, 동일한 인터페이스를 사용하면서도 여러 클래스들의 객체를 다룰 수 있게 해준다.
가능하다 !
객체 지향 프로그래밍에서 상위 클래스
타입으로 여러 하위 클래스
를 다룰 수 있는 중요한 개념이다.
위 처럼 선언하면 하위 클래스
의 특정 동작을 활용하거나
상위 클래스
와 하위 클래스
에 공통적으로 정의된 메서드와 변수를 사용할 수 있다.
상위 클래스 A의 참조변수 a
는 하위 클래스 B의 객체 bb
를 가리킬 수 있으며
를 통해 A 클래스의 멤버에 접근할 수 있다.
다만, 이떄는 A 클래스에 정의된 멤버만 접근 가능하며
B 클래스에 추가로 정의된 멤버에는 접근할 수 없다.
불 가능하다..
ab
가 A 클래스 타입으로 선언되었지만
B 클래스의 객체를 B 클래스의 타입으로 참조할 수 없기 떄문이다.
java에서는 하위 클래스
의 객체를 -> 상위 클래스
의 참조로 변환하는 것은 자동으로 허용되지만
상위 클래스
객체를 하위 클래스
의 참조로 변환하는 것은 명시적인 형 변환이 필요하며,
이러한 casting을 할 수 없는 경우 compile error가 발생하기 떄문이다.
따라서 위 compiler error를 해결하기 위해선
B b = (B)ab 로 선언해야 한다.
이는 ab
가 참조하는 인스턴스가 -> B 인스턴스임
을 프로그래머가 보장한다는 의미이다.
따라서 compiler는 이를 허용한다. 그러니 프로그래머는 이러한 형 변환을 진행하는 경우
대입의 가능성을 정확히 판단하여 치명적인 실수가 발생하지 않도록 주의해야 한다.
하위 클래스에서 상위 클래스의 메서드를 다시 정의하는 과정이며
다형성의 핵심 원리 중 하나로, 상속 관계에 있는 클래스 간에 메서드의 동작을
변경할 수 있게 한다.
오버라이딩 시 주요 특징과 주의점
오버라이딩하는 메서드의 시그니처( 이름, 매개변수 타입, 반환 타입 )는
상위 클래스의 메서드와 정확하게 일치해야 한다. 그렇지 않으면 새로운 메서드를
정의하는 것이 된다.
@Override
어노테이션을 사용하여 컴파일러에게 해당 메서드가 super클래스에서 오버라이딩 된 메서드임을 명시적으로 알려준다.
이렇게하면 오나타 잘못된 시그니처를 방지할 수 있다.
오버라이딩된 메서드의 접근 제어자는
슈퍼 클래스의 메서드와 같거나 더 넓은 범위로 변경할 수 있다.
예를 들어, super 클래스의 protected
로 선언된 메서드를 -> public
으로
오버라이딩 할 수 있다.
final
은 오버라이딩을 금지하고
static
과 private
메서드는 오버라이딩 할 수 없다.
ㄴ 인스턴스에 종속되지 않으므로 오버라이딩 대상이 아님 !
오버라이딩 된 메서드는 super 클래스의 메서드가 던지는 예외와 동일하거나
해당 예외의 하위 클래스를 던질 수 있다.
그러나 더 많은 예외를 던지거나 던지지 않는 것은 허용되지 않는다.
class SuperClass {
void somethingMethod() throws IOException {}
}
class SubClass extends SuperClass {
@Override
// 가능
void somethingMethod() throws FileNotFoundException {}
// 불가능
void somethingMethod() throws Exception {}
}
void somethingMethod() throws Exception 선언이 불가능한 이유는
Super Class에 상속받은 메서드의 IOException보다 Exception이 우선순위가 더 높기 때문
따라서 상속받은 메서드의 excepion의 우선 순위도 생각해야 한다.