현실세계에서의 상속
객체 지향 프로그래밍의 상속
extends
키워드를 사용하여 상속할려는 클래스 이름을 작성하면 된다.class Child extends Parent {
}
Child
클래스와 Parent
클래스는 서로 상속 관계에 있으며, 상속해주는 클래스(Parent
클래스)를 부모 클래스 혹은 상위 클래스라고 부르며, 상속받는 클래스(Child
클래스)를 자식 클래스 혹은 하위 클래스라고 부른다.Parent
클래스에 age
라는 정수형 변수를 선언했다고 가정하에 상속 관계를 다시 살펴보면Parent | Child |
---|---|
age | age |
Child
클래스 또한 상속받기 때문에 Child
클래스에선 자동적으로 age
라는 멤버 변수를 사용할 수 있다.7, 10줄은 Child 클래스 안에 child_age
라는 변수를 선언하여 호출한 것이고
23, 26줄은 상속받은 Parent 클래스 안에 age
라는 변수를 호출하여 사용하는것이다.
invokespecial
은 생성자, private
메소드, 슈퍼 클래스의 메소드를 호출할 때 사용한다.
아직 미숙하지만 나의 생각으론 Parent
클래스의 생성자를 호출한 객체를 만들어 자연스럽게 age
변수를 사용할 수 있는 것 같다.
Child
클래스에 school()
이라는 메소드를 선언해도 Parent
클래스에는 아무런 영향이 없다.Parent | Child |
---|---|
age | age, school() |
Child
클래스의 생성자만 호출한 후 school()
이라는 인스턴스 메소드를 호출한다.
즉 Parent
클래스에는 아무런 영향이 가지 않는다는 의미기도 하다.
사실 아직 무슨 의미인지 눈에 잘 안들어온다..바이트 코드에 대해서 좀 더 공부해보고싶다 ㅎㅎ.. 🤣
상속을 통해 중복된 코드를 줄일 수 있다.
Parent
클래스를 상속받는 Child2
클래스를 하나 더 생성해서 중복된 코드를 줄일 수 있다는 것을 확인해보자
Parent.age
값을 변경하기 전
public class Parent {
int age = 30;
}
public class Test {
public static void main(String[] args) {
/*
단순한 예시로 부모님의 연세가 어떻게 되시니?라고 물어보면
부모님의 자식들은 동일하게 **살 입니다. 라고 대답할것이다.
*/
System.out.println("Parent의 age가 어떻게 되니??");
System.out.println("아 저희 Parent의 age는 " + new Child().age + "살 입니다.");
System.out.println("아 저희 Parent의 age는 " + new Child2().age + "살 입니다.");
}
}
output
Parent의 age가 어떻게 되니??
아 저희 Parent의 age는 30살 입니다.
아 저희 Parent의 age는 30살 입니다.
만약 해가 바뀌어 부모님의 연세가 바뀌었다고 가정해보자
Parent
를 상속받는 Child
, Child2
클래스에 직접 가서 age
값을 수정해도 되지만 만약 Parent
를 상속받는 클래스가 100개 이상이고, age
값을 수정하는 일이 복잡하다고 가정해보자. 그럼 age
값을 수정하는 행위를 100번 해야하고, 과정속에서 에러가 발생할 확률이 높아질 것이다. 상속을 통해 Parent.age
라는 변수가 자연스럽게 하위 클래스에 선언된다는 특징을 알기 때문에 상위 클래스 즉 Parent
의 값만 수정해주면 이를 상속받는 클래스들에 있는 age
값은 자연스럽게 변한다.
Parent.age
값을 변경한 후public class Parent {
int age = 31;
}
public class Test {
public static void main(String[] args) {
/*
단순한 예시로 부모님의 연배가 어떻게 되시니?라고 물어보면
부모님의 자식들은 동일하게 **살 입니다. 라고 대답할것이다.
*/
System.out.println("Parent의 age가 어떻게 되니??");
System.out.println("아 저희 Parent의 age는 " + new Child().age + "살 입니다.");
System.out.println("아 저희 Parent의 age는 " + new Child2().age + "살 입니다.");
}
}
output
Parent의 age가 어떻게 되니??
아 저희 Parent의 age는 31살 입니다.
아 저희 Parent의 age는 31살 입니다.
class Child extends Parent, GrandParent {
// 이와 같은 코드는 다중 상속이므로 자바에선 불가능하다.
}
다중상속을 이용하면 여러 클래스로부터 상속을 받을 수 있다는 장점이 있지만 그 만큼 객체와의 관계가 복잡하다는 단점이 있다.
만약 Parent
, GrandParent
에 문자열 타입에 주소를 의미하는 address
변수가 있다고 가정해보자
class GrandParent {
String address = "";
}
class Parent {
String address = "";
}
다중상속이 된다는 가정하에 코드를 짜보자 Parent
, GrandParent
둘 다 상속받는 Child
클래스를 만들어보면
class Child extends Parent, GrandParent {
public void school() {
System.out.println("학교에 간다.");
}
public static void main(String[] args) {
System.out.println("우리집은 ? " + new Child().address);
}
}
위 코드는 물론 컴파일 에러가 발생하기 때문에 실행조차 되지 않을 뿐더라 만약 실행이 된다고 해도 컴파일러 입장에선 address
가 Parent
클래스인지, GrandParent
클래스를 의미하는지 모를 것이다.
Object
클래스는 모든 클래스 상속계층도의 제일 위에 위치하는 상위클래스이다.Object
클래스는 클래스 계층의 루트이며, 모든 클래스에는 Object
객체가 슈퍼 클래스로 있다.다른 클래스로부터 상속을 받지 않는 Parent
클래스를 정의하였다고 했을때, 컴파일 하면 extends Object
키워드가 붙는다..는데..?
public class Parent {
}
참고한 문서에 의하면 아래와 같이 바이트코드로 분석했을때 extends java.lang.Object
가 붙는다.
근데 내가 한거에는 왜 없즤....동일하게 SimpleProgram 클래스 만들어서 했는데..뭐지..🤔
조금 더 자료를 찾아봐야할 것 같다.
~위에 덮어쓰다(overwrite)
또는 ~에 우선하다.
라는 의미를 가지고 있다.상위 클래스의 메소드가 하위 클래스에서 충분한 기능을 제공하지 못하거나, 부족할 경우 상위 클래스로부터 상속받은 메소드를 재정의 한다.
이해하기 편하게 비유를 한다면, 만약 부모님으로 부터 재산을 상속받았다는 상상을 한번 해보자 재산을 상속받은 자식들은 이 재산을 그대로 둘까? 그대로 둘 가능성은 거의 희박하다. 그 만큼 현실을 살아가면서 상속받은 재산에 대해 자식들이 사용하거나 변경할 수 있기 때문이다.
자바에서도 마찬가지이다. 상위 클래스에서 상속받은 메소드를 본인에 입맞게 맞게 튜닝한다고 생각하면 될 것 같다 💡
class Car {
private boolean engine;
public void showState() {
System.out.println("현재 엔진의 시동이 걸려있는가? : " + engine);
}
public void startEngine(boolean engine) {
this.engine = engine;
}
}
class Audi extends Car {
private boolean quattro;
public void showState() {
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
}
public class Test {
public static void main(String[] args) {
Audi audi = new Audi();
audi.startEngine(true);
audi.setQuattro(true);
audi.showState();
}
}
출력문
현재 4륜 시스템이 켜져있는가? :true
여기서 Audi
클래스는 Car
클래스를 상속받았으므로 Car
클래스의 메소드인 startEngine()
호출할 수 있다.
또한 setQuattro()
메소드를 통해 4륜 시스템의 작동여부를 설정할 수 있으며 showState()
메소드를 호출함으로써 현재 Audi
클래스의 상태를 알 수 있다. 하지만 Car
클래스에도 showState()
메소드가 있는데 호출이 되지 않았다. 그 이유는 Audi
클래스에서 showState()
메소드를 오버라이딩 했기 때문에 재정의된 메소드로 호출된 것이다.
super
키워드를 사용하면 가능!💡
- 이름이 같아야한다.
- 매개변수(갯수, 데이터 타입, 순서)가 같아야 한다.
- 리턴타입이 같아야 한다.
Car
클래스에선 showState()
메소드를 호출했을 때 엔진의 시동여부를 판단하지만, 이를 상속받는 Audi
클래스에선 4륜 시스템의 작동여부를 판단하는 기능으로 재정의하여 사용한다.
super
키워드는 하위 클래스에서 상위 클래스로부터 상속받은 멤버(변수, 메소드)를 참조하는데 사용되는 참조 변수이다. this
키워드를 사용하는것 처럼 상속받는 멤버와 자신의 클래스 내에 이름이 동일한 경우 super
키워드를 작성한다.class Car {
private boolean engine;
public void showState() {
System.out.println("현재 엔진의 시동이 걸려있는가? : " + engine);
}
public void startEngine(boolean engine) {
this.engine = engine;
}
}
class Audi extends Car {
private boolean quattro;
public void showState() {
super.showState();
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
}
public class Test {
public static void main(String[] args) {
Audi audi = new Audi();
audi.startEngine(true);
audi.setQuattro(true);
audi.showState();
}
}
출력물
현재 엔진의 시동이 걸려있는가? : true
현재 4륜 시스템이 켜져있는가? :true
class Car {
private boolean engine;
public String color;
public Car(String color) {
this.color = color;
}
public void showState() {
System.out.println("현재 엔진의 시동이 걸려있는가? : " + engine);
}
public void startEngine(boolean engine) {
this.engine = engine;
}
}
class Audi extends Car {
private boolean quattro;
public Audi() {
super("black");
}
public void showState() {
super.showState();
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
System.out.println("현재 차량의 색상은? : " + color);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
}
public class Test {
public static void main(String[] args) {
Audi audi = new Audi();
audi.startEngine(true);
audi.setQuattro(true);
audi.showState();
}
}
Audi
클래스의 생성자 호출을 통해 객체로 만들 때 상위 클래스인 Car
클래스에 생성자를 super()
키워드를 통해서 호출하여 차량의 색상을 정한다.한가지 예를 들면
자동차를 만드는 제조사는 여러개가 있다. A제조사, B제조사, C제조사는 각 제조사의 특징대로 자동차를 만든다. A자동차는 4륜 시스템을 도입하여 자동차가 달릴 때 안정감을 줄 수 있도록 만들고, B제조사는 후륜 구동 방식을 채택하여 달릴 때 운전자에게 재미난 요소를 제공하게끔 만들고, C제조사는 달릴 때 방지턱, 요철에 대한 충격 흡수를 잘할 수 있는 장치를 만들어 운전자에게 편안함을 제공해준다.
여기서 자동차 제조사들 별로 공통적인 특징을 가지고 있는데 이 특징이 달린다
라는 특징이다. 달린다는 특징을 가지고 있는 Car
클래스 만든 후 각 제조사별로 이 Car
클래스를 상속받아 차를 만들면 어떨까?? 달린다는 본질은 달라지지 않고 각 제조사의 특징을 잘 살려서 자동차를 만들 수 있을 것이다.
여기서 달린다는 기능을 가지고 있는 Car
클래스가 추상 클래스이고, 이 Car
클래스를 상속받아 각 제조사별 자동차를 만들어 내는 클래스가 실체 클래스이다.
즉 실체 클래스는 실체 말 그대로 인스턴스를 만들 수 있는 클래스이고
추상클래스는 인스턴스를 만들지 못하지만, 실체 클래스에 기반이 되는 클래스이다.
그렇다면 왜 추상클래스를 사용할까? 🤔
여기서 나온 예시들은 이해를 돕기 위한 예시일뿐 실제는 아니다 🚀
혼자 개발할땐 문제가 없지만, 실제 개발시 필드와 메소드 이름을 통일하여 여러 사람이 개발을 할 때 앞으로 가기를 누군가는
moveForward()
라고 표현할 것이고, 또 누군가는goForward()
라고 부를 것이다. 때문에 추상 클래스를 사용하여 미리 정의해놓은 필드명와 메소드를 이용하여 실체를 구현한다.
하지만 자동차의 기본요소를 규격해 놓은 상태에서 트럭을 만든다고 하면? 차체에 대한 부분만 개발하면 되기 때문에 만드는데 시간을 단축시킬 수 있을 것이다. 실제 우리가 개발할 때도 규격해놓은 추상클래스만 있으면 기본적인 필드와 메소드에 대해 생각할 필요 없이 구현하는데만 집중할 수 있어 개발 시간을 단축시킬 수 있다.
여기서 추상클래스(규격)을 상속받는 실체클래스(자동차 제조사)는 반드시 추상메소드에 정의 되어있는 메소드를 재정의(오버라이딩)해서 실체클래스에서 작성해야한다. 그렇지 않으면 컴파일 에러가 발생하여 실행조차 되지 않는다.
abstract class Car {
abstract void airbag(); // 에어백 기능
abstract void transmission(); // 미션 기능
}
class Audi extends Car {
private boolean quattro;
void airbag() {
System.out.println("에어백을 운전석에 장착했습니다."); // 추상 메소드를 구현
}
void transmission() {
System.out.println("ZF사의 8단 미션을 장착했습니다."); // 추상 메소드를 구현
}
public void showState() {
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
}
public class Test {
public static void main(String[] args) {
Audi audi = new Audi();
audi.airbag();
audi.transmission();
audi.setQuattro(true);
audi.showState();
}
}
출력물
에어백을 운전석에 장착했습니다.
ZF사의 8단 미션을 장착했습니다.
현재 4륜 시스템이 켜져있는가? :true
class Car {
private boolean engine;
public final boolean airbag = true;
public String color;
public Car(String color) {
this.color = color;
}
public void showState() {
System.out.println("현재 엔진의 시동이 걸려있는가? : " + engine);
}
public void startEngine(boolean engine) {
this.engine = engine;
}
}
class Audi extends Car {
private boolean quattro;
public Audi() {
super("black");
}
public void showState() {
super.showState();
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
System.out.println("현재 차량의 색상은? : " + color);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
}
public class Test {
public static void main(String[] args) {
final Audi audi = new Audi();
audi.setQuattro(true);
audi.showState();
}
}
//final 클래스
final class Car {
private boolean engine;
public final boolean airbag = true;
public String color;
public Car(String color) {
this.color = color;
}
public void showState() {
System.out.println("현재 엔진의 시동이 걸려있는가? : " + engine);
}
public void startEngine(boolean engine) {
this.engine = engine;
}
}
// Car클래스가 Final로 정의되었기 때문에 상속이 불가능하다.
class Audi extends Car {
private boolean quattro;
public Audi() {
super("black");
}
public void showState() {
super.showState();
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
System.out.println("현재 차량의 색상은? : " + color);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
}
Car
클래스가 final
로 정의되었기 때문에 이를 Audi
클래스에선 Car
클래스를 상속받을 수 없다고 에러 창이 나온다.
final 메소드
class Car {
private boolean engine;
public final boolean airbag = true;
public String color;
public Car(String color) {
this.color = color;
}
public final void showState() {
System.out.println("현재 엔진의 시동이 걸려있는가? : " + engine);
}
public void startEngine(boolean engine) {
this.engine = engine;
}
}
class Audi extends Car {
private boolean quattro;
public Audi() {
super("black");
}
// 메소드 오버라이딩 불가능
public void showState() {
super.showState();
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
System.out.println("현재 차량의 색상은? : " + color);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
}
Car
클래스에 showState()
메소드를 final
로 정의한 상태에서 상속받은 Audi
클래스에서 오버라이딩을 하면 아래와 같은 에러가 난다.
메소드의 인자값이 final
을 사용할 경우
class Audi extends Car {
private boolean quattro;
String tire = "";
public Audi() {
super("black");
}
public void showState() {
super.showState();
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
System.out.println("현재 차량의 색상은? : " + color);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
public void setTire(final String tire) {
tire = "한국타이어";
this.tire = tire;
}
}
public class Test {
public static void main(String[] args) {
final Audi audi = new Audi();
audi.setQuattro(true);
audi.setTire("미쉐린");
audi.showState();
}
}
메소드 디스패치의 종류로는 Static Dispatch
, Dynamic Dispatch
가 있다.
컴파일 시 생성된 바이트 코드에도 이 정보가 남아있다.
함수를 오버로딩하여 사용하는 경우, 인자의 타입이나 리턴타입 등에 따라 어떤 메소드가 호출될 지 명확하기 때문에 이러한 경우도 '미리 알 수 있다'라고 할 수 있다.
위에서 작성한 SimpleProgram에 바이트 코드로 예시를 들어보자
빨간줄로 표시된 줄이 System.out.println()
함수를 호출하는 것이며, 런타임 시점이 되지 않아도 미리 결정될 수 있기에 Static Method Dispatch
에 해당한다.
Static Method Dispatch
와는 다르게 런타임 시점이 되서야 알 수 있는 메소드들이 여기에 해당한다. 좀 더 쉽게 이야기하자면 상속, Interface
혹은 Abstract Class
에서 정의된 Method
를 오버라이딩하여 호출할 경우 해당된다.
위에서 작성한 Car
클래스를 예시로 들면
Audi audi = new Audi()는 Static Method Dispatch에 해당하고
Car car = new Audi();로 변경하여 객체를 생성하면 Dynamic Method Dispatch가 된다
class Car {
private boolean engine;
public final boolean airbag = true;
public String color;
public Car(String color) {
this.color = color;
}
public void showState() {
System.out.println("현재 엔진의 시동이 걸려있는가? : " + engine);
}
public void startEngine(boolean engine) {
this.engine = engine;
}
}
class Audi extends Car {
private boolean quattro;
String tire = "";
public Audi() {
super("black");
}
public void showState() {
super.showState();
System.out.println("현재 4륜 시스템이 켜져있는가? :" + quattro);
System.out.println("현재 차량의 색상은? : " + color);
}
public void setQuattro(boolean quattro) {
this.quattro = quattro;
}
public void setTire(String tire) {
tire = "한국타이어";
this.tire = tire;
}
}
public class Test {
public static void main(String[] args) {
Audi audi = new Audi();
// audi.setQuattro(true); // static method dispatch
// audi.setTire("미쉐린");
// audi.showState();
Car car = new Audi();
car.showState();
}
}
출력물
현재 엔진의 시동이 걸려있는가? : false
현재 4륜 시스템이 켜져있는가? :false
현재 차량의 색상은? : black
위 예제 코드와 같이 런타임시 상위 클래스 타입으로 객체를 생성한 후 오버라이딩된 메소드를 호출하는 것을 Dynamic Method Dispatch
라고 한다.
바이트 코드를 보면 조금 더 이해가 쉽다.
Car.showState()
메소드를 호출할뿐 그 외 메소드 호출은 Dynamic Method 이므로 바이트 코드기록에 남지 않는다.
🧾 참고자료
Abstract : https://limkydev.tistory.com/188
Object : https://docs.oracle.com/javase/tutorial/java/IandI/objectclass.html
Override : https://velog.io/@polynomeer/JAVA%EC%9D%98-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9Override
super : http://www.tcpschool.com/java/java_inheritance_super
final : https://coding-factory.tistory.com/525
method dispatch : https://defacto-standard.tistory.com/413