상속과 다형성

JUNG GYUWON·2023년 9월 27일
0

Java

목록 보기
6/18

상속

기존 클래스에 기능을 추가하거나 재정의하여 새로운 클래스를 정의하는 것

왜 쓰는데?

  1. 기존에 작성된 클래스를 재활용할 수 있다

  2. 자식 클래스 설계 시 중복되는 멤버를 미리 부모 클래스에 작성해 놓으면, 자식 클래스에서는 해당 멤버를 작성하지 않아도 된다

  3. 클래스 간의 계층적 관계를 구성함으로써 다형성의 문법적 토대를 마련한다


아래와 같은 문법으로 자식 클래스를 구현할 수 있다
class 자식클래스이름 extend 부모클래스이름 { ... }

자식클래스에는 부모클래스의 필드와 메소드만 상속되고 생성자와 초기화블럭은 상속되지 않는다
(생성자는 애초에 생성하는 객체가 다르고 초기화블록도 클래스가 로드될때 다르게 되어야 하기 때문)

또한 부모클래스에서 private이나 default 접근제어자가 붙어있으면 상속은 되지만 접근할 수 없다

자바에서 클래스는 한개의 클래스만 상속받는 단일 상속만 가능하다

Object 클래스

모든 클래스의 부모 클래스가 되는 클래스

자바의 모든 객체에서 toString()이나 clone()과 같은 메소드를 바로 사용할 수 있는 이유가 해당 메소드들이 Object 클래스의 메소드이기 때문


super

부모 클래스로부터 상속받은 필드나 메소드를 자식 클래스에서 참조하는 데 사용하는 참조 변수

this와 역할을 똑같은데 부모 클래스의 것을 받아온다고 생각하면 됨

class Parent {
    int a = 10;  //default 접근제어자기 떄문에 상속 안됨.
}


class Child extends Parent {
    int a = 20; 

    void display() {
        System.out.println(a);
        System.out.println(this.a);
        System.out.println(super.a);
    }
}


public class Inheritance03 {
    public static void main(String[] args) {
        Child ch = new Child();
        ch.display();
    }
}

super()

super() 메소드는 부모 클래스의 생성자를 호출할 때 사용됨

this()와 비슷하다고 보면 됨

앞서 언급하였듯이 생성자와 초기화 블럭은 상속되지 않기 때문에, 자식 클래스에서 이를 초기화 하기 위해서는 부모 클래스의 생성자까지 호출해야 한다

따라서 자바 컴파일러는 부모 클래스의 생성자를 명시적으로 호출하지 않는 모든 자식 클래스의 생성자 첫 줄에 자동으로 super() 와 같은 명령문을 추가하여, 부모 클래스의 멤버를 초기화할 수 있도록 해준다
(이걸 Object 클래스까지 쭉 올라감)

class Parent {
    int a;
    
    Parent() { a = 10; }
    Parent(int n) { a = n; }
}

 
class Child extends Parent {
    int b;

    Child() {
    	//super(40);
        b = 20;
    }

    void display() {
        System.out.println(a);
        System.out.println(b);
    }
}

 
public class Inheritance04 {
    public static void main(String[] args) {
        Child ch = new Child();
        ch.display();
    }
}

위 예시에서는 주석의 위치에 컴파일러가 자동으로 super();를 호출해서 Parent 클래스의 기본 생성자가 실행된다
(다른 생성자를 호출할때는 첫줄에 꼭 있어야 하기 때문에 첫줄에 존재)

따라서 이 경우에는 Parent에도 기본 생성자를 추가하는게 좋다
(다른 생성자가 있으면 컴파일러가 자동으로 추가해주지 않기 떄문)

하지만 주석을 해제하면 super(40)을 명시적으로 호출했기 때문에 Parent(int n){} 생성자가 호출된다


메소드 오버라이딩

오버라이딩(overriding)이란 상속 관계에 있는 부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 같은 시그니쳐를 갖는 메소드로 다시 정의하는 것

오버로딩(overloading)이란 서로 다른 시그니처를 갖는 여러 메소드를 하나의 이름으로 정의하는 것이었다 (헷갈리니 주의)

override가 덮어쓰다 라는 뜻이니 상속받은 기존의 메소드를 재정의하는 것
오버로딩(overloading)은 새로운 메소드를 정의하는 것입니다.

  1. 오버라이딩이란 메소드의 동작만을 재정의하는 것이므로, 메소드의 선언부는 기존 메소드와 완전히 같아야 한다
  • 하지만 메소드의 반환 타입은 부모 클래스의 반환 타입으로 타입 변환할 수 있는 타입이라면 변경할 수 있다

  1. 부모 클래스의 메소드보다 접근 제어자를 더 좁은 범위로 변경할 수 없다

  2. 부모 클래스의 메소드보다 더 큰 범위의 예외를 선언할 수 없다.

다형성

자바를 공부하다보면 항상 빠지지 않는게 다형성에 대한 내용이다

하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미

다형성이 왜 중요할까?

  • 재사용성, 확장성, 유연성, 코드의 명확성 등의 이유에서 여러가지 이점이 있음.

자바에서는 이러한 다형성을 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있도록 하여 구현하고 있다

class Parent { ... }
class Child extends Parent { ... }

Parent pa = new Parent(); // 허용
Child ch = new Child();   // 허용
Parent pc = new Child();  // 허용
Child cp = new Parent();  // 오류 발생.

클래스는 상속을 통해 확장은 될 수 있어도 축소는 될 수 없기 때문에 자식 클래스에서 사용할 수 있는 멤버의 개수가 부모 클래스보다 항상 같거나 많다

따라서 위의 세번째 예시처럼 부모 클래스 타입의 참조 변수로도 자식 클래스 타입의 인스턴스를 참조할 수 있다
(참조 변수가 사용할 수 있는 멤버의 개수가 실제 인스턴스의 멤버 개수보다 적기 때문)

하지만 네번째 예시처럼 자식 클래스 타입의 참조 변수로는 부모 클래스 타입의 인스턴스를 참조할 수 없다
(참조 변수가 사용할 수 있는 멤버의 개수가 실제 인스턴스의 멤버 개수보다 많기 때문)


참조변수의 타입변환

  1. 서로 상속 관계에 있는 클래스 사이에만 타입 변환을 할 수 있다.
  2. 자식 클래스 타입에서 부모 클래스 타입으로의 타입 변환은 생략할 수 있다.
  3. 부모 클래스 타입에서 자식 클래스 타입으로의 타입 변환은 반드시 명시해야 한다.
    (메소드나 변수의 개수가 더 많아지기 때문)
class Parent { ... }
class Child extends Parent { ... }
class Brother extends Parent { ... }

Parent pa01 = null;

Child ch = new Child();
Parent pa02 = new Parent();
Brother br = null;

pa01 = ch;          // pa01 = (Parent)ch; 와 같으며, 타입 변환을 생략할 수 있음.
br = (Brother)pa02; // 타입 변환을 생략할 수 없음.
br = (Brother)ch;   // 직접적인 상속 관계가 아니므로, 오류 발생.

이러한 다형성으로 인해 런타임에 참조 변수가 실제로 참조하고 있는 인스턴스의 타입을 확인할 필요성이 생긴다.

이때 앞서 배운 자바의 instanceof 연산자를 활용하여, 참조 변수가 참조하고 있는 인스턴스의 실제 타입을 확인할 수 있다

+) 참조 변수의 타입변환은 단순히 타입이 변환하는 것이지 객체의 구조나 데이터를 변화시키는 것은 아니다.


추상 메소드

자식 클래스에서 반드시 오버라이딩해야만 사용할 수 있는 메소드를 의미

abstract 반환타입 메소드이름();

추상 메소드는 위와 같이 선언부만 존재하며 구현부는 작성되어 있지 않다
작성되어 있지 않은 이부분을 자식클래스에서 오버라이딩하여 사용하면 됨


추상 클래스

하나 이상의 추상 메소드를 포함하는 클래스

반드시 사용되어야 하는 메소드를 추상 클래스에 추상 메소드로 선언해 놓으면, 이 클래스를 상속받는 모든 클래스에서는 이 추상 메소드를 반드시 재정의해야 한다

abstract class Animal { abstract void cry(); }

class Cat extends Animal { void cry() { System.out.println("냐옹냐옹!"); } }

class Dog extends Animal { void cry() { System.out.println("멍멍!"); } }


public class Polymorphism02 {
    public static void main(String[] args) {
        Animal a = new Animal(); // 추상 클래스는 인스턴스를 생성할 수 없음.
        Cat c = new Cat();
        Dog d = new Dog();

        c.cry();
        d.cry();
    }

Animal 클래스를 상속받는 자식 클래스인 Dog 클래스와 Cat 클래스는 cry() 메소드를 오버라이딩해야만 비로소 인스턴스를 생성할 수 있다

그럼 이거 왜 쓰냐?
추상 클래스를 상속받는 자식 클래스가 반드시 추상 메소드를 구현하도록 하기 위함


인터페이스

자바에서는 메소드 출처의 모호성과 같은 문제로 인해 다중 상속을 지원하지 않는다
but 다중상속을 하면 좋을 것 같다

=> 인터페이스를 통해 지원

다른 클래스를 작성할 때 기본이 되는 틀을 제공하면서, 다른 클래스 사이의 중간 매개 역할까지 담당하는 일종의 추상 클래스를 의미

자바에서 추상 클래스는 추상 메소드뿐만 아니라 생성자, 필드, 일반 메소드도 포함할 수 있다

하지만 인터페이스(interface)는 오로지 추상 메소드와 상수만을 포함할 수 있습니다

interface Animal { public abstract void cry(); }

class Cat implements Animal {
    public void cry() {
        System.out.println("냐옹냐옹!");
    }
}


class Dog implements Animal {
    public void cry() {
        System.out.println("멍멍!");
    }
}

 
public class Polymorphism03 {
    public static void main(String[] args) {
        Cat c = new Cat();
        Dog d = new Dog();
 
        c.cry();
        d.cry();
    }
}

클래스와는 달리 인터페이스의 모든 필드는 public static final이어야 하며, 모든 메소드는 public abstract이어야 한다
(어디서나 접근 가능해야하고 메소드는 abstract, 필드는 클래스필드이기 때문)

모든 인터페이스 공통이기 때문에 생략시 컴파일 시 자바 컴파일러가 자동으로 추가해 준다


또한 아래와 같이 다중 상속도 지원한다
class 클래스이름 extend 상위클래스이름 implements 인터페이스이름 { ... }와 같이 상속과 구현을 동시에 하는 것도 가능
+) 클래스가 인터페이스를 구현할 떄 implements 키워드를 사용하고 인터페이스가 인터페이스끼리 상속을 받는 것은 extends 키워드를 사용한다

interface Animal { public abstract void cry(); }
interface Pet { public abstract void play(); }

class Cat implements Animal, Pet {
    public void cry() {
        System.out.println("냐옹냐옹!");
    }

    public void play() {
        System.out.println("쥐 잡기 놀이하자~!");
    }
}


class Dog implements Animal, Pet {
    public void cry() {
        System.out.println("멍멍!");
    }

    public void play() {
        System.out.println("산책가자~!");
    }
}

 
public class Polymorphism04 {
    public static void main(String[] args) {
        Cat c = new Cat();
        Dog d = new Dog();

        c.cry();
        c.play();
        
        d.cry();
        d.play();
	}
}

클래스 다중 상속의 모호성 문제

class Animal { 
    public void cry() {
        System.out.println("짖기!");
    }
}


class Cat extends Animal {
    public void cry() {
        System.out.println("냐옹냐옹!");
    }
}


class Dog extends Animal {
    public void cry() {
        System.out.println("멍멍!");
    }
}


class MyPet extends Cat, Dog {}


public class Polymorphism {
    public static void main(String[] args) {
        MyPet p = new MyPet();
		p.cry();
    }
}

위와 같은 코드를 작성하면 MyPet 객체의 instance p에서 cry() 메소드를 호출하면 어떤 cry()가 호출되어야 하는지 알 수 없기 때문에 모호성이 발생하여 다중상속을 지원하지 않는다

interface Animal { public abstract void cry(); }

interface Cat extends Animal { public abstract void cry(); }

interface Dog extends Animal { public abstract void cry(); }

 
class MyPet implements Cat, Dog {
    public void cry() {
        System.out.println("멍멍! 냐옹냐옹!");
    }
}

 
public class Polymorphism05 {
    public static void main(String[] args) {
        MyPet p = new MyPet();
        p.cry();
    }
}

따라서 위와 같이 인터페이스로 구현하면 Cat과 Dog를 동시에 구현한 MyPet 클래스에서만 cry() 메소드의 구현부가 정의되기 때문에 모호성 문제가 사라진다


내부 클래스(inner class)

하나의 클래스 내부에 선언된 또 다른 클래스를 의미한다

class Outer {     // 외부 클래스
    ...
    class Inner { // 내부 클래스
        ...
    }
    ...
}

내부 클래스는 외부 클래스(outer class)에 대해 두 개의 클래스가 서로 긴밀한 관계를 맺고 있을 때 선언할 수 있다.

내부 클래스의 장점

  1. 내부 클래스에서 외부 클래스의 멤버에 손쉽게 접근할 수 있다
  2. 서로 관련 있는 클래스를 논리적으로 묶어서 표현함으로써, 코드의 캡슐화를 증가시킨다
  3. 외부에서는 내부 클래스에 접근할 수 없으므로, 코드의 복잡성을 줄일 수 있다
profile
반가워요😎

0개의 댓글