Last Modified : 2012.01.28

상속

상속을 이용하면 현실세계와 같이 계층적으로 클래스를 만들 수 있다.
수퍼클래스는 자신의 구현내용을 서브 클래스에 물려주고, 서브클래스는 수퍼클래스로부터 구현내용을 상속받는다.

  • 수퍼(super)클래스 : 구현내용을 서브 클래스에 물려주는 클래스, 부모 클래스 라고도 한다.
  • 서브(sub)클래스 : 수퍼클래스로부터 구현내용을 상속받는 클래스, 자식 클래스 라고도 한다.

사람 > 직원 > 관리자

먼저 상속관계가 아닌 예부터 살펴보자.
다음은 사원(Employee) 클래스와 관리자(Manager) 클래스이다.

Employee.java

package net.java_school.example;

public class Employee {
	private String name;
	private String position;
	private String telephone;
	
    public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getPosition() {
		return position;
	}

	public void setPosition(String position) {
		this.position = position;
	}

	public String getTelephone() {
		return telephone;
	}

	public void setTelephone(String telephone) {
		this.telephone = telephone;
	}

	public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append(name);
        sb.append(" ");
        sb.append(position);
        sb.append(" ");
        sb.append(telephone);

        return sb.toString();
    }
	
}

Manager.java

package net.java_school.example;

public class Manager {
	private String name;
	private String position;
	private String telephone;
	private String manageJob;
	
    public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getPosition() {
		return position;
	}

	public void setPosition(String position) {
		this.position = position;
	}

	public String getTelephone() {
		return telephone;
	}

	public void setTelephone(String telephone) {
		this.telephone = telephone;
	}

	public String getManageJob() {
		return manageJob;
	}

	public void setManageJob(String manageJob) {
		this.manageJob = manageJob;
	}

	public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append(name);
        sb.append(" ");
        sb.append(position);
        sb.append(" ");
        sb.append(telephone);
        sb.append(" ");
        sb.append(manageJob);

        return sb.toString();
    }
	
}

예제를 테스트하기 위한 클래스를 다음과 같이 작성한다.

Test.java

package net.java_school.example;

public class Test {
	public static void main(String[] args) {
		Employee im = new Employee();
		im.setName("임꺽정");
		im.setPosition("대리");
		im.setTelephone("ext:19");
		System.out.println(im.toString());	
	
		Manager hong = new Manager();
		hong.setName("홍길동");
		hong.setPosition("과장");
		hong.setTelephone("ext:3");
		hong.setManageJob("프로젝트관리");
		System.out.println(hong);
	}	
}

Employee, Manager 클래스를 보면 코드가 중복되는 부분이 있다.
수퍼 클래스를 만들 수 있느지 살펴보자.
먼저 "~이다." 관계가 적용되는지 확인한다.
"관리자는 사원이다." 는 말이 된다.
따라서 사원을 수퍼클래스로 관리자를 서브클래스로 상속을 적용할 수 있다.
사원이 관리자 보다 넓은 개념이기 때문에 사원이 수퍼클래스, 관리자가 서브클래스가 되는 것이다.
사원클래스를 상속하는 것으로 관리자 클래스는 바꾸면 다음과 같다.

Manager.java

package net.java_school.example;

public class Manager extends Employee {
	private String manageJob;
	
	public String getManageJob() {
		return manageJob;
	}

	public void setManageJob(String manageJob) {
		this.manageJob = manageJob;
	}

	public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append(this.getName());
        sb.append(" ");
        sb.append(this.getPosition());
        sb.append(" ");
        sb.append(this.getTelephone());
        sb.append(" ");
        sb.append(manageJob);

        return sb.toString();
    }
	
}

관리자클래스가 사원클래스의 상속을 받기 위해선 관리자클래스의 선언부에 extends 키워드가 사용하면 된다.
이제 관리자클래스는 Employee클래스가 가지고 있는 필드와 메소드를 마치 자기의 멤버처럼 쓸 수 있게 된다.
여기서 멤버라 함은 클래스 바디에 선언한 인스턴스 변수와 메소드를 말한다.
하지만 자신의 멤버는 아니다. 상속관계에서도 여전히 접근자는 적용된다.
관리자클래스에서 toString()메소드에서 this.getName(), this.getPosition(), this.getTelephone() 을 써야만 한 이유는 수퍼클래스인 사원클래스의 인스턴스변수 name, position, telephone 의 접근자가 private 이기 때문이다.
만약 사원클래스의 인스턴스변수 name, position, telephone 에 바로 접근할 수 있게 하려면 접근자를 변경해야 하다.
사원클래스와 관리자클래스가 같은 팩키지에 있으므로 사원클래스의 name,position,telephone 에 디폴트 접근자 이상으로 변경하면 된다.
Test 클래스 실행하여 결과를 확인한다.

메소드 오버라이딩(Overriding)

부모로부터 받은 메소드 그대로 사용할 수 있지만, 원한다면 "재정의(Override)"해서 사용하는 것을 메소드 오버라이딩 이라고 한다.
이때 리턴 타입, 메소드명, 아규먼트 리스트는 똑같아야 한다.
위 예에서는 Manager 클래스의 toString() 메소드는 Employee 클래스의 toString()를 오버라이딩 했다.

생성자(Constructor)

Manager hong = new Manager(); 는 Manager 객체를 생성하는 코드이다.
이제 이 코드부분에 대해서 좀 더 자세히 말할 때가 되었다.
new 다음에 오는 Manager() 는 메소드 모양을 취하고 있는데 Manager() 이라는 생성자라고 호출하는 코드이다.
이때 Manager() 이라는 생성자는 메소드 경우와 마찬가지로 반드시 클래스에 선언되어 있어야 한다.
즉, new 다음에는 클래스에 선언된 생성자 중에 하나를 호출할 수 있다.
지금까지 우리는 생성자를 만들지 않았다. 그런데도 지금까지 우리의 예제는 생성자가 호출되었기 때문에 실행이 되었던 것이다.
구현하지도 않는 생성자가 어떻게 호출될수 있을까?
답은 컴파일러가 만들어 줬다는 것이다.
여러분이 클래스를 작성하면서 아무런 생성자도 만들지 않았다면 컴파일러가 컴파일할 때 아규먼트가 없고 내용이 빈 생성자를 만들어 컴파일한다.
컴파일러가 자동으로 생성해 준 생성자를 "디폴트 생성자"(default constructor)라고 한다.
만일 여러분이 명시적으로 생성자를 하나라도 만들었다면 컴파일러는 "디폴트 생성자"를 만들지 않는다.
생성자는 다양한 아규먼트 리스트를 가진 생성자가 여러개 만들 수 있다.
생성자는 "객체가 생성된 후" 맨 처음 호출되고 다시는 호출되지 않는다.
"객체가 생성된 후" 란 말에 주목해야 한다.
생성자라는 이름 때문에 생성자가 호출되어야 객체가 생성되는 것이라고 생각하면 오해다.
new 란 키워드에 의해서 힙메모리에 객체를 위한 공간이 할당되고 인스턴스 변수의 값이 초기화된다.
그 다음 new 다음에 있는 생성자가 호출된다.
생성자가 에러없이 마치면 참조형 변수에 생성된 객체를 참조할 수 있는 참조값이 반환된다.
즉, 생성자에 에러가 있다면 그 객체는 사용할 수 없다.

생성자는 객체가 생성 후 자동적으로 호출되므로 객체 초기화나 그 외 초기화와 관련된 작업만을 하도록 작성하는 것이 좋다.
생성자 구현부에 메소드 호출하는 코드가 있다면 좋은 코드가 아닌 경우가 많다.
생성자를 작성할 때 주의해야 할 점은
생성자는 리턴 타입이 없고 생성자명은 클래스명과 같아야 한다는 것이다.
많이 하는 실수 중 하나가 생성자명 앞에 void 를 붙이는 경우이다.
이것은 메소드이지 생성자가 아니다.

이제 상속과 관련된 내용을 다루겠다.
서브 클래스는 수퍼 클래스의 멤버(인스턴스 변수와 메소드)를 상속받는다고 했다.
그럼 생성자는 상속될까?
생성자는 상속되지 않는다.
또한 서브 클래스 생성자 구현부 첫 라인은 반드시 수퍼 클래스의 생성자 중 하나를 호출하는 코드가 있어야 한다. 만약 없다면 컴파일러가 수퍼 클래스의 디폴트 생성자를 호출하는 코드를 첫 라인에 집어넣어 컴파일한다.
지금까지의 내용을 종합하여 Employee클래스와 Manager클래스에 생성자를 추가한다.

Employee.java 에 생성자 추가

	public Employee() {} // 디폴트 생성자
	
	public Employee(String name, String position, String telephone) {
		this.name = name;
		this.position = position;
		this.telephone = telephone;
	}

Manager.java 에 생성자 추가

	public Manager() {} // 디폴트 생성자
	
	public Manager(String name, String position, String telephone, String manageJob) {
		super(name, position, telephone);
		this.manageJob = manageJob;
	}

Test.java 의 메인메소드 구현부 변경

	Employee im = new Employee("임꺽정", "대리", "ext:19");
	System.out.println(im.toString());	

	Manager hong = new Manager("홍길동", "과장", "ext:3", "프로젝트관리");
	System.out.println(hong);

this 키워드

this에는 객체 자신의 참조값을 가지고 있다.
생성자 안에서 다른 생성자를 호출하거나 메소드 안에서 인스턴스 변수와 매개변수를 구별하는데 사용된다.

super 키워드

this와 대비되는 키워드이다.
자식 객체에서 super는 부모 객체에 대한 참조값을 가지고 있다.
자식의 생성자에서 부모의 생성자를 호출하거나 오버라이딩한 멤버가 아닌 부모의 멤버를 접근해야 할 때 사용한다.

상속을 이용하지 않고 클래스를 작성한다 하더라도 그 클래스는 Object 클래스를 상속하게 된다.
클래스들간의 계층관계에서 최상위에 있는 클래스가 바로 Object클래스이다.
위 예에서 사원클래스의 수퍼클래스는 Object 클래스이다.

부모 클래스 타입의 참조형 변수에 자식 클래스 타입의 객체 참조값을 할당할 수 있다.

모양은 같으나 다양한 형태로 실행되는 것 같은 느낌이 들도록 하는 것이 다형성이다.
부모 클래스 타입의 레퍼런스에 자식 클래스 타입의 객체의 참조값을 할당할 수 있다는 점을 이용하면 이러한 다형성을 볼 수 있다.

다형성 그림

피아노 extends 악기
피리 extends 악기
악기 a = new 피아노();
a.연주하다(); // 피아노가 연주된다.
a = new 피리();
a.연주하다(); // 피리가 연주된다.

a.연주하다();
이 코드로 피아노가 연주되고 피리가 연주되도록 할 수 있다. 이것이 다형성이다.
위의 예제는 연주하다()라는 메소드가 다형성을 가지게 된다.
연주하다() 메소드가 실행되는 시점(Runtime)에 피아노가 연주하는지, 피리가 연주하는지가 결정된다.
(컴파일시에 결정되는 것이 아니다)
그런데 부모 클래스 타입의 레퍼런스는 자식 타입의 객체를 참조할 때 자식 타입의 객체의 모든 멤버에 접근할 수 있는 것은 아니다.
자식 타입의 객체의 고유한 멤버는 접근하지 못한다.
여기서 고유한 멤버란 자식 클래스에서 선언된 인스턴스 변수와 메소드를 말한다.
자식 클래스에서 오버라이딩 한 메소드는 부모 타입의 레퍼런스로 접근할 수 있다.
즉, 부모 타입의 레퍼런스는 부모로부터 상속된 것과 부모의 메소드를 오버라이딩 한 것까지 접근할 수 있다.
예제를 통해 알아보자.
Employee 클래스와 Manager 클래스는 그대로 두고 Test 클래스의 메인 메소드에 다음을 추가한다.

Test.java 의 메인메소드 구현부 추가

Object jang = new Manager("장길산", "부장", "ext:2", "영업관리");
System.out.println(jang.toString());
//jang.setManageJob("회계관리"); //에러 Object 형으로 setManagerJob()메소드를 접근할 수 없다.
//만약 장길산 사원객체를 완전하게 사용하기를 원한다면 참조형 변수 형변환을 해야 한다.
Manager janggilsan = (Manager) jang;
janggilsan.setManageJob("회계관리");
System.out.println(jang.toString());

메소드 오버로딩 (method overloading)

아규먼트 리스트가 다르다면 똑같은 이름의 메소드를 얼마든지 만들 수 있다는 것이 메소드 오버로딩이다. 이때 반환값 타입은 메소드 오버로딩과 아무런 상관이 없다.
즉, 아규먼트 리스트가 같으면서 반환값 타입만 다르게 하여 같은 이름의 메소드를 만들 수 없다.

자바에서는 이름을 짓는 것이 중요하다.
메소드 이름만 보고도 이 메소드가 무슨 행위를 하는지 파악이 되도록 이름을 짓도록 하기 위해 프로그래머는 노력을 해야 한다. 메소드 오버로딩은 이름을 짓는데 있어서 프로그래머에게 부담을 줄여준다.

메소드 오버로딩된 메소드를 사용하는 입장에서는 모양은 같으나 다양한 형태로 실행되는 것 같다는 느낌을 받게 된다.
즉, 자바의 메소드 오버로딩은 다형성을 띠고 있다.
메소드 오버로딩의 가장 좋은 예는 System.out.println() 메소드이다.
System.out.println() 메소드를 호출하면서 인자로 뭘 대입하더라도 모두 출력되는 듯이 보인다.
사실 이것은 아규먼트 리스트를 달리해서 모든 경우의 메소드를 만들었기 때문이다.

final 키워드

  1. 클래스 선언부에 사용되면 해당 클래스를 상속하여 서브 클래스를 만들지 못하게 된다. 자바 API 의 대부분이 final 클래스이다.
  2. 메소드 선언부에 사용되면 해당 메소드는 서브 클래스에서 오버라이딩 할 수 없다.
  3. 자바에서 상수를 만들 때는 변수명 앞에 final을 붙인다.

추상클래스(Abstract Class)

클래스 선언부에 abstract가 붙인 클래스를 추상 클래스라고 한다.
추상 클래스는 일반적인 클래스와 달리 new 라는 키워드를 사용해서 객체화 할 수 없다.
추상클래스를 이해하기 위해서는 먼저 추상 메소드의 의미를 알아야 한다.
추상메소드란 메소드 선언부만 있고 body,다시 말해 { 시작해서 } 로 끝나는 구현부가 없는 메소드이다.
추상메소드는 다른 메소드와 구분하기 위해 역시 abstract 붙인다.
만약 추상 메소드가 하나라도 있다면 클래스를 추상 클래스로 선언해야 한다.
추상 클래스를 사용하기 위해서는 추상 클래스를 상속한 후, 추상 메소드가 있다면 이 메소드를 구현한 온전한 서브 클래스를 만들어 사용한다.
좋은 예제는 아니더라도 지금까지의 예제를 수정하여 추상 클래스 예제를 만들어 보자.
아래처럼 추상클래스 AbstractEmployee.java 클래스를 작성한다.

AbstractEmployee.java

package net.java_school.example;

public abstract class AbstractEmployee {
	private String name;
	
	public AbstractEmployee() {}
	
	public AbstractEmployee(String name) {
		this.name = name;
	}
	
    public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
}

Employee 클래스를 다음과 같이 AbstractEmployee 추상클래스를 상속하도록 변경한다.

Employee.java

package net.java_school.example;

public class Employee extends AbstractEmployee {
	private String position;
	private String telephone;
	
	public Employee() {}
	
	public Employee(String name,String position, String telephone) {
		super(name);
		this.position = position;
		this.telephone = telephone;
	}
	
	public String getPosition() {
		return position;
	}
	public void setPosition(String position) {
		this.position = position;
	}
	public String getTelephone() {
		return telephone;
	}
	public void setTelephone(String telephone) {
		this.telephone = telephone;
	}

	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append(this.getName());
		sb.append(" ");
		sb.append(position);
		sb.append(" ");
		sb.append(telephone);
		
		return sb.toString();
	}	
	
}

Manager 클래스는 변경하지 않는다.

Manager.java

package net.java_school.example;

public class Manager extends Employee {
	private String manageJob;
	
	public Manager() {}
	
	public Manager(String name,String position, String telephone, String manageJob) {
		super(name, position, telephone);
		this.manageJob = manageJob;	
	}

	public String getManageJob() {
		return manageJob;
	}

	public void setManageJob(String manageJob) {
		this.manageJob = manageJob;
	}
	
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append(this.getName());
		sb.append(" ");
		sb.append(this.getPosition());
		sb.append(" ");
		sb.append(this.getTelephone());
		sb.append(" ");
		sb.append(manageJob);
		
		return sb.toString();
	}

}

지금까지 작성한 Test 클래스의 메인 메소드를 아래처럼 변경하여 테스트한다.
아래 부분만 변경하면 실행된다.
AbstractEmployee im = new Employee("임꺽정", "대리", "ext:19");

Test.java

AbstractEmployee im = new Employee("임꺽정", "대리", "ext:19");
System.out.println(im.toString());	

Manager hong = new Manager("홍길동", "과장", "ext:3", "프로젝트관리");
System.out.println(hong.toString());

Object jang = new Manager("장길산", "부장", "ext:2", "영업관리");
System.out.println(jang.toString());
//jang.setManageJob("회계관리"); //에러 Object 형으로 setManagerJob()메소드를 접근할 수 없다.
//만약 장길산 사원객체를 완전하게 사용하기를 원한다면 참조형 변수 형변환을 해야 한다.
Manager janggilsan = (Manager) jang;
janggilsan.setManageJob("회계관리");
System.out.println(jang.toString());	

인터페이스(interface)

클래스 선언부에 class 대신 interface 를 사용하며 모든 메소드가 추상 메소드로 구성된 것을 말한다.
모두 추상 메소드이기에 일반 메소드와 구분하기 위한 abstract 키워드는 생략한다.
인터페이스 몸체에 선언된 필드는 무조건 public static final 이다.
즉, 인터페이스에 인스턴스 변수를 선언할 수 없다.
추상클래스와 마찬가지로 인터페이스 역시 단독으로 사용할 수 없고 인터페이스를 구현한 클래스를 객체화해서 사용한다.
인터페이스를 구현한 클래스라면 해당 인터페이스의 모든 추상 메소드를 빠짐없이 구현한 온전한 클래스를 말한다.
인터페이스를 구현한 클래스의 선언부에는 extends 대신 implements 란 키워드를 사용해서 클래스가 어떤 인터페이스를 구현했는지 나타낸다.
implements 키워드 뒤에는 하나 이상의 인터페이스가 올 수 있는데 모양으로 보면 다중상속처럼 보인다.

강사가 이해하는 자바 인터페이스는 전자제품의 인터페이스와 같다.
대부분 TV 가 화면 하단에 - 음량 + 과 - 채널 + 과 같은 인터페이스를 제공한다.
전자제품이 같은 인터페이스를 채택했다는 것은 이들의 사용법이 같다는 것을 의미한다.
자바 클래스가 전자제품이라면 전자제품의 인터페이스에 해당하는 것이 자바 인터페이스이다.
브라운관 TV 에서 PDP, LCD, LED TV 로 구현내용은 달라졌지만 다행히도 인터페이스는 변경되지 않아 사용하는데 문제가 없었다.

언제 인터페이스를 사용해야 하나?

언제 상속을 사용하고 언제 인터페이스를 사용해야 하는지는 어려운 문제이다.
이 문제를 간단하게 설명하면 "~이다" 관계를 발견하면 상속을, "~이다" 관계가 아니고 클래스에 기능을 가지도록 할 때 인터페이스를 사용한다.
인터페이스는 상속처럼 계층형 관계에 제약을 받지 않는다는 점도 염두에 두자.
또한 자바상속은 단일상속만을 지원하는데 반해 인터페이스는 다중상속과도 같은 모양을 가질 수 있다는 점도 염두에 두자.
예를 들어 위 예제에서 Employee 를 상속한 Driver(운전기사)클래스를 만들었다고 가정하자.
그런데 배달맨이라는 클래스가 있는데 배달맨은 사원은 아니면서 운전기사 클래스와 같은 기능이 많다면 그 기능을 가지고 인터페이스를 만들 수 있을 것이다.
이때 배달맨과 운전기사에 상속을 적용할 수는 없다.
왜냐하면 운전기사는 배달맨의 부모클래스와 사원클래스를 동시에 부모클래스로 가지게 되므로 다중상속이 되기 때문이다.
이것이 위에서 언급한 계층형 관계의 제약 중 하나다.

Driver.java

package net.java_school.example;

public class Driver extends Employee {
	private String carNo;
	
	public Driver() {}
	
	public Driver(String name, String position, String telephone, String carNo) {
		super(name, position, telephone);
		this.carNo = carNo;
	}

	public String getCarNo() {
		return carNo;
	}

	public void setCarNo(String carNo) {
		this.carNo = carNo;
	}
	
}
Transportor.java
package net.java_school.example;

public class Transportor {
	private String carNo;
	
	public String getCarNo() {
		return carNo;
	}

	public void setCarNo(String carNo) {
		this.carNo = carNo;
	}

	
}

배달맨클래스와 운전기사 클래스에 driver() 와 transport() 기능을 가져야 한다면 다음 인터페이스로 작성해 보자.
인터페이스 이름을 Drivable 이라고 하겠다.

Drivable.java
package net.java_school.example;

public interface Drivable {
	public void drive();
	
	public void transport();

}

이제 배달맨과 운전기사가 이 인터페이스를 구현하는 것으로 변경한다.

Driver.java
package net.java_school.example;

public class Driver extends Employee implements Drivable {
	private String carNo;
	
	public Driver() {}
	
	public Driver(String name, String position, String telephone, String carNo) {
		super(name, position, telephone);
		this.carNo = carNo;
	}

	public String getCarNo() {
		return carNo;
	}

	public void setCarNo(String carNo) {
		this.carNo = carNo;
	}
	
	@Override
	public void drive() {
		System.out.println(this.getName() + " 운전한다");
	}
	
	@Override
	public void transport() {
		System.out.println(this.getName() + " 물건을 운송한다");
	}
	
}
Transportor.java
package net.java_school.example;

public class Transportor implements Drivable {
	private String carNo;
	
	public String getCarNo() {
		return carNo;
	}

	public void setCarNo(String carNo) {
		this.carNo = carNo;
	}

	@Override
	public void drive() {
		System.out.println("운전하다");
	}
	
	@Override
	public void transport() {
		System.out.println("물건을 운송하다");
	}
	
}

Test 클래스의 메인 메소드에 아래 코드조각을 추가한다.

Test.java

Drivable a = new Driver("슈마허","대리","ext:8","01거 5000");
System.out.println(a);
a.drive();
Drivable b = new Transportor();
// b.setCarNo("01거 7000"); // 컴파일 에러!
b.drive();	

인터페이스에서도 상속에서와 같이 부모 타입의 레퍼런스는 자식 타입의 객체 참조값이 대입될 수 있다는 것과 이럴 때 부모 타입의 레퍼런스는 부모 타입의 멤버(오버라이딩된 메소드는 오버라이드된것이 불린다)만 사용할 수 있다는 제약이 동일하게 적용됨을 예제를 통해 알 수 있다.