본문 바로가기

JAVA/JAVA 이론

[JAVA 이론] 상속(inheritance)

[JAVA 심화]

1. 상속(Inheritance)

2. 캡슐화(encapsulation)

3. 다형성(Polymorphism)

4. 추상화(Abstraction)


개요

자바 심화에서는 자바의 4가지 주요 원칙인 상속성, 캡슐화, 다형성, 추상화에 대해 공부하고자 합니다.

이 4가지의 핵심 원리는 자바뿐만이 아니라 객체 지향 프로그래밍 설계를 지향하는 모든 언어에 공통적으로 적용되는 객체지향의 핵심 중추이자 기둥이라 할 수 있습니다.

자바의 객체지향 프로그래밍의 핵심 기둥 네 가지 중 우리가 첫 번째로 공부하게 될 내용은 상속입니다.


학습 목표

  • 상위 클래스-하위 클래스의 상속 관계의 핵심을 이해하고, 그 장점을 설명할 수 있다.
  • extends 키워드를 사용하여 두 개 이상의 클래스 간 상속 관계를 정의할 수 있다.
  • 포함관계와 상속관계의 차이를 설명할 수 있다
  • 상속 관계에서 사용할 수 있는 메서드 오버라이딩의 정의, 성립 조건, 장점을 이해한다.
  • super와 super()의 차이를 설명할 수 있다.
  • Object 클래스가 자바 클래스의 상속계층도에서 최상단에 위치한다는 사실을 이해한다.

상속

자바 언어에서 상속이란 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소를 의미합니다.

 

가장 단순한 형태는 두 클래스를 상위 클래스와 하위 클래스로 나누어 상위 클래스의 멤버(필드, 메서드, 이너 클래스)를 하위 클래스와 공유하는 형태입니다.

위 두 클래스를 서로 상속관계에 있다고 하며, 하위 클래스는 상위 클래스가 가진 모든 멤버를 상속받게 됩니다.

즉, 하위 클래스의 멤버 개수는 상위 클래스의 멤버 개수와 같거나 많습니다.

두 클래스 간 상속 관계를 설정할 때는 extends 키워드를 사용합니다.

위의 그림에는 총 4개의 클래스가 정의되어 있습니다.

각각 Person, Programmer, Dancer, Singer 클래스에 대한 변수와 메서드가 정의되어 있는 것을 확인할 수 있습니다.

모든 클래스는 name, age, eat(), walk()가 공통적으로 정의되어 있음을 알 수 있습니다.

그러나, 아래의 세 클래스는 각각 직업에 맞는 변수와 메서드가 추가적으로 정의되어 있음을 알 수 있습니다.

즉, Person 클래스가 상위 클래스, 그리고 Programmer, Dancer, Singer 클래스가 상위 클래스로부터 특정한 속성과 기능을 내려받는(또는 확장된) 하위 클래스가 됩니다.

결론적으로 상속을 통해 클래스를 작성하면 위의 그림 처럼 코드를 재사용하여 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있어 코드의 중복을 제거할 수 있습니다.


코드 예제

코드를 통해서 앞서 설명한 상속의 개념에 대해 더 자세히 알아봅시다.

클래스를 상속할 때에는 extends 키워드를 사용하며, 클래스명 다음에 extends 상위 클래스명을 사용하여 정의합니다.

 

class Person {
	String name;
    String age;
    
    void eat(){
    	System.out.println("먹습니다.");
    }
    
    void walk(){
    	System.out.println("걷습니다.");
    }
}

class Programmer extends Person {
	String companyName;
    
    void coding(){
    	System.out.println("코딩을 합니다.");
    }
}

Class Dancer extends Person {
	String groupName;
    
    void dancing(){
    	System.out.println("춤을 춥니다");
    }
}

Calss Singer extends Person {
	String bandName;
    
    void singing(){
    	System.out.println("노래를 부릅니다.");
    }
}

public class Test{
	public static void main(String[] args){
    	Person man = new Person();
        man.name = "놈";
        man.age = "30";
        man.eat();
        man.walk();
        System.out.println(man.name);
        
        Programmer developingMan = new Programmer();
        developingMan.name = "개발하는놈";
        developingMan.age = "31";
        developingMan.eat();
        developingMan.coding();
        System.out.println(developingMan.name);
    }
}

// 출력 결과
먹습니다.
걷습니다.
놈
먹습니다.
코딩을 합니다.
개발하는놈

위의 예제를 보면 Person 클래스로부터 Programmer, Dancer, Singer 클래스가 확장되어 Person 클래스에 있는 속성과 기능들을 사용할 수 있는 것을 확인할 수 있습니다.

또한 각각의 클래스의 개별적인 속성과 기능들은 객체 생성 이후 개별적으로 정의해 주었습니다.

만약 상속이 없었더라면 일일이 속성과 기능들을 선언해주어야 했을 것이고, 이는 같은 코드를 중복 작성해야 하는 번거로운 상황이 생깁니다.

 

마지막으로 자바의 객체지향 프로그래밍에서는 단일 상속(single inheritance)만을 허용합니다.

이는 다른 객체지향 언어와 구분되는 자바 객체지향 프로그래밍의 특징입니다. 다만 자바에서도 인터페이스(interface)라는 문법 요소를 통해 다중 상속과 비슷한 효과를 낼 수 있는 방법이 존재합니다.


포함 관계

포함(composite)은 상속처럼 클래스를 재사용할 수 있는 방법으로, 클래스의 멤버로 다른 클래스 타입의 참조변수를 선언하는 것을 의미합니다.

코드 예제를 보면서 확인해 봅시다.

public class Employee {
	int id;
    String name;
    Address address;
    
	public Employee(int id, String name, Address address){
  		this.id = id;
        this.name = name;
        this.address = address;
    }
    
    void showInfo() {
    	System.out.println(id + " " + name);
        System.out.println(address.city + " " + address.country);
    }
    
    public static void main(String[] args) {
    	Address address1 = new Address("서울","한국");
    	Address address2 = new Address("도쿄","일본");
        
        Employee e1 = new Employee(1, "이놈", address1);
        Employee e2 = new Employee(2, "저놈", address2);
        
        e1.showInfo();
        2e.showInfo();
    }
}
class Address {
	String city, country;
    
    public Address(String city, String country) {
    	this.city = city;
        this.country = country;
    }
}

// 출력 결과
1. 이놈
서울 한국
2. 저놈
도쿄 일본

위의 코드 예제를 보면, 근로자(Employee)를 표현하기 위한 Employee 클래스의 멤버 변수로 근로자가 사는 개략적인 주소를 나타내는 Address 타입의 변수가 정의되어 있습니다.

이러한 방법을 몰랐다면, Address 클래스의 포함되어 있는 인스턴스 변수 city와 country를 각각 Employee 클래스의 변수로 정의해주어야 하지만, Address 클래스로 해당 변수들을 묶어준다음 Employee 클래스 안에 참조변수를 선언하는 방법으로 코드의 중복을 없애고 포함 관계로 재사용하고 있습니다.

 

그렇다면 클래스 간에 관계를 설정할 때 상속 관계를 맺어 줄 것인지 포함 관계를 맺어 줄 것인지를 어떤 기준으로 판별할 수 있을까요?

이는 클래스 간의 관계가 '~은 ~이다(~IS ~A)' 관계인지 ~은 ~을 가지고 있다(HAS~A) 관계인지 문장을 만들어 생각해 보는 것입니다. 

 

위의 코드 예제로 예를 든다면 근로자는 주소이다.라는 문장은 성립하지 않지만 근로자는 주소를 가지고 있다. 는 어색하지 않은 올바른 문장임을 알 수 있습니다. 그렇기에 위의 예제는 상속보다는 포함 관계가 적절합니다.


메서드 오버라이딩

메서드 오버라이딩(Method Overriding)은 상위 클래스로부터 상속받은 메서드와 동일한 이름의 메서드를 재정의하는 것을 의미합니다.

코드 예제를 보면서 확인해 봅시다.

public class Main {
	public static void main(String[] args){
    	Programmer p = new Programmer();
        Dancer d = new Dancer();
        Singer s = new Singer();
        
        p.run();
        d.run();
        s.run();
    }
}

class Person {
	void run() {
    	System.out.println("person is running");
    }
}

class Programmer extends Person {
	void run() {
    	System.out.println("programmer is running");
    }
}

class Dancer extends Person {
	void run() {
    	System.out.println("dancer is running");
    }
}

class Singer extends Person {
	void run() {
    	System.out.println("singer is running");
    }
}

//출력 결과

programmer is running
dancer is running
singer is running

코드 예제를 보면 Person 클래스에서 run() 메서드가 정의되어 있지만 그를 상속받은 Programmer, Dancer, Singer 클래스에서 이를 재정의함으로써 Person 클래스의 run() 메서드를 오버라이딩 하고 있습니다.

따라서 Programmer, Dancer, Singer의 인스턴스를 통해 run() 메서드를 호출한다면 Person class의 run() 메서드가 아닌 각각 오버라이딩한 메서드가 호출됩니다.

이처럼 메서드 오버라이딩은 상위 클래스에 정의된 메서드를 하위 클래스에서 하위 클래스에 맞게 메서드의 동작을 변경하고자 할 때 사용합니다.

상위 클래스의 메서드를 오버라이딩하려면 다음의 세 가지 조건을 반드시 만족시켜야 합니다.

  1. 메서드의 선언부(메서드 이름, 매개변수, 반환타입)가 상위클래스의 메서드와 완전히 일치하여야 한다.
  2. 접근 제어자의 범위가 상위 클래스의 메서드보다 같거나 넓어야 한다.
  3. 예외는 상위 클래스의 메서드보다 많이 선언할 수 없다.

super 키워드와 super()

super 키워드와 super() 메서드는 생김새는 유사하지만 다른 용도로 사용이 됩니다. 먼저 super 키워드는 상위 클래스의 객체를 호출합니다. 그리고 super() 메서드는 상위 클래스의 생성자를 호출하는 것을 의미합니다.

공통적으로 모두 상위 클래스의 존재를 상정하며, 상속 관계를 전제로 합니다.

먼저 super 키워드를 아래의 예제를 통해 알아보도록 합니다.

public class Example {
	pubic static void main(String[] args){
    	SubClass sc = new SubClass();
        sc.callNum();
    }
}

class SuperClass {
	int count = 10;  //super.count
}

class subClass extends SuperClass {
	int count = 15; // this.count
    
    void claaNum() {
    	System.out.println("count = " + count);
        System.out.println("this.count = " + this.count);
        System.out.println("super.count =" + super.count);
}

//출력 결과
count = 15
count = 15
count = 10

 

위의 예제에서 하위 클래스는 상위 클래스로 변수 count를 상속받는데, 공교롭게도 자신의 인스턴 슈변수 count와 이름이 같아 둘을 구별할 필요가 있습니다.

이런 경우, 두 개의 같은 이름의 변수를 구분하기 위한 방법이 바로 super 키워드입니다.

만약 super 키워드를 붙이지 않는다면, 자바 컴파일러는 해당 객체는 자신이 속한 인스턴스 객체를 먼저 참조합니다.

반면 경우에 따라서 상위 클래스의 변수를 참조해야 할 경우가 종종 있는대 그 경우 super 키워드를 사용하면 상위 객체의 멤버 값을 참조할 수 있습니다.

즉, 상위 클래스의 멤버와 자신의 멤버를 구별하는 데 사용된다는 점을 제외한다면 this와 super는 기본적으로 같은 것이라 말할 수 있습니다.

 

다음으로 spuer() 메서드를 코드 예제를 통해 확인해 보도록 하겠습니다

 

public class Test {
	public static void main(String[] args){
    	Programmer p = new Programmer();
    }
}

class Person {
	Person() {
    	System.out.println("Person class constructor");
    }
}

class Programmer extends Person { //Inheritance by Person class
	Programmer() {	
    	super(); //Call constructor of Person class  
    	System.out.println("Programmer class constructor");
    }	
}

//output

Person class constructor
Programmer class constructor

 

Person class를 상속받아 Programmer 클래스를 만들고, Programmer 생성자를 통해 상위 클래스인 Person 클래스의 생성자를 호출하고 있습니다.

super() 메서드 또한 this()와 마찬가지로 생성자 안에서만 사용가능하고, 반드시 첫 줄에 와야 합니다.

만약 super()가 없는 경우 컴파일러가 생성자의 첫 줄에 자동으로 super()를 삽입합니다.

이때 상위 클래스에 기본생서자가 없으면 에러가 발생하게 됩니다.

따라서 클래스를 만들 때는 자동으로 기본 생성자를 생성하는 것을 습관화하는 것이 좋습니다.

 


Object 클래스

Object Class는 자바의 클래스 상속계층도에서 최상위에 위치한 상위클래스입니다. 따라서 자바의 모든 클래스는 Object 클래스로부터 확장됩니다.

자바 컴파일러는 컴파일 과정에서 다른 클래스로부터 아무런 상속을 받지 않는 클래스에 자동적으로 extends Object를 추가하여 Object 클래스를 상속받도록 합니다.

아래의 예시 코드를 보겠습니다.

class ParentEx { // 자바 컴파일러가 "extends Object" 자동 추가
}

class childEx extends ParantEx{

}

위의 예시 코드에서, ParentEx  클래스를 상속받아 childEx 클래스를 만들었을 때 상위 클래스 ParentEx는 Object 클래스를 컴파일러가 자동으로 상속받도록 합니다.

 

Object 클래스는 자바 클래스의 상속계층도에 가장 위에 위치하기 때문에 Object 클래스의 멤버들을 자동으로 상속받아 사용할 수 있습니다.

 

아래의 표를 통해 Object 클래스의 대표적인 메서드를 확인할 수 있습니다.

메서드명 반환 타입 description
toString() String 객체 정보를 문자열로 출력
equals(Object obj) boolean 등가 비교 연산(==)과 동일하게 스택 메모리값을 비교
hashCode() int 객체 위치 정보 관련. Hashtable 도는 HashMap에서 동일 객체여부 판단
wait() void 현재 쓰레드 일시 정지
notify() void 일시 정지 중인 쓰레드 재 동작

이번 글을 통하여 자바 객체 지향 프로그래밍에서 4가지 주요 특성 중 속성에 대하여 공부하여 보았습니다.

 

다음 글을 통하여 캡슐화에 대한 내용을 학습할 예정입니다.

==> [자바 심화] 캡슐화(Encapsulation)