10장. 자바는 상속이라는 것이 있어요

공부하는 감자·2023년 12월 1일
0

자바의 신 3판

목록 보기
10/30

들어가기 전

『자바의 신 3판』 을 읽고 내용 정리 및 공부한 내용을 정리한 글입니다.
서적: 자바의 신 3판 구입처

내용 정리

상속 Inheritance

상속은 부모 클래스에 선언되어 있는 public 및 protected로 선언되어 있는 모든 변수와 메소드를 내가 갖고 있는 것처럼 사용할 수 있는 것이다.

다른 패키지에 선언된 부모 클래스의 접근 제어자가 없거나(package-private) private으로 선언된 것들은 자식 클래스에서 사용할 수 없다.

extends라는 자바의 예약어를 사용해서 그 뒤에 지정한 클래스를 상속받는다.

public class Parent {}

위와 같은 클래스를 Child 클래스가 상속받는다면, 아래처럼 사용한다.

public class Child extends Parent{}

확장을 한 클래스(자식 클래스)가 생성자를 호출하면,
1. 자동으로 부모 클래스의 기본 생성자가 호출된다.
2. 그 다음 자식 클래스의 생성자에 있는 내용들이 수행된다.

상속을 사용하는 이유

코드 재사용

부모 클래스가 갖고 있는 변수와 메소드를 상속받음으로써, 개발할 때 이중, 삼중의 일을 안 해도 된다. 하나의 클래스를 잘 만들어 놓은 게 있으면, 그 클래스를 상속받아 추가적인 기능을 넣을 수 있다.

확장

확장한 클래스는 추가적인 메소드를 만들어도 전혀 문제가 없다. 만약 여러 클래스에 공통된 기능 Print()라는 메소드를 만들었는데, 해당 기능에 문제가 있어 수정할 시 선언한 모든 클래스를 수정해야 하는 상황이 일어난다.

이럴 경우 상속을 사용하면 부모 클래스에 Print()를 선언하고, 해당 메소드만 수정하면 된다.

상속과 생성자

자식 클래스에서는 모든 생성자가 실행될 때 부모의 기본 생성자를 찾는다.

만약 부모 클래스에 기본 생성자가 없다면, 컴파일 시 자동으로 만들어주므로 문제는 없다.

하지만 부모 클래스에 매개 변수가 있는 생성자를 만들 경우에는 기본 생성자가 없다면 오류가 발생한다.

Implicit super constructor 상위클래스() is undefined. Must explicitly invoke another constructor 오류가 발생한다.

이를 방지하기 위해선 두 가지 방법이 있다.

  1. 부모 클래스에 “매개 변수가 없는” 기본 생성자를 만든다.
  2. 자식 클래스에서 부모 클래스의 생성자를 명시적으로 지정하는 super() 를 사용한다.

super()

super 는 부모 클래스를 가리키는 말로, 메소드처럼 super() 로 사용하면 부모 클래스의 생성자를 호출한다는 것을 의미한다.

자바는 부모의 매개 변수가 없는 기본 생성자를 찾는 것이 기본이다. 그래서, 부모 클래스에 매개 변수가 있는 생성자만 있을 경우에는 super() 를 이용해서 부모 생성자를 꼭 호출해야만 한다.

자식 클래스에서 super() 를 명시적으로 지정하지 않으면 컴파일할 때 자동으로 super() 라는 문장이 들어간다.

이를 이용해 부모 클래스를 고치지 않고, 자식 클래스만 고쳐서 컴파일 및 실행이 정상적으로 수행되도록 하려면 다음과 같이 자식 클래스의 생성자를 변경하면 된다.

super() 는 반드시 자식 클래스의 생성자에서 가장 첫줄에 선언되어야만 한다.

// 기본 생성자가 없는 부모 클래스
public class ParentArg {
    public ParentArg(String name) {
        System.out.println("ParentArg("+name+") Constructor");
    }
    public void printName() {
        System.out.println("printName() - ParentArg");
    }
}
// super()로 부모 생성자를 명시적으로 호출하는 자식 클래스
public class ChildArg extends ParentArg{
    public ChildArg() {
        super("ChildArg");
        System.out.println("Child Constructor");
    }
}

여기에서 super("ChildArg") 라고 지정하면, 부모 클래스의 생성자 중 String 타입을 매개 변수로 받을 수 있는 생성자를 찾는다. 그리고 부모 클래스에 String을 매개 변수로 있는 생성자가 있기 때문에 이 생성자가 호출된다.

그런데, 만약 이 생성자처럼 참조 자료형을 매개 변수로 받는 생성자가 하나 더 있을 때 super(null) 이라고 호출하면 컴파일 시 오류가 발생한다.

public class ParentArg {
    public ParentArg(String name) {
        System.out.println("ParentArg("+name+") Constructor");
    }
    public ParentArg(InheritancePrint obj) {
        System.out.println("ParentArg(InheritancePrint) Constructor");
    }
    public void printName() {
        System.out.println("printName() - ParentArg");
    }
}
public class ChildArg extends ParentArg{
    public ChildArg() {
        super(null);//compile error
        System.out.println("Child Constructor");
    }
}

error: reference to ParentArg is ambiguous > super(null)

부모 클래스에 참조 자료형인 String 과 InheritancePrint를 매개 변수로 잡는 생성자가 있으므로, null을 넘겨주면 어떤 생성자를 찾아가야 하는지를 자바 컴파일러가 마음대로 정할 수가 없다.

따라서 super() 를 사용할 때에는 null을 넘기는 것보다는 호출하고자 하는 생성자의 기본 타입을 넘겨주는 것이 좋다.

메소드 오버라이딩 Overriding

상속에서는 부모의 기능을 자식이 모두 포함한다고 볼 수 있다. (일부 private로 선언된 기능은 제대로 활용할 수 없지만)

상속관계를 보다 유연하기 활용하기 위한 것이 메소드 오버라이딩으로, 부모 클래스에 선언되어 있는 메소드와 동일한 메소드를 자식 클래스에서 선언해서 사용하는 것을 말한다.

자식 클래스에서는 접근 제어자, 리턴 타입, 메소드 이름, 매개 변수 타입 및 개수가 모두 동일해야만 메소드 오버라이딩이라고 부른다.

부모 클래스에 선언되어 있는 메소드와 동일하게 선언되어 있는 메소드를 자식 클래스에 선언하면 자식 클래스의 메소드만 실행된다.

💡 시그니처
시그니처는 메소드 이름과 매개 변수의 타입 및 개수를 의미한다. “동일하게 선언되어 있다”는 말은 “동일한 시그니처 Signature를 가진다”라고 표현할 수 있다.

오버라이딩 시 접근 제어자는 더 큰 접근 권한으로 변경할 수 있다. 다만, 접근 제어자가 더 확대되는 것은 문제가 안되지만 축소되는 것은 문제가 된다.

부모에서 public으로 선언한 것을 자식이 private으로 선언하면 안 된다는 말이다. 부모가 만약 private으로 선언했으면 자식은 어떤 것으로 선언하든지 상관 없다.

public > protected > package-private > private 순으로 접근 권한이 좁아짐.

  • 메소드 Overriding은 부모 클래스의 메소드와 동일한 시그니처를 갖는 자식 클래스의 메소드가 존재할 때 성립된다.
  • Overriding된 메소드는 부모 클래스와 동일한 리턴 타입을 가져야만 한다.
  • Overriding된 메소드의 접근 제어자는 부모 클래스에 있는 메소드와 달라도 되지만, 접근 권한이 확장되는 경우에만 허용된다. 접근 권한이 축소될 경우에는 컴파일 에러가 발생한다.

Overloading과 Overriding

  • Overloading : 확장 (메소드의 매개 변수들을 확장하기 때문에, 확장)
  • Overriding : 덮어 씀 (부모 클래스의 메소드 시그니처를 복제해서 자식 클래스에서 새로운 것을 만들어 내어 부모 클래스의 기능은 무시하고, 자식 클래스에서 덮어 씀)

참조 자료형의 형 변환

지금까지 객체를 생성할 때에는 다음과 같이 만들었다.

ParentCasting parent=new ParentCasting();
ChildCasting child=new ChildCasting();

상속 관계가 정립되면 다음과 같이 객체를 생성할 수도 있다.

ParentCasting parent2=child;

그런데, 다음과 같은 객체 생성은 안된다.

ChildCasting child2=parent;//compile error

형 변환이 안되는 이유는 long과 int의 형 변환과 비슷한 이유다.

참조 자료형은 자식 클래스의 타입을 부모 클래스의 타입으로 형 변환하면 부모 클래스에서 호출할 수 있는 메소드들은 자식 클래스에서도 호출할 수 있으므로 전혀 문제가 안된다. 따라서 우리가 명시적으로 해줄 필요가 없다.

하지만 부모 클래스는 자식 클래스에 선언되어 있는 메소드나 변수를 완전히 사용할 수 없다.

따라서, 컴파일 오류를 피하려면 다음과 같이 형 변환을 명시적으로 선언해주어야 한다.

ChildCasting child=new ChildCasting();
ParentCasting parent=child;
ChildCasting child2=(ChildCasting)parent;

parentchild 를 대입한 것이고, childChildCasting 의 객체이다. 그러므로 실제로는 parentChildCasting 의 객체이기 때문에 ChildCasting 로 형 변환해도 전혀 문제가 없다.

instanceof

타입을 구분하는 자바의 예약어이다.

instanceof 의 앞에는 객체를, 뒤에는 클래스(타입)를 지정해 주면 된다. 그러면 이 문장은 boolean 형태의 결과를 제공한다. 클래스의 객체이면 true, 아니면 false를 반환한다.

일반적으로 여러 개의 값을 처리하거나, 매개 변수로 값을 전달할 때에는 보통 부모 클래스의 타입으로 보낸다. 이렇게 하지 않으면 배열과 같이 여러 값을 한 번에 보낼 때 각 타입 별로 구분해서 메소드를 만들어야 하는 문제가 생길 수도 있기 때문이다.

이때, instanceof 로 타입을 확인한 후에 명시적으로 형 변환을 하면, 정확하게 타입을 확인할 수 있고 원하는 메소드도 호출할 수 있다.

단, 가장 하위에 있는 자식 타입부터 확인을 해야 제대로 타입 점검이 가능하다. 상위 타입을 넣었을 경우에도 true로 반환하기 때문이다.

public class InheritanceCasting {
    public static void main(String args[]) {
        InheritanceCasting inheritance = new InheritanceCasting();
        inheritance.objectCastArray();
    }
    public void objectCastArray() {
        ParentCasting[] parentArray=new ParentCasting[3];
        parentArray[0]=new ChildCasting();
        parentArray[1]=new ParentCasting();
        parentArray[2]=new ChildCasting();
        objectTypeCheck(parentArray);
    }
    private void objectTypeCheck(ParentCasting[] parentArray) {
        for(ParentCasting tempParent:parentArray) {
            if(tempParent instanceof ChildCasting) {
                System.out.println("ChildCasting");
            } else if(tempParent instanceof ParentCasting) {
                System.out.println("ParentCasting");
            }
        }
    }
}

형 변환 정리

  • 참조 자료형도 형 변환이 가능하다
  • 자식 타입의 객체를 부모 타입으로 형 변환하는 것은 자동으로 된다.
  • 부모 타입의 객체를 자식 타입으로 형 변환을 할 때에는 명시적으로 타입을 지정해 주어야 한다. 이때, 부모 타입의 실제 객체는 자식 타입이어야만 한다.
  • instanceof 예약어를 사용하면 객체의 타입을 확인할 수 있다.
  • instanceof로 타입 확인을 할 때 부모 타입도 true라는 결과를 제공한다.

다형성 Polymorphism

어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미한다. 대표적인 예로는 같은 이름의 메서드가 상황에 따라 다른 역할을 수행하는 오버로딩과 오버라이딩이 있다.

책에서는 “형 변환을 하더라도, 실제 호출되는 것은 원래 객체에 있는 메소드가 호출된다”는 것이 바로 다형성이라고 말하고 있다.

책의 예제를 함께 보자. Parent 를 상속받은 ChildChildOther 클래스가 있다.

public class Parent {
    public Parent() {
        System.out.println("Parent Constructor");
    }
    public void printName() {
        System.out.println("Parent printName()");
    }
}
public class Child extends Parent{
    public Child() {
        System.out.println("Child Constructor");
    }
}
public class ChildOther extends Parent{
    public ChildOther() {
        System.out.println("ChildOther Constructor");
    }
    public void printName() {
        System.out.println("ChildOther printName()");
    }
}
  1. 부모 = new 부모();
  2. 부모 = new 자식1();
  3. 부모 = new 자식2(); 이때, 자식은 printName()를 오버라이딩 한 상태이다.
public class InheritancePoly {
    public static void main(String args[]) {
        InheritancePoly inheritance=new InheritancePoly();
        inheritance.callPrintNames();
    }
    public void callPrintNames() {
        Parent parent1 = new Parent();
        Parent parent2 = new Child();
        Parent parent3 = new ChildOther();
        parent1.printName();
        parent2.printName();
        parent3.printName();
    }
}
// 1번. Parent parent1 = new Parent();
// 부모의 생성자 호출
Parent Constructor

// 2번. Parent parent2 = new Child();
// 부모 생성자를 호출 후 자식 생성자 호출
Parent Constructor
Child Constructor

// 3번. Parent parent3 = new ChildOther()
// 부모 생성자를 호출 후 자식 생성자 호출
Parent Constructor
ChildOther Constructor

// 4번.parent1.printName();
// 부모의 메소드 호출
Parent printName()

// 5번. parent2.printName();
// 자식에게 printName()이 없으므로 부모의 것을 호출
Parent printName()

// 6번. parent3.printName();
// 자식이 오버라이딩한 printName()를 호출
ChildOther printName()

각 객체의 타입은 모두 Parent 타입으로 선언되어 있는데도 불구하고 printName() 메소드 결과는 상이하다.

다시 말해, 선언 시에는 모두 Parent 타입으로 선언했지만, 실제로 호출된 메소드는 생성자를 사용한 클래스에 있는 것이 호출되었다.

왜냐하면 각 객체의 실제 타입은 다르기 때문이다. 이와 같이 “형 변환을 하더라도, 실제 호출되는 것은 원래 객체에 있는 메소드가 호출된다”는 것이 바로 다형성이다.

상속 규칙 정리

  • 부모 클래스에서는 기본 생성자를 만들어 놓는 것 이외에는 상속을 위해서 어떠한 작업을 할 필요는 없다.
  • 자식 클래스는 클래스 선언 시 extends 다음에 부모 클래스 이름을 적어준다.
  • 자식 클래스의 생성자가 호출되면, 자동으로 부모 클래스의 매개 변수 없는 생성자(기본 생성자)가 실행된다.
  • 자식 클래스에서는 부모 클래스에 있는 public, protected로 선언된 모든 인스턴스 및 클래스 변수와 메소드를 사용할 수 있다.
  • 다중 상속은 안된다. 즉, extends 다음에는 클래스를 하나만 써야 한다.
  • 부모 클래스에 기본 생성자가 없을 시, 자식 클래스에서 super() 를 사용하여 명시적으로 호출할 수 있다.
  • 부모 클래스에 private로 선언된 변수를 제외한 모든 변수가 자신의 클래스에 선언된 것처럼 사용할 수 있다.
  • 부모 클래스에 선언된 변수와 동일한 이름을 가지는 변수를 선언할 수도 있다. 하지만, 이렇게 엎어 쓰는 것은 권장하지 않는다.
  • 부모 클래스에 선언되어 있지 않은 이름의 변수를 선언할 수 있다.
  • 변수처럼 부모 클래스에 선언된 메소드들이 자신의 클래스에 선언된 것처럼 사용할 수 있다.
  • 부모 클래스에 선언된 메소드와 동일한 시그니처를 사용함으로써 메소드를 Overriding할 수 있다.
  • 부모 클래스에 선언되어 있지 않은 이름의 새로운 메소드를 선언할 수 있다.

💡 이 요약 내용은 Oracle에서 제공하는 Java tutorial을 참조했습니다.

정리해 봅시다.

Q. 상속을 받는 클래스의 선언문에 사용하는 예약어는 무엇인가요?

Me: extends

Q. 상속을 받은 클래스의 생성자를 수행하면 부모의 생성자도 자동으로 수행된다.

Me: O

Q. 부모 클래스의 생성자를 자식 클래스에서 직접 선택하려고 할 때 사용하는 예약어는 무엇인가요?

Me: super

Q. 메소드 Overriding과 Overloading을 정확하게 설명해 보세요.

Me: 오버로딩은 동일한 시그니처를 가진 메소드를 매개 변수를 다르게 하여 만드는 것이고, 오버라이딩은 상속받은 메소드를 엎어 쓰는 것이라고 할 수 있다.

Q. A가 부모, B가 자식 클래스라면 A a=new B(); 의 형태로 객체 생성이 가능한가요?

Me: O

Q. 명시적으로 형변환을 하기 전에 타입을 확인하려면 어떤 예약어를 사용해야 하나요?

Me: instanceof

Q. 위의 문제에서 사용한 예약어의 좌측에는 어떤 값이, 우측에는 어떤 값이 들어가나요?

Me: 좌측에는 변수 혹은 객체가, 우측에는 클래스(타입) 이름이 들어간다.

Q. instanceof 예약어의 수행 결과는 어떤 타입으로 제공되나요?

Me: boolean

Q. Polymorphism이라는 것은 뭔가요?

Me: 객체의 속성이나 기능이 상황에 따라 여러가지 형태를 가질 수 있는 성질을 말한다.

질문

💡 책에 있는 내용이 아닙니다.

책을 읽으며 설명이 더 필요하거나, 추가로 궁금한 점에 대해 질문 형식으로 작성 후, 답을 구해보고 있습니다.
참고한 사이트나 영상은 [출처]로 달아두었으며, 오류 지적은 언제나 환영합니다.

Q. package-private는 상속 불가?

책의 설명에 따르면 package-private로 선언된 것은 상속 불가하다고 한다. 실제 접근 제어자의 범위를 보면 맞는 것 같다.

하지만, 실제로 테스트해보면 그렇지 않다.

  • 테스트
    ```java
    public class Parent {
    
        private void prvMethod() {
            System.out.println("call prvMethod()");
        }
    
        void defMethod() {
            System.out.println("call defMethod()");
        }
    
        public void pubMethod() {
            System.out.println("call pubMethod()");
        }
    }
    ```
    
    ```java
    public class Child extends Parent {
        public static void main(String[] args) {
            Child c = new Child();        
        }
        Child() {
            // super.prvMethod();
            super.defMethod();
            super.pubMethod();
        }
    }
    ```

참고 사이트에 따르면
package-private 클래스의 상속은,
1. 같은 패키지 내의 자식 클래스라면, 같은 패키지 내에 있으므로 접근이 가능
2. 다른 패키지 내에 있다면 애초에 자식 클래스로 만들 수조차 없다.

같은 이치로, 상속받을 때 package-privateprivate 으로 선언한 것은 “상속받을 수 없다”라는 말은, “접근 권한이 없다”라고 이해해도 좋을 거 같다.

Q. 부모의 private 멤버의 접근

  • private 변수 생성 확인
    ```java
    public class ParentOverriding {
        private int intValue = 10;
    
        public ParentOverriding() {
            System.out.println("ParentOverriding Constructor");
        }
        private void privateTest() {
            System.out.println("privateTest() - ParentOverriding");
        }
        public int getInt() {
            return intValue;
        }
    }
    ```
    
    ```java
    public class ChildOverriding extends ParentOverriding{
        public ChildOverriding() {
            System.out.println("ChildOverriding Constructor");
        }
        public int getIntChild() {
            return super.getInt();
        }
    }
    ```
    
    ```java
    public class InheritanceOverriding {
        public static void main(String[] args) {
            ChildOverriding child=new ChildOverriding();
            System.out.println(child.getIntChild());
    
            ParentOverriding par = new ChildOverriding();
            System.out.println(par.getInt());
        }
    }
    ```

위 결과를 보면, private 메소드는 애초에 접근이 안되므로 확인이 불가능하다. 다만, private 변수는 상속이 된 것인지 확인이 가능했다.

  1. 자식 객체 생성 후 private변수에 접근하는 부모 메소드를 호출하는 자식 메소드 호출 > 반환함
  2. 부모 객체 = new 자식()으로 받고 private 변수에 접근하는 부모 메소드 호출 > 반환함

즉, 직접 접근은 안 되지만 자식 객체에 private 변수가 포함되기는 한다. "상속이 안 된다"가 자식 클래스에 포하되지 않는다는 의미와 헷갈렸는데, 포함되기는 한다. 접근이 안될 뿐.

Q. 다중 상속이 허용 안되는 이유

다이아몬드 문제가 생길 수 있기 때문이다.

다이아몬드 문제

  • 예제 코드와 함께하는 설명 아래와 같이 부모 클래스를 하나 만든다.
    public class Parent {
    	public Parent() {
    		// 최상위 부모 클래스
    	}
    	public void printName() {
    		System.out.println("내가 원조다!");
    	}
    }
    그 후, 부모 클래스를 상속받는 자식-딸, 자식-아들 클래스를 만든다.
    public class Daughter extends Parent {}
    public class Son extends Parent {}
    그리고 이 둘을 모두 상속받는 조카 클래스를 만든 후 부모의 printName() 를 호출하도록 하자.
    public class Nephew extends Daughter, Son {
    	public Nephew() {}
    	@Override
    	void printName() {
    		super.printName();
    	}
    }
    printName() 는 부모에게서 상속받았으므로 딸에게도 아들에게도 있다. 그렇다면, 조카가 호출한 super.printName() 의 super는 누구를 말하게 되는 걸까?

위와 같이 충돌이 날 수 있으므로, 다중 상속은 금지되고 있다.

Q. 다중 상속의 충돌이 생기는 메소드 조건: 메소드 시그니처가 같아야만 하는가?

테스트해보고 싶은데, 자바에서는 다중상속을 금지시켜서 못하겠다. 따라서, 인터페이스로 다중상속 충돌 구현을 해보겠다.

public interface Mother {
    default void print() {
        System.out.println("I'm Mohter");
    }
}
public interface Father {
    default void print() {
        System.out.println("I'm Father");
    }
}
public class Baby implements Mother, Father {
    public static void main(String[] args) {
        Baby c = new Baby();
        c.print();
    }
}

테스트 결과

  • 1.접근 제어자, 리턴 타입, 동일한 메소드 시그니처일 경우 충돌 발생

    ```java
    public class Baby implements Mother, Father {
    
        public static void main(String[] args) {
            Baby c = new Baby();        
        }
    }
    ```

  • 2.접근 제어자가 다를 경우 충돌 없음 - 제대로 된 테스트 불가
    1) Mother, Father 인터페이스의 print() 메소드를 private으로 변경

    오류 없음. 사진 속 오류는 print() 메소드가 private이어서 접근 권한이 없어 발생한 오류이고, 해당 코드를 주석 처리하고 컴파일하면 오류 없이 된다.

    2) Father 인터페이스의 print() 메소드를 static으로 변경해보았다.
    이 경우엔 오류가 나지 않았다. 그런데, 인터페이스의 Static 메소드는 재정의가 불가능하고 인터페이스에서 직접 호출해야하기 때문에 오류가 나지 않은 것 같다.

    ```java
    public interface Father {
        static void print() {
            System.out.println("I'm Father");
        }
    }
    ```


  • 3.리턴 타입이 다를 경우 충돌 발생

    ```java
    public interface Father {
        default String print() {
            System.out.println("I'm Father");
            return "Father";
        }
    }
    ```

    이 경우엔, 상속받는 인터페이스의 두 메소드 리턴 타입이 다르다고 오류 표시를 해준다.

    1. 매개변수 타입이 다를 경우 충돌 없음
      ```java
      public interface Father {
          default void print(String name) {
              System.out.println("I'm Father" + " " + name);
          }
      }
      ```
      
      ```java
      public class Baby implements Mother, Father {
          public static void main(String[] args) {
              Baby c = new Baby();
              c.print();
              c.print("Jim");
          }
      }
      ```

결론

메소드 충돌을 피하려면 아래 규칙을 지키면 된다.

  • 메소드 명이 달라야 함
  • 매개변수 타입이 달라야 함 (오버로딩)

Q. 업 캐스팅과 다운 캐스팅

자바의 상속 관계에 있는 부모와 자식 클래스 간에는 서로 간의 형 변환이 가능하다. 클래스는 reference 타입으로 분류되니 이를 참조형 캐스팅이라고 부른다. 그리고 참조형 캐스팅에는 업 캐스팅과 다운 캐스팅이 있다.

부모 객체는 자식 객체에 상속을 받고 있으니 더 상위 요소로 판별할 수 있다. 그래서 Up 캐스팅이라고 한다.

반대로 하위 요소인 자식 객체로 형 변환하는 것은 Down 캐스팅이라고 한다.

업 캐스팅

  • 업캐스팅은 자식 클래스가 부모 클래스 타입으로 형 변환되는 것이다.
  • 업캐스팅은 자동으로 형 변환 해준다.
  • 단, 부모 클래스로 형 변환하면 자식 클래스에서만 있는 속성과 메서드는 실행하지 못한다. 즉, 멤버의 개수 감소를 의미한다.
  • 업 캐스팅을 하고 메소드를 실행할 때, 만일 자식 클래스에서 오버라이딩한 메소드가 있을 경우, 부모 클래스의 메소드가 아닌 오버라이딩 된 메소드가 실행되게 된다.

이처럼 업 캐스팅을 하는 이유는 공통적으로 할 수 있는 부분을 만들어 간단하게 다루기 위해서다.

💡 멤버 개수 감소로 인한 멤버 접근 제한
자식 클래스의 객체는 부모 클래스를 상속하고 있기 때문에 부모의 멤버를 모두 가지고 있지만, 부모 클래스의 객체는 자식 클래스의 멤버를 모두 가지고 있지는 않는다.

따라서 업 캐스팅을 하면 부모 클래스 멤버로 멤버 개수가 한정되기 때문에, 자식 클래스 내에 있는 모든 멤버(메소드 및 멤버 필드)에 접근할 수 없게 된다.

객체를 업 캐스팅하게 되면 자식과 부모의 공통된 것만 사용할 수 있고 자식 클래스에서 새로 만들어진 건 사용할 수 없음을 주의하자.

다운 캐스팅

  • 다운 캐스팅은 거꾸로 부모 클래스가 자식 클래스 타입으로 형 변환되는 것이다.
  • 다운 캐스팅은 괄호를 이용해 명시적으로 형 변환해줘야 한다.
  • 다운 캐스팅의 목적은 업캐스팅한 객체를 다시 자식 클래스 타입의 객체로 되돌리는데 목적을 둔다. (복구)

다운 캐스팅의 진정한 의미는 부모 클래스로 업 캐스팅된 자식 클래스를 복구하여, 본인의 필드와 기능을 회복하기 위해 있는 것이다.

즉, 원래 있던 기능을 회복하기 위해 다운 캐스팅을 하는 것이다.

다운 캐스팅은 곧 사용할 수 있는 객체 멤버 증가를 의미하는데, 실제 참조 변수가 가리키는 객체가 무엇인지 모르기 때문에 어떠한 멤버가 추가 되는지 알 수가 없다.

그래서 반드시 형 변환 괄호를 기재함으로써 증가된 클래스의 멤버가 무엇인지 알게 하도록 개발자한테 알려줘야 한다.

💡 다운캐스팅(downcasting) 예외
다운 캐스팅은 업 캐스팅한 객체를 되돌릴 때 적용 되는 것이지, 오리지널 부모 객체를 자식 객체로 강제 형 변환은 불가능하다.
이는 컴파일 단계에서 잡아주지 않기 때문에 조심해서 사용해야 한다.

Q. Composition

조합, 합성. 인터넷 사이트에서는 포함관계라는 이름으로 설명하는 것도 봤다.

한 클래스에서 다른 클래스를 멤버변수로 선언하여 포함시키는 것을 말한다. 단위별로 여러 개의 클래스를 작성하는 것이다.
ex) Circle 클래스 안의 Point 타입 변수

상속과 조합의 차이와 장단점은 무엇인가?

상속의 정의와 장단점

상속은 클래스가 다른 클래스의 필드와 메소드를 물려받아 사용하는 것이다.
장점은 아래와 같다.

  • 클래스 간의 계층 구조를 통해 객체를 분류하고 구조화할 수 있다.
  • 코드의 중복을 줄이고 코드의 재사용의 쉽다.
  • 새로운 기능이나 속성을 추가해 확장할 수 있다.
  • 상위 클래스에서 변경된 내용이 하위 클래스에 자동으로 반영된다.
    • 즉, 한 번만 수정하면 하위 클래스에서도 모두 수정된다.

단점은 아래와 같다.

  • 상위 클래스와 하위 클래스 간의 강력한 결합이 생긴다.
    • 상위 클래스에서 변경했을 경우, 하위 클래스에게 영향이 간다.
    • 이건 한 번만 수정해도 된다는 장점도 되지만, 반대로 수정한 내용이 하위 클래스에서 어떤 오작동을 일으킬지 모른다는 위험도 가지고 있다.
    • 프로그래밍에서 결합도는 낮을수록 좋다.
  • 상속을 과도하게 사용하면 복잡한 계층 구조가 형성되어 가독성이 나빠질 수 있다. 이를 클래스 폭발(class explosion)이라고 한다.
    • 예를 들자면, 핸드폰 클래스를 상속받은 아이폰 클래스와 안드로이드 클래스, 그리고 각 클래스를 상속받은 기종별 클래스 등이 있을 수 있겠다.
  • 하위 클래스에서 상위 클래스의 내부 로직을 알고 있어야할 필요성이 생기기 때문에, 캡슐화가 깨질 수 있다.
    • 상위 클래스에서 어떤 메소드를 어떻게 사용하고 있느냐를 알아야 하위 클래스에서 수정 및 확장이 가능하다.

조합의 정의와 장단점

조합은 클래스가 다른 클래스의 인스턴스를 포함하거나 참조하여 기능을 확장하는 개념이다.
장점은 아래와 같다.

  • 상속에 비해 낮은 결합도
    • 상속처럼 클래스의 필드와 메소드를 공유하는게 아니기 때문에, 한 클래스의 변경이 다른 클래스에 미치는 영향이 적다.
  • 필요한 기능을 조립해 새로운 기능을 만들 수 있으므로 유연성과 재사용성이 증가한다.
    • 다른 클래스의 인스턴스의 메소드를 호출하여 기능을 그대로 사용할 수도 있고, 동작에 필요한 메소드만 호출해 사용할 수 있다.
  • 런타임에 동적으로 교체하면서 사용할 수 있다. (유연성)

단점은 아래와 같다.

  • 각 클래스의 인스턴스를 생성하고 관리해야 하므로, 코드의 양이 증가하고 더 복잡해질 수 있다.
  • 한 객체가 다른 객체에 의존하고 있으므로, 이러한 의존성이 복잡해지면 객체의 변경이나 추가가 다른 객체에 미치는 영향을 예측하기 어려울 수 있다.
    • 결국 조합도 다른 객체를 의존하고 있으므로, 이런 문제점에 대해선 피해갈 수 없는 것 같다.
  • 여러 객체를 많이 사용할 수록, 객체를 생성하고 관리해야 하므로 메모리같은 자원 사용이 증가할 수 있다.

상속과 조합은 어떤 상황에서 사용해야 하는가?

만약 모호하다면 어떤 것을 우선적으로 고민해야할까?

상속을 사용해야 하는 경우

  • 공통된 속성이나 행동이 존재할 경우
  • 계층 구조가 명확하고, 하위 클래스가 상위 클래스의 기능을 확장하거나 수정해야 하는 경우
  • 기존 클래스의 코드를 재사용하고자 할 경우 (공통된 로직을 부모 클래스에 두고 사용)

조합을 사용해야 하는 경우

  • 낮은 결합도가 필요한 경우
  • 유연성이 중요한 경우
    • 런타임에 객체를 동적으로 변경해야 할 경우
  • 다중 상속이 복잡한 경우
    • 개인적으로 공부하면서, 이 경우엔 인터페이스를 더 많이 사용할 것 같다.

요약하자면 간단한 관계와 확장이 중요한 경우에는 상속을 사용하고, 낮은 결합도와 유연성이 중요한 경우에는 조합을 사용한다.

Q. is-a 와 has-a의 차이점

보통 상속과 조합을 이야기할 때는 is-a 관계이냐 has-a 관계이냐를 보고, 사용할 경우를 정하기도 한다.

  • is-a는 “00은 00이다”
    • 사자는 동물이다.
    • 사과는 과일이다.
  • has-a는 “00은 00을 가졌다”
    • 군인은 총을 가졌다.
    • 자동차는 엔진을 가졌다.

'ia-a'관계는 상속을 통해 표현되며, 'has-a' 관계는 조합을 통해 표현된다. 즉, 명확한 계층을 가졌고 일반화 관계라면 상속을 사용하고 소유의 개념을 가졌다면 조합을 사용한다.

여기서 일반화란 공통된 특성을 추상화하여 하나의 부모 클래스로 표현하는 것이다.

Q. 상속과 추상 클래스의 차이점

상속이 자손 클래스를 만드는데 조상 클래스를 사용하는 것이라면, 이와 반대로 추상화는 기존의 클래스의 공통부분을 뽑아내서 조상 클래스를 만드는 것이다.

  • 추상클래스는 추상화할 때, 상속은 공통으로 사용하는 기능을 하나로 묶을 때.

참고 사이트

ChatCPT

Java 접근 제어자와 접근의 의미

객체 지향 프로그래밍의 4가지 특징ㅣ추상화, 상속, 다형성, 캡슐화 -

자바는 왜 다중상속을 지원하지 않을까? (다이아몬드 문제)

☕ JAVA 업캐스팅 & 다운캐스팅 - 완벽 이해하기

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글