https://sundaland.tistory.com/275
상속 (Inheritance)
다른 클래스에서 파생된 클래스를 하위 클래스 (파생/확장/하위 클래스라고도 함)라 한다. 하위 클래스로 파생되는 클래스를 (기본/부모 클래스)라 한다;
슈퍼 클래스가 없는 Object를 제외하고 모든 클래스는 단 하나의 Direct 슈퍼 클래스 (단일 상속)를 갖습니다. 다른 명시적인 슈퍼클래스가 없으면 모든 클래스는 암시적으로 Object의 하위 클래스이다.
# Direct 슈퍼 클래스는 상속 계층 구조에서 바로 위 슈퍼 클래스를 의미한다. Direct 슈퍼 클래스 이외의 슈퍼 클래스에 대한 정식 명칭을 자바에서는 정의하지 않았지만, 일부 ancestor 클래스 또는 상위 클래스 그리고 단지 슈퍼 클래스라 한다.
클래스는 클래스 등에서 파생된 클래스에서 파생될 수 있으며 궁극적으로 최상위 클래스인 Object에서 파생될 수 있다.
이러한 클래스는 Object로 다시 확장되는 상속 체인의 모든 클래스의 자손이라고 한다.
하위 클래스는 상위 클래스에서 모든 멤버(필드, 메서드, 중첩 클래스)를 상속한다.
생성자는 멤버가 아니므로 하위 클래스에서 상속되지 않지만 상위 클래스의 생성자는 하위 클래스에서 호출될 수 있다.
자바 플랫폼의 계층 구조 (Class Hierarchy)
java.lang 패키지에 정의된 Object 클래스는 사용자가 작성한 클래스를 포함하여 모든 크랠스에 공통적인 동장을 정의하고 구현한다. 자바 플랫폼에서는 많은 클래스가 Object에서 직접 파생되고, 다른 클래스는 해당 클래스 중 일부에서 파생되는 식으로 클래스 계층 구조를 형성한다.
Object는 계층 구조 최상위에 있는 모든 클래스 중에서 가장 일반적인 클래스이다. 계층 구조의 맨 아래에 있는 클래스는 보다 특수한 동작을 제공한다.
하위 클래스 (Subclass)는 무엇이 가능한가?
어떤 패키지에 있는지에 관계없이 상위 클래스의 모든 public 및 protected 멤버를 상속한다. 하위 클래스가 상위 클래스와 동일한 패키지에 있는 경우 상위 클래스의 package-private 멤버도 상속한다.
상속된 멤버를 그대로 사용하거나, 바꾸거나, 숨기거나, 새 멤버로 보완할 수 있다.
- 상속된 필드는 다른 필드와 마찬가지로 직접 사용할 수 있다.
- 서브 클래스는 필드를 슈퍼 클래스의 필드와 동일한 이름으로 선언하여 숨길 수 있다. (비권장)
- 슈퍼 클래스에는 없는 새로운 필드를 서브 클래스에서 선언할 수 있다.
- 상속받은 메서드는 그대로 사용이 가능하다.
- 슈퍼 클래스와 동일한 시그니처를 갖는 새로운 인스턴스 메서드를 서브 클래스에 작성하여 오버라이드(재정의) 할 수 있다.
- 슈퍼 클래스에는 없는 새로운 메서드를 서브 클래스에서 선언할 수 있다.
- 암시적으로 또는 키워드 Super로 슈퍼 클래스의 생성자를 호출하는 하위 클래스 생성자를 작성할 수 있다.
Private Members in a Superclass
하위 클래스는 상위 클래스의 private 멤버를 상속하지 않습니다. 그러나 슈퍼 크랠스의 private 필드에 액세스하기 ㅜ이한 pulbic 또는 protected 메서드가 있는 경우 하위 클래스에서 이러한 메서드를 사용할 수 있다.
중첩 클래스는 자신을 둘러싸는 외부 클래스의 모든 private 멤버 (필드와 메서드 모두)에 액세스 할 수 있다. 따라서
하위 클래스에 상속한 public 또는 protected 중첩 클래스는 슈퍼 클래스의 모든 private 멤버에 작접적으로 액세스할 수 있다.
# 중첩 클래스는 정적과 비정적으로 나뉘어진다.
class SuperClass {
private int privateField;
class NestedClass {
void accessPrivateField() {
System.out.println(privateField);
}
}
}
class SubClass extends SuperClass {
class SubNestedClass extends SuperClass.NestedClass {
void accessSuperPrivateField() {
// SubNestedClass는 SuperClass의 NestedClass를 상속받았으므로,
// SuperClass의 privateField에 접근할 수 있습니다.
// 하지만 직접적으로 접근하는 것이 아니라, SuperClass의 인스턴스를 통해
// 간접적으로 접근하게 됩니다.
accessPrivateField(); // 이것은 super.accessPrivateField()와 동일합니다.
}
}
}
간접적으로 액세스할 수 있다는 의미는 하위 클래스에서 상속된 중첩 클래스가 슈퍼 클래스의 private 멤버에 직접 접근 하는 것이 아니라, 슈퍼 클래스의 인스턴스를 통해 접근하는 방식을 의미한다.
이는 접근 제어를 유지하면서 중첩 클래스가 외부 클래스의 내부 상태를 다룰 수 있게 한다.
Casting Objects
객체는 인스턴스화된 클래스의 데이터 타입을 가진다.
public MountainBike myBike = new MountainBike();
myBike는 MountainBike 타입이다. MountainBike는 Bicycle과 Object로부터 상속받았다. 따라서 MountainBike는 Bicycle이며, 또한 Object이다. 그러므로 Bicycle이나 Object 객체가 필요한 곳에서 MountainBike를 사용할 수 있다.
Object obj = new MountainBike();
위의 예제 코드에서는 MountainBike 클래스의 디폴트 생성자가 정의되지 않았다. Bicycle 클래스 또한 디폴트 생성자가 정의되지 않았기 때문에 컴파일 에러가 발생한다.
그러므로 Bicycle 클래스에 디폴트 생성자를 정의해야 한다.
obj는 Object이면서 MountainBikedlek. 나중에 obj가 MountainBike가 아닌 다른 객체에 할당되기 전까지 이를 암시적(implicit) 캐스팅이락 한다.
MountainBike myBike = obj;
해당 코드는 컴파일 타임 오류가 발생한다. 왜냐하면 컴파일러는 obj가 MountainBike 인지 알 수 없기 때문이다. 그러나 우리는 obj에 MountainBike를 할당할 것을 컴파일러에게 명시적(Explicit)로 캐스팅하여 약속할 수 있다.
MountainBike myBike = (MountainBike)obj;
이 캐스팅은 런타임에 obj가 MountainBike로 할당되었는지 확인하는 체크를 삽입한다. 따라서 컴파일러는 안전하게 obj가 MountainBike라고 가정할 수 있다. 만약 런타임에 obj가 MountainBike가 아니라면, 예외가 발생한다.
# instanceof 연산자를 사용하여 특정 객체의 타입에 대한 논리적은 테스트를 수행할 수 있다. 아래의 코드로 부적절한 캐스트로 인한 런타임 오류를 방지할 수 있다.
if (obj instanceof MountainBike) {
MountainBike myBike = (MountainBike)obj;
}
여기에서 instanceof 연산자는 obj가 MountainBike를 참조하는지 확인하므로 런타임 예외가 발생하지 않는다는 사실을
알고 캐스팅을 수행할 수 있다.
구현의 다중 상속 (Mutiple Inheritance of State, Implementaion, and Type)
클래스와 인터페이스의 한 가지 중요한 차이점은 클래스는 필드를 가질 수 있지만 인터페이스는 가질 수 없다. 또한 클래스를 인스턴스화하여 인터페이스로는 수행할 수 없는 객체를 생성할 수 있다.
객체는 클래스에 정의된 필드에 state를 저장한다. 자바 프로그램밍 언어에서 둘 이상의 클래스 확장을 허용하지 않는 이유 중 하나는 state의 다중 상속 문제, 즉 여러 클래스에서 필드를 상속하는 기능을 방지하기 위한 것이다.
인터페이스에는 필드가 포함되어 있지 않으므로 상태의 다중 상속으로 인해 발생하는 문제는 없다.
class SuperClassA {
int state = 1;
SuperClassA() {
state = 2; // 생성자에서 state 값을 2로 설정
}
void printState() {
System.out.println("State from SuperClassA: " + state);
}
}
class SuperClassB {
int state = 3;
SuperClassB() {
state = 4; // 생성자에서 state 값을 4로 설정
}
void printState() {
System.out.println("State from SuperClassB: " + state);
}
}
class SubClass extends SuperClassA /*, SuperClassB*/ {
// 다중 상속이 허용되지 않기 때문에 두 번째 슈퍼클래스를 주석 처리했습니다.
// 메서드를 오버라이드하여 상태를 출력합니다.
@Override
void printState() {
// 만약 다중 상속이 가능하다면, 어떤 state를 출력해야 할지 애매해집니다.
super.printState(); // SuperClassA의 state 출력
// SuperClassB의 state도 출력해야 한다면?
// super.printState(); // SuperClassB의 state 출력 (애매해짐)
}
}
public class MultipleInheritanceDemo {
public static void main(String[] args) {
SubClass sub = new SubClass();
sub.printState();
}
}
구현의 다중 상속은 여러 클래스에서 메서드 정의를 상속하는 기능이다. 이러한 다중 상속에서는 이름 충돌 및 모호함과 같은 문제가 발생한다. 이러한 유형은 다중 상속을 지원하는 프로그래밍 언어의 컴파일러는 동일한 이름의 메서드가 포함된 슈퍼 클래스를 발견할 때 액세스하거나 호출할 멤버나 메서드를 결정할 수 없는 경우가 있다. 또한 프로그래머는 슈퍼 클래스에 새 메서드를 추가하여 자신도 모르게 이름 충돌을 발생 시킬 수 있다.
자바의 디폴트 메서드는 구현의 다중 상속의 한 형태를 도입한다. 자바 클래스는 동일한 이름을 가진 기본 메서드를 선언할 수 있는 둘 이상의 인터페이스를 구현할 수 있다.
자바 컴파일러는 특정 클래스가 사용하는 기본 메서드를 결정한는 몇 가지 규칙을 제공한다.
자바 프로그래밍 언어는 타입의 다중 상속 (Multiple Inheritance of type)을 지원한다. 이는 클래스가 둘 이상의 인터페이스를 구현하는 기능이다. 객체는 자체 클래스의 타입과 클래스가 구현하는 모든 인터페이스의 타입 등 여러 타입들을 가질 수 있다.
변수가 인터페이스 타입으로 선언되면 해당 값은 인터페이스를 구현하는 모든 클래스에서 인스턴스화되는 모든 객체를 참조할 수 있다.
// 인터페이스를 정의합니다.
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
// 클래스를 정의하고 인터페이스들을 구현합니다.
class Duck implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("Duck is flying.");
}
@Override
public void swim() {
System.out.println("Duck is swimming.");
}
void quack() {
System.out.println("Duck is quacking.");
}
}
public class MultipleInheritanceDemo {
public static void main(String[] args) {
// Duck 객체를 생성합니다.
Duck duck = new Duck();
// Flyable 타입 변수에 Duck 객체를 할당합니다.
Flyable flyable = duck;
flyable.fly();
// flyable.swim(); // 컴파일 오류: Flyable 타입에서는 swim() 메서드에 접근할 수 없습니다.
// Swimmable 타입 변수에 Duck 객체를 할당합니다.
Swimmable swimmable = duck;
swimmable.swim();
// swimmable.fly(); // 컴파일 오류: Swimmable 타입에서는 fly() 메서드에 접근할 수 없습니다.
// Duck 타입 변수에 Duck 객체를 할당합니다.
Duck sameDuck = (Duck) flyable;
sameDuck.fly();
sameDuck.swim();
sameDuck.quack();
}
}
구현의 다중 상속과 마찬가지로 클래스는 확장하는 인터페이스에서 정의된 메서드의 다양한 구현을 상속할 수 있다. 이 경우 컴파일러나 사용자가 어느 것을 사용할지 결정해야 한다.
Overriding and Hiddeing Method
인스턴스 메서드
슈퍼 클래스의 인스턴스 메서드와 동일한 시그니처 (이름, 파라미터의 수 및 유형) 및 리턴 타입을 갖는 서브 클래스의 인스턴스 메서드는 슈퍼 클래스의 메서드를 대체한다.
메서드를 재정의(Override)하는 하위 클래스의 기능을 통해 클래스는 동작이 "Close enough" 슈퍼 클래스에서 상석된 다음 필요에 따라 동작을 수정할 수 있다.
오버라이트 메서드에는 오버라이드하는 메서드와 동일한 이름, 파라미터 개수 및 타입, 리턴 타입이 있다. 오버라이드 메서드는 오버리아드된 메서드에서 리턴된 타입의 하위 타입을 반환할 수도 있다.
이 하위 타입을 공변(Covariant) 리턴이라고 한다.
// 슈퍼클래스 Animal
class Animal {
// 슈퍼클래스의 메서드
public Animal speak() {
System.out.println("Animal makes a sound");
return this;
}
}
// 하위 클래스 Dog
class Dog extends Animal {
// 메서드를 재정의 (override)
@Override
public Dog speak() {
System.out.println("Dog barks");
return this;
}
}
// 하위 클래스 Cat
class Cat extends Animal {
// 메서드를 재정의 (override)
@Override
public Cat speak() {
System.out.println("Cat meows");
return this;
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Animal();
Animal myDog = new Dog();
Animal myCat = new Cat();
// 슈퍼클래스의 메서드 호출
myAnimal.speak(); // 출력: Animal makes a sound
// 하위 클래스에서 재정의된 메서드 호출
myDog.speak(); // 출력: Dog barks
myCat.speak(); // 출력: Cat meows
}
}
위 코드에서 Dog 클래스의 speak 메서드는 Dog 타입을 반환하고, Cat 클래스는 Cat 타입을 리턴한다. 이는 자바에서 허용되는 공병 리탄 타입의 예시이다.
메서드를 오버라이드할 때 슈퍼 클래스의 메서드를 오버라이드할 것임을 컴파일러에 지시하는 @Override 어노테이션을
사용할 수 있다. 어떤 이유로 컴파일러가 해당 메서드가 슈퍼 클래스 중 하나에 존재하지 않음을 감지하면 오류가 발생한다.
정적 메서드
서브 클래스가 슈퍼 클래스의 정적 메서드와 동일한 시그니처를 사용하여 정적 메서드를 정의하는 경우 서브 클래스의 메서드는 슈퍼 클래스의 메서드를 숨긴다.
정적 메서드를 숨기는 거소가 인스턴스 메서드를 오버라이드하는 것의 차이점은 중요한 의미를 갖는다.
- 호출되는 오버라이드된 인스턴스 메서드의 버전은 하위 클래스에 있는 버전이다.
- 호출되는 숨겨진 정적 메서드의 버전은 슈퍼 클래스에서 호출되는지, 서브 클래스에서 호출되는지에 따라 다릅니다.
아래의 코드는 하나의 인스턴스 메서드와 하나의 정적 메서드를 포함하는 Animal이다.
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();
}
}
Cat 클래스는 Animal의 인스턴스 메서드를 오버라이드하고 Animal의 정적 메서드를 숨긴다.
이 클래스의 main 메서드는 Cat 인스턴스를 생성하고 Animal 클래스의 정적 메서드인 testClassMethod()를 호출하고 인스턴스 메서드인 testIstanceMethod()를 호출한다.
The static method in Animal
The instance method in Cat
약속대로 호출되는 숨겨진 정적 메서드의 버전은 슈퍼 클래스에 있는 버전이고, 호출되는 오버라이된 인스턴스 메서드의 버전은 하위 클래스에 있는 버전이다.
인터페이스 메서드
디폴트 메서드와 추상 메서드는 인스턴스 메서드처럼 상속된다. 그러나 클래스 또는 인터페이스의 상위 타입이 동일한 시그니처를 가진 여러 디폴트 메서드를 제공하는 경우 자바 컴파일러는 상속 규칙에 따라 이름 충돌을 해결한다.
- 인터페이스 메소드는 인터페이스 디폴트 메서드보다 선호된다.
public class Horse {
public String identifyMyself() {
return "I am a horse.";
}
}
public interface Flyer {
default public String identifyMyself() {
return "I am able to fly.";
}
}
public interface Mythical {
default public String identifyMyself() {
return "I am a mythical creature.";
}
}
public class Pegasus extends Horse implements Flyer, Mythical {
public static void main(String... args) {
Pegasus myApp = new Pegasus();
System.out.println(myApp.identifyMyself());
}
}
I am a Horse
- 다른 후보에 의해 이미 오버라이드된 메서드는 무시된다. 이러한 상황은 상위 타입이 공통 조상을 공유할 때 발생할 수 있다.
public interface Animal {
default public String identifyMyself() {
return "I am an animal.";
}
}
public interface EggLayer extends Animal {
default public String identifyMyself() {
return "I am able to lay eggs.";
}
}
public interface FireBreather extends Animal { }
public class Dragon implements EggLayer, FireBreather {
public static void main (String... args) {
Dragon myApp = new Dragon();
System.out.println(myApp.identifyMyself());
}
}
I am able to lay eggs
독립적으로 정의된 두 개 이상의 디폴트 메서드와 충돌하거나 디폴트 메서드가 추상 메서드와 충동하는 경우 자바 컴파일러는 컴파일러 오류를 발생시킨다. 상위 타입 메서드를 명시적으로 대체해야 한다.
public interface OperateCar {
// ...
default public int startEngine(EncryptedKey key) {
// Implementation
}
}
public interface FlyCar {
// ...
default public int startEngine(EncryptedKey key) {
// Implementation
}
}
OperateCar와 FlyCar를 모두 구현하는 클래스는 startEngine 메서드를 오버라이드해야한다.
super 키워드를 사용하여 디폴트 구현 중 하나는 호출할 수 있다.
public class FlyingCar implements OperateCar, FlyCar {
// ...
public int startEngine(EncryptedKey key) {
FlyCar.super.startEngine(key);
OperateCar.super.startEngine(key);
}
}
super 앞에 오는 이름은 호출된 메서드에 대한 디폴트를 정의하거나 상속하는 직접적인 슈퍼 인터페이스를 참조해야한다. 이러한 형태의 메소드 호촐은 동일한 시그니처를 가진 디폴트 메서드를 포함하는 여러 구현 인터페이스를 구별하는 것으로 제한되지 않습니다. super 키워드를 사용하여 클래스와 인터페이스 모두에서 디폴트 메서드를 호출할 수 있다.
클래스에 상속된 인스턴스 메서드는 추상 인터페이스 메서드를 오버러이드할 수 있다.
public interface Mammal {
String identifyMyself();
}
public class Horse {
public String identifyMyself() {
return "I am a horse.";
}
}
public class Mustang extends Horse implements Mammal {
public static void main(String... args) {
Mustang myApp = new Mustang();
System.out.println(myApp.identifyMyself());
}
}
Mustang.identiyMyself 메서드는 I am a Hore 문자열을 리턴한다. Mustang 클래스는 Hores 클래스로부터 identifyMyself 메서드를 상속받으며, 이는 Mammal 인터페이스에 있는 동일한 이름의 추상 메서드를 대체한다.
# 인터페이스의 정적 메서드는 상속되지 않습니다.
수정자
메서드 오버라이딩에서 액세스 제한자는 오버라이딩된 메서드보다 더 넓은 접근을 허용하지만, 더 좁은 접근은 허용하지 않는다.
예시로 슈퍼 클래스의 protected 인스턴스 메서드는 서브 클래스에서 public으로 변경할 수 있지만 private으로는 변경할 수 없다. 또한 슈퍼 클래스의 인스턴스 메서드를 서브 클래스의 static 메서드로 변경하려고 하거나 반대로 변경하려고 하면 컴파일 에러가 발생한다.
class Animal {
protected void makeSound() {
System.out.println("Generic animal sound");
}
}
class Dog extends Animal {
@Override // 명시적 오버라이딩 표시 (선택사항)
public void makeSound() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
animal.makeSound(); // Generic animal sound
Dog dog = new Dog();
dog.makeSound(); // Woof!
}
}
- Animal 클래스는 makeSound 메서드를 protected 액세스 제한자로 선언한다.
- Dog 클래스는 Animal 클래스를 상속받고 makeSound 메서드를 오버라이딩 한다.
- 오버라이딩된 메서드의 접근 제한자는 public으로 변경되어 더 넓은 접근을 허용한다.
- main 메서드에서 Animal 객체와 Dog 객체를 생성하여 각각의 makeSoumd 메서드를 호출한다.
class Animal {
public void makeSound() {
System.out.println("Generic animal sound");
}
}
class Dog extends Animal {
@Override
static void makeSound() { // 인스턴스 메서드를 static 메서드로 변경하려고 시도
System.out.println("Woof!");
}
}
위 코드는 컴파일 에러를 발생시킨다.
Summary
다음 표는 슈퍼 클래스의 메서드와 동일한 시그니처를 사용하여 메서드를 정의할 때 발생하는 것들이 요약되어 있다.
슈퍼 클래스의 인스턴스 메서드 | 슈퍼 클래스의 정적 메서드 | |
서브 클래스의 인스턴스 메서드 | 오버라이드 | 컴파일 타임 오류 발생 |
서브 클래스의 정적 메서드 | 컴파일 타임 오류 발생 | 숨김 (Hides) |
'자바 튜토리얼' 카테고리의 다른 글
인터페이스와 상속 (Interface and Inheritance) [2] (0) | 2024.07.12 |
---|---|
리팩토링 (Refactoring) (0) | 2024.07.11 |
인터페이스 (Interface) (0) | 2024.07.08 |
클래스와 객체 (Classes and Objects) [4] (0) | 2024.07.05 |
클래스와 객체 (Classes and Objects) [3] (0) | 2024.07.04 |