상속?
상속은 중요하다. 일단 자바에서 상속은 클래스를 선언할때 아래와 같이 extends 키워드를 쓴다.
public class [작성할 클래스 이름] extends [상속받을 클래스 이름]
말로 풀어쓰면, "'내가 작성할 클래스(이하 B)'가 'extends 뒤에 있는 클래스(이하 A)'를 확장할꺼예요~" 정도가 되겠다.
그래서 어떻게 되는데요?
상속을 받으면 B는 A에 있는 자원들 중에서 접근제어자가 public, protected로 선언된 자원들(변수, 메소드)을 마치 자기가 가지고 있는 것 처럼 사용할 수 있다. private인 것들은 상속받은 클래스에서 사용할 수 없다.
package com.molt.parent;
public class A {
public A(){
System.out.println("Call A Constructor");
}
public void printInfo(){
System.out.println("A Class Method - printInfo()");
}
}
package com.molt.child;
import com.molt.parent.A;
public class B extends A {
public B(){
System.out.println("Call B Constructor");
}
}
위 코드를 보면 B클래스는 A클래스를 상속받고 있다. 상속과는 관계 없이 같은 패키지에 있으면 package-private인 것들은 사용할 수 있다. 오염을 피하기 위해서 각각 parent패키지에 A.java, child 패키지에 B.java 파일을 만들었다.
root 디렉토리의 Main.java 파일에서 아래와 같이 B 객체를 만들고 사용했다.
import com.molt.child.B;
public class Main {
public static void main(String[] args) {
B b = new B();
b.printInfo();
}
}
// 결과
Call A Constructor
Call B Constructor
A Class Method - printInfo()
결과를 보자. printInfo 메소드는 A에 있음에도 B객체에서 마치 자신이 가지고 있는 메소드 처럼 사용하고 있다는 것을 알 수 있다.
상속을 위한 준비?
위 실행결과를 다시 보자. 분명 B객체를 생성했는데, "Call A Constructor"라는 문장이 찍힌것으로 보아 A클래스의 생성자가 호출됐다. 상속받은 클래스(B)의 생성자가 호출되면, 자동으로 부모 클래스(A)의 기본 생성자가 호출된다. 코드로 보면 아래와 같다.
package com.molt.child;
import com.molt.parent.A;
public class B extends A {
public B(){
super();
System.out.println("Call B Constructor");
}
}
super()라는 코드를 적어주지 않았지만, 상속을 받은 클래스의 모든 생성자가 호출될때 부모의 기본 생성자를 호출하는 super()코드가 추가된다. 바꿔말하면, 상속을 해주는 클래스는 기본생성자가 있어야 한다. 생성자를 아무것도 만들지 않아도 컴파일 시점에서 컴파일러가 기본생성자를 추가해주기 때문에 크게 문제는 없다.
문제가 되는 것은 매개변수가 있는 생성자를 만들었을때 발생한다. 아래와 같이 기본 생성자를 주석처리하고, 매개변수로 String을 받는 생성자를 추가하자.
package com.molt.parent;
public class A {
// public A(){
// System.out.println("Call A Constructor");
// }
public A(String str){
System.out.println(str);
}
public void printInfo(){
System.out.println("A Class Method - printInfo()");
}
}
Main.java 파일에서 같은 코드를 실행시켜보면, 기본생성자를 찾을수 없다는 오류가 발생한다. 해결방법은 2가지다.
- 상속해주는 클래스에 기본 생성자를 추가하는 방법.
- 상속 받는 클래스에서 매개변수가 있는 생성자를 명시적으로 호출하는 방법.
1번은 간단하게 A클래스의 기본생성자 주석을 풀어주는 방법이다. 만약 A클래스를 변경할 수 없는 상황이라면 아래와 같이 처리한다.
package com.molt.child;
import com.molt.parent.A;
public class B extends A {
public B(){
super("string");
System.out.println("Call B Constructor");
}
}
super()가 매개변수가 없는 기본생성자를 호출하는 방법이라고 했다. 매개변수가 있는 생성자는 super("string")처럼 괄호 안에 매개변수를 넘겨주면 된다. 주의할 점은 부모클래스의 생성자를 호출하는 super()는 반드시 자식 클래스의 생성자에서 가장 첫줄에 선언되어야만 한다.
상속은 몇개나?
메소드 매개변수나 생성자의 매개변수 처럼 상속도 여러개 받을 수 있을까?
안된다. 자바는 다중 상속을 지원하지 않는다. extends 키워드 뒤에는 단 하나의 클래스만 올 수 있다.
그나저나 상속은 왜써?
하나의 클래스를 잘 만들어 놓으면, 그 클래스를 확장해 추가적인 기능을 넣을 수 있다.
메소드 오버라이딩(Overriding)
오버로딩과 오버라이딩은 둘다 메소드에 사용되는 용어라서 헷갈리지만, 다르다.
- 오버로딩(Overloading) : 메소드 또는 생성자 이름이 같을때, 매개변수의 타입 또는 개수, 순서가 다를때 다른 메소드로 인식하는것.
- 오버라이딩(Overriding) : 상속관계에서 부모에 존재하는 메소드를 자식 메소드에서 재정의해 사용하는 것.
부모클래스에 선언된 메소드와 동일한 메소드를 자식 클래스에 동일하게 선언하는 것을 메소드 오버라이딩이라고 한다. 메소드 오버라이딩은 상속관계를 보다 유연하게 활용할 수 있게 해준다. 메소드 오버라이딩은 동일한 시그니처를 가져야 한다. 시그니처는 메소드의 이름과 매개변수의 타입 및 개수를 말한다. 메소드 오버라이딩에서 리턴타입은 바꿀수 없다. 접근제어자는 축소는 불가능하지만, 확대는 가능하다. 예를들어 부모에 public으로 선언되 있는 것은 변경이 불가능하고, protected로 선언되 있는 메소드는 publilc으로 변경 가능하다. 메소드 오버라이딩하면 자식클래스의 메소드만 실행된다.
참조자료형의 형변환
참조 자료형의 형변환을 보기 전에, 한가지 물어보자. 상속해주는 A와 상속받는 B 둘중에 뭐가 더 클까? 정답은 B가 A보다 크거나 같다. B가 A를 상속만 받고, 아무것도 하지 않는다면 B나 A나 동일하다. 하지만 이런 경우는 거의 없으니 보통의 경우는 B가 A보다 크다.

보통 상속을 부모 자식으로 비유하는데, 부모랑 자식중에 누가 더 크냐 라고 묻는다면 어떻게 대답할까? 나의 경우는 쉽게 대답하기 힘들었다. 아무튼 상속받은게 크다! 이점을 꼭 기억하고 가자.
상속관계가 성립되면 지금까지 객체를 생성한 것과는 다르게 객체 생성이 가능하다.
지금까지 객체 생성은 아래와 같다.
Bicycle bicycle = new Bicycle();
상속관계에 있는 객체(A, B)는 아래와 같이 생성 가능하다.
A a = new B();
그런데, 아래는 안된다.
B b = new A();
한 마디로 정리하면 "큰걸로 작은건 만들 수 있는데, 작은걸로 큰건 못 만든다."
자바 컴파일러는 자식(큰거) 객체를 생성할때 부모(작은거) 생성자를 사용하면 안된다고 못을 박는다. 부모타입의 객체를 자식타입의 객체로 형변환하고 싶으면 명시적 형변환을 해줘야 한다. 부모타입의 객체를 자식타입의 객체로 명시적 형변환을 할때는 부모타입 객체가 실제로는 자식 타입의 객체여야 한다. 어떻게 객체의 타입을 구분할 수 있을까?
이때 쓰는게 instanceof라는 예약어, 아래와 같은 형태로 쓴다.
[객체] instanceof 클래스(타입)
위 문장은 true 또는 false 값을 반환한다. [객체]가 검사하는 클래스(타입)의 객체일 경우 true, 아닐 경우 false를 반환한다.
instanceof 사용시 유의점
instanceof로 타입을 점검할때, 부모타입의 객체도 자식 객체 타입에 해당하기때문에 true를 반환한다. 코드로 한번 보자.
import com.molt.child.B;
import com.molt.parent.A;
public class Main {
public static void main(String[] args) {
A a = new B();
if(a instanceof A) {
System.out.println("a is A");
}
if(a instanceof B) {
System.out.println("a is B");
}
}
}
// 결과
...
a is A
a is B
위 결과를 처럼 a는 A(부모)타입도 true, B(자식)타입도 true를 반환하기 때문에 제대로 타입을 체크할수 없다. 그렇기 때문에 가장 하위에 있는(가장 큰) 자식부터 확인을 해야 제대로 타입 점검이 가능하다.
다형성(Polymorphism)
다형성은 형태가 다양하다는 것. 어떤 형태가 다양한지 테스트 해보자.
package com.molt.child;
import com.molt.parent.A;
public class B extends A {
public B(){
System.out.println("Call B Constructor");
}
public void printInfo() {
System.out.println("B Class Method - printInfo()");
}
}
B.java 파일을 위와 같이 printInfo메소드를 오버라이딩 한다.
package com.molt.child;
import com.molt.parent.A;
public class C extends A {
public C() {
System.out.println("Call C Constructor");
}
public void printInfo() {
System.out.println("C Class Method - printInfo()");
}
}
C.java 파일을 위와 같이 만든다. 역시 printInfo메소드는 오버라이딩 한다.
아래와 같이 Main클래스에서 A타입의 객체를 각기 다른 생성자를 써서 만든다.
import com.molt.child.B;
import com.molt.child.C;
import com.molt.parent.A;
public class Main {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
A a3 = new C();
a1.printInfo();
a2.printInfo();
a3.printInfo();
}
}
// 결과
...
A Class Method - printInfo()
B Class Method - printInfo()
C Class Method - printInfo()
각 객체의 타입은 전부 A 타입으로 선언되어있다. 그러나 printInfo메소드의 결과가 다르다. 선언시에는 모두 A 타입으로 선언했지만 실제로 호출된 메소드는 생성자를 사용한 클래스에 있는 것이 호출된다. 각 객체의 실제 타입이 다르기 때문이다. 형 변환을 하더라도 실제 호출되는 것은 원래 객체에 있는 메소드가 호출된다. 이것이 다형성이다.
'Java' 카테고리의 다른 글
| [자바의 신] 인터페이스와 추상클래스 (0) | 2020.11.20 |
|---|---|
| [자바의 신] Object (0) | 2020.10.21 |
| [자바의 신] 접근제어자 (0) | 2020.10.02 |
| [자바의 신] 패키지 (0) | 2020.10.01 |
| [자바의 신] 참조 자료형 - 메소드 (0) | 2020.09.27 |