다형성

이동영·2024년 3월 11일

자바 개념정리

목록 보기
18/21
  • 다양한 형태 여러 형태를 뜻하며 프로그래밍에서 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다.
  • 다형성을 이해하기 위해서는 다형적 참조 메서드 오버라이딩 두가지를 이해해야 한다.

다형적 참조

  • 부모타입의 변수가 자식 인스턴스를 참조할 수 있다.
  • 자식 인스턴스는 메모리 내부에 부모와 자식 두가지 생성이 되는데 이렇게 생성된 참조값을 부모 타입 변수에 담는다.
  • 부모는 자식을 담을 수 있다. 부모는 자식을 얼마든지 품을 수 있다. 하지만 자식들은 속을 썩이기 때문에 부모를 담을 수 없다.
  • 부모 타입은 본인만 알고 있다. 자식 메서드 자체를 호출 할 수 없다.
  • 자바에서는 자신을 기준으로 상속받고 있는 자식 손자들은 모두 다향한 형태로 참조를 할 수 있는데 이것을 다형적 참조라고 한다.
  • 변수에 자식 객체가 들어있다고 하더라도 부모 타입으로 선언된 변수일때 참조값을 사용해 인스턴스에 접근하면 부모타입의 객체부터 매소드를 찾게 되며 이때 메소드가 없다면 위로 올라가서 찾지만 아래로 내려가서 메소드를 찾지는 않는다. 왜냐하면 부모 클래스에는 자식에 대한 정보가 하나도 없기 때문이다. 그래서 자바가 인식을 하지 못하며 컴파일 오류가 발생한다.

다형성 과 케스팅

  • 부모타입으로 선언하여 초기화한 자식 타입의 객체를 참조해보면 자식타입의 메소드를 사용할 수 없다. 타입이 부모 타입이기 때문에 부모타입에 선언된 메서드만 호출할 수 밖에 없다. 상속관계에서 찾는 메소드가 없다면 부모쪽으로 올라가서 찾아 사용할 수 있지만 자식 타입 쪽으로 내려가지는 않는다. 하지만 부모 타입으로 선언된 객체로 자식 타입의 메서드를 사용하고 싶다면 다운케스팅이라는 방법을 이용하여 자식 타입의 메소드를 사용할 수 있다.
  • 자식 타입으로 변수를 선언하고 부모타입으로 선언된 변수로 다시 대입하면 참조값을 복사하여 초기화 할 수 있다. 하지만 여기서 컴파일 오류가 발생하게 되는데 이때 다운 케스팅을 사용하여 부모타입 변수 앞에 선언하여 부모타입을 일시적으로 자식 타입으로 바꾼다면 컴파일 오류 없이 정상적으로 자식 타입의 메소드를 호출할 수 있다.

캐스팅의 종류

  • 다운캐스팅을 하여 자식으로 선언한 변수에 담기까지의 과정이 너무 번거롭다. 그래서 일시적으로 다운케스팅을 하여 인스턴스에 있는 하위 클래스의 기능을 바로 호출 할 수 있다.
  • 부모타입인 poly를 일시적 다운케스팅을 하여 자식 타입의 메소드를 호출할 수 있다. 그런데 아예 poly자체를 자식타입으로 바꾸어 버리는것이 아닌 poly안에 들어있는 참조값을 꺼내서 복사하여 자식타입으로 케스팅하여 메소드를 호출하는 것이다.
  • 일시적 다운케스팅을 이용하면 자식 타입으로 변수를 별도로 선언하지 않고도 다운 케스팅을 사용할 수 있다.

업 케스팅

  • 자바는 업케스팅 시 부모타입을 명시하는것을 생략할 수 있으며 이것을 권장한다.

다운케스팅은과 주의점

  • 다운케스팅을 잘못 사용하면 심각한 런타임 오류가 발생할 수 있다.
  • 문제가 생길 수 있는점 때문에 자바가 다운케스팅시 명시적으로 케스팅 키워드를 붙이도록 하였다.
  • 부모 타입으로 선언하고 생성된 객체도 부모타입으로만 생성을 하였다. 그런데 거기에 자식 타입으로 다운케스팅을 하였을 때 컴파일 오류 없이 잘 되는것 처럼 보였다. 거기에 참조값으로 인스턴스에 접근하면 자식 메소드를 불러와도 컴파일 오류가 발생하지 않았는데 막상 실행해본다면 ClassCastExaption이 발생하였다. 이것은 바로 인스턴스 내부에 부모 객체만 있고 자식 객체는 존재하지 않기 때문이다.
  • 자바에서는 사용할 수 없는 타입으로 다운캐스팅을 한다면 ClassCastExaption이 발생한다. 메모리 내부에는 자식 타입의 객체가 없기 때문에 런타임오류가 발생하는것이다.

업케스팅이 안전하고 다운케스팅이 위험한 이유

  • 업 케스팅을 한 경우 자식이 먼저 메모리에 생성이 되고 그 위에 상위 부모들이 순차적으로 생성이 된다. 그렇기에 메모리상에 자식부터 해서 부모타입의 인스턴스가 모두 존재하기에 안전하다.
  • 자바에서는 메모리에 인스턴스가 여러개 있을 수 있다고 하였다. 그렇기에 호출하고자 하는 메서드가 있다면 자식부터 시작하여 위로 올라가면서 메서드를 호출하는데 애초에 자식이 생성되면 부모가 생성되는것이 보장이 되며 부모가 없어서 오류날일이 없기 때문에 업케스팅시 키워드를 생략하도록 하였다.
  • 다운케스팅시 메모리에 자식이 없을 수도 있다. 그렇게 되면 없는 인스턴스를 참조하도록 하면 당연히 오류가 발생하는것이다. 그렇기에 인스턴스가 있는지 없는지 인지하기 위해 무조건 다운케스팅시 키워드를 사용하도록 한것이다.
  • ClassCastExaption이 발생하였다면 상위 클래스가 다운케스팅을 하여 없는 인스턴스의 메소드 혹은 필드를 사용한 것이다.

컴파일오류 vs 런타임오류

  • 컴파일 오류는 오타등등 프로그램 시작전에 잡아줄 수 있는 오류로 제일 좋은 오류이다. 하지만 런타임 오류는 프로그램이 실행되는 도중 발생하는 오류로 안좋은 오류이다. 고객이 프로그램을 사용하는 도중 서버에서 오류가 생기면 큰일이 나기 때문이다.

instanceof

private static void call(Parent parent ) {
        parent.parentMethod();
        if(parent instanceof Child) {
            System.out.println("Child 인스턴스가 맞음");
            Child child = (Child) parent;
            child.childMethod();
        }
  • 타입이 맞는지 확인하는 키워드이며 if문에서 사용할 수 있다. 이 키워들르 사용하면 안전하게 다운 케스팅을 할 수 있으며 부모타입으로 선언된 변수가 자식 객체의 경우에만 수행될 수 있도록 로직을 만들 수 있으며 이것을 사용하면 ClassCastExaption을 방지할 수 있다.
  • 이 메서드의 의도는 무조건적으로 parent의 메소드를 호출하지만 parent의 인스턴스가 child인 경우 안전하게 형변환을 하여 parent메소드도 호출할 수 있도록 하는것이다.
  • instanceof는 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 확인할 수도 있다. 즉 오른쪽에는 부모타입이고 왼쪽은 자식 타입이라면 true를 반환한다. 왜냐하면 부모는 자식을 품을 수 있기 때문이다.

다형성과 메서드 오버라이딩

  • 다형성을 이루는 또하나의 핵심이론은 메서드 오버라이딩가 다형적 참조이다.
  • 메서드 오버라이딩에서 꼭 기억해야할 점은 오버라이딩 된 메서드가 우선권을 가진다는 것이다.
  • 기존기능을 덮고 새로운 기능을 재정의 하는 뜻에서 오버라이딩 이다.
  • 메서드 오버라이딩은 다형적 참조와 같이써야 진짜 메서드 오버라이딩이다.
  • 부모타입의 변수에 자식객체를 넣고 메소드를 호출하였는데 그 호출한 메서드가 자식클래스에서 메서드 오버라이딩 된 메소드라면 부모 메소드가 호출되는것이 아닌 자식 메소드가 호출이 된다. 오버라이딩 된 메소드는 항상 우선권을 가지며 하위 자식에서 오버라이딩 할 경우 더 호출 우선권을 가지게 된다.
  • 다형성이라는것은 다형성 1과 2를 조합하여 사용하는것이다.

정리

  • 부모는 본인과 본인의 자식들을 참조할 수 있다.
  • 자식을 생성하면 인스턴스 안에 부모도 같이 생성이 된다.
  • 다형적 참조를 하면 본인 타입의 메서드만 만 호출할 수 있다.이것을 하려면 다운 캐스팅을 해야한다.

다형성 활용

package poly.ex1;

public class AnimalSoundMain {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
//        Caw caw = new Caw();

        System.out.println("동물 소리 테스트 시작");
        dog.sound();
        System.out.println("동물 소리 테스트 죵료");

        System.out.println("동물 소리 테스트 시작");
        cat.sound();
        System.out.println("동물 소리 테스트 죵료");

//        System.out.println("동물 소리 테스트 시작");
//        caw.sound();
//        System.out.println("동물 소리 테스트 죵료");

    }
}
  • 서로 다른 객체를 생성하여 메서드를 호출하지만 중복되는 부분이 있다. 이 중복되는 부분은 항상 for문이나 메서드를 활용하여 중복을 제거하곤 하였다. 하지만 중복을 제거하기에는 객체들이 서로 다른 타입이여서 중복을 for문으로 중복을 제거하기는 쉽지 않다.
  • 다형성의 핵심은 다형적 참조와 메서드 오버라이딩 이다. 이 둘을 사용하면 같은 타입을 사용하며 서로 다른 객체를 참조하여 각자의 메서드를 호출 할 수 있다.

다형성 활용2

package poly.ex2;

public class AnimalPolyMail1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Caw caw = new Caw();

        soundAnimal(dog);
        soundAnimal(cat);
        soundAnimal(caw);
    }

    private static void soundAnimal(Animal animal) {
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}
  • 이 코드의 핵심은 soundAnimal메서드 이다. animal매개변수의 타읍은 Animal클래스 이며 이 클래스는 Dog Cat Caw클래스의 부모 클래스로 부모 클래스로 선언된 매개 변수는 인자로 전달된 자식 객체를 품을 수 있다.
  • 그래서 메소드를 호출해보면 부모 메서드가 나오는것이 아닌 자식 객체의 메소드가 호출이 되었다. 인스턴스 내부에 가보면 Animal과 Dog 인스턴스 둘다 생성이 되어있다. 그런데 여기서 하위 객체의 메소드가 오버라이딩이 되어있으면 부모 메서드는 가려지고 자식객체의 메서드가 호출이 된다.
  • 다형적참조 덕분에 하나의 타입으로도 여러 객체의 메소드를 사용할 수 있게 된것이다.

다형성 활용3

package poly.ex2;

public class AnimalPolyMail3 {
    public static void main(String[] args) {
        Animal[] animalArr = {new Dog(), new Cat(), new Caw()};

        for (Animal animal : animalArr) {
            soundAnimal(animal);

        }
    }

    //변하지 않는 부분
    private static void soundAnimal(Animal animal) {
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }

}
  • 새로운 기능이 추가 되었을 때 변하는 부분이 최소화 된 코드가 잘 작성이 된 코드이다.
  • 이렇게 하기 위해서는 변하는 부분과 변하지 않는 부분을 명확히 구별하는것이 좋은것이다.
  • 코드를 바꾸었을 때 어디까지가 변경사항인가 했을 때 ex1번은 좋지 않은 예시이다.
  • 추가될것까지 예상하여 설계를 하면 좋은 코드를 잘 짤 수 있다.

남은문제

  • Animal클래스를 생성할 수 있는 문제
  • Animal클래스를 상속받고 오버라이딩을 하지 않는 문제

Animal클래스를 생성할 수 있는 문제

  • Animal클래스는 동물 클래스이지만 추상적인 개념이며 실체가 없는데 이것을 실체로 사용할 수 있는것은 아니다. 이렇게 생성된 객체는 재대로 동작할 수 있는 기능이 없다.

Animal클래스를 상속받고 오버라이딩을 하지 않는 문제

  • Animal을 상속받은 클래스에서 실수로 오버라이딩을 하지 않을수도 있다. 컴파일 오류도 없으며 코드상에서도 문제는 없다.
  • 그리고 나서 메서드를 호출해보면 pig메서드가 호출이 되는것이 아닌 부모의 메서드가 호출이 되었다. 왜냐하면 상속은 받았지만 오버라이딩을 하지 않았기 때문이다.
  • 이 문제를 추상 클래스와 추상메서드를 활용하여 2가지의 문제를 해결할 수 있다.
  • 좋은 프로그램은 제약이 있는 프로그램이다.

추상클래스

  • 동물과 같이 부모 클래스를 제공해야하지만 실제로는 객체가 생성이 되면 안되는 클래스를 추상클래스라고한다. 말그대로 추상적인 클래스라 형태가 없어야 하기 때문이다.
  • 동물은 하나의 분류이지 실체가 있는것은 아니다. 그렇기 때문에 동물같은 클래스를 생성할 수 있는 클래스를 추상클래스라고 하며 이 클래스는 상속을 목적으로 사용하며 부모 클래스를 담당한다.
  • 추상 클래스는 abstract라는 키워드만 붙여주면 일반적인 클래스와 다를것이 없다. 하지만 여기서 제약이 하나 추가가 된다. 객체를 생성을 할 수 없는 제약이 추가된것이다.

정리 : 추상적이기에 실체화를 할 수 없다.

추상 메서드

  • 부모클래스를 상속 받은 메서드를 반드시 오버라이딩을 해야하는 메소드를 부모 클래스에 정의할 수 있다. 이것을 추상 메서드라고 하며 이름 그대로 추상적인 개념을 제공한다.

  • 추상메서드가 하나라도 있는 클래스는 무조건 추상메서드로 선언을 해야한다. 그렇지 않으면 컴파일 오류가 생긴다.

  • 추상 메서드는 메서드 바디가 없기에 추상메서드를 가진 클래스는 불완전한 클래스가 될 수 있다. 따라서 직접 생성할 수 없도록 추상클래스로 선언해야한다.

  • 추상메서드는 상속받는 자식이 무조건 추상메서드를 반드시 오버라이딩을 해서 사용한다.

  • 추상메서드는 자식클래스가 무조건 오버라이딩을 해야하기 때문에 메서드 바디 부분이 없다.

  • 자식이 추상클래스를 상속 받았는데 추상 메서드를 오버라이딩 하지 않았으면 자식 클래스도 추상클래스가 되어야한다.

  • 추상 메서드도 기존 메서드와 다를것 없지만 바디가 없고 무조건 자식이 오버라이딩을 해야하는 제약을 빼면 다를게 없다.

  • 다형성은 이미 다 배운것이며 추상클래스는 다형성을 좀더 잘 활용하기 위해 사용하는 것이다.

정리 : 추상클래스 덕분에 Animal클래스를 객체화 하는것을 방지해주며 추상메서드덕분에 새로만든 자식클래스가 오버라이딩을 하지 않는 문제를 방지할 수 있다. 추상클래스는 일반 클래스와 다를것 없지만 제약이 약간 추가된것 뿐이다. \

추상 클래스2

순수 추상 클래스 : 모든 메서드가 추상메서드로만 이루어져 있는 클래스를 말함(기능은 하나도 구현되어있지 않음)

  • 추상클래스는 다형성 목적으로 껍데기용 클래스이다.
  • 인스턴스를 생성할 수 없으며 상속을 할 때 자식들은 메서드를 전부 오버라이딩을 해야한다.
  • usb인터페이스는 분명한 규격이 있다. 마우스 키보드는 usb인터페이스에 맞는 규격을 가져야 연결을 할 수 있는데 이 usb규격이라는것이 순수추상클래스가 될 수 있다.
  • 자바는 순수 추상클래스를 더욱더 편하게 사용할 수 있도록 인터페이스를 제공한다.
  • 자바에는 순수추상클래스라는용어 자체가 없다 왜? 인터페이스라는것이 존재하기 때문이다.

인터페이스

  • class가 아닌 interface키워드를 사용한다.
  • public abstract키워드를 생략할 수 있다.

인터페이스는 순수추상클래스와 같은데 약간의 편의기능이 추가된다.

  • 인터페이스의 모든 메서드는 추상메서드이며 public abstract키워드가 제공된다. 기본으로 제공되기 때문에 생략한다.
  • 인터페이스는 다중 구현(다중 상속)이 가능하다.
  • 인터페이스에도 필드를 넣을 수 있으며 필드는 public static final이 모두 포함이 되어있다.
  • 순수추상클래스가 인터페이스가 되었을 뿐이다. 즉 똑같다는 말이다.
  • 자바에서는 순수추상클래스라는 용어가 없다. 모든 메서드를 추상으로 해야하는 경우는 인터페이스로 만든다.

상속 vs 구현

  • 상속은 이름 그대로 부모의 기능을 물려받는것이지만 구현은 모든 메서드가 추상메서드이며 기능이 없다. 오히려 자식이 오버라이딩을 하여 구현화해야한다.
  • 상속은 가져다 쓸 수 있는 기능이 있어 상속이라고 표현하는데, 인터페이스는 하나의 가이드라인이며 가져다 쓸 수 있는 기능이 없기에 구현이라고 표현한다.

인터페이스를 사용해야하는 이유

  • 순수 추상 클래스를 사용하여도 되지만 왜 인터페이스를 사용해야하는가
  • 제약 : 인터페이스를 만드는 이유는 만드시 메소드를 구현하라는 제약을 주기 때문이다. 예를들어 usb인터페이스에 맞춰 키보드 마우스를 개발해야한다. 이것은 반드시 구현을 해야한다.
  • 순수추상클래스는 인터페이스와 달리 미래에 실행 가능한 메소드를 끼워넣을 수 있다고 한다. 이렇게 추가된 메서드는 자식클래스에서 오버라이딩 하여 사용할 수도 있고 그냥 사용할수도 있다. 그렇게 되면 순수추상클래스가 아니게 되어버린다. 하지만 인터페이스는 모든 메서드가 추상메서드이기 때문에 이런 애매한 문제를 그냥 애초에 차단이 가능하다.
  • 인터페이스는 강하게 기능을 구현하도록 약속하는 규정이지만 추상클래스는 일부는 구현하고 일부는 구현안하는 그런 클래스이며 자바에서 정한것이다.
  • 추상클래스는 기능을 넣어봐도 되지 않을까? 하는 생각이 들 수 있지만 애초에 인터페이스로 선언하면 그런 생각을 아예 차단할 수 있다. (제약이 있는 프로그램이 좋은 프로그램이다.)
  • 다중 구현 : 인터페이스는 다중 구현이 가능하다.
  • 클래스로도 다형성을 구현할 수 있지만 추상클래스와 인터페이스는 다형성을 더욱더 잘 쓰기 위해 기존 클래스에서 제약이 추가된 것이다. 개발자는 인터페이스를 보고 의도를 파악할 수 있다.

인터페이스 다중 구현

  • 자바는 다중 상속을 지원하지 않는다. 왜냐하면 다중 상속을 받게 되면 부모끼리 메서드가 똑같을 때 어떤 부모의 메소드를 선택해야할 지 혼란스러워지는데 다이아몬드 문제라고 한다.
  • 하지만 인터페이스는 다중 구현이 가능하다. 왜냐하면 인터페이스는 모두 기능이 없이 추상 메서드로 이루어져이씩 때문이다.
  • 인터페이스는 기능이 없으니 추상메서드의 이름이 두 인터페이스가 같다고 한들 구현하는데는 문제가 없다.
  • 인터페이스는 다이아몬드 문제가 발생할 수 없다. 왜냐하면 구현체가 없고 무조건 오버라이딩된 메소드가 호출이 되며 부모끼리 기능이 겹칠수가 없다.
profile
가치를 제공하는 개발자

0개의 댓글