Java Category/Java

[JAVA] 상속 - 타입 변환과 다형성

ReBugs 2023. 1. 8.

본 게시글은 혼자 공부하는 자바 (저자 : 신용권)의 책과 유튜브 영상을 참고하였고, 개인적으로 정리하는 글임을 알립니다.


기본 타입(기본 자료형)의 타입 변환(형 변환)에는 자동 타입 변환이 있고, 강제 타입 변환이 있다.

클래스에도 이러한 타입 변환이 있다.

메소드 재정의(오버라이딩)와 타입 변환을 이용하면 객체 지향 프로그래밍의 매우 중요한 특징인 다형성을 구현할 수 있다.

다형성은 사용 방법은 동일하지만 다양한 객체를 이용해서 다양한 실행결과가 나오도록 하는 성질이다. 예를 들어 자동차가 타이어를 사용하는 방법은 동일하지만 어떤 타이어를 장착하느냐에 따라 주행 성능이 달라질 수 있다.

어떤 타이어는 고속주행에 유리한 타이어이고, 또 다른 타이어는 눈길에서 미끄러지지 않고 주행할 수 있는 타이어가 있듯이 말이다.

다형성을 구현하려면 오버라이딩과 타입 변환이 필요하다.

 

자동 타입 변환(Promotion)

클래스의 변환은 상속 관계에 있는 클래스 사이에서 발생한다. 자동 타입 변환은 프로그램 실행 도중에 자동적으로 타입 변환이 일어나는 것을 말하는데 자동 타입 변환은 아래와 같은 조건에서 일어난다.

Parent a = new Child();
또는
b = new Child();
parent a = b;

즉, 자식 타입 객체만이 부모타입으로 자동 타입 변환이 일어날 수 있다. Child 클래스로 부터 Child객체를 생성하고 이것을 Parent 변수에 대입하면 자동 타입 변환이 일어난다.

자동 타입 변환의 개념은 자식은 부모의 특징과 기능을 상속받기 때문에 부모와 동일하게 취급될 수 있다는 것이다. 예를 들어, 고양이가 동물의 특징과 기능을 상속받았다면 '고양이는 동물이다.'가 성립한다. 

위 그림에서 알 수 있듯이, a는 부모 타입이므로 부모 객체를 참조하는 것이 맞지 않느냐라고 생각할 수도 있지만 그렇지 않고, a와 b 모두 자식 객체를 참조한다.

또한 바로 위의 부모가 아니더라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있다.

D객체는 B와 A 타입으로 자동 타입 변환이 될 수 있고, E 객체는 C와 A 타입으로 자동 타입 변환이 될 수 있다. 그러나 D 객체는 C 타입으로 변환될 수 없고, 마찬가지로 E 객체는 B타입으로 변환될 수 없다. 상속관계가 아니기 때문이다.

package TestPackage;
class A{}

class B extends A{}
class C extends A{}

class D extends B{}
class E extends C{}

public class Test {
	public static void main(String[] args) {
		B b = new B();
		C c = new C();
		D d = new D();
		E e = new E();		
		
		A a1 = b;
		A a2 = c;
		A a3 = d;
		A a4 = e;
		
		B b1 = d;
		C c1 = e;

		B b3 = e; //상속 관계가 아니므로 컴파일 에러 발생
		C c2 = d; //상속 관계가 아니므로 컴파일 에러 발생
	}
}

 

다형성과 관련있는 중요한 자동 타입 변환의 성질
부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다. 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로만 한정된다. 그러나 예외가 있는데, 메소드가 자식 클래스에서 재정의되었다면 자식 클래스의 메소드가 대신 호출된다.

Parent.java

package TestPackage;
public class Parent {
	public void method1() {
		System.out.println("Parent-method1()");
	}
	
	public void method2() {
		System.out.println("Parent-method2()");
	}
}

Child.java

package TestPackage;
public class Child extends Parent {
	@Override
	public void method2() {
		System.out.println("Child-method2()");
	}
	
	public void method3() {
		System.out.println("Child-method3()");
	}
}

Test.java

package TestPackage;
public class Test {
	public static void main(String[] args) {
		Child child = new Child();

		  Parent parent = child;

		  parent.method1();

		  parent.method2();

		  //parent.method3();  (호출 불가능)
	}
}
/*
Parent-method1()
Child-method2()
*/

 

필드의 다형성

자동 타입 변환을 하는 이유는 다형성을 구현하기 위해서이다.

필드의 타입을 부모 타입으로 선언하면 다양한 자식 객체들이 저장될 수 있기 때문에 필드 사용 결과가 달라질 수 있다.

이것이 필드의 다형성이다.

예를 들어
자동차를 구성하는 부품은 언제든지 교체할 수 있다. 부품은 고장이 날 수도 있고 성능이 더 좋은 부품으로 교체되기도 한다. 객체지향 프로그래밍에서도 마찬가지이다.
이를 자동차 클래스에 포함된 타이어 클래스를 생각해보면 자동차 클래스를 처음 설계할 때 사용한 타이어 객체는 언제든지 성능이 좋은 타이어 객체로 교체할 수 있어야 한다. 새로 교체된 타이어 객체는 기존 타이어와 사용 방법은 동일하지만 실행결과는 더 우수하게 나와야 할 것이다. 이것을 프로그램으로 구현하기 위해서 상속과 오버라이딩, 타입 변환을 이용한다.

 

Tire.java(부모 클래스)

package TestPackage;
public class Tire {
	
		public int maxRotation; //최대 회전수(타이어 수명)
		public int accumulatedRotation;	//누적 회전수
		public String location; //타이어 위치

		//생성자
		public Tire(String location, int maxRotation) {
			this.location = location;
			this.maxRotation = maxRotation;
		}

		public boolean roll() {
			++accumulatedRotation;	//누적 회전수 1 증가
			if(accumulatedRotation<maxRotation) {
				System.out.println(location + " Tire 수명: " + (maxRotation-accumulatedRotation) + "회");
				return true;
			} else {
				System.out.println("*** " + location + " Tire 펑크 ***");
				return false;
			}
		}
}

 

HankookTire.java(자식 클래스)

package TestPackage;
public class HankookTire extends Tire{
		//생성자
		public HankookTire(String location, int maxRotation) {
			super(location, maxRotation);
		}	

		@Override
		public boolean roll() {
			++accumulatedRotation;		
			if(accumulatedRotation<maxRotation) {
				System.out.println(location + " HankookTire 수명: " + (maxRotation-accumulatedRotation) + "회");
				return true;
			} else {
				System.out.println("*** " + location + " HankookTire 펑크 ***");
				return false;
			}
		}
}

 

KumhoTire.java(자식 클래스)

package TestPackage;
public class KumhoTire extends Tire{
		//생성자
		public KumhoTire(String location, int maxRotation) {
			super(location, maxRotation);
		}	

		@Override
		public boolean roll() {
			++accumulatedRotation;		
			if(accumulatedRotation<maxRotation) {
				System.out.println(location + " KumhoTire 수명: " + (maxRotation-accumulatedRotation) + "회");
				return true;
			} else {
				System.out.println("*** " + location + " KumhoTire 펑크 ***");
				return false;
			}
		}
}

 

Car.java

package TestPackage;
public class Car {
		Tire frontLeftTire = new Tire("앞왼쪽", 6);
		Tire frontRightTire = new Tire("앞오른쪽", 2);
		Tire backLeftTire = new Tire("뒤왼쪽", 3);
		Tire backRightTire = new Tire("뒤오른쪽", 4);
		
		int run() {
			System.out.println("자동차 주행시작");
			if(frontLeftTire.roll()==false) { stop(); return 1; };
			if(frontRightTire.roll()==false) { stop(); return 2; };
			if(backLeftTire.roll()==false) { stop(); return 3; };
			if(backRightTire.roll()==false) { stop(); return 4; };
			return 0;
		}
		
		void stop() {
			System.out.println("자동차 정지");
		}
}

 

Test.java

package TestPackage;
public class Test {
	public static void main(String[] args) {
		Car car = new Car();
		
		for(int i=1; i<=5; i++) {
			int problemLocation = car.run();
			switch(problemLocation) {
				case 1:
					System.out.println("앞왼쪽 HankookTire로 교체");
					car.frontLeftTire = new HankookTire("앞왼쪽", 15);
					break;
				case 2:
					System.out.println("앞오른쪽 KumhoTire로 교체");
					car.frontRightTire = new KumhoTire("앞오른쪽", 13);	
					break;
				case 3:
					System.out.println("뒤왼쪽 HankookTire로 교체");
					car.backLeftTire = new HankookTire("뒤오른쪽", 14);	
					break;
				case 4:
					System.out.println("뒤오른쪽 KumhoTire로 교체");
					car.backRightTire = new KumhoTire("뒤오른쪽", 17);		
					break;
			}
			System.out.println("----------------------------------------");
		}
	}
}
/*
자동차 주행시작
앞왼쪽 Tire 수명: 5회
앞오른쪽 Tire 수명: 1회
뒤왼쪽 Tire 수명: 2회
뒤오른쪽 Tire 수명: 3회
----------------------------------------
자동차 주행시작
앞왼쪽 Tire 수명: 4회
*** 앞오른쪽 Tire 펑크 ***
자동차 정지
앞오른쪽 KumhoTire로 교체
----------------------------------------
자동차 주행시작
앞왼쪽 Tire 수명: 3회
앞오른쪽 KumhoTire 수명: 12회
뒤왼쪽 Tire 수명: 1회
뒤오른쪽 Tire 수명: 2회
----------------------------------------
자동차 주행시작
앞왼쪽 Tire 수명: 2회
앞오른쪽 KumhoTire 수명: 11회
*** 뒤왼쪽 Tire 펑크 ***
자동차 정지
뒤왼쪽 HankookTire로 교체
----------------------------------------
자동차 주행시작
앞왼쪽 Tire 수명: 1회
앞오른쪽 KumhoTire 수명: 10회
뒤오른쪽 HankookTire 수명: 13회
뒤오른쪽 Tire 수명: 1회
----------------------------------------
*/

 

매개 변수의 다형성

자동 타입 변환은 필드의 값을 대입할 때에도 발생하지만, 주로 메소드를 호출할 때 많이 발생한다.

메소드를 호출할 때에는 매개 변수의 타입과 동일한 매개값을 지정하는 것이 정석이지만, 매개값을 다양화하기 위해 매개 변수에 자식 객체를 지정할 수도 있다.

예를 들어 다음과 같이 Driver 클래스에는 drive() 메소드가 정의되어 있고 Vehicle 타입의 매개변수가 선언되어 있다.

public class Driver {
	void drive(Vehicle vehicle) {
		vehicle.run();
	}

dirve()메소드를 정상적으로 호출한다면 다음과 같다.

Driver driver = new Driver();
Vehicle vehicle = new Vehicle();
driver.drive(vihicle);

만약 Vehicle의 자식 클래스인 Bus 객체를 drive() 메소드의 매개값으로 넘겨주면

driver.drive(bus);

drive() 메소드는 Vehicle 타입을 매개 변수로 선언했지만, Vehicle을 상속받는 Bus 객체가 매개값으로 사용되면 자동 타입 변환이 발생한다.

매개 변수의 타입이 클래스일 경우, 해당 클래스의 객체뿐만 아니라 자식 객체까지도 매개값으로 사용할 수 있다는 것이다. 즉, 매개값으로 어떤 자식 객체가 제공되느냐에 따라 메소드의 실행결과는 다양해질 수 있다. 자식 객체가 부모의 메소드를 재정의했다면 메소드 내부에서 오버라이딩된 메소드를 호출함으로써 메소드의 실행결과는 다양해진다.

Vehicle.java(부모 클래스)

package TestPackage;
public class Vehicle {
	public void run() {
		System.out.println("차량 주행중");
	}
}

 

Bus.java(자식 클래스)

package TestPackage;
public class Bus extends Vehicle{
	@Override
	public void run() {
		System.out.println("버스 주행중");
	}
}

 

Taxi.java(자식 클래스)

package TestPackage;
public class Taxi extends Vehicle{
	@Override
	public void run() {
		System.out.println("택시 주행중");
	}
}

 

Driver.java

package TestPackage;
public class Driver {
	void drive(Vehicle vehicle) {
		vehicle.run();
	}
}

 

Test.java

package TestPackage;
public class Test {
	public static void main(String[] args) {
		Driver driver = new Driver();
		
		Bus bus = new Bus();
		Taxi taxi = new Taxi();
		
		driver.drive(bus);
		driver.drive(taxi);
	}
}
/*
버스 주행중
택시 주행중
*/

 

강제 타입 변환(Casting)

강제 타입 변환(Casting)은 부모 타입을 자식 타입으로 변환하는 것을 말한다.

강제 타입 변환의 조건은 자식 타입이 부모 타입으로 자동 타입 변환후 다시 자식 타입으로 변환할 때 강제 타입 변환을 할 수 있다.

Parent parent = new Child(); //자동 타입 변환
Child child = (Child)parent; //강제 타입 변환

자식 타입이 부모 타입으로 자동 타입 변환하면, 부모에 선언된 필드와 메소드만 사용가능하다는 제약 사항이 따른다.

만약 자식에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 이용해서 다시 자식 타입으로 변환한 다음 자식의 필드와 메소드를 사용하면 된다.

 

Parent.java(부모 클래스)

package TestPackage;
public class Parent {
public String field1;
	
	public void method1() {
		System.out.println("Parent-method1()");
	}
	
	public void method2() {
		System.out.println("Parent-method2()");
	}
}

 

Child.java(자식 클래스)

package TestPackage;
public class Child extends Parent {
	public String field2;
	
	public void method3() {
		System.out.println("Child-method3()");
	}
}

 

Test.java

package TestPackage;
public class Test {
	public static void main(String[] args) {
		Parent parent = new Child();
		parent.field1 = "data1";
		parent.method1();
		parent.method2();
		/*
		parent.field2 = "data2";  //(불가능)
		parent.method3();         //(불가능)
		*/
		
		Child child = (Child) parent; //강제 타입 변환
		child.field2 = "yyy";  //(가능)
		child.method3();     //(가능)
	}
}
/*
Parent-method1()
Parent-method2()
Child-method3()
*/

 

객체 타입 확인

강제 타입 변환은 자식 타입이 부모 타입으로 변환되어 있는 상태에서만 가능하기 때문에 다음과 같이 처음부터 부모 타입으로 생성된 객체는 자식 타입으로 변환할 수 없다.

Parent parent = new Parent();
Child child = (Child)parent; //강제 타입 변환 불가능

실수로 이런식으로 강제 타입 변환을 하면 에러가 나기 때문에 타입 변환을 할 때 if문을 통해 예외를 처리해주는 것이 좋다.

부모 변수가 참조하는 객체가 부모 객체인지 자식 객체인지 확인하는 방법은 어떤 객체가 어떤 클래스의 인스턴스인지 확인하기 위해 instanceof 연산자를 사용한다.

좌항에는 객체가 오고 우항에는 타입이 오는데, 좌항의 객체가 우항의 인스턴스면, 즉 우항의 타입으로 객체가 생성되었다면 true를 리턴하고 그렇지 않으면 false를 리턴한다.

package TestPackage;
public class Test {
	public static void method1(Parent parent) {
		if(parent instanceof Child) { //예외처리를 한 메소드
			Child child = (Child) parent;
			System.out.println("method1 - Child로 변환 성공");
		} else {
			System.out.println("method1 - Child로 변환되지 않음");
		}
	}
	
	public static void method2(Parent parent) { //예외처리를 하지 않은 메소드
		Child child = (Child) parent;
		System.out.println("method2 - Child로 변환 성공");
	}
	
	public static void main(String[] args) {
		Parent parentA = new Child();
		method1(parentA);
		method2(parentA);
		
		Parent parentB = new Parent();
		method1(parentB);
		method2(parentB); //예외 발생
	}
}
/*
method1 - Child로 변환 성공
method2 - Child로 변환 성공
method1 - Child로 변환되지 않음
Exception in thread "main" java.lang.ClassCastException: class TestPackage.Parent cannot be cast to class TestPackage.Child (TestPackage.Parent and TestPackage.Child are in unnamed module of loader 'app')
	at TestPackage.Test.method2(Test.java:13)
	at TestPackage.Test.main(Test.java:24)
*/

 

java 12부터는 instanceof 연산의 결과가 true일 경우, 우측 타입 변수를 사용할 수 있기 때문에 강제 타입변환이 필요없다.

if(parent instanceof Child child){
	//child 변수 사용
}

 

Person.java

public class Person {
	//필드 선언
	public String name;

	//생성자 선언
	public Person(String name) {
		this.name = name;
	}

	//메소드 선언
	public void walk() {
		System.out.println("걷습니다.");
	}
}

 

Student.java

public class Student extends Person {
	//필드 선언
	public int studentNo;

	//생성자 선언
	public Student(String name, int studentNo) {
		super(name);
		this.studentNo = studentNo;
	}

	//메소드 선언
	public void study() {
		System.out.println("공부를 합니다.");
	}
}

 

Main.java

package ch07.sec09;

public class InstanceofExample {
	//main() 메소드에서 바로 호출하기 위해 정적 메소드 선언
	public static void personInfo(Person person) {
		System.out.println("name: " + person.name);
		person.walk();

		//person이 참조하는 객체가 Student 타입인지 확인
		/*if (person instanceof Student) {
 			//Student 객체일 경우 강제 타입 변환
 			Student student = (Student) person;
 			//Student 객체만 가지고 있는 필드 및 메소드 사용
 			System.out.println("studentNo: " + student.studentNo);
 			student.study();
 		}*/

		//person이 참조하는 객체가 Student 타입일 경우
		//student 변수에 대입(타입 변환 발생)
		if(person instanceof Student student) {
			System.out.println("studentNo: " + student.studentNo);
			student.study();
		}
	}

	public static void main(String[] args) {
		//Person 객체를 매개값으로 제공하고 personInfo() 메소드 호출
		Person p1 = new Person("홍길동");
		personInfo(p1);
		
		System.out.println();

		//Student 객체를 매개값으로 제공하고 personInfo() 메소드 호출
		Person p2 = new Student("김길동", 10);
		personInfo(p2);
	}
}
/*
name: 홍길동
걷습니다.

name: 김길동
걷습니다.
studentNo: 10
공부를 합니다.
*/

댓글