본문 바로가기
  • soobinhand의 기술 블로그
도서/자바 프로그래밍 언어 - James Gosling

[자바 프로그래밍 언어] 3장 클래스 확장

by soobinhand 2021. 12. 23.
728x90

3.1     확장 클래스

  • 주어진 클래스의 객체가 원래 클래스나 이를 확장한 클래스처럼 다양한 형식을 가질 수 있는 것을 다형성이라고 한다.
  • 클래스 확장은 두 가지 형식의 상속을 제공한다.
    • 협약 또는 타입의 상속, 이것에 의해 서브 클래스는 슈퍼 클래스의 타입을 얻어 슈퍼 클래스가 사용될 수 있는 곳에 다형적으로 사용될 수 있다.
    • 구현의 상속, 서브 클래스는 슈퍼 클래스의 접근 가능한 필드와 메소드의 구현을 획득한다.
  • 클래스 확장은 다양한 목적으로 사용할 수 있지만 주로 특수화를 위해 사용한다. 
  • 특수화란 특정 클래스를 확장하는 서브 클래스에서 새로운 행동을 정의하여 서브 클래스가 슈퍼 클래스의 특수화된 버전이 되는 것을 말한다.
  • 외부 클래스에서 접근할 수 있는 메소드와 필드의 집합 그리고 클래스의 멤버들이 어떻게 행동해야 하는지 정의하는 것을 협약(contract)라고 한다. 
  • 슈퍼 클래스가 구현된 방법을 변경하는 것이 합당할지라도 협약을 침범하는 방법으로 구현을 변경해서는 안 된다.
  • 클래스를 확장하는 것은 클래스가 나타내는 협약의 개념을 확장하는 접근 제어 메커니즘과 함께 동작해야 한다.
  • 클래스를 확장하면 협약의 상속과 구현의 상속이 항상 함께 일어난다. 그러나 인터페이스를 사용하면 구현을 독립시킨 새로운 타입을 정의할 수 있다. 또한 집합과 포워딩을 사용하여 타입에 영향을 주지 않고도 이미 작성된 구현을 재사용할 수도 있다.

3.2     확장 클래스의 생성자

  • 확장 클래스의 생성자는 명시적이든 묵시적이든 슈퍼 클래스의 생성자를 호출해서 상속된 상태의 생성을 슈퍼 클래스에 위임해야 한다.
  • 확장 클래스의 생성자는 슈퍼 클래스의 생성자를 명시적으로 직접 호출해야 한다. 이를 슈퍼 클래스의 생성자 호출이라고 하며 이를 위해 super 키워드가 제공된다.
  • 만약 생성자의 첫 번째 줄에서 슈퍼 클래스의 생성자나 서브 클래스의 다른 생성자를 호출하지 않는다면 슈퍼 클래스의 무인자 생성자가 생성자 내의 다른 문장이 실행되기 전에 자동으로 호출된다. 즉, 서브 클래스의 생성자는 마치 다음 코드가 첫 번째 문장에 존재하는 것처럼 인식한다.
  • super();
  • 생성자는 메소드가 아니며 상속할 수 없다. 만약 슈퍼 클래스에 생성자가 여러 개가 정의되어 있고 확장 클래스에도 이와 동일한 형식의 생성자를 제공해야 한다면 확장 클래스는 각각의 생성자를 명시적으로 정의해야 한다.
  • 객체가 생성될 때 필드 초기화가 먼저 이뤄진다. 그 후에 다음과 같은 세 단계를 통해 객체가 생성된다.
    • 슈퍼 클래스의 생성자 호출
    • 클래스 초기자나 초기화 블록을 실행하여 필드 초기화
    • 생성자의 몸체 실행
  • 명시적이든 묵시적이든 가장 먼저 슈퍼 클래스의 생성자가 실행된다.
  • class X{
    
    protected int xMask = 0x00ff;
    protected int fullMask;
    
    public X(){
    	fullMask = xMask;
    	}
    }
    
    class Y extends X{
    
    protected int yMask = 0xff00;
        
    public Y(){
    	fullMask |= yMask;
        }
    }
  • 이 코드에서, Y 객체를 생성하면서 생성 단계를 하나씩 추적하면 다음과 같은 필드 값을 얻을 수 있다.
  • 생성자는 오버라이드가 가능한 메소드 (private, static, final이 아닌 메소드)를 호출하는 일이 없어야 한다.
  • 단계 발생 xMask yMask fullMask
    0 기본 값으로 필드 설정 0 0 0
    1 Y 생성자 호출 0 0 0
    2 X 생성자 호출 (super) 0 0 0
    3 Object 생성자 호출 0 0 0
    4 X 필드 초기화 0x00ff 0 0
    5 X 생성자 실행 0x00ff 0 0x00ff
    6 Y 필드 초기화 0x00ff 0xff00 0x00ff
    7 Y 생성자 실행 0x00ff 0xff00 0xfff

3.3     멤버 상속과 재정의

  • 메소드 오버라이드는 슈퍼 클래스의 구현을 서브 클래스의 메소드로 교체하는 것을 의미한다. 오버라이드한 메소드의 시그니처는 슈퍼 클래스와 같아야 하지만 반환 타입은 다를 수 있다.
  • 오버라이드 메소드는 자신만의 접근 제한자를 가질 수 있다. 
  • 슈퍼 클래스에서 protected로 선언된 메소드는 서브 클래스에서도 동일한 제한자인 protected로 선언하거나 public으로 선언해야 한다.
  • 오버라이드 메소드는 final이 될 수 있지만 오버라이드 대상이 된 상위 메소드는 final이 될 수 없다.
  • 필드는 오버라이드할 수 없으며 숨기는 것만 가능하다.
  • 메소드를 접근할 수 있는 경우에는 오버라이드할 수 있다. 접근할 수 없는 메소드는 상속되지 않으며 상속되지 않는다는 것은 오버라이드할 수 없다는 것이다.
  • private 메소드인 경우에는 항상 현재 클래스 내에 선언된 메소드가 호출된다.
  • 클래스 내의 필드나 메소드 같은 정적 멤버들은 오버라이드할 수 없다. 왜냐하면 정적 멤버들은 항상 숨겨져 있기 때문이다.
  • super 키워드는 클래스의 모든 비정적 메소드에서 사용할 수 있다.
  • 메소드를 오버라이드할 때는 메소드의 시그니처가 슈퍼 클래스에 있는 메소드와 같아야 한다. 만약 다르다면 오버라이드가 아니라 오버로드된 것이다. 오버라이드 메소드의 반환 타입은 다를 수 있다. 만약 반환 타입이 참조 타입이라면 오버라이드 메소드를 슈퍼 클래스에 선언된 타입의 서브 타입으로 선언할 수 있다. 이러한 선언이 가능한 이유는 슈퍼 클래스 참조는 항상 서브 클래스의 인스턴스 참조를 가질 수 있기 때문이다. 이를 공변 반환 타입이라고 한다.

3.4     타입 호환성과 변환

  • 상위 타입을 요구하는 곳에 하위 타입을 전달하면 확장 변환이 일어난다. 이러한 확장 변환은 자동으로 일어난다. 이와는 다르게 상위 타입 참조를 하위 타입 참조로 변환하는 축소 변환은 cast 연산자를 사용하여 명시적으로 변환해야한다.
  • 기본 타입을 래퍼 클래스의 타입으로 변환하는 것을 박싱 변환이라고 한다.
  • cast 는 표현식을 지정한 타입으로 다뤄야 한다는 것을 컴파일러에게 알리고 싶을 때 사용한다.
  • 확장 변환은 업캐스트 (upcast)라고 한다. 왜냐하면 타입 계층도의 하위 타입에서 상위 타입으로 변환하기 때문이다. 이는 항상 유효하기 때문에 안전한 캐스트 (safe cast)라고도 한다.
  • 축소 변환은 상위 타입을 하위 타입으로 변환하므로 다운캐스트 (downcast)라고 한다. 하지만 다운캐스트는 유효하지 않을 수 있어서 불안전 캐스트 (unsafe cast)라고도 한다.
  • instanceof 연산자를 사용하면 표현식 왼쪽에 선언한 타입이 오른쪽에 선언한 타입과 호환되는지를 검사할 수 있다. 호환된다면 instanceof 연산자는 true를 반환한다. instanceof를 사용하면 예외를 발생시키지 않고 다운캐스트를 수행할 수 있다.
  • instanceof 를 사용한 타입 검사는 메소드가 확장된 타입의 객체가 필요하지 않을 때 특히 유용하다.

3.5     protected의 실제 의미

  • 클래스 멤버를 protected로 선언하는 것은 이 클래스를 확장한 클래스에서 접근할 수 있다는 것을 의미한다.
  • 더 정확히 말하면, protected 멤버는 선언된 클래스 내에서 접근할 수 있으며 같은 패키지에서도 접근할 수 있다. 또한 이 클래스와 같은 타입의 객체 참조를 통해서도 protected 멤버에 접근할 수 있다.

3.6     메소드와 클래스를 final로 선언하기

  • 메소드를 final로 선언하면 확장 클래스는 메소드의 행위를 변경하기 위해 메소드 오버라이드를 할 수 없다. 즉, final로 선언된 메소드가 최종 버전이 된다.
  • final로 선언된 클래스는 다른 클래스에서 확장할 수 없다. 그리고 final 클래스의 모든 메소드는 사실상 final이 된다.
  • 메소드를 final로 만드는 것은 이 메소드를 완전히 고정된 것으로 간주한다는 것을 의미한다.
  • 어떤 것을 final로 선언한다는 것은 지금까지 살펴본 제약을 추가하는 것임을 기억하기 바란다.
  • 대부분의 경우 클래스를 확장할 수 없도록 클래스에 final을 선언하는 것으로 클래스의 안정성을 높일 수 있다. 하지만 이렇게 하는 대신에 클래스의 모든 메소드를 final로 만들 수도 있다.
  • final의 다른 기능은 최적화를 통해 내부적인 호출 단계를 줄일 수 있다는 것이다.
  • 최적화는 private과 static 메소드에 동일하게 적용될 수 있다. 왜냐하면 이들은 오버라이드될 수 없기 때문이다.

3.7     추상 클래스와 메소드

  • 추상 클래스는 구현의 일부만을 정의한 클래스를 말하며 이를 통해 일부 메소드나 전체 메소드의 구체적인 구현을 서브 클래스에서 하게 할 수 있다. 추상의 반대는 구체다. 구체 클래스는 상위 클래스로부터 상속받은 추상 메소드를 포함하여 모든 메소드를 완전하게 구현한 클래스를 말한다.
  • 추상 클래스는 abstract로 선언하고 이 클래스에서 구현하지 않을 메소드는 abstract로 선언하면 된다.
  • 메소드를 선언할 필요는 있지만 구현은 제공하고 싶지 않을 때는 인터페이스를 사용하면 된다.
  • abstract 메소드를 한 개라도 선언했다면 해당 클래스를 abstract 로 선언해야 한다.
  • 추상 클래스는 행위의 일부분을 해당 타입의 일부나 전체 객체를 위해 정의하고 다른 행위는 슈퍼 클래스가 아닌 특정 클래스에서만 정의하고 싶을 때 유용하게 사용할 수 있다.

3.8     Object 클래스

  • 동일성은 참조가 같다는 것을 의미한다.
  • 동등성은 값이 같다는 것을 의미한다.

3.9     객체 복제

  • Object.clone 메소드는 클래스에 clone 메소드를 작성하는 것을 도와준다.
  • clone 메소드는 객체의 상태를 복사한, 새로운 객체를 반환한다.
  • clone 메소드를 작성할 때 고려해야 할 세 가지 중요한 요소
    • 객체 복제를 위한 clone 메소드를 제공하기 위해서는 반드시 Cloneable 인터페이스를 구현해야 한다.
    • Object 클래스에 구현된 clone 메소드는 원본 객체의 모든 필드를 새로운 객체에 복사하는 역할만 수행한다. 이 메소드는 많은 클래스에서 동작할 수 있지만 보완할 필요가 있기 때문에 서브 클래스에서는 이를 오버라이드해야 한다.
    • CloneNotSupportedException 은 클래스의 clone 메소드가 호출돼서는 안 된다는 것을 알리기 위해 사용할 수 있다.
  • 클래스는 네 가지 다른 형태의 clone 메소드를 가질 수 있다.
    • clone 메소드를 지원한다. 이런 클래스는 Cloneable을 구현해야 하며 예외를 던지지 않도록 clone 메소드를 작성해야 한다.
    • clone 메소드를 조건부로 지원한다. 이런 클래스는 원칙적으로는 복제할 수 있는 컬렉션 클래스가 되어야 한다. 하지만 컬렉션의 요소를 복제할 수 없으면 클래스도 제대로 복제할 수 없다. 이 종류의 클래스도 Cloneable을 구현할 것이지만 다른 객체를 복제하는 동안 clone 메소드가 CloneNotSupportedException을 발생시킬 수도 있다. 또는 클래스 그 자체는 복제할 수 있기를 바라지만 서브 클래스는 복제되지 않기를 바랄 수도 있다.
    • 서브 클래스가 clone 메소드를 지원하기는 하지만 공개적으로 지원하는 것을 원하지는 않는다. 이런 클래스는 Cloneable을 구현하지 않아야 한다. clone 메소드의 기본 구현이 올바르지 않더라도 클래스의 필드들은 정확히 복제될 수 있도록 clone 메소드를 protected로 구현해야 한다.
    • clone 메소드를 금지한다. 이런 클래스는 Cloneable을 구현하지 않으며 clone 메소드가 CloneNotSupportedException을 항상 발생하게 선언한다.
  • Object.clone 메소드는 Cloneable 인터페이스를 구현한 객체에서 호출된 것인지를 검사하고 만약 구현하지 않았다면 CloneNotSupportedException을 발생시킨다. 만약 구현하였다면 Object.clone 메소드는 clone 메소드가 호출되었을 때 원본 객체와 동일한 객체를 새로 생성한다.
  • 클래스를 복제할 수 있게 클래스를 만드는 가장 단순한 방법은 Cloneable 인터페이스를 구현한 후에 clone메소드를 오버라이드하고 이를 public으로 선언하는 것이다.
  • Object에 선언되어 있는 clone 메소드에는 throws CloneNotSupportedException이 작성되어 있다. 이는 어떤 클래스를 복제될 수 있게 선언할 수 있지만 서브 클래스는 복제될 수 없게 할 수 있다는 것을 의미한다.
  • 원칙적으로 대부분의 객체들은 복제될 수 있다. 
  • clone 메소드의 기본 구현은 필드를 단순히 필드에 복사하는 것으로서 얕은 복제 또는 복사라고 불리는 방식이다.
  • 깊은 복제는 필드가 참조하는 각각의 객체와 배열에 있는 모든 요소를 복제하는 것으로서 한 객체로부터 도달 가능한 모든 객체를 반복하여 복제한다.

3.10     클래스 확장 : 어떻게 그리고 언제

  • 2차원 공간에서 점을 (x, y)로 표현하는 Point 클래스가 있다고 하자. 화면의 색상을 표현하기 위해 Point 클래스를 확장해서 Pixel 클래스를 작성하려고 한다. 이 때, Pixel은 Point와 IsA 관계에 있다고 말하며 영어로는 Pixel is a Point라고 할 수 있다. 즉, 이것은 Point를 사용했을 때 true인 곳에는 Pixel을 사용해도 true라는 것을 의미한다.
  • 이에 반해 원, circle은 점, point가 아니다. 원을 점과 반지름으로 표현할 수는 있지만 원은 점이 될 수 없다. 원의 중심은 점이며 (IsA) 원은 이러한 중심을 가질 수 있지만 (HasA) 원은 반지름을 가진 점이 아니다. (IsNotA) 그러므로 원은 점의 서브 클래스가 될 수 없다.
  • IsA와 HasA 관계를 바르게 사용하는 것은 어려우면서도 중요하다.

3.11     클래스를 확장 가능하게 설계하기

  • final이 아닌 클래스는 선언된 접근 제한자에 따라 두 가지 형태로 구분된다. public 클래스는 클래스를 사용하는 프로그래머를 위한 것이고 protected 클래스는 클래스를 확장하고자 하는 프로그래머를 위한 것이다.
  • 일반적으로 클래스를 확장할 수 있도록 설계하는 것이 가장 좋은 방법이다.
  • 메소드를 final로 선언한 것은 확장 클래스가 이 메소드를 변경하여 행위를 수정할 수 없도록 하기 위해서이다.
  • 클래스를 확장 못하게 설계하면 서브 클래스가 이를 종종 오용할 소지가 생긴다. 어떤 클래스가 서브 클래스를 허용할거라면 이 클래스의 protected 부분은 주의 깊게 설계해야만 한다. 결과적으로 확장 클래스가 접근할 필요가 없는 멤버는 protected로 선언하지 말아야 한다.

3.12     단일 상속과 다중 상속

  • 참조 시에 어떤 클래스에서 나온 상속인지 애매하다. 이런 문제를 피하기 위해서 자바는 단일 상속 모델을 채택하였다.
  • 인터페이스는 다중 구현 상속으로 인한 문제는 발생시키지 않으면서 다중 상속의 장점을 얻을 수 있다.
728x90

댓글