[Java] +82-기본편-정리하기 : 상속 편
- 상속
객체 지향 프로그래밍에서는 부모 클래스의 멤버를 자식 클래스에게 물려줄 수 있다. 부모 클래스를 상위 클래스라고 부르고, 자식 클래스를 하위 클래스 또는 파생 클래스라고 부른다.
상속을 이용하면 부모 클래스의 수정으로 모든 자식 클래스들도 수정되는 효과를 가져온다.
- 클래스 상속
class 자식클래스 extends 부모클래스 {
// 필드
// 생성자
// 메소드
}
자바에서 상속은 다음과 같은 특징을 가지고 있다.
① 자바는 다중 상속을 허용하지 않아 여러 개의 부모 클래스를 상속할 수 없다. extends 뒤에는 단 하나의 부모 클래스만 와야 한다.
② 부모 클래스에서 private 접근 제한을 갖는 필드와 메소드는 상속 대상에서 제외된다. 그리고 부모와 자식 클래스가 다른 패키지에 존재한다면 default 접근 제한을 갖는 필드와 메소드도 상속 대상에서 제외된다.
- 부모 생성자 호출
자식 객체를 생성하면, 부모 객체가 먼저 생성되고 그다음에 자식 객체가 생성된다. 모든 객체는 클래스의 생성자를 호출해야만 생성된다. 부모 객체도 예외는 아니다. 부모 생성자는 자식 생성자의 맨 첫 줄에서 호출된다.
자식 생성자가 명시적으로 선언되지 않았다면, 컴파일러는 super(); 라는 부모의 기본 생성자를 생성한다. 만약 직접 자식 생성자를 선언하고 명시적으로 부모 생성자를 호출하고 싶으면 다음과 같이 작성하면 된다.
자식클래스( 매개변수선언, ... ) {
super( 매개값, ... );
...
}
super( 매개값, ... )는 매개값의 타입과 일치하는 부모 생성자를 호출한다. 만약 매개값의 타입과 일치하는 부모 생성자가 없다면 컴파일 에러가 발생한다.
super( 매개값, ... )가 생략되면 컴파일러에 의해 super()가 자동적으로 추가되기 때문에 부모의 기본 생성자가 존재해야 한다. 부모 클래스에 기본 생성자가 없고 매개 변수가 있는 명시적 생성자만 있다면 자식 생성자에서 반드시 부모 생성자 호출을 위해 super( 매개값, ... )를 명시적으로 호출하거나 부모 클래스에 기본 생성자를 생성하면 된다. super()는 반드시 자식 생성자 첫 줄에 위치해야 하며, 그렇지 않으면 컴파일 에러가 발생한다.
- 메소드 재정의
부모 클래스의 모든 메소드가 자식 클래스에 맞게 설계되어 있지 않다. 이 경우에는 상속된 일부 메소드를 자식 클래스에 맞게 다시 수정해서 사용해야 한다. 이를 위해 자바는 메소드 재정의(오버라이딩: Overriding) 기능을 제공한다.
메소드 재정의는 자식 클래스에서 부모 클래스의 메소드를 다시 정의하는 것을 말한다. 메소드 재정의할 때는 다음과 같은 규칙을 주의하여 작성해야 한다.
- 부모의 메소드와 동일한 시그니처(리턴 타입, 메소드 이름, 매개 변수 목록)를 가져야 한다.
- 접근 제한을 더 강하게 재정의할 수 없다.
>> 부모 메소드가 public 접근 제한을 가지고 있을 경우 재정의하는 자식 메소드는 default나 private 접근 제한으로 수정할 수 없다는 뜻이다. 그러나 반대의 경우는 가능하다.
- 새로운 예외를 throws할 수 없다.
메소드가 재정의되었다면 부모 객체의 메소드는 숨겨지기 때문에 자식 객체에서 메소드를 호출하면 재정의된 자식 메소드가 호출된다.
그렇기에 자식 클래스 내부에서 재정의된 부모 클래스의 메소드를 호출해야 하는 상황이 발생한다면 명시적으로 super 키워드를 붙여서 부모 메소드를 호출할 수 있다.
super.부모메소드();
- final 클래스와 final 메소드
final 키워드는 클래스, 필드, 메소드를 선언할 때 사용할 수 있는 키워드로, 초기값 설정 후 더 이상 값을 변경할 수 없다는 것을 의미한다.
클래스와 메소드를 선언할 때 final 키워드가 지정되면 상속과 관련이 있다는 의미이다.
- 상속할 수 없는 final 클래스
클래스를 선언할 때 final 키워드를 class 앞에 붙이면 이 클래스는 최종적인 클래스이므로 상속할 수 없는 클래스가 된다. 즉, final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다.
public final class 클래스 { ... }
- 재정의할 수 없는 final 메소드
메소드를 선언할 때 final 키워드를 붙이면 이 메소드는 최종적인 메소드이므로 재정의할 수 없는 메소드가 된다. 즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때 부모 클래스에 선언된 final 메소드는 자식 클래스에서 재정의할 수 없다.
public final 리턴타입 메소드( [매개변수, ...] ) { ... }
- protected 접근 제한자
protected 접근 제한자는 상속과 관련이 있다. protected는 public과 default 접근 제한의 중간 쯤에 해당한다. 같은 패키지에서는 default와 같이 접근 제한이 없지만 다른 패키지에서는 자식 클래스만 접근을 허용한다.
서로 다른 패키지에 있는 부모 클래스의 protected 필드, 생성자, 메소드에 자식 클래스가 접근 가능하다. 단 new 연산자를 사용해서 생성자를 직접 호출할 수는 없고, 자식 생성자에서 super()로 부모 클래스의 생성자를 호출할 수 있다.
- 타입 변환과 다형성
다형성은 사용 방법은 동일하지만 다양한 객체를 이용해서 다양한 실행 결과가 나오도록 하는 성질이다. 다형성을 구현하려면 메소드 재정의와 타입 변환이 필요하다.
메소드 재정의 + 타입 변환 >> 다형성
- 자동 타입 변환(Up casting)
클래스도 마찬가지로 타입 변환이 있는데, 클래스의 변환은 상속 관계에 있는 클래스 사이에서 발생한다. 자식 클래스의 객체가 부모 클래스의 타입으로 형변환되는 것을 업캐스팅이라고 한다. 자식은 부모 타입으로 자동 타입 변환이 가능하다는 것을 의미한다.
부모타입 변수 = 자식타입;
예를 들어, 부모 클래스가 Animal, 자식 클래스가 Cat일 때 Cat 클래스로부터 Cat 객체를 생성하고 이것을 Animal 변수에 대입하면 자동 타입 변환이 일어난다.
Cat cat = new Cat();
Animal animal = cat; // up-casting
이때 cat과 animal 변수는 타입만 다를 뿐, 동일한 Cat 객체를 참조한다.
자동 타입 변환의 개념은 자식은 부모의 특징과 기능을 상속받으므로 부모와 동일하게 취급받을 수 있다는 것이다. 바로 위의 부모가 아니더라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있다.
class A {}
class B extends A {}
class C extends A {}
class D extends B {}
class E extends C {}
public class Main {
public static void main(String[] args)
{
B b = new B();
C c = new C();
D d = new D();
E e = new E();
A a1 = b;
A a2 = c;
A a3 = d;
A a4 = e;
B b1 = d;
C c1 = e;
// error (<<상속 관계가 아님)
//B b3 = e;
//C c2 = d;
}
}
부모 타입으로 업캐스팅된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다. 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로만 한정된다. 그러나 메소드가 자식 클래스에서 재정의되었다면 자식 클래스의 메소드가 대신 호출된다. 이는 다형성과 관련이 있다.
public class Parent {
public void method1() { }
public void method2() { }
}
public class Child extends Parent {
@Overrid
public void method2() { }
public void method3() { }
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
Parent parent = child // up-cating
parent.method1();
parent.method2(); // 재정의된 메소드 호출
//parent.method3(); << error
}
}
parent는 Child 객체를 가리키지만, parent가 Parent 타입이므로 Parent 클래스의 멤버에만 접근이 가능하다. 그러므로 parent.method3()에서 컴파일 에러가 발생한다.
필드의 다형성
필드의 타입을 부모 타입으로 선언하면 다양한 자식 객체들이 저장될 수 있기 때문에 필드 사용 결과가 달라질 수 있다. 이것이 필드의 다형성이다.
부모 클래스를 상속하는 자식 클래스는 부모가 가지고 있는 필드와 메소드를 가지고 있으니 사용 방법이 동일할 것이다. 자식 클래스는 부모의 메소드를 재정의해서 메소드의 실행 내용을 변경함으로써 더 우수한 실행결과가 나오게 할 수도 있다. 그리고 자식 타입을 부모 타입으로 변환할 수 있다. 이 세 가지(상속, 재정의, 타입 변환)가 다형성을 구현할 수 있는 기술적 조건이 된다.
매개 변수의 다형성
자동 타입 변환은 필드의 값을 대입할 때에도 발생하지만, 주로 메소드를 호출할 때 많이 발생한다. 메소드를 호출할 때는 매개 변수의 타입과 동일한 매개값을 주는 것이 정석이지만, 매개값을 다양화하기 위해 매개 변수에 자식 객체를 줄 수도 있다.
따라서 매개 변수의 타입이 클래스일 경우, 해당 클래스의 객체뿐만 아니라 자식 객체까지도 매개값으로 사용될 수 있다. 즉, 매개 변수의 다형성은 매개값으로 어떤 자식 객체가 제공되느냐에 따라 메소드의 실행결과가 다양해질 수 있다는 것이다. 자식 객체가 부모의 메소드를 재정의했다면 메소드 내부에서 재정의된 메소드를 호출함으로써 메소드의 실행결과는 다양해진다.
- 강제 타입 변환(Down casting)
부모 타입을 자식 타입으로 변환하는 것을 다운캐스팅이라고 한다. 자식 타입이 부모 타입으로 자동 타입 변환한 후 변환할 때 강제 타입 변환을 사용할 수 있다.
자식타입 변수 = (자식타입) 부모타입;
예를 들어, 다음 코드와 같이 Child 객체가 Parent 타입으로 자동 변환된 상태에서 원래의 Child로 강제 변환하는 것이다.
Parent parent = new Child(); // up-casting
Child child = (Child)parent; // down-casting
자식 타입이 부모 타입으로 업캐스팅되면, 부모에 선언된 필드와 메소드만 사용가능하다. 만약 자식에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해서 다시 자식 타입으로 변환한 다음 자식의 필드와 메소드를 사용하면 된다.
public class Parent {
public String field1;
public void method1() { }
public void method2() { }
}
public class Child extends Parent {
public String field2;
public void method3() { }
}
public class Main {
public static void main(String[] args) {
Parent parent = new Child(); // up-cating
parent.field1 = "data1";
parent.method1();
parent.method2();
/*
parent.field2 = "data2";
parent.method3(); << error
*/
Child child = (Child)parant; // down-cating
child.field2 = "data2";
child.method3();
}
}
- 객체 타입 확인
강제 타입 변환은 자식 타입이 부모 타입으로 변환되어 있는 상태에서만 가능하기 때문에 다음과 같이 처음부터 부모 타입으로 생성된 객체는 자식 타입으로 변환할 수 없다.
Parent parent = new Parent();
Child child = (Child) parent; // 강제 타입 변환 불가능
이때 어떤 객체가 어떤 클래스의 인스턴스인지 확인하기 위해 Instanceof 연산자를 사용한다. instanceof 연산자의 좌항에는 객체가 오고 우항에는 타입이 오는데, 좌항의 객체가 우항의 인스턴스이면, 즉 우항의 타입으로 객체가 생성되었다면 true, 그렇지 않다면 false를 리턴한다.
boolean result = 좌항(객체) instanceof 우항(타입)
public void method(Parent parent) {
if(parent instanceof Child) {
Child child = (Child) parent;
}
}
instanceof 연산자는 주로 매개값의 타입을 조사할 때 사용된다. 메소드 내에서 강제 타입 변환이 필요할 경우 반드시 매개값이 어떤 객체인지 instanceof 연산자로 확인하고 안전하게 다운캐스팅해야 한다.
만약 타입을 확인하지 않고 강제로 타입 변환을 시도한다면 ClassCastException이 발생할 수 있다.
- 추상 클래스: 클래스들의 공통적인 필드와 메소드를 추출해서 선언한 클래스를 말한다.
- 추상 메소드: 추상 클래스에서만 선언할 수 있고, 메소드의 선언부만 있는 메소드를 말한다. 추상 메소드는 자식 클래스에서 재정의되어 실행 내용을 결정해야 한다.
public abstract class Animal { // 추상 클래스
public String Kind;
public void breathe() {
System.out.println("숨을 쉽니다.");
}
public abstract void sound(); // 추상 메소드
} // Animal.java
public class Dog extends Animal {
public Dog() {
this.Kind = "포유류";
}
@Override
public void sound() {
System.out.println("멍멍"); // 추상 메소드 재정의
}
} // Dog.java
public class Cat extends Animal {
public Cat() {
this.Kind = "포유류";
}
@Override
public void sound() {
System.out.println("야옹"); // 추상 메소드 재정의
}
} //Cat.java
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
dog.sound();
cat.sound();
// 변수의 자동 타입 변환
Animal animal = null;
animal = new Dog(); // 자동 타입 변환 및 재정의된 메소드 호출
animal.sound();
animal = new Cat(); // 자동 타입 변환 및 재정의된 메소드 호출
animal.sound();
// 메소드의 다형성
animalSound(new Dog()); // 자동 타입 변환
animalSound(new Cat()); // 자동 타입 변환
}
public static void animalSound(Animal animal) {
animal.sound(); // 재정의된 메소드 호출
}
} // Main.java
① 가장 일반적인 방식으로, Dog와 Cat 변수로 호출
② Animal 변수로 타입 변환해서 sound() 메소드를 호출. 자식은 부모 타입으로 자동 타입 변환될 수 있고, 메소드가 재정의되어있을 경우 재정의된 자식 메소드가 호출되는 다형성의 특징이 그대로 적용된다.
③ 부모 타입의 매개 변수에 자식 객체를 대입해서 메소드의 다형성을 적용했다. ②와 같은 원리로 자식 객체가 부모 타입으로 자동 타입 변환되어 재정의된 sound() 메소드가 호출된다.
참고))
07-2 타입 변환과 다형성
다형성이란 동일하지만 다양한 객체를 이용해서 다양한 실행 결과가 나오도록 하는 성질을 나타낸다. 예를 들어 자동차가 타이어를 사용하는 방법은 동일하지만 어떤 타이어를 사용(장착)하느
yuna96.tistory.com
[자바, Java] 캐스팅 - 업캐스팅(Upcasting), 다운캐스팅(Downcasting)
캐스팅(Casting)이란? 캐스팅이란 타입을 변환하는 것을 말하며 형변환이라고도 한다. 자바의 상속 관계에 있는 부모와 자식 클래스 간에는 서로 간의 형변환이 가능하다. 업캐스팅(Upcasting) 업캐
computer-science-student.tistory.com