[Java/자바] 클래스의 관계 - 상속(is-a)과 포함(has-a)

상속

상속이란, 기존의 클래스를 재사용해 새로운 클래스를 작성하는 것입니다.

상속을 통해서 클래스를 작성하면, 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 도 있어 코드의 추가 및 변경이 용이합니다.

 

상속 방법

상속 방법은 아주 간단하게 상속받고자 하는 클래스의 이름 앞에 "extends"를 붙여주면 됩니다.

만약 새로 작성하고자 하는 클래스의 이름이 "Child"이고 상속받고자 하는 기존 클래스의 이름이 "Parent"라면 다음과 같습니다.

class Child extends Parent {
	...
}

 

두 클래스는 서로 상속관계에 있다고 하며, 상속해주는 클래스를 "조상 클래스", 상속 받는 클래스를 "자손 클래스"라고 합니다.

 

이 상속관계를 그림으로 표현하면 다음과 같습니다.

 

만약 코드가 아래와 같다면, 그림은 다음과 같이 바뀔 것입니다.

class Parent {
	int age;
}

class Child extends Parent {
	void play() {
    	System.out.println("놀자");
        }
}

Parent를 상속 받은 Child는 Parent가 가지고 있는 age 변수를 가지고 있으며, Child에서 추가한 play()메서드도 가지고 있게 됩니다.

즉, Child클래스에 새로운 코드가 추가되어도 조상인 Parent클래스는 아무런 영향도 받지 않습니다.

클래스 클래스의 멤버
Parent age
Child age, play()

 

단일 상속

Java는 오직 단일 상속만을 허용합니다.

그래서 둘 이상의 클래스로 부터 상속 받을 수 없습니다.

class TVCR extends TV, VCR { // 에러 : 조상은 하나만 허용
	...
    }

 

모든 클래스의 조상 Object 클래스

Object 클래스 모든 클래스의 상속계층도 최상위에 있는 조상클래스입니다.

다른 클래스로부터 상속 받지 않은 모든 클래스들은 자동적으로 Object클래스로부터 상속받게 됩니다.

자바의 모든 클래스들은 Object클래스의 멤버들을 상속 받기 때문에 Object 클래스에 정의된 멤버들을 사용할 수 있는데,

우리가 코딩하면서 toString()이나, equals()와 같은 메서드를 따로 정의하지 않고도 사용할 수 있었던 이유가 바로 Object클래스 덕분입니다.

 

 

상속 정리

  • 조상 클래스가 변경되면 자손 클래스는 자동적으로 영향을 받는다.
  • 자손 클래스가 변경되는 건 조상 클래스에게 영향을 주지 못한다.
  • 생성자와 초기화 블럭은 상속되지 않는다. ( 멤버(변수, 메서드)만 상속된다 )
  • 자손 클래스의 멤버 개수는 조상 클래스보다 항상 많거나 같다.

 

상속 예시 코드

class Tv {
	boolean power; 	// 전원상태(on/off)
	int channel;	// 채널

	void power()        {   power = !power; }
	void channelUp()    { 	 ++channel;     }
	void channelDown()  {	 --channel;	    }
}

class CaptionTv extends Tv {
	boolean caption;		// 캡션상태(on/off)
	void displayCaption(String text) {
		if (caption == true) {	// 캡션 상태가 on(true)일 때만 text를 보여 준다.
			System.out.println(text);
		}
	}
}

class CaptionTvTest {
	public static void main(String args[]) {
		CaptionTv ctv = new CaptionTv();
		ctv.channel = 10;				// 조상 클래스로부터 상속받은 멤버
		ctv.channelUp();				// 조상 클래스로부터 상속받은 멤버
		System.out.println(ctv.channel);
		ctv.displayCaption("Hello, World");	
		ctv.caption = true;				    // 캡션기능을 켠다.
		ctv.displayCaption("Hello, World");	// 캡션을 화면에 보여 준다.
	}
}

출력 결과

CationTv는 Tv 클래스로 부터 상속받고 따로 caption 변수와 displayCaption 메서드를 추가하였습니다.

이 코드를 그림으로 표현하면 다음과 같습니다.


포함

포함이란 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것입니다.

 

아래의 Circle 클래스는 Point 클래스가 가지고 있는 멤버변수 x와 y를 가지고 있습니다.

이를 Point클래스를 재사용해 Circle에 포함시킨다면 코드는 다음과 같습니다.

즉, Circle클래스의 멤버변수로 다른 클래스 타입 Point를 선언하고 new Point();로 새로운 객체를 생성했습니다.

 

이처럼 포함을 이용하면 하나의 거대한 클래스를 작성하는 것보다, 단위별로 여러 개의 클래스를 작성한 다음,

이 단위 클래스들을 포함관계로 재사용하여 더욱 간결하고 손쉽게 클래스를 작성할 수 있습니다.

또한 작성된 단위 클래스들은 다른 클래스를 작성하는데 재사용될 수 있습니다.

 

다음 코드처럼 Car라는 클래스를 작성하기 위해 Engine과 Door[] 단위 클래스를 작성하고

Car 클래스에 Engine과 Door[] 단위 클래스를 포함시켜 Car를 만들 수 있습니다.

class Car {
	Engine e = new Engine(); 
        Door[] d = new Door();
        ...
}

 


클래스간의 관계 결정하기

상속과 포함은 비슷해 언제 상속관계를 맺을 지 언제 포함관계를 맺을지 결정하기 힘들 겁니다.

이럴 때는 다음과 같이 간단한 문장으로 만들어 비교한다면 어떤 관계를 사용해야할 지 이해하기가 쉬워집니다.

상속관계 : ~은 ~이다. (is - a)
포함관계 : ~은 ~을 가지고 있다. (has - a)

 

아래 코드를 통해 쉽게 이해봅시다.

class DrawShape {
	public static void main(String[] args) {
		Point[] p = {   new Point(100, 100),
                        new Point(140,  50),
                        new Point(200, 100)
					};

		Circle   c = new Circle(new Point(150, 150), 50);
		Triangle t = new Triangle(p);


		c.draw(); // 원을 그린다.
		t.draw(); // 삼각형을 그린다.

	}
}

class Shape {
	String color = "black";
	void draw() {
		System.out.printf("[color=%s]%n", color);
	}
}

class Point {
	int x;
	int y;

	Point(int x, int y) {
		this.x = x;
		this.y = y;
	}

	Point() {
		this(0,0);
	}

	String getXY() {  
		return "("+x+","+y+")"; // x와 y의 값을 문자열로 반환
	}
}

class Circle extends Shape { // 원은 도형이다. ( 상속 관계 )
	Point center;	// 원의 원점좌표 . 원은 점을 가지고 있다 ( 포함 관계 )
	int r;			// 반지름

	Circle() {		
		this(new Point(0, 0), 100); // Circle(Point center, int r)를 호출
	}

	Circle(Point center, int r) {
		this.center = center;
		this.r = r;
	}

	void draw() { // 원을 그리는 대신에 원의 정보를 출력하도록 했다.
		System.out.printf("[center=(%d, %d), r=%d, color=%s]%n", center.x, center.y, r, color);
	}
}

class Triangle extends Shape { // 삼각형은 도형이다. ( 상속 관계 )
	Point[] p = new Point[3]; // 삼각형은 점을 가지고 있다 ( 포함 관계 )

	Triangle(Point[] p) {
		this.p = p;
	}

	void draw() { 
		System.out.printf("[p1=%s, p2=%s, p3=%s, color=%s]%n", p[0].getXY(), p[1].getXY(), p[2].getXY(), color);
	}
}

출력 결과

Circle은 Shape이다. => 상속관계

Circle은 Point를 가지고 있다. => 포함관계

 

Triangle은Shape이다. => 상속관계

Triangle은 Point를 가지고 있다. => 포함관계

 

이렇게 문장을 만들어 비교하면 상속관계와 포함관계를 비교하기 쉬워집니다.

 

여기서 Circle 클래스는 Shape클래스로 부터 모든 멤버를 상속받았으므로, Shape클래스에 정의된 멤버들을 사용할 수 있습니다.

또한 Point를 포함하기 때문에 멤버변수 x와 y를 가지게 됩니다.

Circle   c = new Circle(new Point(150, 150), 50);로 인스턴스를 생성하게 되면 

Circle(Point center, int r) 생성자가 호출되고 이 생성자안의 Point(int x, int y)생성자가 생성되어

c는 x = 150, y = 150, r = 50 의 값을 갖게 되고 Shape 부모 클래스의 color인 black을 가지게 됩니다.

 

여기서 Circle 생성자 안에서 new Point(150, 150) 인스턴스를 생성한 이유는

Circle 클래스는 포함관계인 Point 클래스를 선언만 했지 초기화 하지 않았습니다.

생성자에서 따로 초기화를 해야하는데 Circle(Point center, int r) 생성자도 Point의 인스턴스를 생성하지 않았기 때문입니다. 

 

 


참고자료 : 자바의 정석