https://github.com/whiteship/live-study/issues/6
목표
자바의 상속에 대해 학습하세요.
학습할 것 (필수)
- 자바 상속의 특징
- super 키워드
- 메소드 오버라이딩
- 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
- 추상 클래스
- final 키워드
- Object 클래스
자바 상속의 특징
public class Vehicle {
protected String brand = "Ford";
public void honk() {
System.out.println("Tuut, tuut!");
}
}
// Vehicle 클래스를 상속받은 Car 클래스
class Car extends Vehicle {
private String modelName = "Mustang";
public static void main(String[] args) {
Car myCar = new Car();
// 부모 클래스의 메소드 사용
myCar.honk();
// 부모 클래스의 brand 변수 사용
System.out.println(myCar.brand + " " + myCar.modelName);
}
}
Tuut, tuut!
Ford Mustang
Process finished with exit code 0
코드 출처: https://www.w3schools.com/java/java_inheritance.asp
다른 클래스에서 파생된 클래스 → 하위 클래스(파생 클래스, 확장 클래스, 자식 클래스)
하위 클래스가 파생된 클래스 → 수퍼 클래스(기본 클래스, 부모 클래스)
수퍼 클래스가 없는 Object를 제외하고 모든 클래스에는 하나의 수퍼 클래스만 존재(단일상속)
수퍼 클래스가 명시돼 있지 않은 경우 모든 클래스는 암묵적으로 Object의 하위 클래스
사진, 내용 출처: https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html
public class Vehicle {
public void honk() {}
}
class Car extends Vehicle {
public static void main(String[] args) {
Car myCar = new Car();
myCar.honk();
}
}
javap -c Vihicle.class
public com.testcompany.Vehicle();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
invokespecial - 인스턴스 메소드를 호출;
superclass, private, 인스턴스 초기화 메서드 호출을 특별히 다루기 위한.
내용 출처: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokespecial
'invokespecial'은 'invokevirtual'과 다르게 객체의 클래스가 아닌 참조형 타입에 따라 메소드를 선택
다시 말해서 동적 바인딩 대신 정적 바인딩을 수행
'invokespecial'이 사용되는 세 가지 상황에서 동적 바인딩은 원하는 결과를 얻지 못합니다.
- 인스턴스 초기화(<init>) 메소드 호출
- private 메소드 호출
- super 카워드를 사용한 메소드 호출
<init> 메소드는 오직 새로운 인스턴스가 생성될 때 호출
새로 생성된 객체의 상속경로를 따라 각 클래스마다 하나 이상의 <init> 메소드가 호출됨
왜 'invokespecial'이 <init> 메소드 호출을 사용할까? subclass <init> 메소드는 superclass <init> 메소드를 호출할 수 있어야 하기 때문. 이것이 인스턴스화 된 객체가 많은 <init> 메소드를 호출하는 방법. 가상 머신은 object's 클래스에 선언된 <init> 메소드를 호출. <init> 메소드는 먼저 같은 클래스의 다른 <init> 메소드를 호출 or superclass의 <init> 메소드를 호출. 이 프로세스는 Object까지 계속 올라감.
class Superclass {
private void interestingMethod() {
System.out.println("Superclass's interesting method.");
}
void exampleMethod() {
interestingMethod();
}
}
class Subclass extends Superclass {
void interestingMethod() {
System.out.println("Subclass's interesting method.");
}
public static void main(String[] args) {
Subclass me = new Subclass();
me.exampleMethod();
// 출력: Superclass's interesting method.
}
}
Subclass에서 main()을 호출하면 "Superclass의 흥미로운 메소드"를 출력해야 함. 'invokevirtual'을 사용하면 "Subclass의 흥미로운 메소드"를 출력. WHY? 가상 머신은 객체의 실제 클래스인 Subclass 기반으로 interestingMethod()를 선택하기 때문.
반면 'invokespecial'을 사용하면 가상 머신이 참조 유형에 따라 메소드를 선택하므로 Superclass interestingMethod()를 호출.
'invokeinterface' opcode는 'invokevirtual'과 똑같이 작동. 단지 다른점이라면 'invokeinterface'는 인터페이스 타입일 때 사용.
호출 명령어와 속도
상상할 수 있듯이 인터페이스 참조가 메소드를 호출하는 것이 클래스 참조가 메소드를 호출하는 것보다 느릴 수 있다. 자바 가상 머신이 'invokevirtual' 명령어를 발견하고 인스턴스 메소드에 대한 직접 참조(direct reference)에 대한 기호 참조를 분석할 때 'direct reference'는 메소드 테이블에 대한 오프셋(offset;상대 주소)이 될 수 있다. 그 시점부터 동일한 오프셋을 사용할 수 있다. 그러나 'invokeinterface' 명령어의 경우 가상 머신은 오프셋이 이전 호출과 동일하다고 가정할 수 없기 때문에 명령어가 발생할 때마다 메소드 테이블을 검색해야 한다.
가장 빠른 명령어는 'invokespecial' 및 'invokestatic'일 가능성이 크다. 이러한 명령어에 의해 호출된 메소드는 정적으로 바인딩 되기 때문이다. JVM이 기호 참조를 해석하고 'direct reference'로 교체하면 실제 바이트 코드에 대한 포인터를 포함할 것이다.
구현 의존성
이러한 모든 속도 예측은 추측에 불과합니다. 자바 가상 머신 디자이너들이 기술을 사용해 속도를 높일 수 있기 때문입니다. 기호 참조 및 호출 메소드를 해결하기 위한 데이터 구조 및 알고리즘은 JVM 사양이 아닙니다. 이러한 결정은 JVM 설계자에게 맡겨집니다.
결론
- 인스턴스 메소드는 <init>메소드, private 메소드 및 super 키워드로 호출된 메소드를 제외하고 동적으로 바인딩 됩니다. 이 세 가지 특수한 경우 인스턴스 메소드는 정적으로 바인딩 됩니다.
- 클래스 메소드는 항상 정적으로 바인딩됩니다.
- 인터페이스 참조로 호출된 인스턴스 메소드는 객체 참조로 호출된 동일한 메소드보다 느릴 수 있습니다.
1997년 6월 JavaWorld에 실린 기사 입니다.
코드, 내용 출처: https://www.artima.com/underthehood/invocation.html
super 키워드
public class Superclass {
public void printMethod() {
System.out.println("Printed in Superclass.");
}
}
public class Subclass extends Superclass {
// overrides printMethod in Superclass
public void printMethod() {
super.printMethod();
System.out.println("Printed in Subclass");
}
public static void main(String[] args) {
Subclass s = new Subclass();
s.printMethod();
// 출력
// Printed in Superclass.
// Printed in Subclass
}
}
메소드가 수퍼 클래스의 메소드 중 하나를 재정의하는 경우 super 키워드를 사용하여 재정의된 메소드를 호출할 수 있음
출처: https://docs.oracle.com/javase/tutorial/java/IandI/super.html
메소드 오버라이딩
인스턴스 메소드
동일한 시그니쳐(이름, 매개 변수의 개수 및 타입)와 리턴 타입을 가진 서브 클래스의 인스턴스 메소드는 슈퍼 클래스의 메소드를 재정의
메소드를 재정의할 때 컴파일러에 superclass의 메소드를 재정의할 것을 지시하는 @Override 주석(annotation)을 사용. 어떤 이유로 컴파일러가 superclass 메소드 중 하나가 존재하지 않음을 감지하면 오류를 생성.
Static 메소드
만약 subclass가 static 메소드이며 superclass의 static 메소드와 동일한 시그니처를 사용하면 subclass의 메소드는 superclass의 메소드를 숨김.
정적 메소드를 숨기는 것과 인스턴스 메소드를 오버라이딩(재정의)하는 것의 차이점은 다음과 같은 중요한 의미가 있다.
- 호출하는 오버라이딩 인스턴스 메소드는 subclass 버전이다.
- 호출하는 숨겨진 static 메소드의 버전은 superclass에서 호출되는지 subclass에서 호출되는지에 따라 다르다.
public class Animal {
public static void testClassMethod() {
System.out.println("The static method in Animal");
}
public void testInstanceMethod() {
System.out.println("The instance method in Animal");
}
}
public class Cat extends Animal {
public static void testClassMethod() {
System.out.println("The static method in Cat");
}
public void testInstanceMethod() {
System.out.println("The instance method in Cat");
}
public static void main(String[] args) {
Cat myCat = new Cat();
Animal myAnimal = myCat;
Animal.testClassMethod();
myAnimal.testInstanceMethod();
// 출력
// The static method in Animal
// The instance method in Cat
}
}
Cat 클래스는 Animal 인스턴스 메소드를 재정의하고 Animal의 static 메소드를 숨긴다.
호출된 static 메소드는 superclass에 있는 버전이고 오버라이드된 인스턴스 메소드는 subclass 버전이다.
출처: https://docs.oracle.com/javase/tutorial/java/IandI/override.html
다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
동적 메소드 디스패치는 컴파일 타임이 아닌 런타임에 오버라이드된 메소드에 대한 호출이 해결되는 메커니즘
출처: https://www.geeksforgeeks.org/dynamic-method-dispatch-runtime-polymorphism-java/
오버라이드된 메소드가 superclass 참조를 통해 호출되면 Java는 호출이 발생할 때 참조되는 객체의 유형에 따라 해당 메소드의 버전(수퍼 클래스 / 서브 클래스)이 실행될 것인지 결정
런타임시 실행될 재정의 된 메소드 버전을 결정하는 것은 참조되는 개체의 유형 (참조 변수 유형이 아님)에 따라 다름
superclass 참조 변수는 subclass 객체를 참조 할 수 있다. 이를 업 캐스팅이라고도 한다. Java는이 사실을 사용하여 런타임에 재정의 된 메서드에 대한 호출을 해결한다.
추상 클래스
추상(abstract) 클래스는 추상적으로 선언된 클래스로 추상 메소드를 포함하거나 포함하지 않을 수 있다.
추상 클래스는 인스턴스화할 수 없지만 subclass화할 수 있다.
abstract void moveTo(double deltaX, double deltaY);
// 클래스에 추상 메소드가 포함된 경우 클래스는 다음과 같이 선언
public abstract class GraphicObject {
// declare fields
// declare nonabstract methods
abstract void draw();
}
추상 클래스가 subclass화될 때 일반적으로 부모 클래스의 추상 메소드에 대한 구현을 제공. 하지만 그렇지 않은 경우에는 subclass도 반드시 추상화 선언을 해야 함.
default or static으로 선언되지 않은 인터페이스의 메소드는 암시적으로 추상이므로 추상 수정자는 인터페이스 메소드와 함께 사용되지 않음. (사용할 수 있지만 불필요)
출처: https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html
final 키워드
class ChessAlgorithm {
enum ChessPlayer {WHITE, BLACK}
...
final ChessPlayer getFirstPlayer () {
return ChessPlayer.WHITE;
}
...
}
메소드 선언에서 final 키워드를 사용해 subclass로 재정의할 수 없음을 나타냄. 'Object' 클래스가 이 작업을 수행하며 많은 메소드가 final이다.
변경되면 안되고 객체의 일관성이 매우 중요한 경우에 사용. ex) ChessAlgorithm 클래스의 getFirstPlayer 메소드를 final로 만들 수 있다.
생성자에서 호출된 메소드는 일반적으로 final로 선언되어야 함. 생성자가 non-final 메소드를 호출하면 subclass가 해당 매소드를 바람직하지 않은 결과로 재정의할 수 있다.
전체 클래스를 final로 선언할 수도 있다. final로 선언된 클래스는 subclass화할 수 없다. 예를 들어 'String' 클래스와 같은 불변의 클래스를 만들 때 특히 유용함.
출처: https://docs.oracle.com/javase/tutorial/java/IandI/final.html
Object 클래스
Object 클래스를 제외한 클래스에는 정확히 하나의 direct superclass가 있다. 클래스는 직접이든 간접적이든 상관없이 모든 superclass의 필드와 메소드를 상속. subclass는 상속하는 메소드를 재정의하거나 상속하는 필드 or 메소드를 숨길 수 있다. (필드를 숨기는 것은 일반적으로 나쁜 프로그래밍 관행입니다.)
Object 클래스는 클래스 계층 구조의 맨 위에 있다. 모든 클래스는 이 클래스의 자손이며 메소드를 상속한다. Object에서 상속받은 유용한 메소드에는 toString(), equals(), clone() 및 getClass()가 있다.
출처: https://docs.oracle.com/javase/tutorial/java/IandI/summaryinherit.html