Java의 상속
- 프로그래밍 언어/JAVA
- 2023. 1. 20.
들어가기 전
이 글은 자바의 정석 7장을 공부하고 개인적으로 정리한 글입니다.
1. Java의 상속
상속은 기존의 클래스를 재사용하여 새로운 클래스를 생성하는 작업이다. 상속을 통해서 클래스를 작성하면 적은 양의 코드로 새로운 클래스를 작성할 수 있고, 공통된 코드를 한 곳에서 관리할 수 있다. 아래와 같이 정리해 볼 수 있다.
- 클래스 간에 부모 - 자식 관계는 있다. 그렇지만 형제 관계는 존재하지 않는다.
- 부모의 모든 것은 자식에게 상속된다. 하지만 자식에서의 변경점은 부모에게 영향을 미치지 않는다.
- 부모 클래스의 private인 필드 / 메서드는 자식 클래스에서 사용할 수 없다.
- 자식 클래스를 생성할 때 부모 클래스가 생성되고, 그것이 자식 클래스에게 포함되어 사용되기 때문이다.
- 자식 클래스를 통해 부모 클래스에만 정의된 함수를 호출할 수 있다. 이 때는 부모 클래스에 정의된 함수를 호출하는 것이기 때문에 부모 클래스에 private로 선언된 함수도 내부적으로 호출한다.
public class Parent {
private String hello = "1";
public Parent() {
System.out.println("parent created");
}
public void sayHello() {
sayHelloWrapper();
}
private void sayHelloWrapper() {
System.out.println("parent hello");
}
}
Parent 클래스를 상속받은 자식 클래스는 자식 클래스 내부에서 직접적으로 sayHelloWrapper(), hello를 참조할 수 없다. 왜냐하면 private로 설정되어있기 때문이다. 아래 코드를 살펴보자
- Child1 클래스에는 sayHello() 메서드가 선언되지 않았다. 그렇지만 누군가가 Child1 객체를 생성해서 sayHello()를 호출한다면, 이 메서드는 호출된다. 부모 클래스의 sayHello()가 자식에게 상속되었고 그것이 호출되기 때문이다. 부모 클래스의 sayHello()가 호출되기 떄문에 이 때 sayHelloWrapper()는 호출된다.
- Child1 클래스 내부에서 메서드를 호출할 때, 부모 클래스의 sayHelloWrapper()를 호출할 수는 없다. 왜냐하면 부모 클래스에 private로 선언되어있기 때문이다. 마찬가지로 부모 클래스의 hello 변수도 참조할 수 없다.
public class Child1 extends Parent {
public Child1() {
System.out.println("child create");
}
}
public class Parent {
private String hello = "1";
public Parent() {
System.out.println("parent created");
}
public void sayHello() {
sayHelloWrapper();
}
private void sayHelloWrapper() {
System.out.println("parent hello");
}
}
2. 상속의 대상
클래스를 상속한다고 해서 클래스의 모든 내용이 상속되지는 않는다. 상속되지 않는 부분은 다음과 같다.
- 생성자와 초기화 블록은 상속되지 않는다.
상속되지만 상속 안되는 것처럼 보이는 녀석들
- 부모 클래스에 private로 선언된 필드와 메서드도 상속된다. 그렇지만 private이기 때문에 자식 클래스에서 접근이 제한된다
3. 상속의 장점
상속을 하게 되면 코드의 재사용성이 증가하고, 공통된 코드를 하나에 몰아서 관리할 수 있다. 공통된 코드가 여러군데에 퍼져있을 때 수정을 하다가 하나라도 빼먹는 경우가 발생할 수 있다. 이렇게 하나씩 빼먹을 수 있는 경우를 미연에 방지하면서 코드의 신뢰성 향상도 도모할 수 있다.
public class Dog {
private void walk() {
System.out.println("walk");
}
}
public class Cat {
private void walk() {
System.out.println("walk");
}
}
위에는 Dog, Cat 클래스가 있고 공통적으로 사용하는 walk() 메서드가 존재한다. 어느 날 walk() 메서드에서 'walk'를 출력하는 것이 아니라 'walk fast'를 출력하도록 바꿔야 한다고 가정해보자. 그러면 2군데를 모두 변경해야한다. 2군데 정도야 실수 안하고 바꿀 수 있지만, 만약 이 코드가 사용되는 부분이 100군데라면 100군데를 놓치지 않고 변경하기는 쉽지 않다. 따라서 이 부분을 상속을 이용해서 해결 해볼 수 있다.
public class Animal {
private void walk() {
System.out.println("walk");
}
}
public class Cat extends Animal{}
public class Dog extends Animal{}
Animal 클래스에 walk() 메서드를 생성하고 Cat, Dog 클래스가 Animal 클래스를 상속받도록 하면서 변경점을 하나로 응집할 수 있다. 상속에는 이처럼 공통적으로 사용하는 부분을 응집하는 장점이 있지만, 반면에 자식 클래스와 부모 클래스가 강하게 결합되어있다는 점이 단점이 될 수 있다.
3. 클래스 간의 관계 및 포함 관계
클래스의 관계는 크게 두 가지 관계가 존재할 수 있다.
- 포함관계(composite) : A has B
- 상속관계(inheritance) : A is B
전통적으로는 has, is 중 어느 문장이 더 맞는지를 판단해서 포함 / 상속 관계를 설정할 수 있다. 아래 문장을 살펴보면 개는 동물이다가 더욱 설득력이 있기 때문에 개와 동물의 관계는 상속 관계로 표현을 할 수 있다. 반면 '자동차는 타이어를 가지고 있다'가 더 맞기 때문에 자동차와 타이어의 관계는 포함관계가 된다.
- 개는 동물이다.
- 개는 동물을 가지고 있다.
- 자동차는 타이어다.
- 자동차는 타이어를 가지고 있다.
포함관계는 하나의 클래스가 다른 클래스를 가지고 있는 관계를 의미하고, 코드로는 아래와 같이 나타낼 수 있다. Car 클래스에 멤버 변수로 Tire를 가지고 있는데, 이런 관계를 포함관계라고 한다.
public class Car {
private Tire tire;
}
4. 단일상속만 지원
C, C++ 같은 언어들은 다중 상속을 지원하지만 Java는 단일 상속만을 지원한다. 단일 상속의 의미는 '딱 하나의 부모 클래스만 상속한다'라는 의미다. 그렇다면 Java는 왜 다중 상속을 지원하지 않을까? 아래의 경우를 한번 고려해보자.
- A, B 클래스에 hello 라는 변수가 각각 선언되어있다. 이 클래스들을 상속받은 자식 클래스가 hello라는 변수를 호출하면 어떤 값이 나와야 할까?
- A, B 클래스에 say() 라는 메서드가 각각 선언되어 있다. 이 클래스들을 상속받은 자식 클래스가 say() 메서드를 실행하면 어떤 메서드가 실행되어야 할까?
다중상속을 하게 되면 위와 같이 고려해야 할 부분이 많아진다. 클래스 간의 관계가 복잡해지기 때문에 자바는 단일 상속만 지원한다고 한다. 대신 아래와 같이 연쇄적인 상속은 지원한다.
5. 모든 클래스들의 부모 클래스, Object
Object 클래스는 모든 클래스들의 최상위 클래스다. 예를 들어 아래처럼 클래스를 선언한다면, 컴파일러가 자동적으로 extends Object를 넣어준다. 즉, TV extends Object와 같은 형식이 된다.
class TV {
...
}
// 컴파일 시점
class TV extends Object {
...
}
class HDTV extends TV {
...
}
만약 HDTV extends TV와 같이 선언하게 되면, HDTV는 TV를 상속받는다. 반면 TV는 상속이 명시된 것이 없으므로 자동적으로 Object를 상속받게 된다. 따라서 HDTV는 Object 클래스에 선언된 메서드와 변수를 사용할 수 있게 된다.
public class Object {
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
...
}
public String toString() {
...
}
public final native void notify();
...
}
Object 클래스에는 equals(), hashCode(), toString() 같은 메서드들이 이미 선언되어 있다. 모든 클래스는 Object 클래스를 상속받기 때문에 모든 클래스는 별다른 작업을 하지 않아도 equals(), hashCode() 같은 메서드들을 사용할 수 있는 것이다.
6. 오버라이딩
오버라이딩은 부모 클래스에 정의된 메서드를 자식 클래스에서 재정의하는 행위를 의미한다. 오버로딩이란 용어도 존재하는데, 오버로딩은 부모 클래스에 정의되지 않은 메서드를 자식 클래스에서 정의하는 것을 의미한다.
- 오버 라이딩 : 부모 클래스에 정의된 것을 자식 클래스에서 재정의
- 오버 로딩 : 부모 클래스에 정의되지 않은 것을 자식 클래스에서 새롭게 정의
코드로 살펴보면 아래와 같은 내용을 살펴볼 수 있다.
public class Child extends Parent{
// 오버라이드
@Override
public void sayHello(String comment) {
System.out.println("comment = " + comment + "comment");
}
// 오버로딩
public void sayBye() {
System.out.println("Bye");
}
}
public class Parent {
public void sayHello(String comment) {
System.out.println("comment = " + comment);
}
}
6.1 오버라이딩의 조건
오버라이딩은 부모 클래스에 선언된 메서드를 자식 클래스에서 재정의하는 것을 의미한다고 했다. 오버라이딩은 아무렇게나 할 수 있는 것은 아니고, 오버라이딩의 조건이 존재한다.
- 메서드의 이름이 같아야 한다.
- 파라메터가 갯수, 타입, 순서가 같아야 한다.
- 파라메터의 이름은 달라도 괜찮다.
- 반환타입이 같아야 한다.
- 부모 클래스보다 자식 클래스의 접근 제어자의 범위가 좁을 수 없다. (public > protected > default > private)
- 부모 클래스보다 자식 클래스에 더 많은 예외가 선언될 수 없다.
예외는 '예외의 갯수'보다는 얼마나 많은 예외를 Catch 할 수 있는지에 대한 척도다.
public class Parent {
public void sayHello(String comment) throws IOException {
System.out.println("comment = " + comment);
}
}
public class Child extends Parent{
@Override
public void sayHello(String comment1) throws Exception {
System.out.println("comment = " + comment1 + "comment");
}
}
예를 들어 Child에는 Exception이 throw 되고, Parent에는 IOException이 throw된다. 그런데 Exception은 모든 예외를 잡을 수 있기 때문에 Child 클래스의 오버라이딩된 메서드가 잡을 수 있는 예외가 더 많다. 따라서 이것은 오버라이딩 규칙을 만족하지 못한다. 만약 위의 코드가 동작하도록 만들고 싶다면 Exception → IOException으로 바꿔줘야한다.
6.2 super 키워드
super는 자식 클래스에서 부모 클래스에게 상속받은 변수 / 메서드를 참조하고 싶을 때 사용하는 키워드다. 즉, 부모를 가리키는 키워드라고 생각하면 된다. this, super는 하나처럼 사용되는 경우가 있기도 한데 기본적으로는 구별해서 사용하는 것이 좋다고 생각한다.
public class MyParent1{
// private라면 this.hello가 안됨.
public String hello;
}
public class MyChild1 extends MyParent1{
public void sayHello() {
System.out.println("hello = " + super.hello);
System.out.println("hello = " + this.hello);
}
}
위 코드에서는 this, super는 같은 의미로 사용된다. hello라는 변수는 자식 변수에서 선언되지 않았기 때문에 자식 클래스로 상속된다. 따라서 자식 클래스는 부모 클래스의 hello라는 변수를 가진다. 따라서 this(자기 자신)을 가리키거나, super(부모 클래스)로 가리키더라도 동일한 의미가 된다. 이 경우에 this, super는 같은 의미가 된다.
public class MyParent1{
public String hello = "hello";
}
public class MyChild1 extends MyParent1{
public String hello = "child hello";
public void sayHello() {
System.out.println("hello = " + super.hello);
System.out.println("hello = " + this.hello);
}
}
반대로 이 코드에서는 서로 다른 의미로 사용된다. 왜냐하면 자식 클래스에서 동일한 이름의 변수가 새롭게 정의되었기 때문이다. 따라서 this.hello / super.hello가 가리키는 값이 다르다.
6.3 super() 부모 클래스 생성자와 생성 순서
자식 객체가 생성되면 부모 클래스의 모든 변수와 메서드를 참조할 수 있어야한다. 따라서 자식 객체가 생성될 때는 부모 클래스의 객체까지 함께 생성되어야 부모 클래스의 변수 / 메서드를 참조할 수 있다. 따라서 자식 객체의 생성자의 코드 라인 첫줄에는 적절한 부모 클래스의 생성자가 반드시 포함되어야 한다.
public class MyParent1{
public String hello = "hello";
}
public class MyChild1 extends MyParent1{
public MyChild1() {
// 부모 생성자
// super(); → 없는 경우 자동 추가됨.
}
}
만약 위 코드처럼 자식 생성자에 적절한 부모 생성자가 들어가지 않을 경우, 컴파일러는 기본적으로 super()를 자동적으로 넣어준다. 이런 형태는 어떨 때는 문제가 될 수 있고, 어떨 때는 문제가 되지 않을 수 있다.
public class MyParent1{
int x;
int y;
}
public class MyChild1 extends MyParent1 {
int z;
public MyChild1(int x, int y, int z) {
// 생략 가능
super();
this.x = x;
this.y = y;
this.z = z;
}
}
위의 경우 부모 클래스에는 기본 생성자가 존재한다. 따라서 MyChild1 생성자로 인스턴스를 생성할 때 super()를 생략해도 문제가 발생하지 않는다. 왜냐하면 super()는 부모 클래스의 기본 생성자를 의미하기 때문이다.
public class MyParent1{
int x;
int y;
public MyParent1(int x, int y) {
this.x = x;
this.y = y;
}
}
public class MyChild1 extends MyParent1 {
int z;
public MyChild1(int x, int y, int z) {
super(x, y);
this.z = z;
}
}
반면 위 경우에는 반드시 super(x, y)라는 부모 생성자를 직접적으로 명시해야한다. 특정 클래스에 생성자가 하나 정의되는 순간 컴파일러는 기본 생성자를 제공해주지 않는다. 따라서 MyParent1() 이라는 생성자가 없기 때문에 super()를 호출할 수 없다. 따라서 위와 같이 적절한 부모 생성자를 명시해줘야 한다.
6.4 상속 클래스의 생성자 실행 순서
public class MyMyParent extends Object{
public MyMyParent() {
print('parent')
}
}
public class MyMyChild extends MyMyParent{
public MyMyChild() {
print('child');
}
}
new MyMYChild();
위와 같은 코드가 있다면 실제로는 어떤 형태로 코드가 수행될까? 다음 순서대로 수행된다.
- MyMyChild()가 호출된다.
- MyMyChild()에서는 super()를 이용해 MyMyParent()를 호출한다.
- MyMyParent()는 super()를 이용해 Object()를 호출한다.
- parent를 출력한다.
- child를 출력한다.
- 리턴한다.
자식 클래스의 생성자가 호출되면, 가장 위에 있는 부모 클래스의 생성자부터 하나씩 순서대로 호출된다는 것을 이해할 수 있다.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Effective Java : 아이템3. 생성자나 열거 타입으로 싱글턴임을 보증하라. (0) | 2023.02.25 |
---|---|
Java : Primitive Type / Reference Type (0) | 2023.02.05 |
Java Stream과 Stream 활용한 예시 (0) | 2022.02.25 |
멀티 쓰레드 관련 공부 (0) | 2022.02.25 |
Java : 사용자 정의 어노테이션 만들기 (0) | 2022.02.24 |