동적 바인딩 / 다형성 (polymorphism) / 메서드에 다형성 활용
목차
📍 동적 바인딩
– 상속 관계에서 업캐스팅 후 메서드를 호출할 때,
컴파일(번역) 단계에서의 실행 대상과 실제 실행 단계에서의 실행 대상이 달라진 것.
– 참조변수의 타입과 무관하게 '실제 인스턴스'의 메서드를 실행하게 됨.
– 업캐스팅 시 일어나는 동적바인딩은 ctrl + click으로 해당 메서드의 출처를 알기가 힘듬.
원래라면 참조하고 있던 메모리가 없어졌으므로 에러가 나야하는데 Java가 에러 안나게끔 길을 바꿔준 것.
EX ) |
• 클래스 생성하기
class Parent{
public void parentPrn() {
System.out.println("슈퍼클래스의 parentPrn()");
}
}
class Child extends Parent{
public void childCrn() {
System.out.println("서브클래스의 childCrn()");
}
@Override
public void parentPrn() {
System.out.println("서브클래스에서 오버라이딩 된 parentPrn()");
}
}
– 슈퍼클래스 Parent와 이를 상속받는 Child클래스를 생성하고 각각 parentPrn(), childCrn() 메서드를 정의함.
– 서브클래스에 슈퍼클래스의 parentPrn() 메서드를 오버라이딩 함.
• 슈퍼클래스 타입 인스턴스 생성
public static void main(String[] args) {
Parent p = new Parent();
p.parentPrn();
✓ 실행 결과
슈퍼클래스의 parentPrn( )
• 서브클래스 타입 인스턴스 생성
Child c = new Child();
c.childCrn(); // 서브클래스에서 정의한 메서드
c.parentPrn(); // 슈퍼클래스로부터 상속받은 메서드
– 슈퍼클래스로부터 상속받은 메서드를 서브클래스에서 재정의 (오버라이딩)하면
기본 슈퍼클래스의 메서드와 동일하게 생긴 메서드를 정의하게 되므로,
서브클래스에서는 더 이상 슈퍼클래스의 메서드가 보이지 않음.
→ 슈퍼클래스의 메서드를 덮어쓰기 때문에 자신의 메서드만 보이게 됨. (슈퍼클래스 메서드는 은닉됨)
→ 따라서 슈퍼클래스에서 정의한 parentPrn() 메서드가 호출되지 않고,
서브클래스에서 오버라이딩한 parentPrn()이 호출되어 해당 출력문이 출력됨.
✓ 실행 결과
서브클래스의 childCrn( )
서브클래스에서 오버라이딩 된 parentPrn( )
• 업캐스팅
p = c; // Parent p = new Child();
– 서브클래스 타입 인스턴스 → 슈퍼클래스 타입으로 업캐스팅 (Child → Parent)
→ 자식클래스 타입에서 부모클래스 타입으로 가는 것
– Parent p가 Child c를 참조하므로 p가 참조하던 Heap영역의 인스턴스는 GC(Garbeage Collection)에 의해 제거됨.
p.parentPrn();
} // main() 메서드
– Child 인스턴스의 오버라이딩 된 메서드가 호출됨.
– 메서드 호출 코드를 작성하는 시점 (컴파일 시점)에서는
참조변수 타입인 Parent 클래스의 parentPrn() 메서드를 호출하는 코드지만,
실제 실행하는 시점에서 참조변수에 저장된 인스턴스가 Child의 인스턴스이므로,
실제 호출되는 메서드는 Child 타입의 오버라이딩 된 메서드가 호출됨.
→ 즉, 컴파일 단계에서의 실행 대상과 실행 단계에서의 실행 대상이 다를 수 있음.
✓ 실행 결과
서브클래스에서 오버라이딩 된 parentPrn()
📍 다형성 (polymorphism)
– 하나의 데이터타입 (참조변수)로 여러 인스턴스를 참조하는 특성.
– 어떤 인스턴스를 업캐스팅하여 슈퍼클래스 타입 변수로 다루면,
하나의 슈퍼클래스 타입으로 여러 서브클래스 타입 인스턴스를 다룰 수 있음.
EX ) |
• 슈퍼클래스 Shape 정의
class Shape{
public void draw() {
System.out.println("도형 그리기");
}
}
– 여러 도형의 공통점인 '그리다' 기능을 수행하는 drwa() 메서드 정의.
• 서브클래스 정의
class Circle extends Shape{
@Override // alt + shift + s + v
public void draw() {
System.out.println("원 그리기");
}
public void CirclePaint() {
System.out.println("원 그리기");
}
}
– draw() 메서드를 오버라이딩 하여 "원 그리기" 출력.
→ 하나하나 메서드를 따로 지정하면 유지보수도 안좋고 나중에 내가 어떤걸 써야 할 지 헷갈림.
class Rectangle extends Shape{
@Override
public void draw() {
System.out.println("사각형 그리기");
}
public void rDraw() {
System.out.println("사각형 그리기");
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("삼각형 그리기");
}
public void design() {
System.out.println("삼각형 그리기");
}
}
– Shape 클래스를 상속받는 Circle, Rectangle, Triangle 서브 클래스를 정의하고,
Shpae 클래스의 draw() 메서드를 각 서브클래스에서 오버라이딩 함.
→ 코드의 유지보수와 통일성이 향상됨.
• 메서드 오버라이딩 하지 않았을 경우
public static void main(String[] args) {
// Circle 인스턴스 생성
Circle c = new Circle();
c.CirclePaint();
// Rectangle 인스턴스 생성
Rectangle r = new Rectangle();
r.rDraw();
// Triangle 인스턴스 생성
Triangle t = new Triangle();
t.design();
– 클래스별로 똑같은 기능을 하는 메서드라도 이름이 다르므로 일일이 기억해야하는 번거로움이 있음.
✓ 실행 결과
원 그리기
사각형 그리기
삼각형 그리기
• 슈퍼클래스의 메서드를 상속받아 오버라이딩 하는 경우
c.draw();
r.draw();
t.draw();
– 각 참조변수에 같은 이름과 기능을 하는 오버라이딩 된 메서드 하나만 호출하면 되므로 코드의 통일성이 향상됨.
✓ 실행 결과
원 그리기
사각형 그리기
삼각형 그리기
• 업캐스팅
– 업캐스팅을 활용하면 코드의 통일성을 더욱 더 향상시킬 수 있음.
– Circle, Rectangle, Triangle의 공통 슈퍼클래스인 Shape타입 (s)으로 세 인스턴스를 컨트롤 가능함.
// Circle → Shape 업캐스팅
Shape s = new Circle();
s.draw();
– Circle 인스턴스를 Shape타입의 참조변수 s에 저장 (업캐스팅)하여 Circle 인스턴스의 draw() 메서드를 호출함.
// Rectangle → Shape 업캐스팅
s = new Rectangle();
s.draw();
// Triangle → Shape 업캐스팅
s = new Triangle();
s.draw();
– 마찬가지로 Shape타입의 참조변수 s에 Rectangle 인스턴스와 Triangle 인스턴스를 각각 저장 (업캐스팅)하여,
해당 인스턴스들의 draw() 메서드를 호출함.
✓ 실행 결과
원 그리기
사각형 그리기
삼각형 그리기
• 배열에 다형성 적용하기
1. 저장
방법1 ) 하나씩 저장하기
// Shape타입 배열 3개 생성
Shape[] sArr = new Shape[3];
– 다형성을 배열에 적용시키는 경우.
– 슈퍼클래스 타입으로 배열을 생성하여, 배열의 인덱스에 각각의 서브클래스 인스턴스 저장 가능.
sArr[0] = new Circle();
– Circle → Shape 으로 업캐스팅.
– 0번 인덱스에 Circle 인스턴스 생성하여 저장함.
sArr[1] = new Rectangle();
– Rectangle → Shape 으로 업캐스팅.
– 1번 인덱스에 Rectangle 인스턴스 생성하여 저장함.
sArr[2] = new Triangle();
– Triangle → Shape 으로 업캐스팅.
– 2번 인덱스에 Triangle 인스턴스 생성하여 저장함.
방법2 ) 한번에 저장하기
Shape[] sArr = {new Circle(), new Rectangle(), new Triangle()};
– 슈퍼클래스 타입으로 배열을 생성하여 문법에 맞게 데이터를 입력해야 할 곳에 각 서브클래스의 인스턴스를 입력함.
2. 배열 호출
방법1 ) 인덱스 사용
sArr[0].draw();
sArr[1].draw();
sArr[2].draw();
– 배열의 각 인덱스에는 인스턴스 주소가 저장되므로,
참조변수와 마찬가지로 배열명[인덱스].메서드명() 형태로 호출 가능함.
✓ 실행 결과
원 그리기
사각형 그리기
삼각형 그리기
방법2 ) 반복문 사용
for(int i=0; i<sArr.length; i++) {
sArr[i].draw();
}
– 반복문을 사용하여 Shape 배열 크기만큼 반복하면서 각 인덱스에 저장된 인스턴스의 draw() 메서드 호출.
✓ 실행 결과
원 그리기
사각형 그리기
삼각형 그리기
📍 메서드에 다형성 활용
1. 이미 다형성이 적용된 배열을 메서드 파라미터로 전달
2. 메서드 파라미터로 인스턴스를 전달
EX ) |
• static 메서드 정의하기
} // main 메서드 끝
public static void polymorphismDraw(Shape[] sArr) {
for(int i=0; i < sArr.length; i++) {
sArr[i].draw();
}
}
public static void polymorphismDraw2(Shape s) {
s.draw();
}
} // Ex2 클래스 끝
– main 메서드 아래 부분에 static 메서드를 정의함.
– 이미 다형성이 적용된 배열 (Shape s)을 polymorphismDraw() 메서드의 파라미터로 전달.
→ Shape 배열 타입으로 줘야하므로 해당 메서드 파라미터 부분에 'Shape[] Arr'를 입력함.
– polymorphismDraw2() 메서드에는 어떤 인스턴스가 전달되더라도 draw() 메서드는 공통으로 호출 가능함.
→ 파라미터로 정의된 Shape타입의 참조변수 s에 Circle, Rectangle, Triangle타입의 인스턴스를 각각 전달하면
s에 해당 인스턴스가 저장되고, 각 서브클래스에서 오버라이딩 된 draw() 메서드가 호출됨.
• main 메서드에서 호출하기
// 이미 다형성이 적용된 배열을 메서드 파라미터로 전달
polymorphismDraw(sArr);
// 메서드 파라미터로 각 인스턴스를 전달
polymorphismDraw2(new Circle());
polymorphismDraw2(new Rectangle());
polymorphismDraw2(new Triangle());
✓ 실행 결과
원 그리기
사각형 그리기
삼각형 그리기
원 그리기
사각형 그리기
삼각형 그리기
📍 총정리
• 직원(Employee) 클래스 정의
class Employee{
String name;
int salary;
// 파라미터 생성자 정의
// alt + shift + s + o
public Employee(String name, int salary) {
super(); // Object
this.name = name;
this.salary = salary;
}
// getEmployee() 메서드 정의
public String getEmployee() {
return name + ", " + salary;
}
// 일반 직원의 연봉 계산
public void salaryCalculation() {
System.out.println("연봉 : " + salary);
}
• 관리자(Manager) 클래스 정의 — Employee 상속
class Manager extends Employee{
String depart; // 부서명
int manageEmployeeCount; // 관리하는 직원 수
// 기본 생성자
// public Manager() {
// super();
// }
– 기본 생성자는 이러한 형태로 생략되어 있는데, 부모클래스에 기본 생성자 정의가 되어 있지 않아 에러 발생.
→ 서브클래스가 부모클래스를 상속받기 때문에 부모클래스의 생성자가 있어야 서브 클래스도 생성 가능.
→ 바로 밑에서 파라미터 생성자로 해결하기.
// 파라미터 생성자
// alt + shift + s + o
public Manager(String name, int salary, String depart, int manageEmployeeCount) {
super(name, salary);
this.depart = depart;
this.manageEmployeeCount = manageEmployeeCount;
}
– 슈퍼클래스 기본 생성자가 없으므로 슈퍼클래스의 파라미터 생성자를 호출하도록 하고,
서브클래스의 파라미터 생성자도 정의함.
– 단축키를 활용하면 슈퍼클래스의 생성자 호출과 동시에 해당 변수 초기화도 맡김. (super 패키지 설명 참조)
→ 서브클래스에서는 자기꺼만 초기화 함.
// getManager() 메서드 정의
public String getManager() {
return getEmployee() + ", " + depart + ", " + manageEmployeeCount;
}
– return에 중복되는 코드를 위에 같은 코드가 리턴되는 getEmployee() 메서드로 처리함.
// salaryCalculation() 메서드 오버라이딩 (관리자 연봉)
// alt + shift + s + v
@Override
public void salaryCalculation() {
int salaryResult = salary + (manageEmployeeCount * 10);
System.out.println("연봉 : " + salaryResult);
}
}
– 매니저 연봉 = 기본연봉 + (관리직원 수 * 10만원) 이므로, 연봉계산 메서드를 매니저의 연봉에 맞게끔 오버라이딩 함.
• 엔지니어(Engineer) 클래스 정의 — Employee 상속
class Engineer extends Employee{
int numOfCertificate; // 자격증 개수
// 파라미터 생성자 정의
public Engineer(String name, int salary, int numOfCertificate) {
super(name, salary);
this.numOfCertificate = numOfCertificate;
}
– 슈퍼클래스 기본 생성자가 없으므로,
슈퍼클래스의 파라미터 생성자를 호출하도록 하고 서브클래스의 파라미터 생성자도 정의함.
// getEngineer() 메서드 정의
public String getEngineer() {
return getEmployee() + ", " + numOfCertificate + "개";
}
// salaryCalculation() 메서드 오버라이딩 (엔지니어 연봉)
// alt + shift + s + v
@Override
public void salaryCalculation() {
int salaryResult = salary + (numOfCertificate * 20);
System.out.println("연봉 : " + salaryResult);
}
}
– 엔지니어 연봉 = 기본연봉 + (자격증 수 * 20만원), 연봉계산 메서드를 엔지니어의 연봉에 맞게끔 오버라이딩 함.
• 직원 클래스 호출하기
Employee emp = new Employee("홍길동", 3000);
System.out.println("Employee 정보 : " + emp.getEmployee());
emp.salaryCalculation();
– Employee 클래스의 인스턴스를 생성, 파라미터 생성자에 알맞은 데이터를 입력함.
– 직원 정보를 알기 위해 참조변수명으로 해당 인스턴스에 접근하여 getEmployee() 메서드를 호출하고,
직원의 연봉을 알기 위해 salaryCalculation() 메서드도 호출함.
✓ 실행 결과
Employee 정보 : 홍길동, 3000
연봉 : 3000
• 관리자 클래스 호출하기
Manager man = new Manager("이순신", 4000, "개발팀", 3);
System.out.println("Manager 정보 : " + man.getManager());
man.salaryCalculation();
– Manager 클래스의 인스턴스를 생성하고, 해당 파라미터 생성자에 알맞은 데이터를 입력함.
– Manager의 정보를 알기 위해 참조변수명으로 해당 인스턴스에 접근하여 getManager() 메서드를 호출하고,
연봉을 알기 위해 Manager의 연봉 조건에 알맞게 오버라이딩 된 salaryCalculation() 메서드도 호출함.
✓ 실행 결과
Manager 정보 : 이순신, 4000, 개발팀, 3
연봉 : 4030
• 엔지니어 클래스 호출하기
Engineer eng = new Engineer("강감찬", 5000, 5);
System.out.println("Engineer 정보 : " + eng.getEngineer());
eng.salaryCalculation();
– Engineer 클래스의 인스턴스를 생성하고, 해당 파라미터 생성자에 알맞은 데이터를 입력함.
– Engineer의 정보를 알기 위해 참조변수명으로 해당 인스턴스에 접근하여 getEngineer() 메서드를 호출하고,
연봉을 알기 위해 Engineer의 연봉 조건에 알맞게 오버라이딩 된 salaryCalculation() 메서드도 호출함.
✓ 실행 결과
Engineer 정보 : 강감찬, 5000, 5개
연봉 : 5100
• 업캐스팅
Employee emp2 = new Engineer("박보검", 10000, 5);
System.out.println("Engineer 정보 : " + emp2.getEmployee());
emp2.salaryCalculation();
– Engineer타입의 인스턴스를 Employee 타입의 emp2에 저장함 → 업캐스팅
– 업캐스팅이 되면서 참조변수의 범위가 좁아졌으므로, 서브클래스의 getEngineer() 메서드를 사용하지 못함.
→ 슈퍼클래스로 부터 상속받은 생성자와 메서드만 사용 가능.
✓ 실행 결과
Engineer 정보 : 박보검, 10000
연봉 : 10100
• 다형성 활용
– 전 직원의 연봉을 슈퍼클래스인 Employee 클래스에서 모두 계산
→ Employee, Manager, Engineer 인스턴스 모두 처리해야 하므로 다형성 필요.
→ 메서드 파라미터로 다형성을 적용한(서브클래스의 인스턴스를 모두 참조하는) Employee타입의 레퍼런스가 필요.
→ 메서드 호출시 파라미터에 해당 서브클래스의 인스턴스를 입력하면 '업캐스팅' 됨.
class Employee{
// salaryCalculationAll() 메서드 정의
public void salaryCalculationAll(Employee emp) { // 업캐스팅 됨
int salaryResult = 0; // 연봉 계산 결과를 저장할 변수
– 참조영역의 축소로 인해 각 서브클래스 타입의 멤버변수는 보이지 않음.
→ Employee를 제외한 Manager, Engineer의 경우 다시 다운캐스팅을 통해 서브클래스의 멤버에 접근해야 함.
– 단, 무작정 다운캐스팅을 수행할 경우 오류가 발생할 수 있으므로
반드시 instanceof 연산자를 통해 타입 판별 후 다운캐스팅 필요.
→ 오류가 발생하면 프로그램이 중단되므로 instanceof 연산자로 안전장치를 거는 것.
if(emp instanceof Manager) {
System.out.println("Employee → Manager로 다운캐스팅 가능");
Manager man = (Manager)emp;
salaryResult = man.salary + (man.manageEmployeeCount * 10);
– emp 인스턴스는 Manager타입인가? 조건이 참이라면 다운캐스팅 가능.
– 자동형변환 불가하므로 강제형변환 필수.
– 업캐스팅으로 축소됐던 참조영역이 다운캐스팅으로 확대되었으므로,
다운캐스팅 된 Manager 타입 인스턴스를 통해 모든 멤버 접근 가능.
→ 기본 연봉과 관리 인원 수에 따른 인센티브를 더해서 계산 가능.
}else if(emp instanceof Engineer) {
System.out.println("Employee → Engineer로 다운캐스팅 가능");
Engineer eng = (Engineer)emp;
salaryResult = eng.salary + (eng.numOfCertificate * 20);
– emp 인스턴스는 Engineer타입인가? 조건이 참이라면 다운캐스팅 가능.
– 자동형변환 불가하므로 강제형변환 필수.
– 업캐스팅으로 축소됐던 참조영역이 다운캐스팅으로 확대되었으므로,
다운캐스팅 된 Engineer 타입 인스턴스를 통해 모든 멤버에 접근 가능.
– 기본 연봉과 자격증 수에 따른 인센티브를 더해서 계산.
}else if(emp instanceof Employee) {
System.out.println("Employee 그대로 사용");
salaryResult = salary;
}
– emp 인스턴스는 Employee타입인가? 조건이 참이라면 다운캐스팅 가능.
→ Employee 그대로 사용.
– 주의! 반드시 하위타입부터 판별을 수행해야 함.
→ 범위가 제일 큰 타입부터 조건으로 설정하면, 첫번째 if문이 무조건 참이 되므로 아래의 조건에 접근이 아예 안됨.
// 각 직원의 계산된 연봉 출력
System.out.println("연봉 : " + salaryResult + "만원 입니다.");
}
}
• 메서드 파라미터로 인스턴스 전달
emp.salaryCalculationAll(emp); // new Employee("홍길동", 3000);
emp.salaryCalculationAll(man); // new Manager("이순신", 4000, "개발팀", 3);
emp.salaryCalculationAll(eng); // new Engineer("강감찬", 5000, 5);
– 위에서 인스턴스 생성 시 파라미터 생성자에 입력한 데이터들이 인스턴스(참조변수)에 저장되었으므로
메서드 파라미터에 해당 인스턴스를 전달해줌.
✓ 실행 결과
Employee 그대로 사용
연봉 : 3000만원 입니다.
Employee → Manager로 다운캐스팅 가능
연봉 : 4030만원 입니다.
Employee → Engineer로 다운캐스팅 가능
연봉 : 5100만원 입니다.