[Java] 6주차 과제: 상속
by Roel Downey스터디 링크 : 링크
상속은 말 그대로 자식이 부모로부터 무언가를 물려받는 것이다.
자바 상속의 특징
클래스 상속을 위해서는 extends 라는 키워드를 사용한다.
- 멤버를 물려주는 클래스: superclass(base class or parent class)
- 멤버를 물려받는 클래스: subclass(derived class or child class)
- Object 클래스는 superclass를 가지지 않는다.
- Object를 제외한 모든 클래스는 단 하나의 superclass를 가진다.
- superclass가 보이지 않는 클래스는 Object 클래스를 이미 상속하고 있다.
- 따라서 Object 클래스를 제외한 모든 클래스는 Object 클래스의 descendants라고 할 수 있다.
- superclass의 멤버는 subclass에 상속된다.
- 접근 제어자에 따라서 달라진다.
- 생성자는 멤버가 아니기에 subclass에 상속되지 않는다.
- 하지만 superclass의 생성자는 subclass에서 호출할 수 있다.
자식클래스 extends 부모클래스 |
이제 Dog 클래스는 Animal 클래스를 상속하게 되었다.
Dog 클래스에 name 이라는 객체변수와 setName 이라는 메소드를 만들지 않았지만 Animal클래스를 상속을 받았기 때문에 사용이 가능하다. Dog 클래스에 main 메소드를 실행시켜 보자.
실행 해보면 poppy 라는 문자열이 출력된다.
보통 부모 클래스를 상속 받은 자식 클래스는 부모 클래스의 기능에 더하여 좀 더 많은 기능을 갖도록 설계한다.
Dog 클래스는 Animal 클래스를 상속받았다. 즉, Dog는 Animal의 하위 개념이라고 할 수 있다. 이런 경우 Dog는 Animal에 포함되기 때문에 "개는 동물이다"라고 표현할 수 있다.
자바는 이러한 관계를 IS-A 관계라고 표현한다. 즉 "Dog is a Animal" 과 같이 말할 수 있는 관계를 IS-A 관계라고 한다.
이렇게 IS-A 관계(상속관계)에 있을 때 자식 객체는 부모 클래스의 자료형인 것처럼 사용할 수 있다.
즉, 다음과 같은 코딩이 가능하다.
Animal dog = new Dog();
하지만 이 반대의 경우, 즉 부모 클래스로 만들어진 객체를 자식 클래스의 자료형으로는 사용할 수 없다.
다음의 코드는 컴파일 오류가 발생한다.
Dog dog = new Animal(); // 컴파일 오류: 부모 클래스로 만든 객체는 자식 클래스의 자료형으로 사용할 수 없다.
이 부분을 좀 더 개념적으로 살펴보자.
Animal dog = new Dog();
위 코드를 읽어보면 "개로 만든 객체는 동물 자료형이다."라고 읽을 수 있다.
또 다음 코드를 보자.
Dog dog = new Animal();
역시 개념적으로 읽어보면 "동물로 만든 객체는 개 자료형이다."로 읽을 수 있을 것이다. 근데 뭔가 좀 이상하지 않은가? 동물로 만든 객체는 "개" 자료형 말고 "호랑이" 자료형 또는 "사자" 자료형도 될 수 있지 않은가?
개념적으로 살펴보아도 두번째 코드는 성립할 수 없다는 것을 알 수 있다.
Object 클래스
자바에서 만드는 모든 클래스는 Object라는 클래스를 상속받게 되어 있다.
사실 우리가 만든 Animal 클래스는 다음과 기능적으로 완전히 동일하다. 하지만 굳이 아래 코드처럼 Object 클래스를 상속하도록 코딩하지 않아도 자바에서 만들어지는 모든 클래스는 Object 클래스를 자동으로 상속받게끔 되어 있다.
public class Animal extends Object {
String name;
public void setName(String name) {
this.name = name;
}
}
따라서 자바에서 만드는 모든 객체는 Object 자료형으로 사용할 수 있다. 즉, 다음과 같이 코딩하는 것이 가능하다.
Object animal = new Animal();
Object dog = new Dog();
super 키워드
super 키워드의 역할은 두가지이다.
1. superclass 멤버에 접근
/* SuperClass.java */
public class SuperClass {
public void printMethod() {
System.out.println("Printed in Superclass.");
}
}
/* SubClass.java */
public class SubClass extends Superclass {
public void printMethod() {
super.printMethod();
System.out.println("Printed in Subclass.");
}
}
/* App.java */
public final class App {
private App() {
}
public static void main(String[] args) {
SubClass s = new SubClass();
s.printMethod();
}
}
실행 결과 :
>> Printed in Superclass.
>> Printed in Subclass.
2. superclass의 생성자 호출
public MountainBike(int startHeight,
int startCadence,
int startSpeed,
int startGear) {
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
메소드 오버라이딩
- 클래스는 instance 메소드와 static 메소드를 가질 수 있다.
- 인터페이스 또한 default 메소드를 가지며 클래스는 이를 오버라이딩할 수 있다.
- superclass의 메소드를 오버라이드하는 메소드는 superclass의 메소드와 같은 메소드 명, 같은 파라미터 타입과 갯수, 같은 반환 타입을 가진다.
instance 메소드 오버라이딩
- @Override 어노테이션을 통해 컴파일러에게 오버라이딩을 알려 줄 수 있다.
- instance 메소드에 대한 오버라이딩에서는 공변 반환 타입을 반환 타입으로 설정할 수 있다.
공변 반환 타입이란? Convariant return type이다. 반환타입(return type)은 서브 클래스라는 범위 안에서 다양할 수 있다는것으로 본래 오버라이딩이 이름이 같아야하고, 매개변수가 같아야 하며, 반환타입 또한 같아야 하는데 Primitive 타입이 아닌 Subclass 타입으로 오버라이딩이 가능하게 된것이다.
한줄로 요약하면 공변 반환 타입을 반환 타입으로 설정할 수 있다는 말은 superclass를 상속 받는 subclass를 반환 타입으로 지정할 수 있다.
public class A {
A get() { return this;}
}
class B extends A {
B get() { return this;}
void message() {
System.out.println("B class message");
}
public static void main(String[] args) {
new B().get().message();
}
}
javatpoint.com/covariant-return-type
static 메소드 오버라이딩(hiding)
static 메소드에 대한 오버라이딩은 hiding이라는 말로 정의된다.
런타임에 생성된 인스턴스의 메소드가 호출되지 않고 컴파일 시에 선언된 객체의 메소드가 호출되는 static 메소드에 대해서는 다형성이 적용되지 않는다.
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");
}
}
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 cat = new Cat();
Animal animal = new Animal();
Animal catAnimal = cat;
Animal.testClassMethod(); // The static method in Animal
//Animal.testInstanceMethod(); // error
System.out.println("=========");
cat.testClassMethod(); // The static method in Cat
cat.testInstanceMethod(); // The instance method in Cat
System.out.println("=========");
animal.testClassMethod(); // The static method in Animal
animal.testInstanceMethod(); // The instance method in Animal
System.out.println("=========");
catAnimal.testClassMethod(); // The static method in Animal
catAnimal.testInstanceMethod(); // The instance method in Cat
}
}
인터페이스 default 메소드 오버라이딩
인터페이스는 기본적으로 메소드 내용을 구현할 수 없지만 메소드에 default 선언을 통해 메소드 바디를 구현할 수 있다. 또한 해당 인터페이스를 구현하는 클래스는 default 메소드를 오버라이딩할 수 있다.
다만 같은 시그니처를 가지는 여러개의 default 메소드를 구현하는 경우 두가지 원칙으로 이름 충돌을 막는다.
-
인스턴스 메소드가 이미 구현되어 있으면 인터페이스의 default 메소드는 무시하고 인스턴스 메소드를 따른다.
class Horse {
public String identifyMyself() {
return "I am a horse.";
}
}
interface Flyer {
default public String identifyMyself() {
return "I am able to fly.";
}
}
interface Mythical {
default public String identifyMyself() {
return "I am a mythical creature.";
}
}
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.
2. 공통된 ancestor를 가지는 인터페이스들이 있을 때, 이미 오버라이딩이 되어있는 인터페이스의 default 메소드를 따른다.
interface Animal {
default public String identifyMyself() {
return "I am an animal.";
}
}
interface EggLayer extends Animal {
default public String identifyMyself() {
return "I am able to lay eggs.";
}
}
interface FireBreather extends Animal { }
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.
3. 만약, default 메소드끼리 충돌이 나거나 추상 메소드와 충돌이 나는 경우는 컴파일러가 에러를 띄운다.
메소드 디스패치
메소드 디스패치는 어떤 메소드를 호출할 지 결정하여 실제로 실행시키는 과정을 말한다.
이런 메소드 디스패치에는 정적 메소드 디스패치(Static Method Dispatch), 동적 메소드 디스패치(Dynamic Method Dispatch), 더블 디스패치(Double Dispatch) 세 가지가 존재한다.
정적 메소드 디스패치(Static Method Dispatch)
public class A{
public void print(){
System.out.println("A");
}
}
public class B extends A { //메소드 오버라이딩 - A를 상속받았으나 함수를 재정의
public void print(){
System.out.println("B");
}
}
public class Test{
public static void main(String[] args){
B b = new B();
b.print(); //B를 출력
}
}
메인 함수에서 b.print()를 호출했을 때 우리는 클래스B의 오버라이딩 된 함수가 불릴 것이라는 것을 알고 있다. 우리가 이미 알고 있는 것과 같이 컴파일러 역시도 이 메소드를 호출하고 실행시켜야되는 것을 명확하게 알고 있는데 우리는 이를 정적 메소드 디스패치라 부른다.
동적 메소드 디스패치(Dynamic Method Dispatch)
동적 메소드 디스패치는 정적 디스패치와는 다르게 컴파일러가 어떤 메소드를 호출해야되는지 모르는 것을 말한다.
class A {
private BB bb;
A(BB bb) {
this.bb = bb;
}
void print() {
bb.print();
}
}
class B implements BB {
public void print() {
System.out.println("B");
}
}
class B1 implements BB {
public void print() {
System.out.println("B1");
}
}
interface BB { void print(); }
위의 예제에서 BB라는 추상클래스는 B, B1으로 각각 구현되고 있다. 또한 A라는 클래스는 이런 BB라는 추상클래스(설계도)를 받아 bb.print()라는 함수를 사용하고 있다.
그렇다면 여기서 A클래스의 print()함수를 사용하면 어떤 함수가 호출될까? 아마도 해당 함수의 객체를 선언할 때에 할당 된 Obejct를 보고 어떤 함수를 실행할 지 결정하게 될 것이다. 우리는 이렇게 유추가 가능하지만 컴파일러는 이에 대해 알 수 있는 방법이 없다. 즉 컴파일러는 어떤 함수가 실행될 지 전혀 모르는 것이다. (B클래스의 print()를 가져와야 할 지 , B1클래스의 print()를 가져와야 할 지 아는 시점은 런타임 시점일 것이다.)
이처럼 컴파일러가 어떤 메소드를 호출해야되는지 모르는 것을 우리는 동적 메소드 디스패치라고 부른다.
더블 디스패치(Double Dispatch)
더블 디스패치는 Dynamic Dispatch를 두 번 하는 것을 의미한다.
기본적인 예제. 4가지 조합이 나오는 추상 Post레벨과 이를 활용하는 SNS레벨간의 조합처리되는 예제입니다.
public class Dispatch{
interface Post{ void postOn(SNS sns);}
static class Text implements Post{
public void postOn(SNS sns){
System.out.println("text -> " + sns.getClass().getSimpleName());
}
}
static class Picture implements Post{
public void postOn(SNS sns){
System.out.println("picture -> " + sns.getClass().getSimpleName());
}
}
interface SNS{}
static class Facebook implements SNS{}
static class Twitter implements SNS{}
public static void main(String[] args){
List<Post> posts = Arrays.asList(new Text(), new Picture());
List<SNS> sns = Arrays.asList(new Facebook(), new Twitter());
/*
for(Post p:posts){
for(SNS s:sns){
p.postOn(s);
}
}
*/
posts.forEach(p->sns.forEach(s.postOn(s)));
}
여기서 개별 SNS를 instanceof를 통해 처리하면 다음과 같은 문제가 발생한다.
1. OCP위반
2. 경우의 수가 변하는 경우 대응이 불가(GooglePlus)
3. 모든 경우의 수를 처리하기 힘듬
public class Dispatch{
interface Post{ void postOn(SNS sns);}
static class Text implements Post{
public void postOn(SNS sns){
if(sns instanceof Facebook){
System.out.println("text -> facebook");
}
if(sns instanceof Twitter){
System.out.println("text -> twitter");
}
}
}
static class Picture implements Post{
public void postOn(SNS sns){
if(sns instanceof Facebook){
System.out.println("picture -> facebook");
}
if(sns instanceof Twitter){
System.out.println("picture -> twitter");
}
}
}
interface SNS{}
static class Facebook implements SNS{}
static class Twitter implements SNS{}
static class GooglePlus implements SNS{}
public static void main(String[] args){
List<Post> posts = Arrays.asList(new Text(), new Picture());
List<SNS> sns = Arrays.asList(new Facebook(), new Twitter());
/*
for(Post p:posts){
for(SNS s:sns){
p.postOn(s);
}
}
*/
posts.forEach(p->sns.forEach(s.postOn(s)));
}
위의 예에서 새롭게 GooglePlus가 도입되었지만 Text와 Picture에 대응을 빼먹게 된다.
이를 막고자 Post레벨에서 하위클래스를 명시하는 메소드를 개별로 정의해도 정적디스패치에서 에러발생하게 된다.
public class Dispatch{
interface Post{
void postOn(Facebook sns);
void postOn(Twitter sns);
}
static class Text implements Post{
public void postOn(Facebook sns){
System.out.println("text -> facebook");
}
public void postOn(Twitter sns){
System.out.println("text -> twitter");
}
}
static class Picture implements Post{
public void postOn(Facebook sns){
System.out.println("picture -> facebook");
}
public void postOn(Twitter sns){
System.out.println("picture -> twitter");
}
}
interface SNS{}
static class Facebook implements SNS{}
static class Twitter implements SNS{}
static class GooglePlus implements SNS{}
public static void main(String[] args){
List<Post> posts = Arrays.asList(new Text(), new Picture());
List<SNS> sns = Arrays.asList(new Facebook(), new Twitter());
/*
for(Post p:posts){
for(SNS s:sns){
p.postOn(s);
}
}
*/
posts.forEach(p->sns.forEach(s.postOn(s))); //컴파일에러발생 (SNS타입이어야함)
}
이러한 문제의 해법 중 하나로 더블디스패치가 있다.
최초 등장한 논문은 1986년의 a simple technique for handing multuple polymophism에서 이다.
파라미터에 타입을 구분지으려던 전략을 포기하고 메소드 내에서 다시 동적디스패치로 처리하는 테크닉이다. 같은 원리를 적용하면 링크드 리스트처럼 몇 번이라도 디스패치할 수 있지만. 현실적으로 트리플디스패치도 일어나기는 힘들다(켄트백 구현패턴중).
더블디스패치를 활용한 수정예제
public class Dispatch{
interface Post{void postOn(SNS sns);}
static class Text implements Post{
public void postOn(SNS sns){
sns.post(this);
}
}
static class Picture implements Post{
public void postOn(SNS sns){
sns.post(this);
}
}
interface SNS{
void post(Text post);
void post(Picture post);
}
static class Facebook implements SNS{
public void post(Text post){
System.out.println("text -> facebook");
}
public void post(Picture post){
System.out.println("picture -> facebook");
}
}
static class Twitter implements SNS{
public void post(Text post){
System.out.println("text -> twitter");
}
public void post(Picture post){
System.out.println("picture -> twitter");
}
}
//구글 플러스 추가!
static class GooglePlus implements SNS{
public void post(Text post){
System.out.println("text -> gplus");
}
public void post(Picture post){
System.out.println("picture -> gplus");
}
}
public static void main(String[] args){
List<Post> posts = Arrays.asList(new Text(), new Picture());
List<SNS> sns = Arrays.asList(new Facebook(), new Twitter(), new GooglePlus());
/*
for(Post p:posts){
for(SNS s:sns){
p.postOn(s);
}
}
*/
posts.forEach(p->sns.forEach(s.postOn(s)));
}
하지만 더블디스패치는 인터페이스쪽의 변화가 구현쪽 전체에 영향을 끼치므로 이런 변화가 예상되는 경우는 구현쪽의 추상층을 인터페이스보다는 기본 구현이 포함되는 추상클래스가 유리하다.
visitor pattern은 이러한 더블디스패치를 이용한 패턴이다.
> 과제를 정리하면서 메소드 디스패치는 정말 어려운것 같다. 아직 어떻게 적용을 해야할까? 어디에 사용될까 고민이 많이 되었다.
결론: 설계의 전제... 다른 쪽 타입을 다 알고 그에 따라 각각 다른 로직을 적용해야 하는 경우... 상속 구조를 잘 써서 공통적인 부분은 구현 안하고 필요한 부분만 오버라이딩 해서 구현하는 식으로 가겠지만 N*M 종류의 로직이 만들어져야 하는 경우는 어쩔수 없이 사용한다고 한다.
추상 클래스
- abstract 키워드로 선언된 클래스를 말한다.
- 이는 abstract 메소드를 포함할 수 있다.
- 인스턴스로는 만들 수 없다.
추상 메소드
- 구현없이 선언만 된 메소드를 말한다.
- abstract 키워드로 선언된다.
Abstract class vs Interface
추상 클래스 | 인터페이스 | |
인스턴스화 | 불가능 | 불가능 |
fields | 선언 가능(static이나 final이 아닌 필드만) | 자동으로 public, static, final이 된다 |
methods | public, protected, private으로 정의 가능 | 모든 메소드가 public |
Predator 인터페이스를 아래와 같이 만든다.
public abstract class Predator extends Animal {
public abstract String getFood();
}
추상클래스를 만들기 위해서는 class 앞에 abstract 라고 표기해야 한다.
또한 인터페이스의 메소드와 같은 역할을 하는 메소드(여기서는 getFood 메소드)에도 역시 abstract 를 붙이도록 한다.
abstract 메소드는 인터페이스의 메소드와 마찬가지로 몸통이 없다. 즉 abstract 클래스를 상속하는 클래스에서 해당 abstract 메소드를 구현해야만 하는 것이다.
Predator 인터페이스를 위와 같이 추상클래스로 변경하면 Tiger 클래스도 다음과 같이 변경되어야 한다.
public class Tiger extends Predator implements Barkable {
public String getFood() {
return "apple";
}
public void bark() {
System.out.println("어흥");
}
}
Tiger 클래스는 이제 Predator 인터페이스를 implements 하던 것에서 Predator 추상클래스를 extends 하도록 변경되었다.
그리고 Predator 추상클래스에서 abstract 메소드로 선언된 getFood 메소드가 위와 같이 구현되었다. 추상클래스에 abstract 로 선언된 메소드(getFood 메소드)는 인터페이스의 메소드와 마찬가지로 추상클래스를 상속하는 클래스에서 반드시 구현해야만 하는 메소드이다.
추상 클래스에는 abstract 메소드 뿐만 아니라 실제 메소드도 추가가 가능하다. 추상클래스에 실제 메소드를 추가하면 Tiger으로 만들어진 객체에서 그 메소드들을 모두 사용할 수 있게 된다.
예를 들어 아래와 같이 isPredator 라는 메소드를 Predator 추상클래스에 추가하면 이 클래스를 상속받은 Tiger 에서 사용가능하게 된다.
public abstract class Predator extends Animal {
public abstract String getFood();
public boolean isPredator() {
return true;
}
}
final 키워드
final 키워드로 선언되는 entity는 그 값이 변경될 수 없음을 의미한다. 그 entity는 변수, 파라미터, 메소드, 클래스가 될 수 있다.
final variables
final 키워드와 함께 선언된 변수는 한번 초기화되면 다시는 바뀔 수 없다. (상수를 의미) 하지만 선언과 함께 초기화될 필요는 없다.
final parameters
final로 선언된 파라미터는 그 함수 어디에서도 파라미터의 값을 변경할 수 없음을 의미한다.
final methods
final로 선언된 메소드는 override나 hide될 수 없다.
final classes
final로 선언된 클래스는 subclass를 가질 수 없다.
'Java' 카테고리의 다른 글
[Java] 8주차 과제: 인터페이스 (0) | 2021.01.05 |
---|---|
[Java] 7주차 과제: 패키지 (0) | 2020.12.28 |
[Java] Binary Tree (0) | 2020.12.16 |
[Java] 5주차 과제: 클래스 (0) | 2020.12.14 |
[Java] 4주차 과제: 제어문 (0) | 2020.12.02 |
블로그의 정보
What doing?
Roel Downey