일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- CPU 스케줄링
- 트랜잭션
- 쉬운 코드
- 김영한
- 시그널 핸들러
- 운영체제
- 백엔드
- 반효경
- Extendable hashing
- 시스템프로그래밍
- vite
- 코딩테스트 [ ALL IN ONE ]
- SDK
- 코딩애플
- 쉬운코드
- 프로세스 주소 공간
- 인터럽트
- 갤럭시 S24
- recoverability
- B tree 데이터삽입
- 커널 동기화
- concurrency control
- 온디바이스AI
- Git
- 데이터베이스
- BreadcrumbsComputer-Networking_A-Top-Down-Approach
- SQL
- 네트워크
- 개발남노씨
- 운영체제와 정보기술의 원리
- Today
- Total
티끌모아 태산
10. 다형성(1) 본문
좋은 개발자가 되기 위해서는 다형성에 대한 이해가 필수다. 다형성(Polymorphism)은 말 그대로 "다양한 형태", "여러 형태"를 뜻한다. 보통 하나의 객체는 하나의 타입으로 고정된다. 그런데 다형성을 사용하게 되면 하나의 객체가 여러 타입으로 사용될 수 있다.
다형성을 이해하기 위해서는 다형적 참조와 메서드 오버라이딩이라는 핵심 이론을 알아야 한다.
다형적 참조
다형적 참조의 핵심은 부모는 자식을 품을 수 있다는 것이다.
ex1) 부모 타입의 변수가 부모 인스턴스 참조
Parent -> Parent: parent.ParentMethod()
- 부모 타입의 변수가 부모 인스턴스를 참조한다.
- Parent parent = new Parent();
- Parent 인스턴스를 만들었다. 부모 타입인 Parent를 생성했기 때문에 메모리 상에 Parent만 생성된다. (자식은 생성되지 않는다.)
- 생성된 참조값을 Parent 타입의 변수인 parent에 담아 둔다.
- parent.parentMethod()를 호출하면 인스터스의 Parent 클래스에 있는 parentMethod()가 호출된다.
ex2) 자식 타입의 변수가 자식 인스턴스 참조
Child -> Child: child.childMethod()
- 자식 타입의 변수가 자식 인스턴스를 참조한다.
- Child child = new Child();
- Child 인스턴스를 만들었다. 이 경우 자식 타입인 Child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성된다.
- 생성된 참조값을 Child 타입의 뱐수인 child에 담아 둔다.
- child.childMethod()를 호출하면 인스턴스의 Child 클래스에 있는 childMethod()가 호출된다.
⭐ex3) 다형적 참조: 부모 타입의 변수가 자식 인스턴스 참조
Parent -> Child: poly.parentMethod()
- 부모 타입의 변수가 자식 인스턴스를 참조한다.
- Parent poly = new Child();
- Child 인스턴스를 만들었다. 이 경우 자식 타입인 Child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성된다.
- 생성된 참조값을 Parent 타입인 poly에 담아 둔다.
⭐부모는 자식을 담을 수 있다.
- 부모 타입은 자식 타입을 담을 수 있다. 반대로 자식 타입은 부모 타입을 담을 수 없다.
- Parent poly = new Child() 는 가능하다. 반대로 Child child1 = new Parent()는 컴파일 오류! 이처럼 Parent 타입은 자신은 물론이고, 자식 타입까지 참조할 수 있다.
- Parent poly = new Parent(), Parent poly = new Child(), Parent poly = new Grandsom() (Child 하위에 손자가 있다면 가능)
따라서 자바에서 부모 타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이것이 바로 다양한 형태를 참조할 수 있다고 해서 다형적 참조라고 한다.
다형적 참조와 인스턴스 실행
다형적 참조의 한계
Parent -> Child: poly.childMethod()
Parent poly = new Child() 처럼 자식 인스턴스를 참조하고 있는 상황에서 poly가 자식 타입인 Child에 있는 childMethod()를 호출하면 어떻게 될까? 부모 타입의 참조변수로 자식 인스턴스를 참조할 수 있다. 하지만 자식의 기능은 호출할 수 없다.
- poly.childMethod()를 실행하면 참조값을 통해 인스턴스를 찾는다.
- 인스턴스 안에서 실행할 타입을 찾아야 한다. 이때, 호출자 타입을 기준으로 선택한다. 그러므로 Parent 클래스부터 시작해서 필요한 기능을 찾는다.
- 상속 관계는 부모 방향으로 찾아 올라갈 수 있지만 자식 방향으로 찾아 내려갈 수 는 없다.
- Parent는 부모 타입이고 상위에 부모가 없다. 그러므로 childMethod()를 찾을 수 없으므로 컴파일 오류가 발생한다.
그렇다면 어떻게 childMethod를 호출할 수 있을까? 정답은 캐스팅! 캐스팅이란 특정 타입으로 변경하는 것이다.
다형성과 캐스팅
다운캐스팅
Child child = (Child) poly // Parent poly
(타입) 처럼 괄호와 그 사이에 타입을 지정하면 참조 대상을 특정 타입으로 변경할 수 있다. 위에서 말했듯이 부모는 자식을 담을 수 있지만 자식은 부모를 담을 수 없다. 이때, 다운캐스팅이라는 기능을 활용해서 부모 타입을 잠깐 자식 타입으로 변경하면 된다.
poly는 Parent 타입니다. 이 타입을 (Child)를 사용해서 일시적으로 자식 타입인 Child 타입으로 변경한다. 그리고 나서 Child child에 대입한다.
구체적인 실행 순서는 다음과 같다.
- Child child = (Child) poly // 다운캐스팅을 통해 부모타입을 자식 타입으로 변환한 다음에 대입 시도
- Child child = (Child) x001 // 이때, poly 자체를 변경하는 것이 아니라 참조값을 꺼내서 읽은 다음 자식 타입으로 지정
- Child child = x001 // 최종결과
❗캐스팅을 한다고해서 Parent poly의 타입이 변하는 것은 아니다. 해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이다. 따라서 poly의 타입은 Parent로 기존과 동일하다.
캐스팅의 종류
- 업캐스팅: 자식을 부모 타입으로 변경
- 다운캐스팅: 부모를 자식 타입으로 변경
// 다운캐스팅(부모 타입 -> 자식 타입)
Child child = (Child)poly; // x001
child.childMethod();
//일시적 다운캐스팅 -> 해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod();
이때도 poly가 Child 타입으로 바뀌는 것은 아니다.
((Child) poly).childMethod(); // 다운캐스팅을 통해 부모타입을 자식 타입으로 변환 후 기능 호출
((Child) x001).childMethod(); // 참조값을 읽은 다음 자식 타입으로 다운캐스팅
이렇게 일시적 다운 캐스팅을 사용하면 별도의 변수 없이 인스턴스의 자식 타입의 기능을 사용할 수 있다. 반대로 업캐스팅은 현재 타입을 부모 타입으로 변경하는 것이다.
Parent parent1 = (Parent) child; // 업캐스팅은 생략 가능, 생략 권장
Parent parent2 = child; // 업캐스팅 생략, 자바가 알아서 해준다.
parent1.parentMethod();
parent2.parentMethod();
항상 부모는 자식을 담을 수 있다. 사실 이때, 자식도 부모로 타입 변환이 필요한데 이는 자바에서 자동으로 변환해 준다. 따라서 (Parent) 부분이 생략이 가능하다.
- 업캐스팅은 생략할 수 있다. 다운캐스팅은 생략할 수 없다. 참고로 업캐스팅은 매우 자주 사용하기 때문에 생략을 권장한다. 다시말하지만 부모는 자식을 담을 수 있다. 하지만 반대는 안된다. (꼭 필요하다면 다운캐스팅을 해야한다.)
그렇다면 다운캐스팅은 왜 개발자가 직접 명시적으로 캐스팅을 해야할까?
다운캐스팅과 주의점
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); // 문제 없음
Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException
child2.childMethod(); // 실행 불가
다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있다. *런타임 오류는 말 그대로 프로그램이 실행되고 있는 시점에서 발생하는 오류다. 런타임 오류는 매우 안좋은 오류다 왜냐하면 보통 고객이 해당 프로그램일 실행하는 도중에 발생하기 때문이다.
다운 캐스팅이 가능한 경우
parent1의 경우 다운캐스팅을 해도 문제가 되지 않는다. 하지만 아래 경우는 다운캐스팅이 불가능 하다.
parent2를 Child 타입으로 다운캐스팅한다. 하지만 parent2는 Parent로 생성되었다. 따라서 메모리 상에 Child 자체가 존재하지 않는다. 즉, Child 자체를 사용할 수 없다. 자바에서는 사용할 수 없는 타입으로 다운캐스팅을 하는 경우에 ClassCastException 이라는 예외를 발생시킨다.
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
업캐스팅은 다운캐스팅과 다르게 이런 문제가 발생하지 않는다. 왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 생성된다. 따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 안전하다. 반면에 다운 캐스팅은 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있다. 왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않는다. 그렇기 때문에 이런 문제를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅 해줘야 한다.
클래스 A, B, C는 상속 관계다. 그러므로 new C()로 인스턴스를 생성하면 메모리 상에 즉, 인스턴스 내부에 자신과 부모인 A,B,C가 모두 생성된다.
하지만 new B()를 하게 되면 인스턴스 내부에 자기 자신인 B와 부모인 A만 생성된다. 즉 C는 B 인스턴스 내부에 존재하지 않기 때문에 C로 다운 캐스팅을 하게 되면 오류가 발생한다. 왜냐하면 C 자체는 존재하지 않기 때문이다.
- A a = new B() : A로 업케스팅
- B b = new B() : 자신과 같은 타입
- C c = new B() : 하위 타입은 대입할 수 없음, 컴파일 오류
- C c = (C) new B() : 하위 타입으로 강제 다운캐스팅, 하지만 B 인스턴스에 C와 관련된 부분이 없으므로 잘못된 캐스팅, ClassCastException 런타임 오류 발생
컴파일 오류 vs 런타임 오류
컴파일 오류는 변수명 오타, 잘못된 클래스 이름 사용 등 자바 프로그램을 실행하기 전에 발생하는 오류다. 이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류다. 반면에 런타임 오류는 프로그램이 실행되는 도중에 발생하는 오류로 좋은 않은 오류다. 왜냐하면 고객이 사용하는 도중에 오류가 발생하기 때문이다.
instanceof
다형성에서 참조형 변수가 참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하고 있는지 확인할 필요가 있다. 이때, instanceof 키워드를 사용한다.
Parent parent1 = new Parent();
Parent parent2 = new Child();
여기서 Parent는 자신과 같은 Parent의 인스턴스도 참조할 수 있고, 자식 타입인 Child의 인스턴스도 참조할 수 있다. 이때, parent1, parent2 변수가 참조하는 인스턴스의 타입을 확인하고 싶다면 instanceof 키워드를 사용할 수 있다.
Parent parent1 = new Parent();
System.out.println("parent1 호출");
call(parent1);
Parent parent2 = new Child();
System.out.println("parent2 호출");
call(parent2);
}
private static void call(Parent parent){
parent.parentMethod();
// parent가 Child 인스턴스 인 경우 childMethod() 실행
if (parent instanceof Child){
System.out.println("Child 인스턴스 맞음");
((Child) parent).childMethod();
}
}
자바 16 - Pattern Matching for instanceof
자바 16부터는 instanceof를 사용하면서 동시에 변수를 선언할 수 있다.
private static void call(Parent parent){
parent.parentMethod();
// Child 인스턴스 인 경우 childMethod() 실행
if (parent instanceof Child child){
System.out.println("Child 인스턴스 맞음");
// 생략 가능
//Child child = (Child) parent;
child.childMethod();
}
}
다형성과 메서드 오버라이딩
메서드 오버라이딩의 핵심은 "오버라이딩 된 메서드가 항상 우선권을 가진다." 메서드 오버라이딩의 진짜 힘은 다형성과 함께 사용할 때다.
// 부모 변수가 자식 인스턴스를 참조(다형적 참조)
Parent poly = new Child();
System.out.println("Parent -> Child");
System.out.println("value = " + poly.value); // 변수는 오버라이딩 x
poly.method(); // 메서드는 오버라이딩!
- poly 변수는 Parent 타입이다. 따라서 poly.value, poly.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아 실행한다.
- poly.value: Parent 타입에 있는 value 값을 읽는다. 즉, 변수(필드)는 오버라이딩 x
- ⭐poly.method(): Parent 타입에 있는 method()가 아니라 하위 타입으로 오버라이딩 된 Child.method()를 호출한다. 따라서 오버라이딩 된 메서드가 항상 우선권을 갖는다.
만약, 자식에서도 오버라이딩 하고 손자에서도 같은 메서드를 오버라이딩을 하는 경우 손자의 오버라이딩 메서드가 우선권을 갖는다. 즉, 더 하위 자식의 오버라이딩 된 메서드가 우선권을 갖는 것이다.
- 다형적 참조: 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있다.
- 메서드 오버라이딩: 기존 기능을 하위 타입에서 새로운 기능으로 재정의
'백엔드 > JAVA' 카테고리의 다른 글
다형성2 (0) | 2024.06.01 |
---|---|
9. 상속 (0) | 2024.05.30 |
2. 기본형과 참조형 (0) | 2024.03.04 |