자바의 상속

June Lee·2021년 1월 28일
0

Java

목록 보기
6/23

자바에서 어떤 클래스가 가진 프로퍼티와 메서드를 확장하여 사용하고 싶을 때, 해당 클래스를 extends 키워드를 통해 상속받는 형식으로 구현한다.

extends는 어떨 때 사용할까?
상속을 이용하는 경우는 부모 자식 클래스 간에 is-a 관계가 성립할 때이다.
이와 대립되는 개념으로 has-a 관계가 있다.
is-a 관계: '스포츠카는 자동차이다.'와 같이 ~는 ~이다가 성립하는 관계.
has-a 관계: '자동차는 핸들을 가지고 있다.'와 같은 클래스 간 연관 관계

연관 관계와 의존 관계?
연관 관계: 멤버 변수로 객체를 갖는 경우. 클래스 차원에서의 결합이며, 소스와 타겟 요소 간이 완전히 연결되 있다.
의존 관계: 클래스 내 특정 메서드에서 객체를 매개 변수 등으로 활용하는 경우. 메서드 차원의 관계이며, 소스 요소가 타겟 요소의 변환에 영향을 받는다.

집합 관계와 합성 관계?
합성 관계: 각 요소가 전체의 부분으로써만 의미를 가지는 경우
집합 관계: 각 요소가 독립성을 가지고 떼어서도 사용할 수 있는 경우


상속 시 내부적으로 일어나는 일

자식 클래스의 생성자 첫 줄에서는 반드시 부모 클래스의 생성자가 super()를 통해 호출되어야 한다.

Student student = new Student();

따라서 위와 같은 코드가 있다면,

자식 클래스의 생성자 호출 -> 부모 클래스의 생성자 호출 -> 부모 객체 생성(메모리 할당) -> 자식 객체 생성(메모리 할당)

과 같은 순으로 내부적으로 동작한다.

메서드 오버라이딩

오버라이딩과 오버로딩은 매우 다른 개념이다. 오버라이딩은 상속에서 나오는 개념으로, 부모 클래스의 메서드를 자식 클래스에서 수정하여 사용하는 것이다.
오버라이딩 시에는 부모 클래스에서 수정하고 싶은 메서드의 이름과 같은 이름의 메서드를 자식 클래스에서 만들어준다.

cf.
오버로딩: 한 클래스 내에 같은 이름의 메서드를 여러 개 정의하는 것.
메서드 이름이 같지만, 매개변수의 개수 또는 타입이 다른 경우.
(반환 타입은 관계 없음)
@Override
public int withdraw(int amount) throws Exception {
	// TODO Auto-generated method stub
	if ((getBalance() + creditLine) < amount) {
		throw new Exception("인출이 불가능합니다.");
	} else {
		setBalance(getBalance() - amount);
		return getBalance();
	}
}

단순히 부모 클래스에서 수정하고 싶은 메서드가 있는 거라면, 자식 클래스의 특정 메서드에서 해당 수정하고 싶은 메서드를 호출한 후 이를 포함하여 수정하는 방식으로도 충분히 구현할 수 있다. 어쨌든 이름이 다른 새로운 메서드를 만들면 된다는 말인데, 이렇게 하지 않는 가장 큰 이유가 바로 다형성(Polymorphism)이다.
객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미한다.

다형성을 설명할 때 가장 자주 등장하는 예시가 바로 도형이다. 예를 들어 부모인 도형 클래스에 draw()라는 메서드가 있고, 이를 상속받은 삼각형, 사각형, 원 클래스에서도 각각 이를 수정한 draw()를 구현하고 싶다고 할 때, 부모 클래스의 draw()를 override하여 같은 이름의 draw() 메서드를 구현한다.

도형 - draw(){}

삼각형 - draw(){}
사각형 - draw(){}-draw(){}

솔직히 처음 다형성의 개념을 접했을 때는 그래서 이게 왜 좋은건데?라는 생각이 들었다. 그런데 다음 예시를 보고 다형성의 장점을 확실히 느낄 수 있었다.

도형 arr[] = {new 삼각형(), new 사각형(), new()};

for(int i = 0; i < ar.length; i++){
	arr[i].draw();
}

위와 같이 도형 타입의 객체 배열에 삼각형, 사각형, 원의 객체를 생성하여 주소를 담아두는 경우, 겉으로 보기에는 draw()라는 같은 이름의 메서드인데도 서로 다르게 오버라이딩된 메서드를 호출할 수 있다.

그리고 위의 코드가 가능한 것은 자식 데이터형에서 부모 데이터형으로는 자동 형변환이 가능하기 때문이다.


상속에서 형변환

Car car = new SportsCar();

Car라는 이름의 부모 클래스와 Sportscar라는 이름의 자식 클래스가 있을 때, 위와 같이 작성하면, 1) SportsCar 클래스형을 가진 객체가 만들어지고, 2) 이 객체가 Car 클래스형으로 자동 형변환되어서 car에 저장되는 과정을 거친다. 이를 풀어서 코드로 설명하면 다음과 같다.

SportsCar sportsCar = new SportsCar();
Car car = (Car) sportsCar;

위의 경우, 데이터 타입은 Car이지만 생성된 것은 SportsCar 객체이다. 이와 같이 자식 클래스형에서 부모 클래스형으로 묵시적 형변환이 가능한 것은, 조상타입으로 형변환하면 다룰 수 있는 멤버의 개수가 항상 줄어들거나 같으므로 안전하기 때문이다.

다룰 수 있는 멤버의 개수가 줄어들었는 말은,

Car car = new SportsCar();

위 상황에서 car를 이용해 접근할 수 있는 변수와 메서드의 범위과 부모 클래스에서 오버라이딩한 메서드로만 제한된다는 뜻이다. 즉,

class SportsCar extends Car {
	...
    @override
    public void ride(){
    	...
    }
    
    public void openCeiling(){
    	...
    }
	

}

위와 같은 메서드들이 자식 클래스에 구현되어있다고 할 때, 이 중 Car 클래스에서 오버라이딩한 ride 메서드만을 사용할 수 있다는 뜻이다. (따라서 자식 클래스의 멤버 변수에 또한 접근할 수 없다)

Car car = new SportsCar();
SportsCar sportsCar = (SportsCar) car; // (o)

그러나 위와 같이 부모 클래스형으로 형변환한 자식 객체의 경우, 강제 형변환을 통해 다시 자신의 데이터 타입으로 형변환할 수 있다.

이와 반대로, 원래 부모 클래스형으로 만들어진 부모 객체의 경우, 강제 형변환을 하더라도 자식 클래스형으로는 바꿀 수 없다.

Car car = new Car();
SportsCar sportsCar = (SportsCar) car; // (x)

부모 클래스형으로의 형변환이 필요한 이유?

그런데 이 시점에서 자식 클래스형을 대체 왜 부모 클래스형으로 변환해야하지에 대해 근본적인 의문이 들 수 있다.
자료형 간 형변환이 자유롭지 않은 것은 서로 다른 자료형의 객체들을 한 데에 묶을 때에 어려움을 초래한다.

도형 arr[] = {new 삼각형(), new 사각형(), new()};

예를 들어 위의 예시에서와 같이 서로 다른 데이터형을 가진 객체를 하나의 배열에 넣고 싶을 수가 있는데, 부모 클래스형으로의 형변환이 불가능하다면 이와 같은 조작이 불가능하다.

또한, 어떤 메소드에 매개변수로 들어오는 데이터 타입이 SportsCar, Truck,.. 과 같이 다양할 경우, 클래스형 간의 형변환이 불가능하다면 모든 경우에 서로 다른 메서드를 만들어주어야 한다.
그런데 부모 클래스형으로의 형변환이 가능하기 때문에,

public void add(A a){
	...
}

public void add(B a){
	...
}

위와 같은 코드를

public void add(Object a){
	...
}

다음과 같이 하나의 케이스로 합쳐서 표현할 수 있다. (Object는 모든 클래스의 최상위 조상 클래스이다)

그러나 위와 같이 형변환을 해서 여러 클래스형을 함께 사용할 때는 주의해야할 점이 있다! 매개변수로 넘겨진 객체들은 다 Object 클래스형으로 자동 형변환되었기 때문에, 실제로 넘겨진 객체들의 메서드를 사용하기 위해서는 다시 해당 객체들의 type으로 강제 형변환을 해줄 필요가 있다. 그렇지 않으면 Object 클래스에는 그런 메서드나 변수가 없기 때문에 이를 이용할 수 없다.

public void add(Object a, Object b) {
	this.sum = 0;
	if (a instanceof Hi) {
		Hi tmp = (Hi)a;
		this.sum += tmp.sum;
	} 
	if (b instanceof Bye) {
		Bye tmp = (Bye)b;
		this.sum += tmp.sum;
	}
	System.out.println(this.sum);//a의 sum과 b의 sum을 더한 값
}
Hi, Bye라는 이름의 자식 객체 -> 부모 객체 -> 자식 객체
=> 반대로 부모 객체 -> 자식 객체 -> 부모 객체는 불가능하다.
profile
📝 dev wiki

0개의 댓글