extends
키워드를 이용해 상속을 구현한다.- 조상 클래스
부모 클래스, 상위(super) 클래스, 기반(base) 클래스
- 자손 클래스
자식 클래스, 하위(sub) 클래스, 파생된(derived) 클래스
- 생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
- 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.
참고) 접근 제어자가 private 또는 default인 멤버들은 상속되지 않는다기보다 상속은 받지만 자손 클래스로부터 접근이 제한되는 것이다.
만약 한 조상 클래스를 상속 받고 있는 두 개의 다른 자손 클래스가 있을 때, 클래스 간의 관계에서 형제 관계 같은 것은 없으므로 공통적으로 추가 되어야 하는 멤버변수나 메서드가 있다면 공통 조상 클래스에 추가하는 것이 좋다.
자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버도 함께 생성되기 때문에 따로 조상 클래스의 인스턴스를 생성하지 않고도 조상 클래스의 멤버들을 사용할 수 있다.
자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.
상속 이외에도 클래스를 재사용하는 또 다른 방법이 존재한다. 바로 클래스 간의 포함(Composite)
관계를 맺어주는 것이다.
클래스 간의 포함 관계를 맺어 주는 것은 한 클래스의 멤버 변수로 다른 클래스 타입의 참조 변수를 선언하는 것을 뜻한다.
class Circle {
Point c = new Point();
int r;
}
class Point {
int x;
int y;
}
하나의 거대한 클래스를 작성하는 것보다 단위 별로 여러 개의 클래스를 작성한 다음, 이 단위 클래스들을 포함 관계로 재사용하면 보다 간결하고 손쉽게 클래스를 작성할 수 있다.
~은 ~이다 (is-a
) : 상속 관계
SuperCar은 Car이다
~은 ~을 가지고 있다 (has a
) : 포함 관계
Circle은 Point를 가지고 있다`
+) 참조변수의 출력이나 덧셈 연산자를 이용한 참조 변수의 문자열의 결합에는 toString()이 자동적으로 호출되어 참조변수를 문자열로 대치한 후 처리한다.
단일 상속이 하나의 조상 클래스만을 가질 수 있기 때문에 다중상속에 비해 불편한 점도 있지만 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 만들어준다는 점에서 다중상속보다 유리하다.
class TVCR extends Tv{
VCR vcr = new VCR();
void play() {
vcr.play();
}
void stop() {
vcr.stop();
}
...
}
Object 클래스는 모든 클래스 상속 계층도의 최상위에 있는 조상클래스
조상 클래스로부터 상속 받은 메서드의 내용을 변경하는 것
class Point {
int x;
int y;
String getLocation() {
return "x: " + x + ", y :" + y;
}
}
class Point3D extends Point{
int z;
@Override
String getLocation(){
return "x: " + x + ", y : " + y + ", z : " + z;
}
}
오버라이딩은 메서드의 내용만을 새로 작성하는 것으로, 메서드의 선언부는 조상의 것과 완전히 일치해야 한다.
오버라이딩 성립 조건 - 메서드 선언부
자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와
1. 이름이 같아야 한다.
2. 매개변수가 같아야 한다.
3. 반환 타입이 같아야 한다.
선언부가 서로 일치해야 하지만, 접근 제어자와 예외는 제한된 조건 하에서만 다르게 변경할 수 있다.
오버라이딩 성립 조건 - 접근 제어자와 예외
1. 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
만일 조상 클래스에 정의된 메서드의 접근 제어자가 protected라면, 이를 오버라이딩하는 자손 클래스의 메서드는 접근 제어자가 protected나 public이어야 한다.
대부분 같은 범위의 접근 제어자를 사용한다.
(접근 제어자 접근 범위 : public - protected - (default) - private)
2. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
아래는 Exception 수가 더 적어도 잘못된 예다. Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있기 때문class Parent{ void parentMethod() throws IOException, SQLException { ... } } class Child extends Parent { void parentMethod() throws IOException { ... } }
조상 클래스의 메서드를 자손 클래스에서 오버라이딩할 떄
1. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
2. 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
3. 인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.
+) 참고
클래스 이름.메서드 이름()
으로 하는 것이 바람직하다. static 멤버들은 자신들이 정의된 클래스에 묶여있다고 생각할 수 있다.오버로딩 : 기존에 없는 새로운 메서드를 정의하는 것(new)
오버라이딩 : 상속 받은 메서드의 내용을 변경하는 것(modify, change)
자손 클래스에서 조상 클래스로부터 상속 받은 멤버를 참조하는 데 사용되는 참조 변수
멤버 변수와 지역 변수의 이름이 같을 때 this를 붙여서 구별했듯이, 상속 받은 멤버와 자신의 멤버와 이름이 같을 때는 super을 붙여서 구별할 수 있다.
조상클래스로부터 상속받은 멤버도 자손 클래스 자신의 멤버이므로 super 대신 this를 사용할 수 있다.
그래도 조상 클래스의 멤버와 자손 클래스의 멤버가 중복 정의되어 서로 구별해야 하는 경우에만 super을 사용하는 것이 좋다
조상의 멤버와 자신의 멤버를 구별하는데 사용된다는 점을 제외하고는 super와 this는 근본적으로 동일하다.
모든 인스턴스 메서드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데, 이것이 참조 변수인 this와 super의 값이 된다.
static 메서드(클래스 메서드)는 인스턴스와 관련이 없다. 그래서 this와 마찬가지로 super 역시 static 메서드에서는 사용할 수 없고, 인스턴스 메서드에서만 사용할 수 있다.
class SuperTest2{
public static void main(String args[]){
Child c = new Child();
c.method();
}
}
class Parent{
int x = 10;
}
class Child extends Parent{
int x = 20;
void method(){
System.out.println(x): // 20
System.out.println(this.x); // 20
System.out.println(super); // 10
}
}
조상 클래스에 선언된 멤버 변수와 같은 이름의 멤버 변수를 자손 클래스에서 중복해서 정의하는 것이 가능하며 참조변수 super를 이용해 서로 구별할 수 있다.
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;
return super.getLocation() + ", z : " + z; // 조상 메서드 호출
}
}
조상 클래스의 메서드 내용에 추가적으로 작업을 덧붙이는 경우라면 이처럼 super을 사용해 조상 클래스의 메서드를 포함시키는 것이 좋다.
this()와 마찬가지로 super() 역시 생성자
this()는 같은 클래스의 다른 생성자를 호출하는 데 사용되지만, super()는 조상 클래스의 생성자를 호출하는 데 사용된다.
자손 클래스의 인스턴스를 생성하면, 자손의 멤버와 조상의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다.
이때 조상 클래스 멤버의 초기화 작업이 수행되어야 하기 때문에 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야 한다.
자손 클래스의 멤버가 조상 클래스의 멤버를 사용할 수도 있으므로 조상의 멤버들이 먼저 초기화되어야 한다. 따라서 생성자 첫 줄에서 조상 클래스의 생성자를 호출해야 한다. 조상 클래스 생성자 호출은 상속 관계를 거슬러 올라가면서 계속 반복된다.
Object 클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자.this() 또는 super()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동으로
super();
을 생성자의 첫 줄에 삽입한다.
인스턴스를 생성할 때는 클래스를 선택하는 것만큼 생성자를 선택하는 것도 중요하다.
1. 클래스 - 어떤 클래스의 인스턴스를 생성할 것인가?
2. 생성자 - 선택한 클래스의 어떤 생성자를 이용해 인스턴스를 생성할 것인가?
class PointTest{
public static void main(String args[]){
Point3D p3 = new Point3D(1,2,3);
}
}
class Point{
int x, y;
Point(int x, int y){
this.x = x;
this.y = y;
}
}
class Point3D extends Point{
// 생성자 첫 줄에서 다른 생성자를 호출하지 않으면
// 컴파일러는 여기에 super()을 삽입한다.
// 여기서 super()는 조상 클래스 Point 클래스의 기본 생성자 Point()
this.x = x;
this.y = y;
this.z = z;
}
이 예제를 컴파일하면 컴파일 에러가 발생한다. Point3D클래스의 생성자에서 조상 클래스의 생성자인 Point()를 찾을 수 없다는 내용이다.
문제 원인은 다음과 같다.
1. Point3D 클래스의 생성자의 첫 줄이 생성자를 호출하는 문장이 아니다.
2. 따라서 컴파일러는 자동적으로super();
을 생성자의 첫 줄에 넣어준다.
3. 그래서 Point3D 클래스의 인스턴스를 생성하면, 생성자 Point3D(int x, int y, intz)가 호출된다.
4. 이때 조상 클래스인 Point 클래스에서의 기본 생성자 Point()를 호출하나 실제로는 Point 클래스에 생성자 Point()가 정의되어있지 않아 찾을 수 없다.
5. 컴파일 에러가 발생한다.
문제 해결 방안은 다음 두 가지다. 둘 중 한 가지로 해결할 수 있다.
1. Point 클래스에 생성자 Point()를 추가해준다.
2. 생성자 Point3D(int x, int y, int z)의 첫 줄에서 Point(int x, int y)를 호출하도록 변경한다.Point3D(int x, int y, int z){ super(x, y); // 조상 클래스의 생성자 Point(int x, int y)를 호출해준다. this.x= y; }
✔ 정리
- this()는 같은 클래스의 다른 생성자를 호출하는 데 사용되지만, super()는 조상 클래스의 생성자를 호출하는 데 사용된다.
- 자손 클래스는 생성자 첫 줄에서 조상 클래스의 생성자를 호출해야 한다.
- 조상 클래스의 생성자가 없으면 컴파일러가 자동으로
super();
을 첫 줄에서 추가한다.- 생성자가 정의되어있는 클래스에는 컴파일러가 기본 생성자를 자동으로 추가하지 않는다.
- 조상 클래스의 멤버변수는 조상의 생성자에 의해 초기화되도록 해야 한다.
참고)
Point3D p3 = new Point3D();
와 같이 인스턴스를 생성하면, 아래와 같은 순서로 생성자가 호출된다.
Point3D() -> Point3D(int x, int y, int z) -> Point(int x, int y) -> Object()