<Java> 객체지향프로그래밍 3
지금까지 클래스의 정의와 함께 기본적인 사용법들을 배웠습니다.지금부터는 객체지향의 꽃인 상속(Inheritance)에 대해서 배워보겠습니다.
상속
상속의 정의
- 상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것입니다.상속을 통해서 클래스를 작성하면 보다 효율적으로 새로운 클래스를 작성할 수 있는 동시에 코드를 공통적으로 관리할 수 있기 때문에 유지보수가 굉장히 용이해집니다.
- 자바에서 상속을 받으려는 방법은 굉장히 간단합니다.새로 작성하고자 하는 클래스의 이름 뒤에
extends
라는 키워드와 함께 상속 받고자하는 클래스의 이름을 적어주면 됩니다.아래와 같은 방식입니다.
class Child extends Parent{//...}
- 위의 Child와 Parent 클래스들은 서로 상속 관계에 있다고 하며, 상속해주는 클래스를 부모 클래스(Parent),상속 받는 클래스를 자식 클래스(Child)라 합니다.
- 위의 관계를 다이어그램으로 나타내 보면 아래와 같습니다.
- 자손 클래스는 조상 클래스의 모든 멤버를 상속받기 때문에, Child 클래스는 Parent클래스의 멤버들을 포함합니다.
- 만약 Parent 클래스에 sex라는 성별을 나타내는 멤버변수를 가지게 되면, Child 클래스는 자동적으로 sex라는 멤버변수가 추가됩니다.
- 살짝 감이 오시나요?부모 클래스의 모든 요소는 자식 클래스가 상속 받습니다.이는 곧 부모 클래스에 어떠한 멤버 변수가 추가되면 자동적으로 자식 클래스에도 추가된다는 의미입니다.
- 반면에 자식 클래스에 어떠한 멤버 변수가 추가될 경우에는 부모 클래스에는 자동적으로 추가되지 않습니다.결국 자식 클래스는 부모 클래스의 모든 멤버를 상속 받으므로 항상 부모 클래스보다 같거나 많은 멤버를 가집니다. 즉,상속에 상속을 진행될수록 상속받는 클래스의 멤버 개수는 점점 늘어납니다.
- 위 언급처럼 점점 늘어난다는 의미로부터 extends라는 키워드가 상속을 받을 경우 사용하는 키워드임을 유추할 수 있습니다.
- 필자가 이러한 개념을 처음 접할 때는 철없는 자식이 부모의 모든것을 가져간다라는 비유로 교수님이 설명을 해주셨습니다.적절한 비유인것 같으니 만약 이해가 안가시는 분들은 참고하시는 것도 좋을 것 같습니다.
- 이제 상속이 왜 유지보수에 유리한지 알아보기 위해, 두개의 자식을 가지는 구조로 예를 들어봅시다.
class Child1 extends Parent{//...}
class Child2 extends Parent{//...}
- 만약 위와 같은 상황에서 Child1와 Child2 클래스에 공통적으로 추가해야하는 멤버가 있다면,각각의 클래스에 추가해주기보다는 Parent 클래스에 추가해주는게 효율적입니다.
- 이제 한층 더 상속 관계를 만들어보겠습니다.새롭게 생성된 GrandChild는 Child1의 멤버뿐만 아니라 Parent의 멤버까지 모두 상속받습니다.
- 만약 추가적으로 age라는 멤버변수를 Parent에 추가하게되면 Child부터 GradeChild까지 상속관계를 맺고 있는 모든 클래스들에 age라는 멤버변수가 추가됩니다.
- 이제 코드를 통해 상속을 알아봅시다.
class Tv{
boolean power;
int channel;
void power() {
power = !power;
}
void channelUp() {
++channel;
}
void channelDown() {
--channel;
}
}
class CaptionTv extends Tv {
boolean caption;
void displayCaption(String text) {
if (caption) {
System.out.println(text);
}
}
}
public class CaptionTvTest {
public static void main(String[] args) {
CaptionTv captionTv = new CaptionTv();
captionTv.channel = 10;
captionTv.channelUp();
System.out.println("captionTv.channel = " + captionTv.channel);
captionTv.displayCaption("Hello,OOP");
captionTv.caption = true;
captionTv.displayCaption("Hello,OOP");
}
}
- Tv라는 부모 클래스와 CaptionTv라는 자식 클래스가 존재하고 있습니다.앞서 배운바와 같이 CaptionTv의 인스턴스들은 Tv클래스 모든 멤버값들을 가진 상태입니다.반대로 Tv클래스의 인스터스들은 CaptionTv의 멤버들을 모두 가지지는 않습니다.
- 방금 설명한 부모와 자식간의 역 관계가 성립하지 않음을 이해하신다면 상속의 첫발을 잘 내딛은 것입니다.
클래스간의 또 다른 관계, 포함관계
- 앞서 배운 상속관계와 같은 듯 다른 또 하나의 관계가 존재합니다.
- 바로 포함관계 입니다.두 가지의 관계 모두 클래스의 재활용성을 높이기 위해 사용하는 방법입니다.
- 포함관계는 온전히 하나의 클래스가 다른 하나의 클래스의 구성 요소일 경우 사용됩니다.예를 들어, 원,선 그리고 점에 빗대어 설명하겠습니다.원의 경우 선으로 구성되며 선의 경우 점으로 구성됩니다.즉,점은 선에 포함되며 선은 원에 포함된다고 볼 수 있습니다.
- 이러한 경우 포함관계를 사용하여,원에 선을 포함시키고 선에 점을 포함시킵니다.
- 위의 상황을 코드를 통해 풀어내보겠습니다.
class Circle{
Line line = new Line();
double radius;
}
class Line{
Point point = new Point();
double length;
}
class Point{
int x;
int y;
}
- 위와 같이 한 클래스를 작성하는데 다른 클래스를 멤버변수로 선언하여 포함시키는 것은 상속과 마찬가지로 코드의 유지보수성을 높여줍니다.
- 그러면 상속과 포함을 어떻게 구분할 수 있을까요? 아래의 원칙이 있습니다.
- 상속관계는
~은(는)~이다 : is-a
라는 문장이 클래스 간 성립 - 포함관계는
~은(는)~을(를) 가지고 있다 : has-a
라는 문장이 클래스 간 성립
- 앞서 진행한 상속관계 일반 티비와 자막 티비를 is—a에 빗대어 보겠습니다.자막티비는 일반티비이다.라는 말이 자연스럽기에 상속관계를 통해 클래스의 관계를 정립했습니다.
- 반면에 점과 원의 경우, 원은 점을 가지고 있다.라는 말이 자연스럽기에 포함관계가 성립하게됩니다.
- 예제를 통해 좀 더 이해해봅시다.
public class DrawShape {
public static void main(String[] args) {
Point[] p = {
new Point(100, 100),
new Point(140, 50),
new Point(200,100)
};
Triangle triangle = new Triangle(p);
Circle circle = new Circle(new Point(50, 50), 13);
triangle.draw();
circle.draw();
}
}
class Shape{
String color = "black";
void draw() {
System.out.printf("[color=%s] \n", color);
}
}
class Point{
int x;
int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
Point() {
this(0, 0);
}
String getXY() {
return "(" + "x" + "," + "y" + ")";
}
}
class Circle extends Shape {
Point center;
int radius;
Circle() {
this(new Point(0, 0), 100);
}
public Circle(Point center,int r) {
this.center = center;
this.radius = r;
}
void draw() {
System.out.printf("[center=(%d,%d) / radius = %d] \n",center.x,center.y,radius);
}
}
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(),this.color);
}
}
- 도형을 의미하는 Shape클래스와 Point 클래스를 통해 효율적으로 클래스의 관계를 정립하는 예제입니다.예제 자체의 난이도 어렵지 않아 설명을 생략합니다.
- 다만, Circle과 Triangle 클래스에서 부모 클래스인 Shape의 메서드인 draw()를 재정의하는 코드가 있습니다.이렇게 부모 클래스에 정의된 메서드와 같은 메서드를 자식 클래스에 정의하는것을 Overriding(오버라이딩)이라 하며 추후 배울 예정이니 참고해 두시길 바랍니다.
단일 상속
- 자바의 경우, 다른 객체지향언어와 다르게 여러 부모 클래스로부터 상속받는 것이 가능한 “다중상속”이 금지되어있습니다.결국 하나의 클래스는 하나의 부모를 상속하는 단일 상속만 가능합니다.
- 다중상속의 경우,여러 클래스로부터 복합적으로 다양한 기능을 받아 올 수 있다는 장점이 있지만 그에 반해 클래스간의 관계가 상당히 복잡해지는 경우가 빈번합니다.
- 예를 들어,Camera라는 클래스와 MP3라는 클래스를 모두 상속받아 아이팟이라 클래스를 생성한다고 가정합시다.그럴 경우, 카메라의 power()라는 메서드와 MP3의 power()라는 메서드 중 어느 메서드를 아이팟 클래스는 상속받아야 할까요?이러한 문제를 해결하기 위해서는 부모 클래스의 매개변수나 메서드명을 변경하는 방법을 거쳐야 할 것입니다.이러한 수고로움을 덜기 위해 자바에서는 단일 상속만을 사용합니다.
- 아래 예제는 다중 상속을 자바의 상속과 포함관계를 통해 구현한 예제입니다.아래와 같이 자바의 단일 상속이 가지는 한계를 뛰어 넘을 수도 있습니다.
class VCR{
boolean power;
int counter = 0;
void power() {
power = !power;
}
void play() {}
}
//Tv 클래스는 기존 예제에서 사용한 클래스입니다.
public class TVCR extends Tv{
VCR vcr = new VCR();
int counter = vcr.counter;
void play() {
vcr.play();
}
}
- TVCR 클래스는 TV 클래스를 부모로 하여 상속받고,VCR클래스의 경우 포함을 시켜버려 두가지 클래스의 멤버들을 모두 사용하고 있습니다.
- play()메서드의 경우 외부적으로는 TVCR 클래스의 인스턴스의 메서드를 사용하는 것처럼 보이지만 실제로는 VCR 클래스로부터 생성된 인스턴스의 메서드를 호출하여 사용하고 있습니다.
- 이러한 구조를 통해 VCR 클래스의 메서드 내용이 변경되더라도 TVCR클래스의 메서드들에도 변경 내용을 적용 시킬 수 있습니다.
Object 클래스 - 모든 클래스의 조상
- Object클래스는 모든 클래스 상속계층도의 최상위에 있는 조상 클래스입니다.다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object클래스로부터 상속을 받게 합니다.
- 바로 위의 예제의 VCR 클래스의 경우 실제로
class VCR extends Object
라는 문장에서 Object 클래스의 내용이 생략된 것입니다. - 결론적으로 toString()이나 equals() 등의 메서드들을 따로 정의하지 않고 사용할 수 있었던 이유도 Object 클래스를 상속받기 때문입니다.
Overriding(오버라이딩)
- 이제 앞서 한번 언급된 Overriding이라는 개념에 대해서 배워봅시다.정의부터 말씀드리면 부모 클래스로부터 상속받은 메서드의 내용을 변경하는 것입니다.상속을 받을 경우, 기존 부모 클래스의 메서드의 기능을 그대로 사용하는 경우도 있지만, 자식 클래스의 상황에 맞게 변경해야하는 경우 오버라이딩을 이용하여 변경합니다.
- Overriding이라는 단어의 어원은 Overwrite로부터 파생되었습니다.즉, 덮어쓴다라는 의미를 내포하고 있습니다.어원에 대해 알고 계시면 좀 더 개념을 이해하는데 도움이 될 것입니다.
- 참고로 앞서 배운 오버로딩과 오버라이딩은 명백히 다른 개념임을 알고 계셔야 합니다.복습 겸 언급하면 오버로딩은 메서드 이름만 동일하고 기존에 없는 새로운 메서드를 매개변수만 달리하여 정의하는것입니다.
오버라이딩 조건
- 오버라이딩은 부모 클래스의 메서드의 내용만을 새로 작성하는것이므로 메서드의 선언부는 부모의 것과 완전히 일치해야 합니다.
- 다만 아직 배우지 않은 접근 제어자와 예외는 제한된 아래의 조건하에서만 다르게 변경 가능 합니다.
- 접근 제어자는 부모 클래스의 메서드보다 좁은 범위로 변경 할 수 없다.
- 부모 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
- 인스턴스메서드를 static메서드로 또는 그 반대로 변경할 수 없다.
- 1번의 경우,만약 부모 클래스의 메서드의 접근 제어자가 protected인 경우,더 넓은 범위인 public 또는 동일한 범위로만 오버라이딩 가능합니다.접근 제어자의 경우 아직 배우지 않은 내용이니 읽고 넘어갑시다.
- 2번도 마찬가지입니다.예외처리 파트를 배우고 나서 복습할때, 다시 읽어보시길 바랍니다.
참조 변수 super
- super는 자식 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조 변수입니다.즉,주솟값입니다.앞서 클래스내에서 멤버변수와 지역변수의 이름이 같을 때,this를 붙여 구별을 했듯이 상속받은 멤버와 자신의 멤버와 이름이 같을 경우 super라는 키워드를 통해 구별합니다.
- 부모의 멤버와 자신의 멤버를 구별하는데 사용된다는 점을 제외하고는 super와 this는 근본적으로 같습니다.모든 인스턴스 메서드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데,이가 바로 참조변수인 this와 super입니다.
- 당연하게도 static메서드는 인스턴스 메서드와 근본적으로 다르기에 this와 super 역시 사용할 수 없습니다.
public class SuperTest {
public static void main(String[] args) {
Child child = new Child();
child.method();
}
}
class Parent{
int x = 10;
}
class Child extends Parent{
int x = -10;
//Instance Method => super,this 참조변수 가진다.
void method() {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("super.x = " + super.x);
}
}
- Child 클래스에서 멤버 변수로
int x = -10
을 통해 자신의 멤버 변수로 선언하였습니다.그래서 this.x와 super.x의 값이 각각 다르게 출력됩니다.즉, 두개의 참조변수가 서로다른 주솟값을 가지고 있게 됩니다.
생성자 super()
- this()와 마찬가지로 super() 역시 생성자입니다.this()는 동일한 클래스의 내의 다른 생성자르 호출하는데 사용되지만,super()는 조상 클래스의 생성자를 호출하는데 사용됩니다.
- 자식 클래스의 인스턴스를 생성하면,자식의 멤버와 부모의 멤버가 모두 합쳐진 하나의 인스턴스가 생성됩니다.그래서 자식의 인스턴스가 부모 클래스의 모든 멤버를 사용할 수 있는 것입니다.이러한 과정 중 부모 클래스의 멤버의 초기화 작업이 수행되어야 하기 때문에 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야합니다.
- 생성자의 첫줄에 super()를 통해 부모 클래스의 생성자를 호출하는것은 자식 클래스에서 부모 클래스의 멤버를 먼저 사용할 수 있기 때문입니다.
- Object 클래스를 제외한 모든 클래스의 생성자 첫줄에는 this() 또는 super()를 호출해야합니다. 만약 그렇지 않을 경우, 컴파일러가 자동으로 추가해줍니다.
public class PointTest {
public static void main(String[] args) {
Point3D point3D = new Point3D(5, 5, 5);
System.out.println(point3D.toString());
Point3D defaultInstance = new Point3D();
System.out.println(defaultInstance.toString());
}
}
class Point{
int x;
int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
Point() {
//아래 코드 주석 처리
this(0, 0);
}
String getXY() {
return "(" + "x" + "," + "y" + ")";
}
}
class Point3D extends Point {
int z;
public Point3D() {
this(100, 50, 70);
}
public Point3D(int x, int y, int z) {
//아래 코드 주석 처리
super(x,y);//Point(int x,int y) 생성자 호출
this.z = z;
}
}
- Point3D 클래스의 두가지 생성자에 주목해보겠습니다.매개변수가 없는 생성자의 경우,this를 통해 또 다른 본인의 생성자를 호출하여 초기화하려합니다.이후 호출된 매개변수가 있는 생성자는 super()를 통해 부모 클래스의 생성자를 호출 한 다음,본인의 멤버 변수인 z를 초기화 시킵니다.
- 만약 여기서 좀 더 깊은 이해를 원한다면 주석 처리하라는 주석이 있는 코드를 주석처리 해본뒤,실행보시는것도 Default Constructor에 대한 이해도를 높이실 수 있을 것입니다.
Package와 import
- 두가지 개념 모두 지금껏 예제들을 코딩해보면서 여러번 사용해본 것들입니다.하지만 제대로된 정의는 모른채로 써왔을 수도 있습니다.
- 앞서 언급된 요소들에 비하면 개념적 정의의 중요도는 떨어지지만 자바의 상식에 있어 알아둬야할 내용입니다.
Package
- 위의 사진은 제가 관리하는 해당 포스팅의 예제 소스들입니다.챕터별로 예제 클래스를 나누어 정리하고 있습니다.이런식으로 클래스의 묶음을 관리해주는 개념이 패키지입니다.패키지 안에는 여러개의 클래스를 관리 할 수 있습니다.
- 동일한 이름의 클래스일지라도 패키지가 다르다면 클래스의 존재유무에 상관 없습니다.즉, 자신만의 패키지 체계를 유지함으로써 다른 개발자가 개발한 클래스와의 충돌을 피할 수 있습니다.
- 또 다른 패키지에 대한 설명은 폴더(디렉토리)입니다.저희는 컴퓨터를 사용할 때 수많은 폴더를 만들어서 파일들을 관리합니다.클래스의 경우 클래스 파일의 형태로 컴퓨터에 저장됩니다.이러한 클래스파일을 보관하는 폴더가 패키지라고 이해하셔도 됩니다.(폴더와 디렉토리는 다른 의미이지만 이해를 위해 동일 취급함)
- 폴더 아래에 또 다른 폴더를 가질 수 있는것처럼 패키지도 패키지 내에 또 다른 패키지를 가질 수 있습니다.
- 여기서 위 사진의 ch7 패키지의 SuperTest 클래스의 코드 전부를 보겠습니다.
package ch7;
public class SuperTest {
public static void main(String[] args) {
Child child = new Child();
child.method();
}
}
class Parent{
int x = 10;
}
class Child extends Parent{
int x = -10;
void method() {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("super.x = " + super.x);
}
}
- SuperTest 클래스 내의 코드는 이미 여러분들도 쳐보신 코드일 것입니다.하지만 제일 위의
package ch7;
의 경우 여러분들과 제 코드가 다를 수 있습니다.왜냐하면 서로서로 다른 폴더에 SuperTest 클래스를 생성했기 때문입니다. package ch7;
와 같이 패키지를 선언하는 코드는 모든 클래스에 반드시 포함되어야 합니다.만약 패키지를 선언하지 않았는데 정상적으로 코드가 실행되었다면 자바에서 기본적으로 제공하는 unnamed package때문입니다.
import문
- 소스코드를 작성할 때 다른 패키지의 클래스를 사용하려면 패키지명이 포함된 클래스 이름을 사용해야합니다.저희가 어떠한 값을 키보드로부터 입력받기 위해 스캐너 클래스를 사용할때, 자동으로 해당 패키지와 클래스가 import되는 것을 확인 할 수 있습니다.아래와 같은 코드가 자동으로 들어오는것을 볼 수 있습니다.
import java.util.Scanner;
- 또 다른 예제를 통해 자동으로 패키지와 클래스를 import해봅시다.
import java.text.SimpleDateFormat;
import java.util.Date;
public class ImportTest {
public static void main(String[] args) {
Date today = new Date();
SimpleDateFormat date = new SimpleDateFormat("yyyy/MM/dd");
SimpleDateFormat time = new SimpleDateFormat("hh:mm:ss a");
System.out.println("today = " + today);
System.out.println("date.format(today) = " + date.format(today));
System.out.println("time.format(today) = " + time.format(today));
}
}
- 추후 배우게될 SimpleDateFormat과 Date 클래스를 활용해서 import문을 사용해보았습니다.
- 만약 java.util 패키지내의 모든 클래스를 불러오고 싶다면 아래와 같은 코드를 사용하면 됩니다.
import java.util.*;
- 흔히 별 표시(shift + 8)는 모든이라는 뜻은 포함하고 있는 특수기호이라서 위와 같이 모든 클래스를 import해올 수 있습니다.
static import문
- static import문을 사용하면 사용하려는 클래스의 패키지명을 생략할 수 있습니다.예제를 통해 바로 알아보겠습니다.
import static java.lang.System.out;
import static java.lang.Math.*;
public class StaticImportEx {
public static void main(String[] args) {
out.println(random());
out.println("Math.PI : " + PI);
}
}
- static import를 사용해서 표준출력
System.out.println()
의 코드를 간결하게 줄인것을 확인 할 수 있습니다.
Ref:자바의 정석 - 남궁성