다형성
객체 지향 프로그래밍의 5가지 항목중에 하나인 다형성에 대해서 오늘을 서술하고자 한다.
다형성은 하나의 객체가 여러개의 형태를 가질 수 있는 의미를 담고 있는 말로 객체 지향 언어인 자바에서는
부모 클래스 타입의 참조 변수로 자식 클래스의 인스턴스를 참조할 수 있도록 구현을 했다.
다형성 예제 #1
class Parent {
public void classInfo() {
System.out.println("저는 부모클래스 입니다.");
}
}
class FirstChild extends Parent {
public void classInfo() {
System.out.println("저는 자식1 클래스 입니다.");
}
}
class SecondChild extends Parent {
public void classInfo() {
System.out.println("저는 자식2 클래스 입니다.");
}
}
public class ClassTest {
public static void main(String[] args) {
Parent parent = new Parent(); // 객체 타입과 참조변수 타입의 일치
FirstChild firstchild = new FirstChild();
Parent secondchild = new SecondChild(); // 객체 타입과 참조변수 타입의 불일치
//SecondChild secondchild = new Parent(); // 하위클래스 타임의 상위 클래스 객체 참조
parent.classInfo();
firstchild.classInfo();
secondchild.classInfo();
}
}
// Output
저는 부모클래스 입니다.
저는 자식1 클래스 입니다.
저는 자식2 클래스 입니다.
위의 예제를 통해 알 수 있는 사실은 상위클래스의 참조 변수를 통해 하위 클래스의 객체를 참조할 수 있다.
상위클래스를 참조 변수로 지정했기 때문에 당연하게도 참조변수가 사용가능한 멤버의 개수는 상위 클래스의 멤버 수가된다.
주석처리된 하위에서 상위의 객체 참조에서 오류를 일으키는 것으로 보았을때 우리가 추측할 수 있는건
상위에서 하위는 참조할 수 있지만 반대의 경우는 안된다는것을 확인된다 그 이유는 멤버의 개수와 상관이 있는데
하위에서 상위를 참조하게 될 경우 하위의 멤버의 개수가 많은데 참조중인 인스턴스에는 구현되지 않은 기능이 있기 때문이다.
전의 블로그 포스트중에 서술하였던 메서드 오버라이딩/오버로딩이 둘 다 동일한 이름의 메서드를 재사용이나 덮어 쓰기등
다르게 사용한다는 점도 다형성과 비슷한 개념이기도 하다.
참조변수의 경우 타입 변환을 지원하고 있다.
사용하고자 하는 멤버의 개수를 조절하는 것에 대해서 의미하고 있는데 다형성에 있어서 중요한 요소라고 할 수 있다.
타입의 변환을 사용하려고 하면 3가지의 조건이 충족되면 되는데
- 서로 상속관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능하다.
- 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자(괄호)를 생략할 수 있다.
- 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)를 반드시 명시해야한다.
이를 즉 업캐스팅과 다운캐스팅으로 명명하고 있다. 하여간 위의 조건에서 보면 상속자들 간에서만 변환이 이루어지고 있다.
타입 변환 예제 #1
public class VehicleTest {
public static void main(String[] args) {
Car car = new Car();
Vehicle vehicle = (Vehicle) car; // 상위 클래스 Vehicle 타입으로 변환(생략 가능)
Car car2 = (Car) vehicle; // 하위 클래스 Car타입으로 변환(생략 불가능)
MotorBike motorBike = (MotorBike) car; // 상속관계가 아니므로 타입 변환 불가 -> 에러발생
}
}
class Vehicle {
String model;
String color;
int wheels;
void startEngine() {
System.out.println("시동 걸기");
}
void accelerate() {
System.out.println("속도 올리기");
}
void brake() {
System.out.println("브레이크!");
}
}
class Car extends Vehicle {
void giveRide() {
System.out.println("다른 사람 태우기");
}
}
class MotorBike extends Vehicle {
void performance() {
System.out.println("묘기 부리기");
}
}
예제에서는 타입 형변환에 대한 예시를 보이고 있다
Vehicle을 정의하고 Car와 MotorBike가 클래스들이 상속받고 있는 상황이다
하위 클래스가 상위 클래스를 변환하고자 하면 형변환을 생략할 수 있고
상위 클래스가 하위 클래스로 변환하고자 하면 형변환을 생략할 수 없다.
instanceof 연산자
앞서 설명한 캐스팅의 유무를 boolean 타입으로 True/False로 받을 수 있는 자바의 문법요소로
규모가 방대한 프로젝트의 경우는 일일히 확인하는데 시간을 할애할 수 없을것이다. 그래서 프로젝트 진행시
instanceof 연산자 를 통해서 형변환 여부를 체크해서 에러가 나타날 수 있는 위험성을 줄일 수 있다.
추상화
추상화는 기존 클래스들의 공통적인 요소들을 뽑아서 상위 클래스를 만들어 내는것 이라고 할 수 있다.
abstract 제어자를 통해 선언할 수 있는데 가지고 있는 의미는 미완성이다.
주로 메서드나 클래스에 형용하는 키워드로 사용되는데 앞에 붙으면 추상 메서드와 추상 키워드라고 각각 부르게 된다.
abstract class AbstractExample { // 추상 메서드가 최소 하나 이상 포함돼있는 추상 클래스
abstract void start(); // 메서드 바디가 없는 추상메서드
}
AbstractExample abstractExample = new AbstractExample(); // 에러발생.
아주 중요한 가장 핵심적인 개념은 미완성 즉 추상적이라는 형용사가 가진 의미를 생각해보면
추상 클래스와 추상 메서드는 미완성된 클래스 그리고 미완성된 메서드이다. 즉 미완성된 설계도라고 할 수 있다.
추상 클래스
앞서 예시를 통해 미완성 설계도라고 추상 클래스를 거론한 이유는 추상 클래스를 통해서 자바 프로그램을 개발함에 있어서
많은 이점이 있기도 하다.
추상 클래스를 통해서 새로운 클래스를 작성하는데 매우 유용한데 먼저 상위 클래스를 추상 클래스를 구현하고
하위 클래스를 구현하면서 유연하게 설계 변경에 대체할 수도 있다.
추상 클래스 예제 #1
abstract class Animal {
public String kind;
public abstract void sound();
}
class Dog extends Animal { // Animal 클래스로부터 상속
public Dog() {
this.kind = "포유류";
}
public void sound() { // 메서드 오버라이딩 -> 구현부 완성
System.out.println("멍멍");
}
}
class Cat extends Animal { // Animal 클래스로부터 상속
public Cat() {
this.kind = "포유류";
}
public void sound() { // 메서드 오버라이딩 -> 구현부 완성
System.out.println("야옹");
}
}
class DogExample {
public static void main(String[] args) throws Exception {
Animal dog = new Dog();
dog.sound();
Cat cat = new Cat();
cat.sound();
}
}
// 출력값
멍멍
야옹
위의 예제에서는 Animal 클래스와 내부 메서드를 추상화 했고 그 뒤에 상속받는 Dog와 Cat 클래스 안에
Animal의 추상 메서드인 sound() 를 오버라이딩해서 메서드를 호출해서 메세지를 출력하고 있다.
이와 같이 추상 클래스를 통해서 각 상황에 맞는 메서드 구현이 가능하기도 하며 OOP개념의 하나인 추상화를 구현하는데 있어서
핵심적인 역활을 가지고 있기도 하다
추상화는 객체의 공통적인 속성(클래스)과 기능(메서드)을 추출하여 정의하는것 이라 정리할 수 있다.
이를 통해서 여러사람과의 협업을 할때 발생될 수 있는 여러가지 문제들을 미연에 방지할 수 있기도 하다.
즉 공통적 요소들을 묶어서 하나의 상위 클래스를 만드는 설계도라고 이해하면 될거같다.
final 키워드
인터페이스에는 추상 클래스들만이 모여있다고는 하는데 사실은 아니다 사용상의 편의를 위해 자바의 특정버전에 추가된 개념이 있는데
그것들의 설명은 나중에하고 그중에 하나인 final 키워드에 대해 간편하게 설명하고자 한다.
final class FinalEx { // 확장/상속 불가능한 클래스
final int x = 1; // 변경되지 않는 상수
final void getNum() { // 오버라이딩 불가한 메서드
final int localVar = x; // 상수
return x;
}
}
final 키워드를 한마디로 정의하자면 즉 값을 변경할 수 없고 확장하지도 못하는 상수라고 할 수 있다.
인터페이스
자바에서의 추상화에 중요한 역활을 하는 인터페이스는 앞에서 서술해둔 추상클래스에 비하여 더 높은 추상성을 가지고 있다.
추상클래스는 메서드 바디가 없는 추상 메서드를 포함하고 있는 클래스로 일반 클래스와는 다를게 거의 없다.
반면에 인터페이스는 추상메서드와 (final)상수만을 멤버로 가질 수 있는 추상 메서드의 집합이라고 부른다.
public interface InterfaceEx {
public static final int rock = 1; // 인터페이스 인스턴스 변수 정의
final int scissors = 2; // public static 생략
static int paper = 3; // public & final 생략
public abstract String getPlayingNum();
void call() //public abstract 생략
}
위의 예제는 인터페이스의 선언에 대한 예제이다. 상수와 추상 메서드로만 구성되있는걸 볼 수 있다.
주석의 설명대로 실사용때는 public과 final 키워드를 생략해서 사용할 수 있다 컴파일러에서 자동으로 추가해준다는 의미이다.
인터페이스의 구현
그렇다면 선언한 인터페이스를 구현하는 방법은 아래와 같다.
class 클래스명 implements 인터페이스명 {
... // 인터페이스에 정의된 모든 추상메서드 구현
}
여기서 유의해야 할 점은 인터페이스에 정의한 모든 추상 메서드는 구현되어야 한다.
이 의미는 정의한 클래스에 인터페이스를 구현한다는건 그 클래스에 있어서 구현한 인터페이스의 추상 메서드를 강제적으로 구현하도록 한다는것이다. 위에서 추상 클래스 설명시 메서드 오버라이딩에 대해서 이야기를 했는데 이 이야기와 궤를 같이 한다고 볼 수 있다,
즉 가지고 있는 모든 추상 메서드들을 정의한 클래스 내에서 오버라이딩 해 완성 시킨다는 의미라고 할 수 있을것이다,
인터페이스의 다중 구현
상속에 대해서 우리는 배울때 자바는 단일 상속만을 지원한다고 했다. 하지만 인터페이스는 다중 구현을 지원하고 있다.
class ExampleClass implements ExampleInterface1, ExampleInterface2, ExampleInterface3 {
... 생략 ...
}
즉 하나의 클래스에서 여러가지의 인터페이스를 구현할 수 있다 다만 인터페이스는 인터페이스로만 상속이 가능하다는 점은
잊지 말아야 한다 인터페이스는 Object 같은 최고 조상이 존재하지 않는다.
그렇디면 다중 구현이 가능한 이유는 무엇일까? 그것의 해답은 바로 추상화 개념으로 만들어졌기 때문이다.
즉 인터페이스는 추상 메서드로만 만들어져 있기 때문에 서로 충돌될 가능성이 없다 클래스 들의 경우는 만약에
멤버들의 이름이 같을경우 충돌이 발생하여 에러가 출력되게 된다. 추가적으로 상속과 범용으로 사용하기도 한다.
인터페이스를 사용하는 이유?
이미 상속이나 추상 클래스의 개념도 존재하는데 왜 인터페이스를 쓰려고 하는가?
그것은 바로 인터페이스가 가지고 있는 큰 장점인 기능의 역할과 구현을 분리하여 복잡한 구현의 소스들을 변경부분과
상관없이 기능을 사용할 수 있다는 점이다. 코드의 유지 보수성이 높아진다는 의미이다.
인터페이스 사용에 대한 예제를 보자
interface Cover { // 인터페이스 정의
public abstract void call();
}
public class Interface4 {
public static void main(String[] args) {
User2 user2 = new User2();
// Provider provider = new Provider();
// user2.callProvider(new Provider());
user2.callProvider(new Provider2());
}
}
class User {
public void callProvider(Cover cover) { // 매개변수의 다형성 활용
cover.call();
}
}
class Provider implements Cover {
public void call() {
System.out.println("무야호~");
}
}
class Provider2 implements Cover {
public void call() {
System.out.println("야호~");
}
}
//출력값
야호~
인터페이스 Cover를 선언했고 Interface4에서 user2 참조 변수로 객체를 만들어서 각 Provider 기능들을 호출하고 있습니다.
코드의 수정없이 여러가지 클래스들을 추가와 수정 할 수 있개 된것입니다.
이처럼 인터페이스를 통해서 OOP 개념중의 하나인 추상화의 개념을 즉 코드 변경의 복잡함과 번거로움을 최소화하고 기능 구현에 대한 업무의 능률을 높일 수 있게 된다.