상속
상속이란 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속을 통해 클래스를 작성하면 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고, 코드를 공통적으로 관리할 수 있으므로 코드의 추가 및 변경이 매우 용이하다. 결국 상속을 통해 코드의 재사용성을 높이고 코드의 중복을 제거하여 프로그램의 생산성과 유지보수에 크게 기여할 수 있다.
자바에서 상속은 다음의 형태를 띈다.
class Child extends Parent {
// ...
}
위의 두 클래스는 서로 상속 관계에 있다고 하며, 상속해주는 클래스를 조상 클래스라 하며, 상속 받는 클래스를 '자손 클래스라 한다. 서로 상속 관계에 있는 두 클래스를 다음의 용어로 표현하기도 한다.
- 조상 클래스: 부모(parent)클래스, 상위(super)클래스, 기반(base)클래스
- 자손 클래스: 자식(child)클래스, 하위(sub)클래스, 파생된(derived)클래스
자손 클래스는 조상 클래스의 모든 멤버를 상속 받으므로 항상 조상 클래스보다 같거나 많은 멤버를 갖는다. 즉, 상속에 상속을 거듭할수록 상속받는 클래스의 멤버 개수는 점점 늘어난다. 결국 상속을 받는다는 것은 조상 클래스를 확장(extend)한다는 의미로 해석할 수 있으며, 이것이 'extends' 키워드가 상속에 사용되는 이유다.
Object 클래스
Object 클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스로, 자바의 모든 클래스들은 Object 클래스의 멤버들을 상속받기 때문에 Object 클래스에 정의된 멤버들을 사용할 수 있다. toString( )이나 equals( )와 같은 메소드를 사용할 수 있었던 이유도 모두 Object 클래스를 상속받기 때문이었다.
오버라이딩
오버라이딩이란 조상 클래스로부터 상속받은 메소드의 내용을 변경하는 것이다. 상속받은 메소드를 그대로 사용하기도 하지만, 자손 클래스 자신에 맞게 변경해야 하는 경우가 있다. 이럴 때 조상의 메소드를 오버라이딩한다.
2차원 좌표를 표현하는 Point 클래스와 3차원 좌표를 표현하는 Point3D 클래스를 상속과 오버라이딩을 이용하여 다음과 같이 표현할 수 있다.
public class Main {
public static void main(String[] args) {
Point p = new Point();
Point3D p3 = new Point3D();
System.out.println(p.getLocation()); // x:0 y:0
System.out.println(p3.getLocation()); // x:0 y:0 z:0
}
}
class Point {
int x;
int y;
String getLocation() {
return "x:" + x + " y:" + y;
}
}
class Point3D extends Point{
int z;
String getLocation() {
return "x:" + x + " y:" + y + " z:" + z;
}
}
이러한 오버라이딩에는 조건이 다음과 같은 조건이 있다.
자손 클래스에서 오버라이딩하는 메소드는 조상 클래스의 메소드와
1. 이름이 같아야 한다.
2. 매개변수가 같아야 한다.
3. 반환타입이 같아야 한다.
조상 클래스의 메소드를 자손 클래스에서 오버라이딩할 때
1. 접근 제어자를 조상 클래스의 메소드보다 좁은 범위로 변경할 수 없다.
- 접근 제어자의 접근 범위는 넓은 순서대로 public, protected, (default), private이다.
2. 예외는 조상 클래스의 메소드보다 많이 선언할 수 없다.
3. 인스턴스 메소드를 static 메소드로 또는 그 반대로 변경할 수 없다.
또한, 메소드에 final 키워드를 붙이면 오버라이딩을 할 수 없게 된다. 이는 아래 제어자에서 좀 더 자세히 다루도록 하겠다.
오버로딩과 차이
오버로딩과 오버라이딩은 혼동하기 쉽지만, 다음과 같은 차이가 있다.
오버로딩(overloading): 기존에 없는 새로운 메소드를 정의하는 것(new)
오버라이딩(overriding): 상속받은 메소드의 내용을 변경하는 것(change, modify)
다음의 코드를 보면서 오버로딩과 오버라이딩의 차이를 파악해보자.
class Parent {
void parentMethod() {}
}
class Child {
void parentMethod() {} // 오버라이딩
void parentMethod(int i) {} // 오버로딩
void childMethod() {}
void childMethod(int i) {} // 오버로딩
void childMethod() {} // 에러발생. 중복정의되었음
}
super
super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는 데 사용되는 참조변수이다. 멤버변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속받은 멤버와 자신의 클래스에 정의된 멤버의 이름이 같을 때는 super를 붙여서 구별할 수 있다.
class Parent {
int x=10;
}
class Child extends Parent {
int x=20;
void method() {
System.out.println("x=" + x); // x=20
System.out.println("this.x=" + this.x); // this.x=20
System.out.println("super.x=" + super.x); // super.x=10
}
}
super ( )
this( )는 같은 클래스의 다른 생성자를 호출하는 데 사용되고, 유사하게 super( )는 조상 클래스의 생성자를 호출하는 데 사용된다. 자손 클래스의 인스턴스를 생성하면, 자손의 멤버와 조상의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다. 이 때 조상 클래스의 멤버의 초기화 작업이 수행되어야 하기 때문에 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야 한다.
따라서 모든 클래스의 최고 조상인 Object 클래스를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야 한다. 그렇지 않으면 컴파일러는 생성자의 첫 줄에 super( )를 자동적으로 추가할 것이다. 코드를 통해 살펴보자.
class Point {
int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
String getLocation() {
return "x:" + x + " y:" + y;
}
}
class Point3D extends Point{
int z;
Point3D(int x, int y, int z) {
// 여기에 컴파일러가 super();를 삽입한다.
// 조상인 Point 클래스에는 기본 생성자가 정의되어 있지 않기 때문에에러가 발생한다.
this.x = x;
this.y = y;
this.z = z;
}
String getLocation() {
return "x:" + x + " y:" + y + " z:" + z;
}
}
Point3D 클래스의 생성자를 다음과 같이 수정함으로써 에러를 해결할 수 있다.
class Point3D extends Point{
int z;
Point3D(int x, int y, int z) {
super(x, y); // 조상 클래스의 생성자 Point (int x, int y)를 호출한다.
this.z = z;
}
String getLocation() {
return "x:" + x + " y:" + y + " z:" + z;
}
}
제어자
제어자(modifier)란 클래스, 변수 또는 메서드의 선언부에 함께 사용되어 부가적인 의미를 부여한다. 제어자의 종류는 크게 접근 제어자와 그 외의 제어자로 나눌 수 있다.
- 접근 제어자: public, protected, default, private
- 그 외의 제어자: static, final, abstact, native, transient, synchronized, volatile, strictfp
접근 제어자
접근 제어자는 멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다. 접근 제어자가 default임을 알리기 위해 실제로 default를 붙이지는 않는다. 클래스나 멤버변수, 메소드, 생성자에 접근 제어자가 지정되어 있지 않다면 접근 제어자가 default 임을 뜻한다.
- private: 같은 클래스 내에서만 접근이 가능
- default: 같은 패키지 내에서만 접근이 가능
- protected: 같은 패키지 내에서, 그리고 다른 패키지의 자손 클래스에서 접근이 가능
- public: 접근 제한이 전혀 없음
접근 제어자 | 같은 클래스 | 같은 패키지 | 자손 클래스 | 전체 |
public | ○ | ○ | ○ | ○ |
protected | ○ | ○ | ○ | X |
default | ○ | ○ | X | X |
private | ○ | X | X | X |
그 외의 핵심 제어자
static - 클래스의 또는 공통적인
- static이 붙은 멤버변수와 메소드, 그리고 초기화 블럭은 인스턴스가 아닌 클래스에 관계된 것이기 때문에 인스턴스를 생성하지 않고도 사용할 수 있다.
final - 마지막의, 변경될 수 없는
- 변수에 사용되면 값을 변경할 수 없는 상수가 된다.
- 메소드에 사용되면 오버라이딩을 할 수 없게 된다.
- 클래스에 사용되면 자신을 확장하는 자손 클래스를 정의하지 못하게 된다.
abstract - 추상의, 미완성의
- 메소드의 선언부만 작성하고 실제 수행 내용은 구현하지 않은 추상 메소드를 선언하는 데 사용된다.
추상 클래스
클래스를 설계도에 비유한다면 추상 클래스는 미완성 설계도에 비유할 수있다. 미완성 설계도란, 단어의 뜻 그대로 완성되지 못한 채로 남겨진 설계도를 의미한다. 클래스가 미완성이라는 것은 단지 미완성 메소드(추상 메소드)를 포함하고 있다는 의미이며, 추상 클래스는 상속을 통해 자손 클래스에 의해서 완성될 수 있다.
추상 메소드
메소드는 선언부와 구현부로 구성되어 있다. 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨둔 것이 바로 추상메소드다. 즉, 설계만 해놓고 실제 수행될 내용은 작성하지 않았기 때문에 미완성 메소드인 것이다. 메소드를 이와 같이 미완성 상태로 남겨 놓는 이유는 메소드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만을 작성하는 것이다.
/* 주석을 통해 어떤 기능을 수행할 목적으로 작성했는지 설명한다. */
abstract 리턴타입 메소드이름( );
abstract class Player { // 추상 클래스
abstract void play(int pos); // 추상 메소드
abstract void stop(); // 추상 메소드
}
class AudioPlayer extends Player {
void play(int pos) { /* ... */ } // 추상 메소드를 구현
void stop() { /* ... */ } // 추상 메소드를 구현
}
abstract class AbstractPlayer extends Player { // 추상 클래스를 상속받은 추상 클래스
void play(int pos) { /* ... */ } // 추상 메소드를 구현
}
추상 클래스의 작성
여러 클래스에 공통적으로 사용될 수 있는 클래스를 바로 작성하기도 하며, 기존 클래스의 공통적인 부분을 뽑아서 추상 클래스로 만들어 상속하도록 하는 경우도 있다. 상속이 자손 클래스를 만드는 데 조상 클래스를 사용하는 것이라면 이와 반대로 추상화는 기존 클래스의 공통 부분을 뽑아내서 조상 클래스를 만드는 것이라고 할 수 있다.
- 추상화: 클래스간의 공통점을 찾아내서 공통의 조상을 만드는 작업
- 구체화: 상속을 통해 클래스를 구현, 확장하는 작업
이와 같이 기존의 클래스로부터 공통된 부분을 뽑아내어 추상클래스를 만드는 예제를 살펴보자.
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() {}
void unload() {}
}
클래스로 정의된 각각의 유닛들에서 공통 부분을 뽑아내어 하나의 클래스로 만들고, 이 클래스로부터 상속받도록 변경하면 다음과 같다.
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() {}
void unload() {}
}
'프로그래밍 언어 > Java + Kotlin' 카테고리의 다른 글
[Java] 스터디 8주차: 인터페이스 (0) | 2021.07.22 |
---|---|
[Java] 스터디 7주차: 패키지 (0) | 2021.07.15 |
[Java] 스터디 5주차: 클래스 (0) | 2021.07.07 |
[Java] 스터디 4주차: 제어문 (0) | 2021.06.24 |
[Java] 스터디 3주차: 연산자 (0) | 2021.06.23 |