상속 03

오늘·2021년 3월 15일
0

Java

목록 보기
18/42

타입 변환과 다형성

다형성(多形性)은 같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질을 말한다. 부모의 타입에 모든 자식 객체가 대입될 수 있다. 이것을 이용하면 객체는 부품화가 가능하다

ex)
public class Car {
	// 상위 클래스인 Tire에 자식 클래스인 ATire, BTire 대입해 객체 생성
	Tire t1 = new ATire();
	Tire t2 = new BTire();
}

타입 변환이란 데이터 타입을 다른 데이터 타입으로 변환하는 행위를 말한다.

작은 타입 -> 큰 타입
: 변환하는 것은 따로 처리해주지 않아도 자동으로 변환

큰 타입 -> 작은타입
: 강제 형변환을 해주어야 한다
int intValue = 'A';
char charValue = (char) intValue;

기본 타입 변환처럼 클래스 타입도 타입 변환이 있다. 클래스 타입의 변환은 상속 관계에 있는 클래스 사이에서 발생한다.


자동 타입 변환(Promotion)

-프로그램 실행 도중에 자동적으로 타입 변환이 일어나는 것을 말한다
-하위클래스가 상위의 특징과 기능을 상속받기 때문에 상위클래스와 동일하게 취급될 수 있다는 것이다.

Cat 클래스가 Animal 클래스를 상속받는다고 할 때

// Cat클래스로부터 Cat객체를 생성하고
// 이것을 Animal 변수에 대입할 수 있다
Cat cat = new Cat();
Animal animal = cat;

// 이것은 이렇게 한 줄로 사용할 수도 있다
Animal animal = new Cat();

-부모타입으로 자동 타입변환 된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다. 비록 변수는 자식 객체를 참조하더라도, 변수로 접근이 가능한 멤버는 부모 클래스 멤버로만 한정된다.
-그러나, 만약 메소드가 자식 클래스에서 오버라이딩 되었다면 자식 클래스의 메소드가 대신 호출된다.

(1) 상위 클래스
public class Parent {
	public void method1() {
		System.out.println("Parent-method1()");
	}
	
	public void method2() {
		System.out.println("Parent-method2()");
	}
}



(2) 하위 클래스
public class Child extends Parent{
	@Override
	public void method2() {
		System.out.println("Child-method2()");
	}
	
	public void method3() {
		System.out.println("Child-method3()");
	}
}



(3)출력 클래스
public class ChildEx {
	public static void main(String[] args) {
		Child child = new Child();
		Parent pa = child;
		// 위 두줄은 아래 한 줄과 같다
		// Parent pa = new Child();
		
		pa.method1();
		pa.method2();
		// 호출 불가능
		// Parent에는 method3가 없으니까!
		// pa.method3();
		
		
		// method3()를 부르고 싶다
		child.method3();
		
	}
}

-여기서 Child 객체는 method3() 메소드를 가지고는 있지만,
출력해보면 Parent 타입으로 변환 된 이후에는 method3()을 호출할 수 없는 걸 확인할 수 있다.

-하지만 부모와 자식 모두 가지고 있는 method2()의 경우에는
하위 단계에서 오버라이딩 되었기 때문에 출력시 하위의 것이 호출되는 모습을 확인할 수 있다.

실행결과

갠적으로 정리 해보자면,

타입 변환이 된 이후에는 자식클래스에 있는 내용을
메모리에만 올리고 사용하지 못하니 아까운거 아니야?
-> 그래서 사용할 수 있는 방법이 있음


1. 다운캐스팅 하여 자식의 필드나 메소드를 이용함
 : 부모와 자식의 실행 내용이 아예 다른 메소드인 경우 사용
   예를 들어
      -> 부모는 리턴타입 int, 자식은 리턴타입 boolean 
         매개변수가 부모는 2개 자식은 3개일때
	 부모는 void 인데 자식은 리턴이 있을 때
    
  2. 메소드 오버라이딩
   : 부모에게 있는 메소드를 자식에서 오버라이딩(재정의)
     실행했을때 부모의 것이 아니라 자식에서 재정의한 내용으로 실행함
     예를 들어
        -> 실행내용이 부모는 더하기, 자식은 곱하기로 다르지만
           받는 매개변수의 수가 2개로 동일하고, 리턴이 int타입으로 같을 때

필드의 다형성

  • 다형성
    : 동일한 타입을 사용하지만 다양한 결과가 나오는 성질을 말한다.
    : 주로 필드의 값을 다양화함으로써 실행 결과가 다르게 나오도록 구현하는데, 필드 타입은 변함이 없지만, 실행도중에 어떤 객체를 필드로 저장하느냐에 따라 실행 결과가 달라질 수 있다.

  • 자동 타입변환은 왜 사용하는 것일까
    : 다형성을 구현하는 기술적 방법 때문이다.
    : 부모를 상속하는 자식은 부모가 가지고 있는 필드와 메소드를 가지고 있으니 사용방법이 동일할 것이고, 부모의 메소드를 오버라이딩해 메소드 실행 내용을 변경함으로써 더 우수한 결과가 나오게 할 수도 있다.

1) 부모 클래스
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() {
		// 누적 회전수 1 증가
		++accumulatedRotation;
		// 만약 최대 회전 수보다 현재 누적된 회전수가 작다면
		if (accumulatedRotation < maxRotation) {
			// 정상 회전 실행
			System.out.println(location + "Tire 수명 : "
            				+ (maxRotation - accumulatedRotation) + " 회");
			return true;
		} else {
			System.out.println("*** " + location + "Tire 평크 ***");
			return false;
		}
	}
}




2) Tire를 객체화해 가지는 클래스
/*
-자동차의 4개 바퀴를 각각 변수로 잡아
 Tire 클래스를 객체화 시킨다
 
 (Tire의 생성자)
 public Tire(String location, int maxRotation) {
	this.location = location;
	this.maxRotation = maxRotation;
}
 */
public class Car {
	// 필드
	Tire frontLeftTire = new Tire("앞 왼쪽", 6);
	Tire frontRightTire = new Tire("앞 오른쪽", 2);
	Tire backLeftTire = new Tire("뒤 왼쪽", 3);
	Tire backRightTire = new Tire("뒤 오른쪽", 4);
	
	// 한도를 넘어 펑크났을 때 수행하는 메소드
	void stop() {
		System.out.println("[ 자동가가 멈췄습니다 ]");
	}
	
	// 메소드
    	// 모든 타이어를 1회 회전시키기 위해 각 Tire객체에 roll() 메소드를 호출한다.
        // false를 리턴하는 roll()이 있을 경우 stop()을 호출하고 해당 타이어 번호를 리턴
	int run() {
		System.out.println("[ 자동차가 달립니다 }");
		// 변수화된 객체 값이 false가 되었을 때 수행 할 if문
		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;
	}
}




3) Tire를 상속받는 자식 클래스
3-1)
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;
		}
	}
}

3-2)
public class HankookTire extends Tire{
	// 생성자
	public HankookTire(String location, int maxRotation) {
		super(location, maxRotation);
	}
	
	// 메소드
	// 부모 클래스의 것을 받아와 오버라이딩 한것, 내용은 같다
	@Override
	public boolean roll() {
		// 누적 회전수 1 증가
		++accumulatedRotation;
		// 만약 최대 회전 수보다 현재 누적된 회전수가 작다면
		if (accumulatedRotation < maxRotation) {
			System.out.println(location + "Hankook Tire 수명 : "
					+ (maxRotation - accumulatedRotation) + " 회");
			return true;
		} else {
			System.out.println("*** " + location + "Hankook Tire 평크 ***");
			return false;
		}
	}
}




4) 실행하는 클래스
public class CarExample {
	public static void main(String[] args) {
		// 객체 생성
		Car car = new Car();
		
		// car 객체의 run을 5번 실행
		for(int i=1; i<=5; i++) {
			int problemLocation = car.run();
			
			
			// 총 5번 돌아가며 리턴값이 오면 실행되는 구간
			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;
			}
			// 출력을 보기 좋게 만들기 위함
			// 1회전시 출력되는 내용을 구분
			System.out.println("----------------------------------------");
		}
	}
}

하나의 배열로 객체 관리

위 Car 예제에서는 4개의 타이어 객체를 각각 따로 객체 생성해 필드로 저장했지만, 동일한 타입의 값들은 배열로 관리하는 것이 깔끔하다.

//먼저 배열 기본형을 다시 기억해보자
// 1)
int arr = new int [4];
arr[0] = 10;
arr[1] = 20;
...
// 2)
int arr[] = {10, 20, 30, 40};



// Car예제 中
public class Car {
	Tire frontLeftTire = new Tire("앞 왼쪽", 6);
	Tire frontRightTire = new Tire("앞 오른쪽", 2);
	Tire backLeftTire = new Tire("뒤 왼쪽", 3);
	Tire backRightTire = new Tire("뒤 오른쪽", 4);
}

// 이걸 배열로 바꿔보자
class Car {
	Tire[] tires = {
    		new Tire("앞 왼쪽", 6), 
            	new Tire("앞 오른쪽", 2),
                new Tire("뒤 왼쪽", 3),
                new Tire("뒤 오른쪽", 4)
        };
}

이런 방법으로 Car 예제를 수정한다면, 실행 메소드인 run() 메소드도 인덱스를 이용해 변경해 줄 수 있다. 예를 들어 1번 인덱스에 있는 Tire를 KumhoTire로 교체해라
=tires[1] = new KumhoTire("앞 오른쪽", 13);

// 수정된 Car 예제
public class Car {
	Tire[] tires = {
    		new Tire("앞 왼쪽", 6), 
        	new Tire("앞 오른쪽", 2),
            new Tire("뒤 왼쪽", 3),
            new Tire("뒤 오른쪽", 4)
	};
	
	// 메소드
	int run() {
		System.out.println("[자동차가 달립니다]");
		for (int i=0; i<tires.length; i++) {
			if(tires[i].roll() == false) {
				stop();
				return (i+1);
			}
		}
		return 0;
	}
	
	void stop() {
		System.out.println("[자동차가 멈춥니다]");
	}
}



// 사용하는 CarEx 에제도 변경해준다
public class CarExample {
	public static void main(String[] args) {
		
		Car car = new Car();
		
		for(int i=0; i<=5; i++) {
			int problemLocation = car.run();
			if(problemLocation != 0) {
				System.out.println(car.tires[problemLocation-1].location + "HankookTire로 교체");
				car.tires[problemLocation-1] = 
						new HankookTire(car.tires[problemLocation-1].location, 15);
			}
			System.out.println("-----------------------------------------------");
		}
    	}
}

매개 변수의 다형성

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

// Driver 클래스
Class Driver{
	void drive(Go go) {
    		go.run();
        }
}

// Driver 메소드를 호출한다면
Driver driver = new Driver();
Go go = new Go();
driver.drive(go);

drive() 메소드는 Go 타입을 매개변수로 선언했지만,
Go를 상속받는 Bus 객체가 매개값으로 사용되면 자동 타입 변환이 발생된다.
-> 매개 변수의 타입이 클래스일 경우, 해당 클래스 뿐 아니라 자식 까지 매개값으로 사용 가능하다는 것. 이때 어떤 자식 클래스가 들어가느냐에 따라 실행 결과가 다양해질 수 있는 것이다.

1) Driver 클래스
public class Driver {
	// 매개 변수로 다형성 받기
	public void drive(Vehicle vehicel) {
		vehicel.run();
	}
}



2) 상위 클래스 (기본 출력형이 되는 클래스)
public class Go {
	public void run() {
		System.out.println(" 차량이 달리고 있습니다 ");
	}
}



3) Go 클래스를 상속받는 Taxi 클래스
public class Taxi extends Go {
	@Override
	public void run() {
		System.out.println(" 택시가 달리고 있습니다 ");
	}
}



4) Go 클래스를 상속받는 Bus 클래스
public class Bus extends Go {
	@Override
	public void run() {
		System.out.println(" 버스가 달립니다 ");
	}
}



5) 출력 클래스
public class DriverEx {
	public static void main(String[] args) {
		/*
		 drive 메소드를 원래대로 호출한다면 다음과 같다 
		 
		 Driver driver = new Driver();
		 Vehicle vehicle = new Vehicle();
		 driver.driver(vehicle);
		 */
		Driver driver = new Driver();
        	Bus bus = new Bus();
		Taxi taxi = new Taxi();
        
        	// Go 의 자식 클래스인 Bus와 Taxi가
            	// Go 클래스를 받는 곳에 들어감
		driver.drive(bus);
		driver.drive(taxi);
	}
}

강제 타입변환(Casting)

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

// 상위 클래스
public class Parent {
	public String field1;
	
	public void method1() {
		System.out.println("Parent-method1()");
	}
	
	public void method2() {
		System.out.println("Parent-method2()");
	}
}



// 하위 클래스
public class Child extends Parent {
	public String field2;
	
	public void method3() {
		System.out.println("Child-method3()");
	}
}



// 메인 클래스
public class ChildEx {
	public static void main(String[] args) {
		// 일반 클래스 객체화
		Parent parent = new Parent();
		Child child = new Child();
		
		// 다형성
		Parent parent1 = new Child();	  // up_casting : 묵시적 형 변환
		Child child1 = (Child) parent1;	  // down_casting : 강제 형 변환
		
        	// 강제 형변환으로 하위 클래스에 있는
            	// field2와 method3() 사용이 가능해졌다
		child1.field2 = "홍길동";
		child.method3();
        }
}

객체 타입 확인(instanceof)

강제 타입 변환은 자식 타입이 부모 타입으로 변환되어 있는 상태에서만 가능하기 때문에 다음과 같이 부모 타입의 변수가 부모 객체를 참조할 경우 자식 타입으로 변환할 수 없다

Parent parent = (new) Parent();
Child child  = (Child) parent;

그렇다면 부모 변수가 현재 참조하고 있는 객체가 자식인지 부모인지 확인하는 방법이 있을까. 그것이 instanceof 이다.

boolean result = 좌항(객체) instanceof 우항(타입)

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

public void method(Parent parent) {
	if (parent instanceof Child) {
    		Child child = (Child) parent;
        }
}

만약 타입을 확인하지 않고 강제타입 변환을 시도한다면 ClassCastException 예외가 발생할 수 있다

public class InstanceofEx {
	public static void method1(Parent parent) {
		if(parent instanceof Child) {	//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);	//Child 객체를 매개값으로 전달
		method2(parentA);	//Child 객체를 매개값으로 전달
		
		Parent parentB = new Parent();
		method1(parentB);	//Parent 객체를 매개값으로 전달
		method2(parentB);	//Parent 객체를 매개값으로 전달
	}
}

이 경우에서 method1()과 method2()를 호출할 경우, Child 객체를 매개값으로 전달하면 두 메소드 모두 예외가 발생되지 않지만, Parent 값으로 매개값을 전달하면 method2()에서는 ClassCastException이 발생한다.

method1()에서는 가능 여부를 확인 후 변환하지만, method2()는 무조건 변환하려 했기 때문이다.

또 다른 예제를 보자

// instanceof 연산자의 좌항은 객체가 오고, 우항은 타입이 오는데
// 좌항의 객체가 우항의 인스턴스이면
// 즉 우항의 타입으로 객체가 생성되었다면 true를 산출하고, 그렇지 않으면 false 산출
boolean result = parent instanceof Parent;
System.out.println(result);

result = child1 instanceof Parent;
System.out.println(result);
		
// child1객체는 child1 타입인가요?
// = child1 참조변수로 child 클래스를 가리킬 수 있나요?
// = child1 참조변수의 주소를 따라가면 메모리 안에서 child 클래스를 찾을 수 있나요?
result = child1 instanceof Child;
System.out.println(result);
	
result = parent instanceof Child;
System.out.println(result);

추상클래스

추상 클래스의 개념

-사전적 의미로 추상(abstract)는 실체간에 공통되는 특성을 추출한 것
-추상 클래스와 실체 클래스는 상속 관계를 가지고 있다. 추상이 부모이고 실체가 자식이다
-추상 클래스는 새로운 실체 클래스를 만들기 위해 부모 클래스로만 사용된다

(강의 정리)
추상 메소드를 가지는 클래스는 추상 클래스
1. 추상 메소드
: 미완성된 메소드
: { } <- 안에 내용이 없다
: 추상 메소드를 가지는 클래스는 '반드시' 추상 클래스여야 오류 안남

  1. 추상 클래스
    : 추상 메소드를 넣을 수 있는 클래스
    : 하지만 '반드시' 추상메소드를 가지고 있지 않아도 된다

추상 클래스의 용도

  1. 실체 클래스들의 공통된 필드와 메소드의 이름을 통일할 목적
    =같은 기능이라면 부모에게서 오버라이딩해 사용하니 이름이 통일되는 것

  2. 실체 클래스 작성시 시간 절약
    =이미 기능을 대강 만들어 놓았거나 이름을 정해놨기 때문에, 오버라이딩해 그대로 사용/수정만 하면 되니 시간이 절약된다

추상 클래스 선언

클래스 선언시 abstract 키워드를 붙여야 한다.
public abstract class 클래스명 { }

// 추상 클래스에 넣을 수 있는 멤버들
public abstract class Animal {
	// 필드
	String name;
	//생성자
	public Animal() {	}
	// 일반 메소드
	void printAll() {	}
	//추상메소드
	abstract void abMethod();
}

추상 메소드와 오버라이딩

1) 부모가 추상이라면, 자식이 추상 메소드를 오버라이딩하는 것이 필수
: 추상 클래스를 부모로 사용하는 자식 클래스는 반드시 부모클래스의 추상메소드를 재정의해야 오류가 나지 않는다.

즉, 자식이 '반드시' 부모 클래스의 메소드를 재정의 하게 하고자 할 때
추상 클래스를 사용한다

public abstract class Person {
	// 필드
	int age;
	// 생성자
	Person(){	}
	// 메소드
	void eat() {
		//내용
	}
	
	// 추상 메소드 : 미완성
	// {	}이 없는 메소드
	// {	}가 업으므로 내용이 아직 미완성
	abstract void method();
	abstract int add();
}

//부모가 추상 클래스이기 때문에
//상속 받기 위해서는 부모의 추상 메소드를 오버라이딩 해줘야 한다
class ChildePerson extends Person {
	@Override
	void method() {	}
	
	@Override
	int add() {
		return 0;
	}
}

2)
추상클래스는 new 연산자를 이용해 인스턴스 생성이 불가능하다
추상클래스를 재정의한 자식 클래스를 객체생성해 사용하는 것은 가능하다

public class PersonMain {
	public static void main(String[] args) {
		// Person person = new Person();
		ChildePerson cp = new ChildePerson();
	}
}

사용예시
1. 상위 클래스인 Animal을 추상클래스로 작성
-> Animal을 단독으로 객체생성해 사용할 수 없고, 누군가 상속받아 사용하는 건 가능하다

public abstract class Animal {
	public String kind;
	
	public void breathe() {
		System.out.println("숨을 쉽니다.");
	}

	public abstract void sound();
}
  1. Animal을 상속받아 재정의한 Cat, Dog
1. 
public class Cat extends Animal {
	public Cat() {
		this.kind = "포유류";
	}

	@Override
	public void sound() {
		System.out.println("야옹");
	}
}


2.
public class Dog extends Animal {
	public Dog() {
		this.kind = "포유류";
	}
	void dogEat() {
		System.out.println("강아지는 뼈다귀를 좋아함");
	}
	@Override
	public void sound() {
		System.out.println("멍멍");
	}
}
  1. 불러와 사용하는 Main 클래스
public class AnimalExample {
	public static void main(String[] args) {
    		// dog와 cat을 각자 객체 생성해 출력해보기
		Dog dog = new Dog();
		Cat cat = new Cat();
		dog.sound();
		cat.sound();
		dog.dogEat();
		cat.breathe();
		System.out.println("-----");

		// 변수의 자동 타입 변환 후 사용해보기
		Animal animal = null;
		animal = new Dog();
		animal.sound();
		// animal.dogEat();
		// animal은 부모클래스를 참조하는 변수기 때문에
		// 자식클래스에 있는 dogEat()메소드를 실행할 수 없다
		animal = new Cat();
		animal.sound();
		System.out.println("-----");

		// 매개변수의 자동 타입 변환
		animalSound(new Dog());
		animalSound(new Cat());
	}

	public static void animalSound(Animal animal) {
		// 재정의된 메소드 호출
		animal.sound();
	}
}

0개의 댓글