자바(8) - 상속과 다형성
2021년08월05일상속
객체 지향 프로그래밍의 중요한 특징 중 하나가 상속(inheritance)이다. 상속은 우리 일상생활에서 흔히 부모님의 재산을 물려받을 때 많이 쓰이는 용어이다. 자바에서의 상속도 마찬가지로 상위 클래스의 내용을 물려 받는다는 의미로 상속이 사용된다.
문법으로는 아래와 같다. extends는 연장, 확장하다의 의미로 Mammal 클래스가 가진 속성이나 기능을 추가 확장하여 Huamn 클래스를 구현한다는 뜻이다.
class Mammal {
private int age;
String name;
void walk() {}
}
class Dog extend Mammal {
void bark() {}
}
class Human extends Mammal {
void programming() {}
}
포유류들의 공통 기능으로 나이와 이름 또 걷는 메서드가 있다. 이런 속성들은 상속받고, Dog 클래스만의 기능인 짖는 메서드를 구현해주고, 사람의 고유 기능인 프로그래밍 메서드를 지어주면 된다.
그런데 위 Mammal에서 int age를 private으로 선언해 주었다. private의 범위는 클래스내에서만 이기 때문에 상속받은 클래스들이 위 멤버 변수를 활용 못한다. 이럴 때는 우리가 이전에 배웠던 protected
로 선언해주면
상속받는 클래스들도 문제없이 사용할 수 있다.
상속받은 클래스의 인스턴스를 생성하면 상위 클래스의 생성자가 먼저 호출된다. 그렇게 하여 상위 클래스의 멤버 변수가 메모리에 생성되는 것이다.
super
super예약어는 하위 클래스에서 상위 클래스로 접근할 때 사용한다. 하위 클래스는 상위 클래스의 주소, 즉 참조 값을 알고 있다. 이 참조 값을 가지고 있는 예약어가 바로 super이다. 반대로 this는 자기 자신의 참조 값을 가지고 있다 알면 된다. 기본적으로 하위 클래스의 생성자가 실행될 때 상위 클래스의 생성자가 실행된다 했는데, 이것은 컴파일러에서 자동으로 super()를 호출해 주기 때문이다. 이는 상위 클래스의 디폴트 생성자를 호출하는 것이다.
Implicit super consturctor 클래스() is undefined. Must explicitly invoke another consturcotr. 이 에러는 묵시적으로 호출될 디폴트 생성자가 정의되지 않았기 때문에, 반드시 명시적으로 다른 생성자를 호출해야 한다는 뜻이다.
super를 활용하면 상위 클래스의 멤버 변수나 메서드를 하위 클래스에서 참조할 수 있다.
public String showVIPInfo() {
return super.showCustomerInfo() + "담당 상담원의 아이디는 " + agentID + "입니다.";
}
이런식으로 하면 상위 클래스의 showCustomerInfo메섣드를 호출해서 사용하는 것이다.
상위 클래스로 묵시적 클래스 형 변환
상속에서 중요하게 다뤄야 할 부분이 클래스 간의 형 변환이다. Customer와 VIPCustomer의 관계를 봐보면, 개념적인 면에서는 Customer가 일반적인 개념이고, 기능적인 면에서는 VIPCustomer가 더 많다. 왜냐하면 상속받은 클래스는 상위 클래스 기능을 모두 사용할 수 있고 추가로 더 많은 기능을 구현했기 때문이다. 따라서 VIPCustomer는 VIPCustomer형이면서 Customer형이기도 한다. 그렇기에 VIPCustomer클래스로 인스턴스를 생성할 때 이 인스턴스의 자료형을 Customer형으로 클래스 형 변환하여 선언할 수 있다. 하지만 반대로 Customer 인스턴스를 생성할 때 VIPCustomer형으로 선언할 수 없다. 즉 반대로는 불가하다. 그런데 VIPCustomer로 new 해서 만들면 인스턴스가 만들어져 메모리에 변수하고 올라갈 것이다. 그런데 클래스의 자료형이 Customer로 한정되어서 Customer의 멤버 변수와 메서드만 접근할 수 있게 된다.
//상위 클래스형 하위 클래스형
Customer vc = new VIPCustomer();
책을 참고한 거라 코드 필요한 분은 Do it 자바 검색히서 봐보길 바란다.
메서드 오버라이딩
오버라이딩이란 상위 클래스에서 이미 정의된 내용을 현재 클래스에 맞게 재정의함을 의미한다. 오버라이딩을 하려면 반환형, 메서드 이름, 매개 변수 개수, 매개변수 자료형이 반드시 같아야 한다. 그렇지 않으면 자바 컴파일러는 재정의한 메서드를 기존 메서드와 다른 메서드로 인식한다.
// 기존 메서드
public int calcPrice(int price) {
bonusPoint += price * bonusRatio;
return price;
}
// 오버라이드한 메서드. 반환형, 이름, 매개변수 개수, 자료형이 같다.
@Override
public int calcPrice(int price) {
bonusPoint += price * bonusRatio;
return price - (int)(price * saleRatio);
}
@Override는 어노테이션이라고 컴파일러에게 알려주는 주석같은 것이다. 나중에 스프링 부트를 보면 알지만 이 어노테이션가지고 많은 활용을 한다. 얼른 공부해서 이해해서 써보고 싶다..!
묵시적 클래스 형 변환과 메서드 재정의
다음 코드를 봐보자.
Customer vc = new VIPCustomer("10030", "seokju", 2000);
vc.calcPrice(10000);
이 코드는 묵시적 형 변환이 일어나 VIPCustomer가 Customer형으로 변환이 되었다. 현재 calcPrice는 Customer와 VIPCustomer에 둘다 있는데 어느께 실행될까? 정답은 VIPCustomer의 메서드가 실행된다. 상속에서 상위 클래스와 하위 클래스에 같은 이름의 메서드가 존재할 때 호출되는 메서드는 인스턴스에 따라 결정된다. 즉 선언한 클래스형이 아닌 생성되니 인스턴스의 메서드가 호출되는 것이다. 이것이 가능한 이유는 가상 메서드
덕분이다.
가상 메서드
자바의 클래스는 멤버 변수와 메서드로 이루어져 있다. 클래스를 생성하여 인슨턴스가 만들어지면 멤버 변수는 힙 메모리에 위치한다. 그런데 메서드는 다르다. 변수는 인스턴스마다 갖는 값이 다르기에 인스턴스가 만들어질 때마다 새로 생성되지만, 메서드는 실행해야 할 명령 집합이기에 인스턴스가 달라도 같은 로직을 수행한다. 다시 말해 메서드는 인스턴스가 생성될 때마다 만들어지는 것이 아니다. 메서드는 메서드 영역에 만들어져 메서드를 호출하면 그 영역의 주소를 참조하여 명령이 실행되는 것이다. 이를 가상 메서드 테이블에서 관리하게 되고 재정의가 된 메서드는 새로운 주소에 위치하고 참조하게 되는 것이다.
다형성
묵시적 클래스 형 변환과 가상 메서드를 바탕으로 객체 지향의 중요한 특성인 다형성(polymorphism)을 알아보자. 다형성이란 하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 말한다. 다형성은 추상 클래스, 인터페이스에서 구현되며 안드로이드, 스프링 등 자바 기반의 프레임워크에서 응용하는 객체 지향 프로그램에서 중요한 개념이다. 동물 클래스가 있고 사람, 호랑이, 독수리 클래스를 만들어 보자.
class Animal {
public void move() {
System.out.println("동물이 움직입니다.");
}
}
class Human extends Animal {
public void move() {
System.out.println("사람이 두 발로 걷습니다.");
}
}
class Tiger extends Animal {
public void move() {
System.out.println("호랑이가 네 발로 움직입니다.");
}
}
class Eagle extends Animal {
public void move() {
System.out.println("독수리가 하늘을 납니다.");
}
}
public class AnimalTest {
public static void main(String[] args) {
AnimalTest aTest = new AnimalTest();
aTest.moveAnimal(new Human());
aTest.moveAnimal(new Tiger());
aTest.moveAnimal(new Eagle());
}
public void moveAnimal(Animal animal) {
animal.move();
}
}
결과
사람이 두 발로 걷습니다. 호랑이가 네 발로 움직입니다. 독수리가 하늘을 납니다.
moveAnimal 메서드의 매개변수를 보면 Animal임을 볼 수 있다. 그런데도 동작이 잘 되는 이유는 사람, 호랑이, 독수리 모두 Animal 클래스를 상속받았기에 묵시적 형 변환이 일어나는 것이다. 그리고 가상 메서드의 원리에 따라 각 인스턴스의 move() 메서드가 호출된 것이다. 이처럼 어떤 매개변수가 넘어왔느냐에 따라 출력문이 달라지는 효과를 다형성이라 볼 수 있다. 이렇듯 다형성을 활용하면 코드 양도 줄어들고 유지보수도 편리해진다. 또한 확장성도 넓어진다. 이렇게 다형성을 쓰면서 상속이라는 것이 좋아 보이지만 무조건적으로만 쓰면 안된다. 클래스간의 관계를 잘 살펴보아 따져야한다. IS-A와 HAS-A 관계가 있는데, IS-A(is a relationship; inheritance)관계는 일반적인 개념과 추제적인 개념의 관계이다. 가령 ‘사람은 표유류이다’와 같은 관계다. 이런 관계일 때는 상속을 사용하는 것이 좋다. HAS-A(has a relationship; association) 관계는 한 클래스가 다른 클래스를 소유한 관계이다. 예로 학생 클래스와 과목 클래스가 있으면 학생이 여러 과목을 가지는 형태 같은게 되는 것이다. 이런 경우는 멤버 변수같은 것으로 선언하는 것이 적절하다.
다운 캐스팅과 instanceof
위에서 상위 클래스로 형 변환이 묵시적으로 이루어지는 과정을 보았는데, 이번에는 명시적으로 하위 클래스로 형 변환을 하는 것을 알아보겠다. Animal ani = new Human();
같은 코드가 있다 보자. 이렇게 Animal형으로 형 변환이 이루어진 경우에는 Animal 클래스에서 선언한 메서드와 멤버 변수만 사용할 수 있다. 이러면 기껏 만들어둔 메서드와 변수를 못쓰게 되기에 원래 자료형으로 돌릴 필요가 있다. 이렇게 형 변환하는 것을 다운 캐스팅이라고 한다.
instanceof
상속 관계를 보면 모든 인간은 동물이지만 모든 동물이 인간은 아니다. 따라서 다운 캐스팅을 하기 이전에 형 변환된 인스턴스의 원래 자료형을 확인해야 오류를 막을 수 있다. 이때 사용하는 예약어가 instanceof이다. 아래와 같이 사용하면 된다.
Animal hAnimal = new Human();
if(hAnimal isinstanceof Human) { // hAnimal 인스턴스 자료형이 Human이면
Human human = (Human)hAnimal; // Human형으로 다운 캐스팅
}
상위 클래스는 묵시적으로 형 변환이 되지만, 하위 클래스로 형 변환을 할 때는 명시적으로 해야 한다.