https://github.com/whiteship/live-study
백기선님 자바 기초 스터디 6주차
목표
자바의 상속에 대해 학습하세요.
학습할 것 (필수)
- 자바 상속의 특징
- super 키워드
- 메소드 오버라이딩
- 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
- 추상 클래스
- final 키워드
- Object 클래스
자바 상속의 특징
자바 상속
상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.
상속은 코드의 재사용성을 높여 프로그램의 생산성과 유지보수에 크게 기여한다.
자바에서 상속을 구현하는 방법은 간단하다.
class Child extends Parent {
// ...
}
새로 작성하고자 하는 클래스의 이름(Child) 뒤에 상속 받고자하는 클래스의 이름(Parent)을 키워드 'extends'와 함께 써 주기만 하면 된다.
이 두 클래스(Parnet, Child)는 서로 상속 관계에 있다고 하며,
1. 상속해주는 클래스(Parent)를 '부모 클래스'라 하고
2. 상속 받는 클래스(Child)를 '자식 클래스'라 한다.
자식 클래스는 부모 클래스의 멤버를 모두 상속받기 때문에,
만일 Parent 클래스가 다음과 같다면
class Parent {
int age;
}
Child클래스는 자동적으로 age라는 멤버변수가 추가된 것과 같은 효과를 얻는다.
반대로 자식 클래스에 새로운 멤버가 추가되어도 부모 클래스는 아무런 영향도 받지 못한다.
즉, 부모 클래스가 변경되면 자식 클래스는 자동적으로 영향을 받게 되지만, 자식 클래스가 변경되는 것은 부모 클래스에 아무런 영향을 주지 못한다.
자바 상속의 특징은 다음과 같다.
1. 생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
2. 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.
3. 자식 클래스는 하나의 부모만 상속할 수 있다. (다중 상속이 안 된다.)
이번엔 상속 구조를 추가해보자.
class Parent { }
class Child extends Parent { }
class Child2 extends Parent { }
class GrandChild extends Child { }
- 만약 Parent에 age 멤버를 추가하면 Child, Child2, GrandChild 모두 영향을 받게 된다.
- Child, Child2는 같은 부모를 상속하지만 서로 영향을 주고 받지 않는다.
- GrandChild는 Parent 뿐만 아니라 Child에 대해서도 영향을 받는다.
- 또한 new GrandChild() 처럼 자식 클래스의 인스턴스를 생성하면 부모 클래스의 멤버도 함께 생성되기 때문에 따로 부모 클래스의 인스턴스를 생성하지 않고도 부모 클래스의 멤버를 사용할 수 있다.
이처럼 상속 구조에서 공통적인 부분은 부모 클래스에서 관리하고 자식 클래스는 자신에 정의된 멤버들만 관리하면 되므로 각 클래스의 코드가 적어져서 관리가 쉬워진다.
전체 프로그램을 구성하는 클래스들을 면밀히 설계 분석하여, 클래스간의 상속관계를 적절히 맺어 주는 것이 객체지향 프로그래밍에서 가장 중요한 부분이다.
super 키워드
참조변수
super 키워드는 자식 클래스에서 부모 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조 변수이다.
물론 부모 클래스로부터 상속 받은 멤버도 자식 클래스 자신의 멤버이므로 super 대신에 this를 사용할 수 있다.
그래서 부모 클래스의 멤버와 자식 클래스의 멤버가 중복 정의되어 있는 경우에만 super를 사용하는 것이 좋다.
다음 예시를 보자.
class Parent {
int x = 10;
}
class Child extends Parent {
void method() {
System.out.println("super.x = " + super.x);
System.out.println("this.x = " + this.x);
}
}
method() 결과
super.x = 10
this.x = 10
이 경우 super.x, this.x가 같은 변수를 의미하므로 같은 값이 출력되었다.
다음 예시는 어떨까
class Parent {
int x = 10;
}
class Child extends Parent {
int x = 20;
void method() {
System.out.println("super.x = " + super.x);
System.out.println("this.x = " + this.x);
}
}
method() 결과
super.x = 10
this.x = 20
이전 예시와 달리 부모 클래스의 멤버와 같은 이름의 멤버가 자식 클래스에도 있기 때문에 super.x와 this.x는 서로 다른 값을 참조하게 된다.
생성자
this()와 마찬가지로 super() 역시 생성자이다.
this()는 같은 클래스의 다른 생성자를 호출하는데 사용하지만,
super()는 부모 클래스의 생성자를 호출하는데 사용된다.
자식 클래스는 부모 클래스의 멤버가 합쳐진 인스턴스를 생성하기 때문에, 부모 클래스 생성자가 먼저 수행되고 자식 클래스의 생성자가 수행되어야 한다.
따라서 자식 클래스 생성자의 첫 줄에는 항상 부모 클래스의 생성자가 호출되어야 한다.
만약 따로 부모 클래스의 생성자를 호출하지 않았다면 컴파일러가 생성자의 첫 줄에. super()를 자동으로 추가한다.
다음 예시를 보자.
class Parent {
int x, y;
Parent(int x, int y) {
this.x = x;
this.y = y;
}
}
class Child extends Parent {
int z;
Child(int x, int y, int z) {
//에러
this.x = x;
this.y = y;
this.z = z;
}
}
이 예시는 컴파일에러가 발생한다.
Child의 생성자 첫 줄에서 Parent 생성자를 호출하지 않았기 때문에 컴파일러는 자동으로 super()를 추가한다.
Child(int x, int y, int z) {
super();
this.x = x;
this.y = y;
this.z = z;
}
그래서 Child클래스의 인스턴스를 생성하면 super(), 즉 Parent()를 호출하는데 Parent클래스에 Parent()가 정의되어 있지 않기 때문에 컴파일 에러가 발생하는 것이다.
이 에러를 수정하려면
1. Parent클래스에 Parent()를 추가해주던가
2. 생성자 Child(int x, int y, int z)를 다음과 같이 수정하면 된다.
Child(int x, int y, int z) {
super(x, y);
this.z = z;
}
부모 클래스의 멤버변수는 이처럼 부모의 생성자에 의해 초기화되도록 하는 것이 좋다.
메소드 오버라이딩
오버라이딩
부모 클래스로부터 상속받은 메소드의 내용을 변경하는 것을 오버라이딩이라고 한다.
메소드도 클래스의 멤버이기 때문에 자식 클래스는 부모 클래스의 메소드를 사용할 수 있다.
다만, 자식 클래스 자신에 맞게 메소드를 변경해야 하는 경우가 있기 때문에, 이 때, 부모의 메소드를 오버라이딩한다.
다음 예시를 보자.
class Parent {
int x, y;
Parent(int x, int y) {
this.x = x;
this.y = y;
}
void printLocation() {
System.out.println("x :" + x + ", y : " + y);
}
}
class Child extends Parent {
int z;
Child(int x, int y, int z) {
super(x, y);
this.z = z;
}
void printLocation() { //오버라이딩
System.out.println("x :" + x + ", y : " + y + "z : " + z);
}
}
Parent클래스의 printLocation은 한 점의 좌표 x, y를 출력하는 메소드라고 하자.
이 때, Child클래스는 printLocation을 상속받지만, 3차원 좌표계의 한 점을 표현하고자 한다.
메소드를 새로 만드는 것 보다 오버라이딩을 통해 Child클래스에게 맞게 메소드를 수정한다면
개발자 입장에서 Child클래스의 printLocation 역시 Parent클래스의 printLocation처럼 좌표를 출력할 것이라고 기대할 수 있다.
오버라이딩의 조건
파라미터의 개수나 타입을 변경해 새로운 메소드를 정의하는 오버로딩과 달리,
오버라이딩은 메소드의 내용만을 새로 작성하는 것이므로 메소드의 선언부는 부모의 것과 완전히 일치해야 한다.
즉, 자식 클래스에서 오버라이딩하는 메소드는 부모 클래스의 메소드와
- 이름이 같아야 한다.
- 매개변수가 같아야 한다.
- 반환타입이 같아야 한다.
단, 접근제어자(access modifier)와 예외(exception)는 제한된 조건 하에 다르게 변경할 수 있다.
1. 접근 제어자는 부모 클래스의 메소드보다 좁은 범위로 변경할 수 없다.
class Parent {
protected void parentMethod() {
...
}
}
class Child extends Parent {
public void parentMethod() { //오버라이딩
...
}
}
만약 부모 클래스 메소드의 접근 제어자가 protected라면, 이를 오버라이딩하는 자식 클래스의 메소드는 접근 제어자가 protected 혹은 public이어야 한다.
대부분의 경우 같은 범위의 접근 제어자를 사용한다.
<접근 제어자 접근범위(넓음 -> 좁음)>
public, protected, (default), private
2. 부모 클래스의 메소드보다 많은 수의 예외를 선언할 수 없다.
class Parent {
public void parentMethod() throws IOException, SQLException {
}
}
class Child extends Parent {
public void parentMethod() throws IOException { //오버라이딩
}
}
이렇게 부모 클래스의 메소드에서 선언된 예외의 개수보다 자식 클래스 메소드에서 선언된 예외의 개수가 적어야 한다.
여기서 주의해야할 점은 단순히 선언된 예외의 개수의 문제가 아니라는 것이다.
class Child extends Parent {
public void parentMethod() throws Exception { //오버라이딩
}
}
만일 위와 같이 오버라이딩 하였다면, 분면히 부모 클래스에 메소드에 비해 적은 개수의 예외를 선언한 것처럼 보이지만 Exception은 모든 예외의 최고 부모 클래스이므로 가장 많은 개수의 예외를 던질 수 있도록 선언한 것이다.
다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
다이나믹 메소드 디스패치
메소드 디스패치(Method DIspatch)란 어떤 메소드를 호출할 것인가를 결정하는 과정을 말한다.
정적 메소드 디스패치(Static Method Dispatch)는 컴파일 타임 때, 메소드 디스패치가 가능한 것을 말한다.
예를 들어, 오버로딩(Overloading)은 메소드 파라미터의 인자의 개수와 타입에 따라 컴파일 타임 때, 메소드를 결정할 수 있기 때문에 정적 메소드 디스패치에 속한다.
반면, 다이나믹 메소드 디스패치(Dynamic Method Dispatch)는 컴파일 타임이 아닌 런타임 때, 메소드가 결정된다.
다음 예시를 보자.
class Parent {
void print() {
System.out.println("Parent");
}
}
class Child1 extends Parent {
void print() {
System.out.println("Child1");
}
}
class Child2 extends Parent {
void print() {
System.out.println("Child2");
}
}
public class Main {
public static void main(String[] args) {
Parent p1 = new Child1();
p1.print();
Parent p2 = new Child2();
p2.print();
}
}
Parent클래스를 상속받는 두 클래스 Child1, Child2가 있다.
메인 메소드를 보면 자식 클래스 각각을 부모 클래스 타입으로 받고 있다.
그리고 두 인스턴스에서 각각 print()메소드를 수행한다.
이 때, p1.print()와 p2.print()에서 어떤 print()를 수행해야하는지는 new Child1(), new Child2() 같은 실제 인스턴스가 생성되야 알 수 있다. 즉, 런타임 때 메소드 디스패치가 가능한 것이다.
더블 디스패치
더블 디스패치에 관련한 좋은 예시가 있다.
1. SNS클래스와 이를 상속받는 Facebook, Instagram클래스가 있다.
class SNS {
class Facebook extends SNS { }
class Instagram extends SNS { }
2. Post 클래스와 이를 상속받는 Text, Picture클래스가 있다.
class Post {
void postOn(SNS sns) { }
}
class Text extends Post {
void postOn(SNS sns) {
if (sns instanceof Facebook) {
System.out.println("Text - Facebook");
} else if (sns instanceof Instagram) {
System.out.println("Text - Instagram");
}
}
}
class Picture extends Post {
void postOn(SNS sns) {
if (sns instanceof Facebook) {
System.out.println("Picture - Facebook");
} else if (sns instanceof Instagram) {
System.out.println("Picture - Instagram");
}
}
}
만약 여기서 Post 타입 객체의 postOn()을 접근하면, 다이나믹 디스패치가 적용되어 Text인지 Picture인지 판단하고 알맞은 클래스의 postOn()을 실행한다.
하지만 postOn() 메소드에서 문제가 있는데, 바로 SNS클래스에 의존한다는 점이다.
만약 Twitter 같은 SNS자식 클래스가 또 추가된다면 postOn() 메소드에 또 else if( ~ ) 로직을 추가해야 하는 번거로움이 있다.
이를 해결하기 위한 기법이 바로 더블 디스패치이다.
더블 디스패치를 적용한 코드를 보자.
class Text extends Post {
void postOn(SNS sns) {
sns.post(this);
}
}
class Picture extends Post {
void postOn(SNS sns) {
sns.post(this);
}
}
class SNS {
void post(Text text) { }
void post(Picture picture) { }
}
class Facebook extends SNS {
void post(Text text) {
System.out.println("Text - Facebook");
}
void post(Picture picture) {
System.out.println("Picture - Facebook");
}
}
class Instagram extends SNS {
void post(Text text) {
System.out.println("Text - Instagram");
}
void post(Picture picture) {
System.out.println("Picture - Instagram");
}
}
더블 디스패치는 SNS에서 새로운 자식 클래스가 생겼다고 해도 Post 자식 클래스들은 변경점이 없다.
Post p = new Text();
p.postOn(new Facebook());
위와 같은 코드에서 p.postOn() 메소드를 호출할 때,
1. p가 Text 인스턴스이기 때문에 -> Text클래스의 postOn호출
2. postOn으로 받은 인스턴스가 Facebook이기 때문에 Facebok클래스의 post(Text text) 호출
-> 더블 디스패치
정리하자면,
1. 다이나믹 디스패치에서 넘어온 파라미터에 따라서 조건을 걸지 말고(if, else if)
2. 파라미터의 메소드를 호출해서 자기 자신을 넘기자는 것이다. (sns.post(this))
더블 디스패치는 실무에 적용할 일은 거의 없지만,
두 가지 이상의 객체가 계층적 구조를 형성하고 메소드 호출이 자기 자신과 연관이 있는 경우 더블 디스패치를 적용하면 좋다.
추상 클래스
추상 클래스
추상 클래스는 추상 클래스 자체로 인스턴스를 생성할 수 없고, 상속을 통한 자식 클래스에 의해서만 인스턴스를 생성할 수 있다.
추상 클래스 자체로는 클래스로서의 역할은 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 부모 클래스로서 의미를 갖는다.
추상 클래스는 클래스에 'abstract' 키워드를 붙이기만 하면 된다. 이렇게 함으로써 이 클래스를 사용할 때, 클래스 선언부의 abstract를 보고 이 클래스는 상속을 통해서 구현해주어야 한다는 것을 쉽게 알 수 있다.
abstract class Parent {
}
추상 메소드
추상 메소드는 메소드의 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨 둔 것이다.
추상 메소드는 추상 클래스에서만 선언할 수 있다. 그리고 자식 클래스가 이를 이어 받아 추상 메소드를 구현해야 한다.
1. 부모 클래스에서는 선언부만을 작성하고
2. 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려주고
3. 실제 내용은 상속받는 클래스에서 구현하도록 비워두는 것이다.
abstract class Parent {
/* 주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명한다.*/
abstract void print();
}
class Child extends Parent {
void print() {
System.out.println("Child");
}
}
다시 말해, 추상 클래스를 상속받는 자식 클래스는 부모의 추상 메소드를 상황에 맞게 적절히 구현해주어야 한다.
만약 부모의 추상 메소드를 구현하지 않으면 컴파일 오류가 발생한다.
추상 클래스 작성
상속이 자식 클래스를 만드는데 부모 클래스를 사용하는 것이라면,
이와 반대로 추상 클래스는 보통 기존의 클래스들의 공통 부분을 뽑아내고자할 때 부모 클래스로서 만들어진다.
이를 추상화와 구체와를 비교해보면 이해하기 쉽다.
- 구체화: 상속을 통해 클래스를 구현, 확장하는 작업
- 추상화 클래스간의 공통점을 찾아내서 공통의 부모를 만드는 작업
다음 예시를 보자.
class Marine { //보병
int x, y; //현재 위치
void move(int x, int y) { /* 지정된 위치로 이동 */ }
void stop() { /*현재 위치에 정지 */ }
void stimPack() { /* 스팀팩을 사용한다. */ }
}
class Tank { //탱크
int x, y; //현재 위치
void move(int x, int y) { /* 지정된 위치로 이동 */ }
void stop() { /*현재 위치에 정지 */ }
void changeMode() { /* 공격모드를 변환한다. */ }
}
class Dropship { //수송선
int x, y; //현재 위치
void move(int x, int y) { /* 지정된 위치로 이동 */ }
void stop() { /*현재 위치에 정지 */ }
void load() { /* 선택된 대상을 태운다. */ }
}
이 유닛들은 각자 나름대로의 기능을 가지고 있지만 공통부분을 뽑아내어 하나의 부모 클래스를 만들고, 이를 상속받도록 변경해보자.
abstract class Unit {
int x, y;
abstract void move(int x, int y);
void stop() { /* 현재 위치에 정지 */ }
}
class Marine extends Unit { //보병
void move(int x, int y) { /* 지정된 위치로 이동 */ }
void stimPack() { /* 스팀팩을 사용한다. */ }
}
class Tank extends Unit { //탱크
void move(int x, int y) { /* 지정된 위치로 이동 */ }
void changeMode() { /* 공격모드를 변환한다. */ }
}
class Dropship extends Unit { //수송선
void move(int x, int y) { /* 지정된 위치로 이동 */ }
void load() { /* 선택된 대상을 태운다. */ }
}
각 클래스의 공통부분을 뽑아내서 Unit클래스를 정의하고, 이로부터 상속받도록 하였다.
1. move(int x, int y)
Marine, Tank는 지상유닛이고, Dropship은 공중유닛이기 때문에 이동하는 방법이 서로 달라서 move메소드의 실제 구현 내용이 다를 것이다.
그래도 move메소드의 파라미터는 현재 위치(x, y)로 모두 같기 때문에 이를 뽑아서 부모 클래스에 정의할 수 있다.
abstract class Unit {
int x, y;
void move(int x, int y) { /* 지정된 위치로 이동 */ }
}
공통 부분을 뽑아내기 위한 것이기도 하지만, 모든 유닛은 이동할 수 있어야 하므로 반드시 move를 구현하기 위해 추상 메소드로 정의했다.
abstract class Unit {
int x, y;
abstract void move(int x, int y);
}
2. stop()
stop메소드는 모든 유닛의 선언부와 구현부 모두 공통이기 때문에 부모 클래스에 정의하는 것만으로 충분하다.
3. stimPack(), changeMode(), load()
특정 유닛에서만 있는 메소드들은 각자 따로 정의해준다.
<추상 클래스를 굳이 왜 사용할까?>
추상 클래스는 상속을 통해서만 인스턴스를 생성한다는 의미도 있지만,
추상 메소드를 사용해 자식 클래스에서 반드시 구현해야 하는 메소드를 인식시킬 수도 있다.
그리고 유닛 클래스는 앞으로 다른 유닛을 위한 클래스를 작성하는데 재활용될 수 있을 것이다.
final 키워드
final
final 키워드는 '마지막의' 또는 '변경될 수 없는'의 의미를 가지고 있으며, 거의 모든 대상에 사용될 수 있다. (클래스, 메소드, 변수)
대상 | 의미 |
클래스 | 변경될 수 없는 클래스 -> 확장될 수 없는 클래스 final로 지정된 클래스는 다른 클래스의 부모가 될 수 없다. |
메소드 | 변경될 수 없는 메소드 final로 지정된 메소드는 오버라이딩을 통해 재정의 될 수 없다. |
변수 | 변경될 수 없는 변수 -> 즉, 값을 변경할 수 없는 상수가 된다. |
final class FinalTest { //부모가 될 수 없는 클래스
final int MAX_SIZE = 10; //값을 변경할 수 없는 상수
final int getMaxSize() { //오버라이딩할 수 없는 메소드
return MAX_SIZE;
}
}
final 변수 초기화
final이 붙은 변수는 상수이므로 일반적으로 선언과 초기화를 동시에 하지만,
인스턴스 변수의 경우 생성자에서 초기화되도록 할 수 있다.
이 기능을 활용하면 각 인스턴스마다 final이 붙은 인스턴스 변수가 다른 값을 갖도록 하는 것이 가능하다.
다음 예시를 보자.
class Card {
final int NUMBER;
final String KIND;
Card(int num, String kind) {
NUMBER = num;
KIND = kind;
}
}
각 카드마다 다른 종류와 숫자를 갖지만, 일단 카드가 생성되면 카드의 값이 변경되어서는 안되기 때문에 위와 같이 final 변수 초기화 방법이 적절하다.
Object 클래스
Object 클래스는 모든 클래스 상속계층도 최상위에 있는 부모클래스이다.
다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object 클래스로부터 상속받는다.
class Parent { }
이렇게 어떤 클래스도 상속 받지 않는 클래스가 있을 때, 위 코드를 컴파일하면
class Parent extends Object { }
자동으로 Object 클래스를 상속받도록 한다.
결국 Object 클래스는 모든 클래스의 부모 클래스가 되는 것이고, 만일 다른 클래스로부터 상속을 받는다고 하더라도 상속 계층도롤 따라 올라가다 보면 결국 마지막 최상위 부모는 Object 클래스일 것이다.
이처럼 자바의 모든 클래스들은 Object 클래스의 멤버들을 상속 받기 때문에 Object클래스에 정의된 멤버들을 사용할 수 있다.
그동안 toString()이나 equals()와 같은 메서드를 따로 정의하지 않고도 사용할 수 있엇던 이유는 이 메서드들이 Object클래스에 정의된 것들이기 때문이다.
'java > java' 카테고리의 다른 글
[Java] 인터페이스 (2) | 2021.07.27 |
---|---|
[Java] 패키지 (2) | 2021.07.21 |
[Java] 클래스 (1) | 2021.07.07 |
[Java] 제어문 (3) | 2021.06.29 |
[Java] Java 환경 변수 설정 - MacOS (0) | 2021.06.29 |