Java/Java

상속_타입 변환과 다형성

이게뭐여 2022. 2. 21. 21:18

1. 다형성

다형성은 다양한 객체를 이용하여 사용 방법은 동일하지만 다양한 실행결과가 나오도록 하는 성질을 말한다.

이러한 다형성을 구현하기 위해서는 메소드 재정의와 타입 변환이 요구된다.

 

 

2. 타입 변환

타입 변환이란 말 그대로 기존 타입이 다른 타입으로 바뀌는 것을 말한다.

앞서서 기본 타입이 변화하는 것을 살펴보았다(ex: int -> double).

기본 타입과 마찬가지로 클래스 간의 타입 변환도 가능한데, 클래스 간의 타입 변환은 상속 관계에서 이뤄진다.

클래스 간의 타입 변환에서는 자식 타입이 부모 타입으로 자동 타입 변환(프로그램 실행 중 자동으로 변환) 가능하다.

 

아래의 예처럼 자식 클래스인 Cat클래스는 부모 클래스인 Animal클래스로 자동 타입 변환 가능하다.

cat과 animal은 변수 타입만 다를뿐 동일한 Cat객체를 참조하게 된다.

  • Cat cat  = new Cat();
  • Animal animal = cat;

 

또한 아래의 예제 코드처럼 직접 상속관계가 아니더라도, 계층적으로 상위 타입이라면 자동 타입 변환이 가능하다.

A <- B <- D.

A <- C <- E.

package sec02.exam01;

class A {}

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

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



public class PromotionExample {

	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 b2 = e;
		//C c2 = d;

	}

}

 

클래스 간의 자동 타입 변환에서 주의할 점이 있다.

자식 타입이 부모 타입으로 자동 변환된 이후에는 부모 타입에서 선언된 필드 및 메소드에만 접근 가능하다.

또한 메소드의 경우, 자식 타입에서 재정의되었다면 재정의된 메소드가 호출된다.

 

아래의 예제 코드는 부모 클래스인 Parent클래스와 자식 클래스인 Child클래스의 자동 타입 변환을 보여준다.

Child인스턴스는 method3() 메소드를 갖고 있지만, 해당 메소드는 부모 클래스가 아닌 자식 클래스에서 선언된 메소드이기 때문에 자동 타입 변환 이후에는 접근 불가능하다.

또한 method2() 메소드는 부모 클래스에서 선언되었기 때문에 접근 가능하며, 자식 클래스에서 재정의되었기 때문에 재정의된 method2() 메소드가 호출된다.

package sec02.exam02;

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

}
package sec02.exam02;

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

}
package sec02.exam02;

public class ChildExample {

	public static void main(String[] args) {
		Child child = new Child();
		
		//자동 타입 변환
		Parent parent = child;
		
		//부모클래스 메소드 호출
		parent.method1();
		//자식클래스에서 재정의된 메소드 호출
		parent.method2();
		//자식클래스에만 존재하는 메소드 호출 불가!
		//parent.method3();
		
		
	}

}

 

3. 필드의 다형성

필드의 타입을 부모 타입으로 선언하면, 자식 타입의 인스턴스를 참조할 수 있다.

따라서 필드가 다양한 자식 타입 인스턴스를 참조하고, 자식 타입에 따라 다른 결과를 낼 수 있다.

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

 

아래의 예제 코드들은 Tire클래스 타입의 필드를 가진 Car클래스를 통해 필드의 다형성을 보여준다.

Car클래스는 Tire필드 4개를 가지고 있으며, Tire클래스는 HankookTire클래스와 KumhoTire클래스의 부모 클래스이다.

 

우선 Tire클래스를 살펴보자.

Tire클래스는 최대회전수를 저장하는 maxRotation필드, 누적회전수를 저장하는 accumulateRotation필드, 타이어의 위치(전후좌우)를 저장하는 location필드를 가지고 있다.

Tire객체가 생성될 때 매개변수를 통해 위치와 최대회전수를 결정할 수 있다.

또한 roll() 메소드를 가지고 있는데, 해당 메소드가 호출될 때마다 누적 회전수가 1씩 증가하고 true를 반환한다. 그러나 누적 회전수가 최대 회전수에 도달하면 타이어는 펑크가 나고 false를 반환한다.

package sec02.exam03;

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;
		if(accumulatedRotation < maxRotation) {
			System.out.println(location + " Tire 수명: " + (maxRotation - accumulatedRotation) + "회");
			return true;
		} else {
			System.out.println("*** " + location + " Tire 펑크 ***");
			return false;
		}
	}

}

 

다음으로는 네개의 Tire클래스 필드를 가진 Car클래스를 살펴보자.

Car클래스는 Tire클래스 타입의 전좌타이어, 전우타이어, 후좌타이어, 후우타이어 필드를 가지고 있다.

해당 필드들은 각각 특정 타이어 위치와 최대 회전수가 매개값으로 지정되어 생성된다. 

run()메소드는 각 타이어 필드의 roll()메소드를 호출하여 각 타이어의 누적 회전수를 증가시키고, 그 결과 해당 타이어가 펑크난 경우 stop()메소드를 호출하여 자동차를 멈추고 펑크난 타이어의 번호를 반환한다.

package sec02.exam03;

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("[자동차가 멈춥니다.]");
	}

}

 

이제 Tire클래스의 자식 클래스인 HankookTire클래스와 KumhoTire클래스를 살펴보자.

두 클래스의 생성자는 타이어 위치와 누적 회전수를 매개값을 받아서 부모 클래스의 생성자에 넘겨준다.

또한 해당 클래스들에는 roll()메소드가 재정의 되어있는데, roll()메소드를 호출했을 때 Tire클래스와는 다른 내용을 출력한다.

package sec02.exam03;

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;
		}
	}

}
package sec02.exam03;

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클래스, Tire클래스, HankookTire클래스, KumhoTire클래스를 통해 어떻게 필드의 다형성이 활용되는지 알아보자.

아래의 예제 코드는 우선 Car인스턴스를 참조하는 car변수를 생성한다. 

그 후, 반복문을 통해 car변수의 run()메소드를 5번 실행한다.

run()메소드는 호출될 때마다 네 개 Tire필드의 roll()메소드를 호출한다. roll()메소드는 누적 회전수를 증가시키고 최대 회전수와 비교하여 펑크 여부를 반환한다. 다시 run()메소드는 roll()메소드의 반환값을 통해 펑크난 Tire필드의 위치 번호를 반환한다. 그 반환 값은 problemLocation변수에 저장된다.

마지막으로 problemLocation을 매개변수로 가진 switch문을 통해 펑크난 타이어를 Tire의 자식인 HankookTire 또는 KumhoTire로 교체한다.

package sec02.exam03;

public class CarExample {

	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);
			}
			System.out.println("-------------------------------");
		}

	}

}

 

 

4. 매개변수의 다형성

자동 타입 변환을 통한 다형성은 필드에 대해서도 자주 활용되지만, 메소드를 호출할 때 매개 변수에 대해서도 자주 활용된다.

특정 메소드의 매개 변수를 부모 클래스 타입으로 지정했다고 가정하자. 

해당 메소드의 매개 변수에는 부모 인스턴스가 값으로 사용될 수도 있지만, 자동 타입 변환을 통해 자식 인스턴스도 값으로 사용될 수 있다.

이럴 경우, 다양한 자식 인스턴스를 매개 값으로 활용하여 다양한 결과를 나타낼 수 있다.

 

아래의 예제 코드는 매개 변수의 다형성를 활용한 예를 보여준다.

Driver클래스, 부모 클래스인 Vehicle클래스, 자식 클래스인 Bus클래스, Taxi클래스가 존재한다.

Vehicle클래스에는 run()메소드가 선언되어있다. 그리고 Bus클래스와 Taxi클래스에는 run()메소드가 재정의되어있다.

Driver클래스는 drive()메소드를 갖고 있으며, 해당 메소드를 통해 Vehicle클래스 객체의 run()메소드를 호출한다.

package sec02.exam04;

public class Vehicle {
	public void run() {
		System.out.println("차량이 달립니다.");
	}

}
package sec02.exam04;

public class Bus extends Vehicle {
	@Override
	public void run() {
		System.out.println("버스가 달립니다.");
	}

}
package sec02.exam04;

public class Taxi extends Vehicle {
	@Override
	public void run() {
		System.out.println("택시가 달립니다.");
	}

}
package sec02.exam04;

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

}

 

아래는 위의 네가지 클래스를 활용한 예제 코드이다.

우선 Driver, Bus, Taxi 인스턴스를 참조하는 driver, bus, taxi변수를 선언한다.

그리고 자식 타입인 bus, taxi변수를 매개 값으로 사용하여 driver변수의 drive()메소드를 호출한다.

이러면 각각 재정의된 Bus인스턴스의 run()메소드, Taxi인스턴스의 run()메소드가 호출되어 자식 타입 마다 다른 결과를 나타낼 수 있다.

package sec02.exam04;

public class DriverExample {

	public static void main(String[] args) {
		Driver driver = new Driver();
		Bus bus = new Bus();
		Taxi taxi = new Taxi();
		
		//자식객체 bus, taxi가 부모 객체 vehicle로 자동변환되고, 각 자식 클래스에서 재정의된 메소드를 호출한다. 
		driver.drive(bus);
		driver.drive(taxi);

	}

}

 

 

5. 강제 타입 변환

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

이러한 변환은 자동적으로 처리되지 않고 코드를 통해 직접 명령해줘야한다.

또한 모든 경우에 부모 타입이 자식 타입으로 변환되는 것은 아니며, 자식 타입이 부모 타입으로 변환된 뒤 다시 부모 타입을 자식 타입으로 변환할 때만 가능하다.

 

아래는 강제 타입 변환의 예이다. Parent클래스는 부모 클래스이며, Child클래스는 자식 클래스이다.

  • Parent parent = new Child(); //자식 타입을 부모 타입으로 변환.
  • Child child = (Child) parent; //부모 타입을 자식 타입으로 변환.

강제 타입 변환은 자식 타입에서 선언된 필드 및 메소드를 꼭 사용해야할 때 활용된다.

 

아래의 예제 코드는 강제 타입 변환의 예를 보여준다.

부모 클래스인 Parent클래스에는 field1필드, method1()메소드, method2()메소드가 선언되어있다.

그리고 자식 클래스인 Child클래스에는 field2필드, method3()메소드가 선언되어있다.

Child타입이 Parent타입으로 자동 변환된 parent변수의 경우, field1필드, method1()메소드, method2()메소드만 사용 가능하다.

Child child = (Child) parent;를 통해 자식 타입으로 강제 변환된 child변수의 경우, 부모 클래스와 자식 클래스에 선언된 모든 필드와 메소드에 접근 가능하다.

package sec02.exam05;

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

}
package sec02.exam05;

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

}
package sec02.exam05;

public class ChildExample {

	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();
		
		

	}

}

 

 

6. 객체 타입 확인

앞서 말했듯이 처음 부터 부모 타입으로 생성된 변수는 자식 타입으로 강제 타입 변환될 수 없다.

즉, 부모 타입 변수가 자식 인스턴스를 참조하고 있어야 다시 자식 타입 변수로 강제 변환될 수 있는 것이다.

 

그렇다면, 부모 타입 변수가 부모 인스턴스, 자식 인스턴스 중 어떤 인스턴스를 참조하고 있는지 확인하는 방법은 무엇일까?

어떤 변수가 어떤 인스턴스를 참조하고 있는지 확인하는 방법은 instanceof 연산자를 사용하는 것이다.

  • boolean result = 좌항(참조 변수) instanceof 우항(참조 인스턴스 타입 = 클래스);

위의 코드에서 만약 좌항(참조 변수)가 우항(참조 인스턴스 타입 = 클래스)를 참조하고 있다면, 즉 우항 타입을 참조하고 있는 라면 true를 반환하고 그렇지 않다면 false를 반환한다.

instanceof 연산자는 주로 매개값의 참조 인스턴스 타입을 확인할 때 사용된다. 

강제 타입 변환이 필요한 경우, 반드시 instanceof 연산자를 통한 확인이 선행되어야한다.

만약 확인하지 않고 강제 타입 변환을 실행할 경우, ClassCastException이 발생할 수 있다.

 

아래의 예제 코드는 부모 클래스인 Parent클래스와 자식 클래스인 Child클래스를 통해 instanceof 연산자의 활용을 보여준다.

main()메소드 내부를 살펴보자.

parentA변수는 Child 타입에서 Parent 타입으로 자동 타입 변환된 변수이다. 즉, Child인스턴스를 참조하고 있다.

parentB변수는 Parent인스턴스를 참조하는 변수이다.

 

method1은 instanceof 연산자를 통해 강제 타입 변환 전 변수가 Child인스턴스를 참조하고 있는지 확인한다.

method1메소드의 매개값으로 parentA를 지정할 경우, if문을 통해 parentA가 Child인스턴스를 참조하고 있음이 확인되고, Parent 타입에서 다시 Child 타입으로 강제 타입 변환된다.

method1메소드의 매개값으로 parentB를 지정할 경우, if문을 통해 parentB가 Child인스턴스를 참조하고 있지 않음이 확인되고, 강제 타입 변환되지 않는다.

 

method2은 instanceof 연산자를 사용하지 않고 바로 강제 타입 변환을 실시한다.

parentA를 매개값으로 지정할 경우, 문제가 발생하지 않는다.

그러나 Child인스턴스를 참조하지 않는 parentB를 매개값으로 지정할 경우, ClassCastException이 발생한다.

package sec02.exam06;

public class Parent {

}
package sec02.exam06;

public class Child extends Parent {

}
package sec02.exam06;

public class InstanceofExample {

	public static void method1(Parent parent) {
		//Child 타입으로 변환 가능한지 확인.
		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) {
		//ClassCastException이 발생할 가능성 있음.
		Child child = (Child) parent;
		System.out.println("method2 - Child로ㅓ 변환 성공");
	}
	
	public static void main(String[] args) {
		Parent parentA = new Child();
		//Child 객체를 매개값으로 전달.
		method1(parentA);
		method2(parentA);
		
		Parent parentB = new Parent();
		//Parent 객체를 매개값으로 전달. 
		method1(parentB);
		method2(parentB); //예외 발생!

	}

}

 

 

출처: 혼자 공부하는 자바(신용권)