
자바 상속의 특징
상속
- 한 클래스(자식 클래스)가 부모 클래스의 모든 속성(필드)과 행동(메소드)를 취득하는 메커니즘
- 즉, 부모 클래스를 기반으로 새로운 클래스를 만들 수 있다
- OOP의 중요한 요소 중 하나
상속을 사용하는 이유
- 메소드 오버라이딩 (runtime polymorphism이 가능해진다)
- 코드 재사용성
상속 문법
extends키워드를 사용
public class Parent {}
---
public class Child extends Parent {}
상속 구조
자바에서 가능한 구조
- Single
- Multilevel
- 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 {...}
자바에서 (클래스로는) 불가능한 구조
- Multiple
- 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사용 예시
- 부모 클래스에 기본 생성자가 존재하는 경우
- 이 경우, 자식 클래스의 생성자에서 명시하지 않아도 항상 부모의 기본 생성자를
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);
// 출력 :
// 부모 생성자 호출
// 자식 생성자 호출
- 부모 클래스에 기본 생성자가 존재하지 않는 경우
- 이 경우, 부모 클래스의 생성자를
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메소드 사용이 가능해졌지만, 인스턴스 변수 등의 선언은 불가능하므로 이런 부분에서 추상 클래스와 역할이 다르다고 생각
- 자바 8부터 인터페이스에도
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 |
객체의 클래스 타입을 반환 |
| 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[])를 사용해 내부 값을 고려한 해시 코드를 만들어준다. - 해시 코드 규약
- 변경 되지 않은 객체의
hashCode()를 여러번 호출해도 항상 동일한 int 값이 나와야 한다 equals()가 같다고 판단한 두 객체의hashCode()호출 결과는 항상 동일한 int 값이어야 한다- 그러나,
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예외가 발생한다
추가 정리할 것들
- 더블 디스패치
- visitor 패턴
- 별도 포스트로 정리하기
참고문헌
- 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/
