GeehDev

[Java] 클래스의 상속과 다형성 본문

Study/Java

[Java] 클래스의 상속과 다형성

geehyun 2024. 9. 15. 14:27

velog에서 이관해온 글

 


클래스의 상속

클래스의 상속은 부모 클래스로부터 멤버(필드, 메서드, 이너 클래스)를 내려받아 자식 클래스 내부에 포함시키는 문법 요소 입니다.

상속 이미지


위 이미지 처럼 공통으로 사용하는 필드와 메서드를 부모 클래스에서 정의하여 자식 클래스에서는 해당 클래스에서만 사용하는 멤버만 정리하여 활용할 수 있습니다.

// 상속 없이 작성
class Student {
    String name;
    int age;
    int studentNo;

    void eat() {...}
    void sleep() {...}
    void goToSchool() {...}
}
class Businessman {
    String name;
    int age;
    int employeeNo;

    void eat() {...}
    void sleep() {...}
    void goToOffice() {...}
}
// 상속 이용하여 작성
public class Person {
    String name;
    int age;

    void eat() {...}
    void sleep() {...}
}
class Student extends Person {
    int studentNo;

    void goToSchool() {...}
}
class Businessman extends Person {
    int employeeNo;

    void goToOffice() {...}
}

상속 문법

class 자식 클래스 extends 부모 클래스 {
    ...
}

 

extends라는 키워드로 상속 받을 부모 클래스를 작성하여 사용합니다.

 

상속 시에는 다음의 사용 유의사항이 있습니다.
1. 클래스는 다중 상속이 불가 합니다.
=> 한 자식 클래스가 여러 부모클래스를 가질 수 없음을 의미합니다. 단, 한 부모 클래스가 여러 자식 클래스를 가질 수는 있습니다.
2. 부모 클래스로 부터 상속받는 요소는 멤버요소(필드, 메서드, 이너클래스)에 한정 됩니다.
=> 생성자는 상속되지 않습니다.

💡 생성자는 왜 상속되지 못할까?
생성자는 1. 클래스명과 동일해야한다. 2. 리턴타입이 없다 라는 2가지 특징을 갖습니다.
이러한 생성자가 자식 클래스 내부로 상속된다하면 어떻게 될까요?
1. 부모 클래스와 자식 클래스는 이름이 다르니 생성자의 첫번째 특징에 위배됩니다.
2. 리턴타입이 없으니 상속받은 생성자는 메서드가 될 수도 없습니다.
즉, 생성자를 상속받게된다면 상속받는 즉시 오류가 발생하게될 것입니다.
따라서 생성자는 상속되지 않으며 대신 super() 라는 메서드로 부모의 생성자를 사용할 수 있습니다.

상속의 장점

상속을 사용하게될 시 장점으로는 다음의 2가지가 있습니다.

1. 코드의 중복성이 제거 됩니다.
2. 클래스의 다형적 표현이 가능합니다.

 

상속 다이어그램

 

위 그림 처럼 상속의 관계를 그림으로 표시할 경우 자식클래스에서 부모 클래스로 화살표로 표시할 수 있습니다. 이는 상속의 다형적 특성을 반영한 화살표 표기법으로 화살표가 가르키는 방향으로는 항상 다형적 표현을 할 수 있습니다.

public class A {...}
class B extends A {...}
class C extends A {...}
class D extends C {...}

//다형적 표현
A aa = new A();       // 가능
B bb = new B();       // 가능
C cc = new C();       // 가능
D dd = new D();       // 가능

A ab = new B();       // 가능
A ac = new C();       // 가능
A ad = new D();       // 가능

B ba = new A();       // 불가능
B bc = new C();       // 불가능
B bd = new D();       // 불가능

C ca = new A();       // 불가능
C cb = new B();       // 불가능
C cd = new D();       // 가능

D da = new A();       // 불가능
D db = new B();       // 불가능
D dc = new C();       // 불가능

A[] abcd = {new A(), new B(), new C(), new D()} //부모 클래스 밑으로 배열로 객체 저장 가능

상속할 때 메모리 구조

상속의 메모리 구조


JVM은 자식 클래스의 객체가 만들어질 때 부모 클래스이 객체를 먼저 생성 후 자식 클래스의 객체를 생성하며 이 때 자식 클래스 객체 내부에 부모 클래스 객체가 포함되어있습니다.

💡 어떤 클래스로 생성하냐에 따라 변수가 바라보는 객체가 다른것 같아요
위에 그림을 보면 객체의 모양자체는 둘다 동일하나, B bb = new B()로 생성했을 때는 B객체를 바라보고 있지만, A ab = new B()로 생성했을 떄는 B객체 안에 A객체만을 바라보고 있는 것을 확인할 수 있습니다.

객체를 생성할 때 작성한 생성자로 객체의 모양을 생성하고 선언한 타입(자료형)으로 해당 객체 내 바라볼 주소를 결정합니다. 

따라서 B bb = new B()로 생성 시 내부의 A클래스 객체 멤버를 자유자재로 사용할 수 있지만,
A ab = new B() 생성하게될 경우 실제 바라보는 객체는 A 클래스 객체이기 때문에 A 클래스 내부 멤버만 사용할 수 있다는 점 유의가 필요합니다.

객체의 타입 변환

  • 업캐스팅
    : 자식 클래스에서 부모 클래스쪽으로 변환되는 것으로, 위에서 본 상속 다이어그램에서 화살표가 가르키는 방향으로는 항상 업캐스팅이 가능합니다. => 코드 작성 시 명시적으로 작성하지 않아도 컴파일러가 대신 작성해줍니다.
  • 다운캐스팅 :
    : 반대로 부모 클래스에서 자식 클래스 쪽으로 변환되는 것 으로 이 경우 가능한 경우도 있고, 불가능한 경우도 있습니다. 가능한 경우도 꼭 명시적으로 변환형을 작성해줘야합니다.

그렇다면, 다운 캐스팅이 가능한 경우는 어떤 경우일까?

class A{...}
class B extends A {...}
//-----------------------
A aa = new A();
B ba = (b) bb;           // 다운캐스팅 불가
A ab = new B();
B ba = (b) ab;          // 다운캐스팅 가능

 

위 경우를 보면 A aa = new A();, A ab = new B(); 둘다 A타입으로 선언한 객체이지만 생성자가 달라 실제 생성된 객체의 모양이 다릅니다.

 

앞서 작성했다시피, 객체를 생성할 때 작성한 생성자로 객체의 모양을 생성하고 선언한 타입(자료형)으로 해당 객체 내 바라볼 주소를 결정합니다.


따라서, A aa = new A();의 경우 객체를 생성할 때 애초에 B객체가 아예 존재하지않으므로 없는 항목으로 다운캐스팅 할 수는 없습니다. A ab = new B();의 경우는 객체 생성할 때 A, B객체 모두 생성후 A객체만 바라보는 방향으로 생성된 경우로 이 경우 B 타입으로 다운 캐스팅이 가능합니다.

 

실제 객체를 생성할 때 어떤 생성자로 객체를 생성했는지에 따라 해당 객체의 위에 위치한 모든 타입(클래스)로는 항상 캐스팅이 가능합니다.

💡 와 내가 만든 소스코드도 아닌데 다 따라다니면서 캐스팅여부 찾아야하나?

// 사용법
참조변수 instanceof 타입
// 사용예시
A aa = new A();
A ab = new B();
aa instanceof A;   // true
aa instanceof B;   // false
ab instanceof B;   // true
ab instanceof A;   // true 
//if문으로 이용
if(aa instanceof B) {
	B ba = (B) aa;
    System.out.println("aa를 B로 캐스팅 했습니다.")
} else {
	System.out.println("aa를 B로 캐스팅 불가합니다.")
}


instanceof라는 키워드를 사용하여 캐스팅 가능여부를 boolean 값으로 반환 받을 수 있습니다.
해당 키워드를 이용해 조건문(또는 삼항연산자)으로 캐스팅 가능 여부 체크 후 캐스팅할 수 있습니다.

메서드 오버라이딩

오버라이딩이란 부모 클래스로 부터 받은 메서드를 자식 클래스에서 재정의하는 개념입니다.

  • 부모 클래스의 메서드와 시그니처리턴 타입이 동일해야 합니다.
  • 부모 클래스의 메서드 보다 접근 지정자의 범위가 같거나 넓어야 합니다.
    오버라이딩을 하게 되면 부모 클래스의 객체에서는 부모 클래스에서 작성한 원본 메서드로 이용할 수 있고, 자식 클래스 객체에서는 오버라이딩한 메서드로 사용할 수 있게 됩니다.
class A {
    void abc(int a, int b) {
        System.out.println("오버라이딩 전 원본 메서드입니다.");
    }
}
class B extends A {
    int abc(int a, int b) {            // 리턴 타입이 원본과 달라 에러
        System.out.println("오버라이딩 실패!");
        return a;
    }
    void abc(int a) {                 // 매개변수(시그니처) 개수가 원본과 달라 오버로드 발생
        System.out.println("오버라이딩 실패!");
    }
    void abc(int a, int b) {           // 정상적인 오버라이딩
        System.out.println("정상적으로 오버라이딩한 메서드입니다.");
    }
}

💡 그러고보니 나 비슷한거 본적있는데...오버로딩? 오버라이딩?

  • 오버로드 : 시그니처를 기준으로 메서드명은 동일하나, 매개변수의 타입, 개수를 달리하여 동일한 메서드명으로 메서드를 중복정의하는 것.
    이 때 상속과 상관없이 중복정의할 수 있으며, 리턴타입은 상관이 없다는 점이 차이점이 있습니다.
  • 오버라이딩 : 상속을 통해 부모로부터 받은 메서드를 자식 클래스에서 재정의하는 것으로, 시그니처와 리턴타입까지 정확히 일치해야합니다.

오버라이딩 발생 시점

오버라이딩은, 오버라이딩을 정의한 객체가 생성될 때 발생하게된다.

public class Main {
    public static void main(String[] agrs) {
        A aa = new A();
        aa.abc();                //원본 메서드 실행

        A ab = new B();
        ab.abc();               //오버라이딩된 메서드 실행

        B bb = new B();
        bb.abc();               //오버라이딩된 메서드 실행
    }
}
//-------------------------------
class A {
    void abc() {
        System.out.println("오버라이딩 전 원본 메서드입니다.");
    }
}
class B extends A {
    void abc() {
        System.out.println("정상적으로 오버라이딩한 메서드입니다.");
    }
}

위 처럼 실행할 경우

  • A aa = new A(); : A타입으로 A()생성자를 사용해 A객체의 형태를 생성했으므로 B메서드에서 작성한 오버라이딩이 발생하지 않습니다.
  • A ab = new B(); : A타입으로 생성했지만 B()생성자를 사용함으로써 B객체가 생성되게 되어 오버라이딩이 발생하게 됩니다. 따라서 A타입의 abc()메서드를 사용해도 이미 해당 메서드 자체가 오버라이딩되어 오버라이딩된 메서드가 실행됩니다.
  • B bb = new B(); : B타입으로 생성 후 B()생성자를 이용해 B객체를 만들었으므로 오버라이딩이 발생합니다.

인스턴스 멤버와 정적 멤버의 중복

인스턴스 멤버와 정적 멤버에서 부모 클래스와 자식 클래스의 필드명, 메서드의 시그니처와 리턴 타입이 동일하게 될 경우 모두 오버라이딩이 발생할까?

  • 인스턴스 필드 : 각 객체의 저장공간은 완벽하게 분리되어있기 때문에 필드명이 동일해도 오버라이딩이 발생하지 않고, 각 객체 안에 잘 따로 따로 잘 저장됩니다.
  • 인스턴스 메서드 : 메서드의 시그니처와 리턴타입이 같을 경우 오버라이딩이 발생합니다.
  • 정적 필드 : 마찬가지로 정적 필드는 해당 클래스 내부에 각각 저장되어 동리한 필드명이라도 완벽히 분리되어있기 때문에 오버라이딩이 발생하지 않습니다.
  • 정적 메서드 : 정적 필드와 마찬가지로 해당 클래스 내부에 각각 저장되기 때문에 오버라이딩은 발생하지 않습니다.

super 키워드와 super()메서드

자식클래스에서 부모클래스 내부요소를 이용하기 위한 키워드와 메서드 입니다.

    • super : 부모의 객체 내 멤버에 접근할 수 있습니다.
    • super() : 부모의 생성자를 사용할 수 있습니다.
      1. 생성자의 내부에서만 사용할 수 있습니다.
      2. 생성자의 첫 줄에 위치해야합니다.
        (this()메서드오 첫줄에 와야하므로 super(), this()는 둘 중 하나만 사용할 수 있습니다.)
💡어떻게 Java는 자식 요소 객체를 생성하면서 부모 요소 객체를 자동 생성할까
앞서 JVM은 자식객체를 생성할 때 먼저 부모 객체부터 생성한다고 하는데, 이는 사실 모든 생성자는 첫줄에 super(), this()를 작성해줘야하며, 생략 시 JVM이 자동으로 super()를 자동으로 삽입합니다. 따라서 자식 요소 객체의 생성자를 실행하면 자동으로 super() 생성자가 삽입되어 부모 요소 객체가 생성된다는 원리 입니다.
class A {
	A(int a) {}
}
class B extends A {
	...
}​

위 경우 자식 클래스 B의 객체를 생성하게될 때 오류가 발생하게 됩니다.
이유는, B객체를 생성할 때 JVM은 자동으로 
super()를 삽입해 실행하게되는데, A클래스를 보면 사용자 정의 생성자 하나만 있고 기본생성자가 없어서 
super()를 실행하지 못하게되는 상황입니다.

 

최상위 클래스 Object

Object는 Java의 최상위 클래스로 모든 클래스는 클래스르 상속 받는다.
클래스 생성 시 아무런 클래스를 상속 받지 않으면 자동으로 extends Object를 삽인하게된다.

💡 다중 상속 불가 문제
Java에서는 모든 클래스는 하나의 부모만 가질 수 있다. 따라서 특정 클래스를 이미 상속 받은 클래스는Object클래스를 상속 받을 수 없다.
하지만 해당 클래스가 상속 받은 부모 클래스 아니면 그 부모의 부모든 부모 중 누군가는 Object클래스를 상속받았을 것이기 때문에 결국 Java의 모든 클래슨느 Object클래스를 상속 받는다.

 

따라서 Java의 모든 클래스는 아래와 같이 Object 타입으로 객체를 생성할 수 있습니다.

Object oa = new A();
Object ob = new B();

Object 클래스의 주요 메서드

메서드명 주요 내용 사용법
toString() Object 객체의 정보 - 패키지명.클래스명@해쉬코드 객체명.toString()
equals(Object obj) 입력매개변수 ob객체와의 스택 메모리(번지) 비교 객체명.equals(비교객체명)
hashCode() 객체의 hashCode값 리턴 객체명.hashCode()
wait()
wait(long timeout)
wait(long timeout, int nanos)
현재의 쓰레드를 일시정지 상태로 전환 -
notify()
notifyAll()
wait()로 일시정지한 상태 1개 또는 전체(All) 쓰레드의 일시정지 해제 -

 

💡toString(), equals(obj) 은 오버라이딩해서 주로 사용

  • toString()
class A {
	int a = 3;
    @Override
    public String toString() {
    	return "필드값: a =" + a;
    }
}
//------------------------
public class Main {
	public static void main(String[] args) {
    	A a = new A();
        System.out.println(a); 
        // 객체명을 바로 출력하면 기본적으로 객체명.toString()으로 전환해서 출력함
        // 이 기능을 이용해 toString()을 오버리아딩해서 객체명 출력 시 내가 원하는대로 출력되게 가능
    }
}
  • equals(Object obj)
class A {
	String name;
    A(String name) {
    	this.name = name;
    }
    @Override
    public boolean equals(Object obj) {
    	if (obj instanceof A) {
        	if (this.name == ((B) obj).name) {
            	return true;
            }
        } return false;
    }
}
//------------------------
public class Main {
	public static void main(String[] args) {
    	B b1 = new B("난 B객체");
        B b2 = new B("난 B객체");
        System.out.println(b1 == b2);       //번지값만 비교하므로 결과는 false
        System.out.println(b1.equals(b2));  //오바라이딩으로 실제 값을 비교 함 true
        //원래 equals()는 ==연산자와 동일하게 번지값만 비굑하는 메소드 였으나
        //이런식으로 오버라이딩 해서 실제 값의 일치여부를 확인하게끔 할 수 있습니다.
    }
}

참고

Do it! 진짜 개발자가 되는 Java 프로그램 입분서 자바 완전 정복 - 김동형
위 책을 공부하며 작성하고 있습니다!