1. 클래스 상속
2. 메서드 재정의
객체 지향 프로그래밍의 중요한 특징 중 하나는 재사용성입니다. 이 재사용성을 가장 잘 나타내고 있는 부분이 바로 상속인데요, 상속은 부모로부터 클래스의 변수나 메서드를 물려받는 것으로, 클래스를 만들 때 처음부터 모든 것을 새로 만드는 것이 아니라, 부모로부터 물려 받고 추가 되는 것만 새로 만들면 되는 것입니다.
프로그램 관점으로 볼 때도 역시 프로그램을 전부 개발하는게 아니라 추가되는 것만 개발하면 되니, 개발 시간도 단축되고, 이미 사용중인 프로그램을 재사용하게 되니 안정성도 높아지게 됩니다.
1. 클래스 상속
이미 만들어져 있는 클래스로 상속을 해주는 클래스를 부모 클래스, 또는 상위 클래스라고 부릅니다. 반대로 부모 클래스로부터 기존 변수나 메서드들을 그대로 물려받는 즉, 상속을 받는 클래스를 자식 클래스 또는 하위 클래스라고 부릅니다. 용어 또한 다양하게 사용되고 있으니 기억해 두세요.
사람마다, 책마다, 자료마다 다르게 사용되는 경우가 많으니, 외우는것보다 개념적으로 이해하는 것이 중요합니다. 또한 프로그래밍 언어에서의 상속이란 개념은 일상에서의 상속과 약간 다릅니다. 일상에서는 부모가 자식을 선택에서 물려주지만, 프로그래밍 언어에서는 자식이 부모를 선택하게 됩니다. 즉, 자식 클래스가 상속 받고 싶은 클래스를 선택하게 되는 것입니다.
자바에서 상속을 정의할 때 extends라는 키워드를 사용합니다. extends는 확장하다라는 의미로 부모에게 물려받는 것 외에 추가로 확장할 수 있다라고 이해하면 됩니다.
상속 정의 방법
class 자식클래스 extends 부모클래스 {
...
}
상속의 특징
1. 단일 상속만 가능 - 자식 클래스는 하나의 부모 클래스에서만 상속 가능
2. 자식 클래스를 객체로 생성할 때, 부모 클래스가 먼저 객체화 됨
3. 모든 클래스는 Object 클래스로 부터 시작하는 상속관계의 하위 객체 (모든 클래스의 가장 상위 클래스는 Object 클래스)
아래 예제는 부모 클래스가 Phone이고, 이 부모클래스를 상속받는 클래스가 SmartPhone이라는 클래스입니다. 그리고 SmartPhoneMain 클래스의 main() 메서드에서 부모 클래스와 자식 클래스의 객체를 각각 생성해서 변수 값도 변경하고 메서드도 실행해 보겠습니다.
Phone.java
package example;
public class Phone {
String name;
String color;
String company;
void call() {
System.out.println("전화를 건다");
}
void receive() {
System.out.println("전화를 받다");
}
}
SmartPhone.java
package example;
public class SmartPhone extends Phone {
public void installApp() {
System.out.println("앱 설치");
}
}
SmartPhoneMain.java
package example;
public class SmartPhoneMain {
public static void main(String[] args) {
Phone p = new Phone();
p.name = "전화기";
p.company = "현대";
p.color = "화이트";
System.out.println("Phone 출력");
System.out.println(p.name);
System.out.println(p.company);
System.out.println(p.color);
p.call();
p.receive();
SmartPhone sp = new SmartPhone();
sp.name = "갤럭시";
sp.company = "삼성";
sp.color = "블랙";
System.out.println("SmartPhone 출력");
System.out.println(sp.name);
System.out.println(sp.company);
System.out.println(sp.color);
sp.call();
sp.receive();
sp.installApp();
}
}
실행 결과
Phone 출력
전화기
현대
화이트
전화를 건다
전화를 받다
SmartPhone 출력
갤럭시
삼성
블랙
전화를 건다
전화를 받다
앱 설치
자바 파일이 세개라 조금 어려워 보일 수 있지만, 상속관계를 나타내기 위해 파일을 분리한 것입니다.
Phone -> SmartPhone의 관계를 머리속으로 잘 그려놓고 소스를 확인해 보세요.
먼저 Phone 클래스는 name, company, color 세 개의 변수와 call(), receive() 메서드 두 개를 가지고 있습니다. 메서드는 출력만 하는 단순한 기능으로 정의되어 있습니다.
이제 SmartPhone 클래스를 보면 extends Phone 이라고 정의되어 있습니다. Phone 클래스를 상속받겠다는 의미입니다. SmartPhone 클래스는 installApp() 이라는 메서드 하나밖에 없지만 실제로는 name, company, color, call(), receive() Phone 클래스에 있던 모든 구성 요소와 추가로 installApp() 까지 가지고 있는 것입니다. extends (확장) 한 것이기 때문이죠.
이제 SmartPhoneMain이라는 클래스는 이 두 클래스를 객체로 생성하고 있는데 먼저, Phone 클래스를 p라는 객체로 생성해서 name, company, color 변수에 값을 대입했고 p 객체의 변수를 출력하고 p 객체의 메서드도 실행하고 있습니다.
또, SmartPhone 클래스의 객체를 sp 변수에 생성하고, 마찬가지로 name, company, color 변수에 각각 값을 대입합니다. sp 객체의 변수를 출력하고 메서드를 실행했습니다.
SmartPhone 클래스는 installApp() 메서드 밖에 없었지만 Phone 클래스에 있던 모든 변수와 메서드는 그대로 전부 사용할 수 있습니다. SmartPhone 클래스에 있는것처럼 말이죠. 이 관계를 그림으로 나타내면,
SmartPhone 클래스는 Phone 클래스를 상속 받는 관계를 표현할 때 자식 클래스에서 부모 클래스 쪽으로 화살표 방향이 가도록 표현합니다.
SmartPhone 클래스의 회색 영역은 실제 구현되어 있지 않은 변수와 메서드지만 Phone 클래스로 부터 상속 받았기 때문에 존재한다고 생각하면 됩니다.
그리고 부모-자식, 상위-하위 관계이지만 위 예제에서 Phone 클래스의 p객체, SmartPhone 클래스의 sp 객체는 전혀 연관이 없는 별개의 독립적인 객체입니다.
super
super는 자식 객체에서 부모 객체를 가리키는 참조변수입니다.
super.변수명;
super.메서드명();
위 형태로 부모 객체의 변수와 메서드를 사용하게 됩니다.
또하나의 형태는 super() 인데, 괄호가 붙어서 메서드처럼 사용하고, 부모 객체의 생성자를 실행할 때 사용합니다. this와 super 모두 static 메서드에서는 사용할 수 없습니다. 따라서 main() 메서드 내에서도 사용할 수 없습니다.
package example;
public class SuperEx {
public static void main(String[] args) {
Child child = new Child();
child.print();
}
}
class Parent {
int number = 3;
Parent() {
System.out.println("부모 객체 생성");
}
}
class Child extends Parent {
int number = 2;
Child() {
System.out.println("자식 객체 생성");
}
void print() {
int number = 1;
System.out.println(number); // 메서드 지역변수 number
System.out.println(this.number); // 자신 객체의 number
System.out.println(super.number); // 부모 객체의 number
}
}
실행 결과
부모 객체 생성
자식 객체 생성
1
2
3
부모 클래스인 Parent 클래스는 number 필드에 3으로 초기화 했고, Parent 클래스를 상속받고 있는 Child 클래스는 number 변수를 2로 초기화 했습니다.
Child 클래스의 print() 메서드 내의 number 변수는 1로 초기화 되어 있습니다. 이 변수는 print() 메서드 안에서 선언된 것으로 Child 클래스의 인스턴스 변수 number와는 다른 변수입니다.
출력 결과를 확인해 보겠습니다.
먼저 “부모 객체 생성”이라는 출력 구문은 Parent 클래스의 생성자에서 정의되어 있는데, 이 예제는 Parent 객체를 생성하지 않았습니다. Child 객체만 생성했는데, 부모 클래스인 Parent() 클래스의 객체가 더 먼저 생성된 것을 알 수 있습니다. 그리고 나서 “자식 객체 생성”이라고 출력 됐으니 객체 생성 순서는 부모 객체 -> 자식 객체 순이 됩니다.
그리고 print() 메서드에서 number 변수로 출력하고 있는데, 변수 앞에 아무것도 붙이지 않으면 가장 가까이에 있는 메서드내 지역변수 number인 1이 출력되었고, 그 아래 this.number 에서 this는 자기 자신 객체를 가리키는 참조변수라고 했습니다.
Child 클래스의 객체 child의 인스턴스 변수 number의 값 2가 출력되고, super.number의 super는 부모 객체를 가리키는 참조변수 이기 때문에 부모(Parent) 객체의 인스턴스 변수 number의 값 3이 출력되었습니다.
만약 이 예제에서 Child 클래스의 인스턴스 변수 number가 존재 하지 않는다면 this.number와 super.number는 동일한 값으로 출력되게 됩니다.
package example;
public class SuperEx {
public static void main(String[] args) {
Child child = new Child();
child.print();
}
}
class Parent {
int number = 3;
Parent() {
System.out.println("부모 객체 생성");
}
}
class Child extends Parent {
//int number = 2;
Child() {
System.out.println("자식 객체 생성");
}
void print() {
int number = 1;
System.out.println(number); // 메서드 지역변수 number
System.out.println(this.number); // 자신 객체의 number
System.out.println(super.number); // 부모 객체의 number
}
}
실행 결과
부모 객체 생성
자식 객체 생성
1
3
3
super()
super라는 키워드에 메서드처럼 ()가 붙어 있는데, 바로 부모 객체의 생성자를 의미합니다. 이 super() 로 부모 객체의 생성자를 실행할 수 있는데, 아래 예제와 같이 부모 클래스의 생성자에 매개변수가 있는 경우 자식 클래스의 생성자에서 반드시 super() 로 부모 생성자를 실행해줘야 합니다.
package example;
public class SuperEx2 {
}
class Parent2 {
String name;
Parent2(String name) {
this.name = name;
}
}
class Child2 extends Parent2 { // 에러 발생
}
부모 클래스인 Parent2 클래스는 기본 생성자가 없고, String 문자열을 매개변수로 받는 생성자만 존재합니다. 객체를 생성할 때 생성자가 실행되는데, 지금 Parent2 클래스는 name을 매개변수로 넘겨줘야만 객체를 생성할 수 있는 것입니다. 앞 예제에서 보았듯이 자식 클래스의 객체가 생성되기 전 부모 클래스의 객체가 먼저 생성된다는 것을 배웠습니다. 그래서 Child2의 생성자에서 반드시 부모 생성자를 실행해 주어야 합니다. Child2 클래스를 아래와 같이 수정해 보세요.
package example;
public class SuperEx2 {
}
class Parent2 {
String name;
Parent2(String name) {
this.name = name;
}
}
class Child2 extends Parent2 {
Child2(String name) {
super(name);
}
}
Child2 생성자에서 name 매개변수를 부모 클래스의 생성자 super(name) 형태로 실행한 것입니다. 이제 에러가 사라졌죠? 이 Child2 생성자가 반드시 매개변수를 받아야만 하는 것은 아니고, super() 에 매개변수만 전달해주면 됩니다.
앞의 예제들은 생성자를 따로 정의하지 않아도 정상적으로 컴파일도 잘 되고, 실행도 잘 되었는데, 이는 컴파일러가 자식 클래스의 기본생성자를 자동 생성할때 super()도 추가해서 컴파일한다는 것을 알 수 있습니다. 메서드 안에 메서드가 있는 경우 안 쪽 메서드가 먼저 실행이 끝나게 된다고 했었죠? 그래서 안쪽 메서드는 super()가 먼저 실행되어, 부모 객체가 먼저 생성되는 것입니다.
2. 메서드 재정의
상속관계에서 부모 클래스의 메서드를 자식 클래스가 변경해서 정의하는 것을 메서드 재정의 (overriding) 이라고 합니다. overriding(오버라이딩)은 중단하다, 우선시 하다라는 의미를 가진 단어인데, 상속관계에서도 동일한 이름의 메서드를 자식 클래스가 똑같이 생성했다면 부모클래스의 메서드보다 우선적으로 적용됩니다.
메서드 재정의가 가능하려면
첫번째, 부모 클래스의 메서드와 자식 클래스의 메서드의 선언부가 동일해야 합니다. 다르면 메서드 재정의가 아니라 오버로딩이 되기 때문입니다.
두번째는 자식클래스의 재정의된 메서드의 접근 제한자가 부모 클래스의 메서드 접근제한자보다 사용 범위가 같거나 커야 합니다. 예를 들어 부모 클래스의 메서드가 private 이라면 자식 클래스의 재정의된 메서드가 public이 가능하지만, 반대로 부모 클래스의 메서드가 public이라면 재정의된 메서드는 private으로 선언이 불가능합니다.
다시 말하면, 부모 클래스에서 정의된 접근 제한자를 자식 클래스에서 더 좁은 제한자로 정의할 수 없다는 뜻입니다.
Car.java
package example;
public class Car {
String color;
String name;
public void go() {
System.out.println("전진");
}
void back() {
System.out.println("후진");
}
}
Taxi.java
package example;
public class Taxi extends Car {
public void go() {
System.out.println("미터기를 켜고 전진");
}
}
TaxiMain.java
package example;
public class TaxiMain {
public static void main(String[] args) {
Taxi t = new Taxi();
t.go();
}
}
실행 결과
미터기를 켜고 전진
위 예제는 Car 클래스를 상속받는 Taxi 클래스를 생성하고, Taxi 클래스에서 go() 메서드를 재정의 했습니다. TaxiMain 클래스에서는 Taxi 클래스를 객체로 생성해서 go() 메서드를 실행한 것입니다. 실행 결과는 재정의된 메서드가 우선 적용되어, Taxi 클래스에서 재정의된 go() 메서드의 “미터기를 켜고 전진”이 출력되었습니다.
처음 자바와 객체지향을 배울 때 오버라이딩과 오버로딩이라는 용어가 이름이 비슷해서 많이 혼동되는데, 표로 정리해서 살펴 보겠습니다.
구분 | 오버라이딩 | 오버로딩 |
관계 | 상속 관계 | 같은 클래스 |
메서드명 | 동일 | 동일 |
매개변수 | 동일 | 다름 |
리턴타입 | 동일 | 상관없음 |
접근 제한 | 같거나 넓은 범위 | 상관없음 |
조금 더 쉽게 기억할 수 있도록 오버로드, 로드(적재)를 오버(넘치다)했다고 생각해 볼까요? 메서드에 2개의 매개변수만 넣을 수 있는데, 3개, 4개 오버해서 적재하는 경우는 오버로드(오버로딩),
오버라이트(overwrite)는 덮어 쓴다라는 뜻으로 부모의 메서드를 자식이 새로 정의해서 덮어 쓰는 것이 오버라이드(오버라이딩)입니다. 이 두 용어는 자주 헷갈리므로 이렇게 기억하면 조금은 쉽게 기억할 수 있을것입니다.