[자바 스터디] 6주차 : 상속

2023. 1. 23. 15:44·스터디/자바

자바 상속의 특징

상속

  • 한 클래스(자식 클래스)가 부모 클래스의 모든 속성(필드)과 행동(메소드)를 취득하는 메커니즘
    • 즉, 부모 클래스를 기반으로 새로운 클래스를 만들 수 있다
  • OOP의 중요한 요소 중 하나

상속을 사용하는 이유

  • 메소드 오버라이딩 (runtime polymorphism이 가능해진다)
  • 코드 재사용성

상속 문법

  • extends 키워드를 사용
public class Parent {}

---

public class Child extends Parent {}

상속 구조

자바에서 가능한 구조

  1. Single
  2. Multilevel
  3. Hierarchical

public class ClassA {...}

// Single
public class ClassB extends ClassA {...}

// Multilevel
public class ClassB extends ClassA {...}
public class ClassC extends ClassB {...}

// Hierarchical
public class ClassB extends ClassA {...}
public class ClassC extends ClassA {...}

자바에서 (클래스로는) 불가능한 구조

  1. Multiple
  2. Hybrid

public class ClassA {...}
public class ClassB {...}

// Multiple
public class ClassC extends ClassA, ClassB {...} // 불가 (컴파일 오류)

// Hybrid
public class ClassB extends ClassA {...}
public class ClassC extends ClassB {...}
public class ClassC extends ClassB, ClassC {...} // 불가 (컴파일 오류)
  • 자바에서 다중 상속이 불가능한 이유
public class ClassA {
    public void printMessage() {
        System.out.println("A");
    }
}
public class ClassB {
    public void printMessage() {
        System.out.println("B");
    }
}

public class ClassC extends ClassA, ClassB {
    public void someMethod() {
        printMessage(); // ClassA와 ClassB 중 어떤 부모 클래스의 메소드를 호출해야할지 모호한 문제 발생 -> 아예 지원 X
    }
}

super 키워드

  • 부모 클래스의 필드, 메소드 등에 접근하기 위해 사용
    • 자식 클래스에는 없고 부모 클래스에만 있는 메소드의 경우 super 없이 사용해도 부모의 것을 호출
    • 따라서, 주로 이름이 동일한 메소드, 생성자 등에서 자식 / 부모 클래스의 멤버를 구분하기 위해 사용
  • super 사용 예시
class Parent {
    protected int age = 50;
}

class Child extends Parent {
    protected int age = 20;

    public int getAge() {
        return age;
    }

    public int getParentAge() {
        return super.age;
    }
}

---

Child child = new Child();
System.out.println(child.getAge()); // 20
System.out.println(child.getParentAge()); // 50

생성자에서의 super()

  • 생성자에서의 super 사용 예시
  1. 부모 클래스에 기본 생성자가 존재하는 경우
    1. 이 경우, 자식 클래스의 생성자에서 명시하지 않아도 항상 부모의 기본 생성자를 super()로 호출해준다.
public class Parent {
    public Parent() {
        System.out.println("부모 생성자 호출");
    }
}

public class Child extends Parent{
    private int age;

    public Child() {
        System.out.println("자식 생성자 호출");
    }

    public Child(int age) {
        this();
        this.age = age;
    }
}

---

Child child1 = new Child();
// 출력 :
// 부모 생성자 호출
// 자식 생성자 호출

Child child2 = new Child(10);
// 출력 :
// 부모 생성자 호출
// 자식 생성자 호출
  1. 부모 클래스에 기본 생성자가 존재하지 않는 경우
    1. 이 경우, 부모 클래스의 생성자를 super(인자)로 명시적으로 호출해야 한다. 아니면, 컴파일 오류
class Parent {
    private int age;

    Parent(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

class Child extends Parent {
    Child(int age) {
        // 부모의 생성자 명시적 호출 필수
        // 없으면 컴파일 오류
        super(age);
    }

    Child() {
        // 자식 클래스의 **기본 생성자**에서도 부모의 생성자 명시적 호출 필수
        // 없으면 컴파일 오류
        super(10);
    }
}

---

Child child = new Child(20);
System.out.println(child.getAge()); // 20

메소드 오버라이딩

  • 부모 클래스에 이미 선언된 메소드를 자식 메소드에서 다시 작성하는 것

사용 이유

  • 부모 클래스에서 이미 제공되던 동작을 자식 클래스의 특정 구현에 맞게 제공하기 위해 사용
  • 은행에 따라 달라지는 이자율 계산 과 같이 구현체마다 다른 동작/값을 수행하고자 할 때 사용
public class Bank {
    public double getRateOfInterest() {
        return 2.5;
    }
}

public class BankA extends Bank {
    @Override
    public double getRateOfInterest() {
        return 3.0;
    }
}

public class BankB extends Bank {
    @Override
    public double getRateOfInterest() {
        return 1.5;
    }
}

자바의 메소드 오버라이딩 규칙

  • 부모 클래스의 메소드와 그 이름이 같아야 함
  • 부모 클래스의 메소드와 파라미터가 같아야 함
  • 반환 타입도 같아야함
    • 다만, 자식 메소드의 반환 타입이 부모 메소드의 반환 타입의 sub-type인 경우도 가능
  • 메소드 오버라이딩 시 작성하는 @Override는 필수는 아니지만, 명시적으로 작성하는게 좋음
    • @Override는 단순 마커 역할뿐 아니라, 실제로 제대로 오버라이드 되었는지 검증 역할도 수행

접근제어자

  • 부모 클래스 메소드보다 더 허용할 순 있으나, 덜 허용하진 못함
    • 즉, default -> public 은 가능하지만, protected -> private 는 불가능
  • 추가로, private 메소드는 오버라이딩이 불가능하다
    • private 메소드는 컴파일 시 바인딩되기 때문

final 키워드

  • final로 선언된 메소드는 오버라이딩이 불가능하다
    • 클래스 관점에서는 뒤의 항목에서 다시 작성함

static 메소드

  • static 메소드는 compile-time에 메소드가 메모리에 올라감
    • 즉, 컴파일러가 어떤 메소드를 실행할지를 컴파일 시에 결정함
    • 따라서, 자식 클래스에서 static 메소드를 재정의해도 실제 타입에 관게없이 선언된 타입에 따라 호출되는 메소드가 결정됨
  • 위와 같은 이유로 static 메소드의 재정의는 overriding이 아닌 hiding이라고 불림
  • 예시
class Parent {
    public static void staticPrint() {
        System.out.println("부모");
    }

    public void print() {
        System.out.println("부모");
    }
}

class Child extends Parent {
    public static void staticPrint() {
        System.out.println("자식");
    }

    public void print() {
        System.out.println("자식");
    }
}

---

Parent instance = new Child();
instance.print(); // 자식
instance.staticPrint(); // 부모 (컴파일 시 Parent 타입이므로 항상 Parent의 static 메소드 호출)

오버라이딩 시 예외처리

  • 부모 메소드에서 예외를 던질 경우, 자식 메소드에서는 해당 예외 포함 하위의 예외만 던질 수 있다
    • 예외를 던지지 않아도 문제가 없음
  • 부모 메소드에서 예외를 던지지 않는 경우, 자식 메소드에서는 Unchecked Exception만 던질 수 있다

예시 - 예외를 던지는 경우

class Parent {
    void logic() throws RuntimeException {
        ...;
    }
}

class Child1 extends Parent {
    // 동일한 예외 가능
    @Override
    void logic() throws RuntimeException {
        ...;
    }
}

class Child2 extends Parent {
    // 하위 예외 가능
    @Override
    void logic() throws ArithmeticException {
        ...;
    }
}

class Child3 extends Parent {
    // 예외 던지지 않는 것도 가능
    @Override
    void logic(){
        ...;
    }
}

class Child4 extends Parent {
    // 단, 상위 예외 타입은 불가능
    // 컴파일 에러
    @Override
    void logic() throws Exception {
        ...;
    }
}

예시 - 예외를 던지지 않는 경우

class Parent {
    void logic() {
        ...;
    }
}

class Child1 extends Parent {
    // unchecked exception 가능
    @Override
    void logic() throws ArithmeticException {
        ...;
    }
}

class Child2 extends Parent {
    // checked exception 불가능
    // 컴파일 에러
    @Override
    void logic() throws Exception {
        ...;
    }
}

다이나믹 메소드 디스패치 (Dynamic Method Dispatch)

  • 실시간으로(Dynamic, runtime) 어떤 메소드(Method)를 호출할 지 결정(Dispatch) 하는 메커니즘
    • 런타임에 자식 클래스의 오버라이딩된 메소드를 호출하는 것을 말함
    • 부모 클래스로 자식 클래스가 upcasting 되는 경우, 부모 타입에 실제 어떤 자식 클래스가 들어있는지는 런타임에만 알 수 있다. (컴파일 시에는 알 수 없음)
class Parent {
    public int getInteger() {
        return 0;
    }
}

class Child1 extends Parent {
    public int getInteger() {
        return 1;
    }
}

class Child2 extends Parent {
    public int getInteger() {
        return 2;
    }
}

---

Parent instance = new Parent();
instance.getInteger(); // 0

// 같은 Parent 타입인 instance에 어떤 객체가 런타입에 들어왔는지 판단하고, 해당 객체의 메소드를 호출해주는 기술이
// 다이나믹 메소드 디스패치
instance = new Child1();
instance.getInteger(); // 1

instance = new Child2();
instance.getInteger(); // 2

더블 디스패치

  • 디스패치를 두 번 적용하여 유연성을 높이는 것
    • 즉, receiver 타입과 argument 타입 두 가지에 의해 어떤 메소드를 호출할지를 결정

Method Overloading 과 혼동되지만 둘은 다르다.

  • 부모 타입과 자식 타입, 각각을 인자로 메소드 오버로딩이 되어있으면 어떤 메소드를 호출할지는 어떤 객체가 변수에 할당되어 있는지가 아닌 오직 변수의 타입에 의해 실행할 메소드가 컴파일 타임에 정해진다
    • 즉, 이는 (더블) 다이나믹 디스패치가 아니다

추상 클래스

  • 객체 간의 공통적 특성을 추출한 클래스
    • 이때, 추상 클래스는 구체적인 실체가 없고 공통 특성을 추상적으로 갖고 있게 됨
  • 추상 클래스는 인스턴스화 할 수 없다.
// 아래와 같이 선언
class abstract AbstractClassA {...}

추상 메서드

  • abstract가 붙은 메소드로 메소드 본문을 갖지 않음
  • 추상 클래스는 추상 메소드를 하나 이상 가져야 함
    • 반대로, 추상 메소드를 하나 이상 가진 클래스는 무조건 추상 클래스가 되어야 함
// 아래와 같이 선언
class abstract AbstractClassA {
    public abstract void methodA();
}

추상 클래스 사용 이유

  • 객체 간의 필드와 메서드의 이름을 통일하여 소스의 가독성을 높이기 위함
    • 마치 인터페이스에 api 규약을 설정하는 것처럼 추상 클래스를 통해 하위 클래스가 따라야할 필드, 메서드의 이름을 규정함
    • (추상 클래스에 선언된 abstract 메소드는 반드시 하위 클래스에서 재정의해야 함 - 하지 않으면 추상 클래스로 밖에 존재할 수 없음)
  • 중복 코드 제거
    • 모든 하위 클래스에 공통적인 필드, 메서드 등을 추상 클래스에 정의하고 하위 클래스에서는 상속받아 사용하게하여 코드의 중복을 제거함
  • 인터페이스와의 차이
    • 자바 8부터 인터페이스에도 default 메소드 사용이 가능해졌지만, 인스턴스 변수 등의 선언은 불가능하므로 이런 부분에서 추상 클래스와 역할이 다르다고 생각

final 키워드

  • 클래스나 메소드에 final 키워드가 붙어있으면 상속이 불가능하다
    • 상속을 금지하고자 할 때 사용
    • Kotlin은 클래스에 final 이 기본 값
// final 클래스
public final class ClassA {...}
public class ClassB extends ClassA {...} // 컴파일 에러

// final 메소드
public class ClassA {
    public final void methodA() {...}
}
public class ClassB extends ClassA {
    @Override
    public final void methodA() {...}  // 컴파일 에러
}

상속을 금지해야 하는 경우?

  • 무분별한 상속으로 인한 예상하기 어려운 부수효과를 방지하기 위해
    • 하위 클래스에서 메소드 오버라이딩 등을 통해 개발자의 의도와 다르게 동작하는 것을 방지하기 위함
  • 불변성을 유지하기 위해 (상속받은 클래스에서 불변을 유지해줄 것이라고 보장할 수 없음)
  • 예로, 자바의 String 클래스는 final로 선언되어 있다

Object 클래스

  • 모든 자바 클래스의 최고 조상 클래스
    • 즉, 모든 자바 클래스는 Object 클래스를 상속받는다
    • 따라서, 모든 자바 클래스는 Object 클래스에 정의된 메소드를 사용할 수 있음
  • Object 클래스는 필드 없이 11개의 메서드만으로 구성됨
  • java.lang 패키지에 위치
    • 이 패키지는 자바에서 가장 기본적인 동작을 수행하는 클래스들의 집합으로, 별도로 import 하지 않아도 사용 가능
    • Math, Long, Integer, 등등이 이 패키지에 포함됨

메서드 목록

메소드 시그니처 설명
boolean equals(Object obj) 전달받은 객체와 현 객체가 같은지 여부를 반환
String toString() 객체의 정보를 문자열로 반환
protected Object clone() 객체의 복제본 생성 후 반환
int hashCode() 객체의 해시 코드 값을 반환
Class getClass() 객체의 클래스 타입을 반환
protedted void finalize() GC가 객체의 리소스를 정리하기 위해 호출(?) -> GC 정리 시 다시 참고하기
void notify() 객체의 wait 상태 쓰레드 하나를 다시 실행할 때 호출
void notifyAll() 객체의 wait 상태 쓰레드 모두를 다시 실행할 때 호출
void wait() 다른 쓰레드가 notify()/notifyAll()을 해줄 때 까지 현재 쓰레드를 대기 시킴
void wait(long timeout) 다른 쓰레드가 notify()/notifyAll()을 해주거나 timeout이 지날 때 까지 현재 쓰레드를 대기 시킴
void wait(long timeout, int nanos) 다른 쓰레드가 notify()/notifyAll()을 해주거나 timeout이 지날 때 까지 (nanos 추가 고려) 현재 쓰레드를 대기 시킴

equals()

public boolean equals(Object obj) {
    return (this == obj);
}
  • 인자로 받은 객체 obj와 자신 this이 같은 객체인지 비교
  • 기본적으로는 객체의 참조 값이 같은지만 비교한다
    • 따라서, 객체 내부의 값이 실제로 같은지 비교하기 위해서는 equals() 메소드를 오버라이드하여 사용해야 함
    • Intellij에서 자동생성해주는 equals() 활용하기

hashCode()

  • 객체의 해시코드 값(int)을 리턴
    • 해시 테이블을 사용하는 콜렉션 (HashMap, HashSet, …) 에서의 성능 향상을 위해 사용
  • 기본적으로는 System.identityHashCode()를 사용해 객체의 주소 값을 사용하여 해시 값을 만들어낸다
    • 따라서 hashCode()를 재정의하지 않으면 내부 값이 같더라도 주소 값이 다르므로 다른 해시 값이 반환되어 해시 테이블을 사용하는 컬렉션에서 성능이 저하될 수 있다.
  • Intellij를 통해 hashCode()를 자동 생성하면, Objects.hash(Object... values) -> Arrays.hashCode(Object a[])를 사용해 내부 값을 고려한 해시 코드를 만들어준다.
  • 해시 코드 규약
    1. 변경 되지 않은 객체의 hashCode()를 여러번 호출해도 항상 동일한 int 값이 나와야 한다
    2. equals()가 같다고 판단한 두 객체의 hashCode() 호출 결과는 항상 동일한 int 값이어야 한다
    3. 그러나, equals()가 다르다고 판단한 두 객체의 hashCode() 값은 다를 수 있다.
    • 해시 테이블에서 객체가 같은지 판단할 때 먼저 hash 값을 비교하고 같다면, 다음으로 equals() 를 호출해 최종적으로 같은지 판단하기 떄문
    • 단, 서로 다른 객체가 서로 다른 hash 값을 가져야 해시 테이블 성능이 향상된다

toString()

  • 객체의 정보를 문자열로 반환한다
  • Object의 기본 구현은 아래와 같이 클래스의 이름과 @, hashCode() 값을 unsigned hexadecimal로 표현한 값이 출력된다.
    • 객체를 사람이 이해하기 쉬운 텍스트로 표현하기 위해 toString()은 모든 subclasses에서 오버라이드하도록 권장된다
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  • String은 자기 자신(문자열)을 반환하도록, LocalDateTime은 LocalDate와 LocalTime을 각각 문자열로 반환한 값을 합쳐서 문자열로 반환하도록 오버라이드되어있다.
// String
public String toString() {
    return this;
}

// LocalDateTime
@Override
public String toString() {
    return date.toString() + 'T' + time.toString();
}

clone()

  • 객체 자신을 복제하여 새로운 인스턴스를 생성해 반환한다
    • 단, Object.clone()의 기본 구현은 얕은 복사를 수행한다.
    • 따라서, clone()을 통해 얻은 새로운 객체에 수행한 내용이 기존 인스턴스에 영향을 주지 않게 하려면 clone()을 오버라이드하여 깊은 복사를 구현해야 한다.
    • ArrayList나 HashMap 같은 자바 콜렉션도 껍데기만 다르고 내부의 원소들은 복사되어진 객체와 공유하기 때문에, 완벽하게(깊은) 복사 하기 위해선 별도의 오버라이드나 다른 로직이 필요하다.
  • clone()을 호출하려면 해당 클래스가 Cloneable 인터페이스를 implements 해야 한다.
    • Cloneable 인터페이스는 메소드가 선언되어있지 않은 빈 인터페이스이다. 오로지 clone()에 의해 복사될 수 있음을 나타내기 위해 사용한다. (이러한 인터페이스를 marker interface라 한다. 일종의 타입 체크만 수행)
    • 해당 인터페이스를 구현하지 않은 클래스의 clone() 메소드를 호출하면 clone() 을 재정의했더라도 CloneNotSupportedException 예외가 발생한다

추가 정리할 것들

  1. 더블 디스패치
    1. visitor 패턴
    2. 별도 포스트로 정리하기

참고문헌

  • https://www.javatpoint.com/inheritance-in-java
  • https://www.javatpoint.com/method-overriding-in-java
  • https://www.geeksforgeeks.org/overriding-in-java/
  • https://velog.io/@cchloe2311/Java-static-method-%EC%83%81%EC%86%8D
  • https://velog.io/@maigumi/Dynamic-Method-Dispatch
  • https://coding-factory.tistory.com/866
  • http://www.tcpschool.com/java/java_api_object
  • https://velog.io/@onionlily123/6%ED%9A%8C%EC%B0%A8.-%EC%83%81%EC%86%8D
  • https://woovictory.github.io/2019/01/04/Java-What-is-Marker-interface/

'스터디 > 자바' 카테고리의 다른 글

[자바 스터디] 8주차 : 인터페이스  (0) 2023.01.30
[자바 스터디] 7주차 : 패키지  (0) 2023.01.30
[자바 스터디] 5주차 : 클래스  (0) 2023.01.23
[자바 스터디] 3, 4주차 : 연산자, 제어문  (0) 2023.01.22
[자바 스터디] 2주차 : 자바 데이터 타입, 변수 그리고 배열  (0) 2023.01.22
'스터디/자바' 카테고리의 다른 글
  • [자바 스터디] 8주차 : 인터페이스
  • [자바 스터디] 7주차 : 패키지
  • [자바 스터디] 5주차 : 클래스
  • [자바 스터디] 3, 4주차 : 연산자, 제어문
gmelon
gmelon
백엔드 개발을 공부하고 있습니다.
  • gmelon
    gmelon's greenhouse
    gmelon
  • 전체
    오늘
    어제
    • 분류 전체보기 (91)
      • 개발 공부 (28)
        • Java (6)
        • Spring (10)
        • 알고리즘 (11)
        • 기타 (1)
      • 프로젝트 (12)
        • [앱] 플랭고 (4)
        • 졸업 프로젝트 (8)
      • 스터디 (0)
        • 자바 (30)
      • 기록 (15)
        • 후기, 회고 (9)
        • SSAFYcial (5)
        • 이것저것 (1)
      • etc. (6)
        • 모각코 (6)
  • 블로그 메뉴

    • 홈
    • 방명록
    • github
    • 스크랩
  • 인기 글

  • 태그

    졸업프로젝트
    2024 회고
    싸피 회고
    groupingBy mapping
    2024 상반기 회고
    프리티어 종료
    groupingBy()
    한글프로그래밍언어
    Java Collector
    자바 Collector
    Collector groupingBy()
    자바
    태초마을이야
    java
    AWS 프리티어 종료
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
gmelon
[자바 스터디] 6주차 : 상속
상단으로

티스토리툴바