공부방/JAVA

상속 - 백기선 자바라이브스터디

EVO. 2023. 8. 19. 15:20

문제제기


예를들어 상속이라는 개념을 모르는 상태에서 강아지와 고양이 클래스를 만들어 보겠습니다.

 

강아지

 

고양이

공통적인게 눈에 띕니다. 이름, 종류 의 필드도 같고 소리를 낸다는 것과 먹는다는 것은 공통적인 행위 입니다. 

 

그렇다면 이 부분은 이제 상속의 개념을 적용할 수가 있습니다

 

상속 다이어그램

또한 name과 종류는 Animal의 부모 클래스가 가지고 있으니 생성자를 통해 이름과 종류를 받아서 저장하고 bark()는 Dog와 Cat마다 소리내는 방식이 다르니 @Override 시키면 될 거 같습니다.

아래는 이러한 요구사항을 가지고 만들어낸 결과 입니다.

 

Animal.java

Dog.java

 

Cat.java

 

보다싶이 Animal이라는 클래스의 공통 필드와 메서드를 적용받기위해 extends라는 키워드를 사용하였고 
shouting이라는 공통 메서드는 각 클래스마다 다르게 적용하므로 오버라이딩을 했습니다. 그리고 공통필드인 name과 종류는 인스턴스마다 다르게 받아오기 때문에 생성자를 통해 초기화하도록 하였으며 super()라는 상속때 쓰는 부모생성자호출메서드를 사용했습니다.

 

앞으로 이에 대해 조금 더 자세히 짚어보겠습니다.

 

상속의 특징


  1. 다중상속을 지원하지 않는다. 즉 extends 키워드 뒤에는 단 하나의 부모 클래스만 올 수 있다.
  2. 부모의 생성자는 상속되지 않는다.
  3. 자식 클래스는 부모 클래스가 가진 멤버변수와 메소드를 모두 상속받는다.
  4. 부모클래스 내에서 멤버 변수 또는 메소드가 private 접근제어자를 사용하면
    멤버변수는 바로 접근이 불가능하므로 부모클래스가 만든 getter나 다른 메소드를 통해 접근 
    메소드는 상속되지 않는다.
  5. static 메서드 또는 변수도 상속이 된다.
  6. 동일한 이름의 변수가 부모 클래스와 자식 클래스에 둘 다 존재할 경우 부모 클래스의 변수는 가려진다.
    (보통 @Override을 붙여서 같은 메서드임을 컴파일러에게 체크 받는다)
  7. 상속에 대한 횟수를 제한하지 않는다.
  8. 최상위 클래스는 [Object] 클래스이며, Object 클래스만이 유일하게 부모 클래스를 가질 수 없다. 즉 , 모든 클래스들은 Object 클래스의 자식 클래스 이다.

 

super 키워드


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

 

인스턴스의 변수의 이름과 지역 변수의 이름이 같은 경우, 인스턴스 변수 앞에 this 키워드로 구분할 수 있었던 것 처럼

부모 클래스의 멤버와 자식 클래스의 멤버 이름이 같은 경우 super 키워드를 사용하여 구분할 수 있습니다.

 

출처: https://velog.io/@rhdmstj17/java.-super%EC%99%80-super-%EC%99%84%EB%B2%BD%ED%95%98%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

 

super()

상속의 생성자는 상속되지 않습니다. 따라서 부모클래스의 멤버를 초기화하기 위해선 부모 클래스의 생성자를 호출해야 합니다. 그럴때 쓰는 super()

 

  1. 명시적으로 자식 클래스에서 부모 생성자를 호출하지 않아도 컴파일러는 super 키워드를 이용한 생성자 호출이 자동 삽입되어 부모 클래스의 생성자를 호출한다
  2. 자식클래스의 기본 생성자에만 자동으로 추가하고 매개변수를 가진 생성자에서는 명시적으로 super()를 입력해야한다.
  3. 부모클래스에 기본 생성자가 존재하지 않고 매개변수 생성자만 있는 경우 자식 기본 클래스에서는 무조건 명시적으로 super(매개변수)를 적어야한다.


    안하면 이렇게 오류가 생깁니다.

다행히 컴파일러가 잡아주는데 이를 해결하는 방법은 세가지 있습니다

1. 부모클래스에 기본 생성자를 추가

2. 자식클래스의 기본생성자에서 부모클래스의 매개변수 생성자 호출

3. 기본생성자를 지워버리기

상황에 맞는 방식으로 택하면 될것 같습니다. 저는 당장 super()를 명시하는 경우가 거의 없어서 1번을 선호하고 있습니다.

 

메소드 오버라이딩


하위 클래스(자식 클래스)에 부모 클래스에서 선언된 메서드와 동일한 메서드가 있는 경우 이를 method overriding 이라 합니다

 

사용하는 곳

메소드 오버라이딩은 슈퍼클래스에서 이미 제공된 메서드의 특정 구현을 제공하는 데 사용됩니다.

메소드 오버라이딩은 다형성에 사용됩니다.

 

주의할 점

  • 메서드의 이름은 부모 클래스와 같아야 한다
  • 메서드는 부모 클래스와 동일한 매개변수를 가져야 한다
  • 상속관계여야 한다

SBI s = new SBI(); 하고 getRateOfInterest()를 호출하면 당연히 8이 나옵니다. 

 

단, 정적메서드는 오버라이드 할 수 없습니다.

출처: https://hsik0225.github.io/java/2020/12/17/Static-Override/

 

정적메소드는 static binding 즉, 컴파일 시점에 올라갑니다. 따라서 static 메서드는 어디에 쓰일지 이미 컴파일 시점에 다 정해서 런타임 때는 더이상 static을 찾는 작업을 시행하지 않습니다

 

dynamic binding은 런타임 시점에 올라갑니다. 따라서 override 된 인스턴스 메서드는 런타임에 어떤 메서드가 실행될지 결정합니다

 

JVM이 메서드를 호출할 때, 부모의 인스턴스 메서드의 경우 런타임 시 해당 메서드를 구현하고 있는(오버라이딩한) 실제 객체를 찾아 호출합니다(다형성) 

하지만 컴파일러와 JVM 모두 static 메서드에 대해서는 객체를 찾는 작업을 시행하지 않기 때문에 static 메소드의 경우 컴파일 시점에 선언된 타입의 메서드를 호출 합니다. 애초에 static메서드는 힙이 아니라 data영역에 있습니다 이미 정해지고 변경이 불가능한 (read only) data영역은 jvm이 런타임때 data영역에 있는 static을 꺼내어 쓰지 다형성을 적용하지 않습니다.

 

정리

private, final and static methods and variables 들은 정적 바인딩을 사용하기 때문에 컴파일 시점에 결정됩니다. 

이들은 오버라이딩 하는 것이 불가합니다. 

 

다이나믹 메소드 디스패치 


Dynamic Method Dispatch는 오버라이드된 메서드에 대한 호출이 컴파일 타임이 아닌 런타임에 해결되는 매커니즘 입니다.

오버라이드된 메서드가 수퍼클래스 참조를 통해 호출되면 Java는 호출 시 참조되는 객체의 유형에 따라 해당 메서드의 

어떤 버전(수퍼/서브 클래스)을 실행할 지 결정합니다. 

런타임에는 참조되는 객체의 유형(참조변수의 유형 x)에 따라 재정의된 메서드의 어떤 버전을 실행될지 결정됩니다.

 

메인함수에서 a.m1()을 호출 했을 때 우리는 클래스 B의 오버라이딩된 함수가 호출될 것이라는 것을 알고 있습니다. 

하지만 컴파일러는 A a 라는 문장을 보고 A를 상속받는 B가 있구나(서브클래스 존재) 하고 더이상 누굴 오버라이드하는지 컴파일 타임에 결정 짓지 않습니다. 

그리고 a.m1()을 봤을 때 아직 결정되지 않았으므로 런타임시점에 실행되어 B의 메소드 m1이 실행됩니다.

이러한 것을 Dynamic Method Dispatch 라고 합니다.

 

실제 프로그램 실행 시에는 m1() 메서드 호출 과정에서 receiver parmeter가 전달됩니다.

receiver parameter는 클래스의 this에 해당하는 Object입니다. 

위 코드에서는 B의 receiver parmeter가 전달되기 때문에 B의 m1()이 실행되는 것입니다. 

 

장점

  • 동적 메서드 디스패치를 통해 다형성의 핵심인 메서드 오버라이딩 기능을 지원합니다
  • 클래스는 모든 파생 클래스에 공통으로 적용되는 메서드를 지정할 수 있으며, 서브클래스는 이러한 메서드의 일부 또는 전부에 대한 특정 구현을 정의할 수 있습니다

그와 반대인 static method dispatch도 존재하는데 이는 컴파일 시점에 결정됩니다

 

추상 클래스


추상클래스는 추상 메서드를 선언해 놓고 상속을 통해 자식 클래스에서 메서드를 완성하도록 유도하는 클래스 입니다. 

이러한 특성에 의해 미완성 설계도라고 표현을 하며 미완성 설계도 이므로 따로 인스턴스를 생성할 수 없습니다.

abstract class 클래스{
	...
    public abstract void 메서드();
    
}

주의할점

  • 추상 클래스는 추상 메서드를 가지지 않아도 된다
  • 추상메서드를 하나라도 가지면 그 클래스는 추상클래스가 되므로 abstract 예약어를 붙인다
  • 추상메서드를 선언했다면 자식 클래스는 해당 메서드를 반드시 구현해야 한다 만약 구현하지 않았다면 자식클래스도 추상클래스가 되어야 한다 
  • 자바는 다중 상속을 지원하지 않기 때문에 여러 개의 추상 클래스를 상속할 수 없다
  • 추상클래스는 static이나 final이 아닌 필드를 가질 수 없다
  • 추상메서드의 접근 제어자에는 private 사용 불가능 

 

final 키워드


final 키워드는 크게 세개의 부분에서 사용될 수 있습니다

  1. 변수에 final : 해당 변수는 더이상 재할당 할 수 없게 된다 
  2. 메서드에 final : 해당 메서드는 오버라이드 할 수 없다
  3. 클래스에 final : 클래스는 상속 할 수 없다. 즉 부모 클래스가 될 수 없다

final 키워드를 사용하면 ‘어디에선가 재할당되지 않았을까?’, '값이 예측하지 못한 곳에서 변하는 상황을 막기 위해 검증 로직을 추가해야하나?' 라는 불안감에서 벗어나 심리적 안정감을 얻을 수 있다. 이를 검증하는 데에 에너지를 쏟지 않아도 되고, 한번에 한가지만 집중할 수 있게 된다. (hudi 블로그 구절)

 

 

변수에 final

final int number = 1;
number = 2; // Cannot assign a value to final variable 'number'

재할당시 컴파일 에러 발생 

 

final 키워드를 사용한 변수에 초기값을 설정하는 방법은 두가지

  • 단순한 리터럴 값이면 클래스의 필드에 선언
 private static final int MOVE_STEP = 1;

 

static을 같이 붙이는 이유는 static을 사용하면 컴파일 타임에 메모리 할당을 딱 한번 하기 때문에 메모리 효율 측면에서 좋습니다

  • 생성자에 초기화
    별도의 로직이 필요할 때 사용합니다
    하지만 static을 붙일 수 없기 때문에  생성자 대신 static{} 블록을 사용할 수 있습니다

메소드에 final

메소드에 final 키워드를 사용하는 이유는 누군가에 의해 변경을 원치 않을 메소드라는 것을 명시하기 위해 사용됩니다.

 

클래스에 final

클래스에 final 키워드를 사용하면 상속할 수 없는 클래스가 됩니다. 

대표적으로 java.lang 패키지의 String 클래스가 final 키워드를 사용합니다

 

 

 

이처럼 불변성을 지키기위해 final 키워드를 사용하지만 완벽한 불변성을 의미하지는 않습니다

final은 해당 변수의 재할당을 막아줄 뿐, 참조하고 있는 객체 내부의 상태가 변하지 않았음을 보장해주지는 않습니다

final이 사용되었지만 이처럼 내부의 값이 변경될 수 있습니다.

 

Object 클래스


모든 클래스는 Object 클래스의 자손

우리가 단지 extends 키워드 없이 클래스를 선언해도 컴파일러가 자동으로 주입해준다

Object 클래스의 주요 메서드들

  • clone()
  • equals()
  • finalize()
  • getClass()
  • hashcode()
  • notify()
  • notifyAll()
  • toString()
  • wait()

이중 가장 자주 쓰이는 equals(), hashCode(), toStirng()을 소개하겠습니다

 

equals()

동일성 vs 동등성

 

동일성은 비교 대상이 실제로 똑같은 상태 여야 합니다 즉 둘은 실제로 하나입니다 자바에서는 두 비교대상을 '=='연산자로 비교

 

동등성은 비교 대상이 같은 값이라고 우리가 정의하는 것입니다 equals() 메서드를 오버라이딩 하여 비교합니다 

 

equals를 오버라이딩 하여 코드를 보면 다음과 같습니다

equals는 보시다 싶이 동일성도 비교하지만 결국엔 동등성 까지 비교 합니다.

하지만 equals를 오버라이딩 하지 않으면 Object가 제공하는 equals를 사용할텐데 이는 원하는 결과를 가져올 수 없습니다 따라서 equals를 무조건 오바라이딩해야 합니다

이대로 질문에 대한 대답을 했더니 점수를 잘 주는 것 같다(flab : ai 멘토)

추가적인 답변

 

hashCode()

equals와 마찬가지로 동등성을 확인하기 위해 사용됩니다 하지만 이 역시 오버라이딩하여 변환을 하지 않으면 Object의 hashCode()를 사용할텐데 이는 동일성만을 확인 합니다

 

먼저 hash 값을 사용하는 Collection(HashSet, HashMap, HashTable)을 사용할 때 어떤 과정을 거치냐면 어떤 객체를 map에 넣을때 객체 그대로 key로 넣지 않고 hashCode()로 변환합니다 그리고나서 해당 객체의 hashCode를 용량(capacity)으로 나눈 값을 key로 집어넣습니다

 

 

이제 2의 객체를 key로 HashMap에서 확인을 위해 hashCode()로 변환합니다 그리고 용량이 16이면 16으로 나누게 되는데 그러면 당연히 key가 겹칠 가능성이 매우 높습니다 만약 2를 hashCode하고 나눈 키랑 3을 hashCode하고 나눈 키랑 같으면 equals()연산을 추가적으로 실행합니다. 그리고나면 정확한 키를 찾고 value를 얻을 수 있습니다. 

 

HashSet을 예를 들어보겠습니다. 저희는 동일성을 가진 객체는 아니지만 동등성을 가진 객체를 집어넣는다고 가정을 해보겠습니다 

그러면 이 객체를 넣을 때 hashCode를 오버라이딩 하지 않으면 Object의 hashCode를 이용할텐데 이는 동등성을 가진 객체라도 hashCode변환후 capacity로 나눈 키를 집어넣기 때문에 

 

hashCode가 다르게 나올것이고 map의 사이즈도 2로 찍힐겁니다.

 

출처: https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/

이렇게 되서는 안되기에 재정의가 필요합니다. 

@Override
    public int hashCode() {
        return Objects.hash(name);
    }

이렇게 하면 hashCode리턴값도 같고 equals도 재정의를 해서 같게 판단하기 때문에 동등객체로 판단하여 키값을 중복하는 일이 없어집니다. 

 

toString()

인스턴스를 문자열로 만드는 것으로 재정의 하여 인스턴스 내부의 값을 적어 의미있는 값으로 재정의 합니다

 

 

 

 

 

 

 

 

 

 

출처


1. https://hsik0225.github.io/java/2020/12/17/Static-Override/

2. https://www.javatpoint.com/method-overriding-in-java

3. https://ttl-blog.tistory.com/776

4. https://www.geeksforgeeks.org/dynamic-method-dispatch-runtime-polymorphism-java/

5. https://code-lab1.tistory.com/287#:~:text=%EC%B6%94%EC%83%81%20%ED%81%B4%EB%9E%98%EC%8A%A4(Abstract%20Class)%EB%8A%94,%EB%A5%BC%20%EC%83%9D%EC%84%B1%ED%95%A0%20%EC%88%98%20%EC%97%86%EB%8B%A4. 

6. https://blog.naver.com/swoh1227/222181505425

7. https://hudi.blog/java-final/

8. https://school.programmers.co.kr/app/courses/17778/dashboard

9. https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/