다형성(多形性)은 같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질을 말한다. 부모의 타입에 모든 자식 객체가 대입될 수 있다. 이것을 이용하면 객체는 부품화가 가능하다
ex)
public class Car {
// 상위 클래스인 Tire에 자식 클래스인 ATire, BTire 대입해 객체 생성
Tire t1 = new ATire();
Tire t2 = new BTire();
}
타입 변환이란 데이터 타입을 다른 데이터 타입으로 변환하는 행위를 말한다.
작은 타입 -> 큰 타입
: 변환하는 것은 따로 처리해주지 않아도 자동으로 변환
큰 타입 -> 작은타입
: 강제 형변환을 해주어야 한다
int intValue = 'A';
char charValue = (char) intValue;
기본 타입 변환처럼 클래스 타입도 타입 변환이 있다. 클래스 타입의 변환은 상속 관계에 있는 클래스 사이에서 발생한다.
-프로그램 실행 도중에 자동적으로 타입 변환이 일어나는 것을 말한다
-하위클래스가 상위의 특징과 기능을 상속받기 때문에 상위클래스와 동일하게 취급될 수 있다는 것이다.
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);
}
}
-부모타입을 자식타입으로 변환하는 것
-만약 자식 타입에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해서 다시 자식 타입으로 변환한 다음 자식 타입의 필드와 메소드를 사용하면 된다.
// 상위 클래스
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();
}
}
강제 타입 변환은 자식 타입이 부모 타입으로 변환되어 있는 상태에서만 가능하기 때문에 다음과 같이 부모 타입의 변수가 부모 객체를 참조할 경우 자식 타입으로 변환할 수 없다
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. 추상 메소드
: 미완성된 메소드
: { } <- 안에 내용이 없다
: 추상 메소드를 가지는 클래스는 '반드시' 추상 클래스여야 오류 안남
실체 클래스들의 공통된 필드와 메소드의 이름을 통일할 목적
=같은 기능이라면 부모에게서 오버라이딩해 사용하니 이름이 통일되는 것
실체 클래스 작성시 시간 절약
=이미 기능을 대강 만들어 놓았거나 이름을 정해놨기 때문에, 오버라이딩해 그대로 사용/수정만 하면 되니 시간이 절약된다
클래스 선언시 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.
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("멍멍");
}
}
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();
}
}