Java 프로그래밍, 상속 ( Inheritance )
상속(Inheritance)의 개념
Java 언어에서는 한 클래스로부터 다른 클래스를 파생하여 정의할 수가 있다. 즉, 한 클래스의 멤버변수와 멤버 메소드를 그대로 가진 또 하나의 클래스를 정의할 수가 있다는 의미이다. 파생하여 생성되는 클래스는 파생 클래스(derived class), 하위 클래스(sub class), 자식 클래스(child class), 확장 클래스(extended class) 등으로 불리고, 파생을 해주는 클래스는 기본 클래스(base class), 상위 클래스(super class), 부모 클래스(parent class)라고 한다.
Java 언어의 클래스는 Object 라는 클래스를 제외하고 모든 클래스는 반드시 하나의 부모 클래스를 선언해야 한다. 클래스 작성자가 명시적으로 해당 클래스의 부모 클래스를 지정하지 않으면 컴파일러에 의해 자동으로 Object 클래스가 부모 클래스로 지정된다. 그러므로 자바의 모든 클래스는 Object 클래스의 자손 클래스라고 할 수 있다.
상속(Inheritance)은 매우 단순한 개념이며 어떤 클래스가 가진 동일한 속성과 메소드를 그대로 가진 또 하나의 클래스를 작성하고자 한다면 상속을 이용할 수 있다. 상속의 대상이 되는 멤버는 멤버변수, 멤버 메소드, 멤버 클래스 등이며 생성자는 상속에서 제외된다. 그러나 하위 클래스에서 상위 클래스의 생성자를 호출할 수는 있다.
Object 는 자바의 모든 클래스의 조상 클래스이므로 모든 자바 클래스에는 Object 클래스의 속성과 메소드가 포함되어 있다. 그러므로 Object 클래스는 모든 클래스가 가져야 할 매우 일반적인 속성과 메소드가 정의되어 있고 Object 의 바로 하위에 있는 클래스들은 Object 클래스의 메소드를 변경 없이 그대로 포함하고 있거나 약간 변경(오버라이드, Override, 재정의)하여 포함하고 있다.
상속과 IS-A 관계 ( IS-A Relationship )
상속은 2개의 클래스가 서로 특별한 관계로 설정되는 일이므로 생각 없이 무분별한 상속은 하지 않는 것이 좋다. 예를 들어, 사원 클래스(Employee)가 있을 때 새로운 과장 클래스(Manager)를 생성하고자 한다면, 과장도 사원이므로 사원이 갖는 모든 속성을 동일하게 가지는 것은 당연하다. 그러므로 과장(Manager) 클래스는 사원(Employee) 클래스를 상속하여 작성하면 작성도 편리하고 실물세계의 상식을 그대로 소프트웨어 개발시에도 적용할 수 있기 때문에 여러모로 도움이 된다. 그러므로 상속을 할 때는 2개의 클래스가 IS-A 관계에 해당하는지 확인해서 IS-A 관계가 성립할 때만 상속을 적용해야 한다
HAS-A Relationship
Java와 같은 객체지향 언어를 사용하여 소프트웨어를 개발할 때는 특히 객체간의 관계에 신경을 써야 한다. 객체지향 언어의 객체는 원래 실물객체를 본떠 도입된 개념이므로 실물세계 즉, 상식을 따라야 하며, 객체간의 관계를 구상할 때 상식을 벗어나는 구상을 한다면 개발이 어렵고 유지보수는 더 어렵게 될 수도 있다. 그러므로 객체간의 관계를 설정할 때 크게 2가지의 관계를 고려해 볼 필요가 있다. 위에서 언급한 IS-A 관계와 HAS-A 관계가 그것인데, IS-A 관계에 해당하면 실제 코딩시에 상속(Inheritance)으로 표현하고, HAS-A 관계에 해당하면 한 클래스가 한 클래스의 인스턴스를 멤버로 선언하여 사용하면 된다
IS-A / HAS-A
Employee를 Manager 가 상속한 예
import java.util.Date; public class InheritanceMain { public static void main(String[] args) { // Manager는 Employee를 상속한 클래스이다 Manager m = new Manager(20010215,"홍과장", "기획"); // Employee에 선언되어 있고 Manager 클래스에 상속된 printInfo()를 호출해본다 m.printInfo(); } } //이 클래스는 명시적으로 부모 클래스를 지정하지 않았기 때문에 묵시적으로 Object 클래스가 부모로 지정된다 class Employee { // Encapsulation & Hiding private int empno; private String ename; private Date hiredate; private int deptno; private int sal; // 생성자 오버로드(Constructor Overload) public Employee(){} public Employee(int empno, String ename) { this.empno = empno; this.ename = ename; } public void printInfo() { System.out.printf("사번:%d %n",this.empno); System.out.printf("이름:%s %n",this.ename); } // getters & setters public int getEmpno() { return empno; } public void setEmpno(int empno) { this.empno = empno; } public String getEname() { return ename; } public void setEname(String ename) { this.ename = ename; } public Date getHiredate() { return hiredate; } public void setHiredate(Date hiredate) { this.hiredate = hiredate; } public int getDeptno() { return deptno; } public void setDeptno(int deptno) { this.deptno = deptno; } public int getSal() { return sal; } public void setSal(int sal) { this.sal = sal; } } // 과장(Manager)도 사원(IS-A Relationship)이므로 Employee 클래스로부터 파생하는 것이 합리적이다 class Manager extends Employee { private String field; // 과장은 일반사원과 달리 관리분야가 추가된다 // 생성자는 상속되지 않으므로 필요하다면 별도로 정의해야 한다 public Manager(){} public Manager(int empno, String ename, String field) { // 아래 라인은 오류, empno, ename 등은 부모 클래스의 private 멤버이므로 접근 및 상속불가 //this.empno = empno; // 부모 클래스의 생성자는 자식 클래스에서 상속은 안되지만 호출은 가능하다 super(empno,ename); // 생성자의 첫 라인에 위치해야 한다 // 자식 클래스에서 추가된 속성은 자식의 생성자에서 직접 초기화할 수 있다 this.field = field; } // 자식 클래스에서 추가된 속성에 대한 setters & getters public String getField() { return field; } public void setField(String field) { this.field = field; } }
상속에서 자식 클래스에 적용할 수 있는 것들
- 부모 클래스에 있는 public, protected 멤버는 자식 클래스로 무조건 상속된다
- 부모 클래스에 있는 package-private(default) 멤버는 자식 클래스가 부모 클래스와 동일 패키지에 있을 때만 상속된다
- 부모 클래스에 있는 private 멤버는 상속되지 않는다
- 상속된 멤버는 자식 클래스에서 마치 자식 클래스에 있는 멤버와 같이 직접 접근하여 사용할 수 있다
- 부모 클래스에 있는 속성이름과 동일한 속성을 자식 클래스에도 선언할 수 있다(hiding)
- 자식 클래스에서 부모 클래스의 속성을 가리는(hiding) 것은 권장되지는 않는다
- 부모 클래스에 없는 속성을 자식 클래스에서 새로 선언할 수 있다
- 상속된 메소드는 마치 자식 클래스에 선언된 메소드처럼 직접 접근하여 사용할 수 있다
- 부모 클래스의 인스턴스 메소드 시그니처(Signature)를 사용하여 자식 클래스에 새로운 인스턴스 메소드를 정의할 수 있다(Override)
- 부모 클래스에 있는 static 메소드의 시그니처를 사용하여 자식 클래스에 새로운 static 메소드를 선언할 수 있다(hiding)
- 부모 클래스에 없는 새로운 메소드를 자식 클래스에 선언할 수 있다
- 자식 클래스의 생성자를 정의할 수 있고 그 생성자 안에서 부모 클래스의 생성자를 호출할 수 있다
부모 클래스의 private 멤버는 자식 클래스로 상속되지 않는다. 그러나...
- 부모 클래스의 private 멤버에 접근하는 메소드가 부모 클래스에 정의되어 있고 그 메소드가 자식 클래스로 상속된 경우에는 자식 클래스에서 그 메소드를 사용하여 부모 클래스의 private 멤버에 간접 접근(Indirect-Access)할 수는 있다
- 위와 같은 개념으로, 부모 클래스의 private 멤버에 접근하는 중첩 클래스(Nested Class)가 부모에 있고 그 중첩 클래스가 자식에게 상속된 경우에도 자식 클래스는 상속된 중첩 클래스를 사용하여 부모의 private 멤버에 간접 접근(Indirect-Access)할 수 있다
객체의 형변환 ( Casting Objects )
객체의 참조를 저장하는 참조변수는 객체를 생성할 때 사용된 클래스의 이름을 자료형으로 사용한다
예를 들어, 위의 코드와 같이, Manager m = new Manager(20010215,"홍과장", "기획"); 할 수 있다
Manager 클래스를 이용하여 인스턴스를 생성했기 때문에 Manager 클래스의 이름을 객체의 참조형 변수로 사용하는 것은 자연스러운 것이다. 그러나 참조형 변수의 자료형이 이와 같이 되어 있지 않아도 상식이 적용되어 있다면 자연스러운 경우가 있다. 여기서 '상식', '자연스러운' 과 같은 말은 실물세계의 상식이 적용된 클래스 설계 및 클래스간 관계설정이 되어 있는 상태를 말하고 있다.
상식적으로 '과장(Manager)은 사원(Employee)이다' 라는 말에 누구나 동의할 것이므로 이 말은 상식이다. 객체지향 설계에서 이와 같은 클래스 간의 관계를 IS-A 관계(IS-A Relationship)라고 하며 클래스가 이런 관계로 판단될 때 Manager 클래스가 Employee 클래스를 상속하도록 작성해야 상식만으로도 클래스 간의 관계가 파악되므로 코드의 해석이 쉽게 되고 관리도 용이하게 된다. 그러므로 아래와 같은 코드는 상식이 통하므로 자연스러운 것이고 문법적으로도 문제가 없다. 즉, 객체지향 언어의 문법은 실물세계의 상식으로 이해해야 깊이가 있고 잊혀지지 않으며 직관적으로 해석할 수 있게 된다.
Employee emp = new Manager(20010215,"홍과장", "기획");
위와는 반대로 한다면 말이 될까? 이 문제도 상식으로 해결될 때 제대로 했다고 말할 수 있다. 즉, '사원(Employee)은 과장(Manager)이다' 라는 말은 상식이 아니므로 문법적으로도 컴파일 오류를 발생한다
Downcast / Upcast
객체의 형변환 테스트
package tutorials; import java.util.Date; public class InheritanceMain { public static void main(String[] args) { // Manager는 Employee를 상속한 클래스이다 Manager m = new Manager(20010215,"홍과장", "기획"); // 묵시적 형변환, 문제없음 Employee e = m; // Manager(과장)는 Employee(사원)이다 : OK // 묵시적 형변환의 경우 (Implicit Type Casting), 컴파일 오류 //m = e; // Employee(사원)은 Manager(과장)이다 : NO // 명시적 형변환(Explicit Type Casting), 문제 없음 m = (Manager)e; // Down Casting 시에도 실행 오류가 없음(원래 Manager 인스턴스이므로) m.printInfo(); } } class Employee { private int empno; private String ename; private Date hiredate; private int deptno; private int sal; // 생성자 오버로드(Constructor Overload) public Employee(){} public Employee(int empno, String ename) { this.empno = empno; this.ename = ename; } public void printInfo() { System.out.printf("사번:%d %n",this.empno); System.out.printf("이름:%s %n",this.ename); } // getters & setters public int getEmpno() { return empno; } public void setEmpno(int empno) { this.empno = empno; } public String getEname() { return ename; } public void setEname(String ename) { this.ename = ename; } public Date getHiredate() { return hiredate; } public void setHiredate(Date hiredate) { this.hiredate = hiredate; } public int getDeptno() { return deptno; } public void setDeptno(int deptno) { this.deptno = deptno; } public int getSal() { return sal; } public void setSal(int sal) { this.sal = sal; } } class Manager extends Employee { private String field; public Manager(){} public Manager(int empno, String ename, String field) { super(empno,ename); this.field = field; } public String getField() { return field; } public void setField(String field) { this.field = field; } }
객체의 형변환시 오류가 발생하는 경우
public static void main(String[] args) { Employee e = new Employee(20001021, "SMITH"); // 묵시적 형변환의 경우, 컴파일 오류 //Manager m = e; // 사원은 과장이다(안됨): 컴파일 오류 (묵시적 형변환, Implicit Type Casting) // 명시적 형변환, 컴파일시에는 문제 없음 Manager m = (Manager)e; // 사원은 과장이다(안됨) // 실행시에 오류 발생, Employee cannot be cast to Manager m.printInfo(); // 원래 Employee 인스턴스였는데 Manager형으로 캐스팅해도 없는 것이 생성되는 것은 아니다 }
객체의 형변환과 instanceof 연산자의 사용
public static void main(String[] args) { Employee e = new Employee(20001021, "SMITH"); if(e instanceof Employee){ // true System.out.println("1. e는 Employee의 인스턴스이다 "); } if(e instanceof Manager){ // false System.out.println("2. e는 Manager의 인스턴스이다 "); } Employee e2 = new Manager(20101516, "홍길동", "개발팀"); if(e2 instanceof Employee){ // true System.out.println("3. e2은 Employee의 인스턴스이다 "); } if(e2 instanceof Manager){ // true System.out.println("4. e2는 Manager의 인스턴스이다 "); } // '사원은 과장이다'는 틀린 말이므로 아래의 코드는 컴파일 오류를 발생한다 // 그러나 e2의 실체는 Manager이므로 이 경우 강제 형변환(명시적 형변환, Explicit Type Casting)해도 된다 //Manager m = e2; // '사원은 과장이다' 이말은 성립하지 않지만 // e2의 실체는 Manager이므로 Manager로 명시적 형변환해도 컴파일시나 실행시에 아무런 문제가 없다 Manager m = (Manager)e2; m.printInfo(); }