Java

8. 상속

문정훈 2022. 2. 6. 16:28
더보기

목차

1. 상속에 대해 알아보자

  1) 상속
  2) final클래스, final메소드, protected 접근 제한자
  3) 자동타입변환
  4) 다형성
  5) 강제 타입 변환
  6) 추상클래스

 

2. 상속에서 익명 자식 객체
  1) 필드에서 익명 자식 객체
  2) 변수에서 익명 자식 객체 
  3) 매개변수에서 익명 자식 객체

 

 

1. 상속에 대해 알아보자

1) 상속

상속이란 

public A extends B {
	...
}

extends키워드를 통해 부모 클래스를 지정하게 된다. A는 자식 클래스 B는 부모 클래스가 되며 자식 클래스는 부모 클래스의 필드와 메소드를 물려받아 자식 클래스에서 부모 클래스 B의 필드와 메소드를 사용할 수 있는 것을 상속이라고 말한다. 하지만 모든 부모 클래스의 필드와 메소드를 상속받는 것은 절대 아니다. 

 

 

부모 클래스에서 상속의 대상 : 같은 패키지라면 private 맴버는 상속대상에서 제외된다. 다른 패키지 라면 private를 포함해 default맴버 또한 상속대상에서 제외된다.

 

부모 클래스를 상속받는것이란?
부모 클래스 타입의 변수를 선언해 자식 객체를 넣게 되면 변수는 부모 클래스의
필드로 접근하게 되면 자식 클래스의 부모 클래스의 필드가 있기 때문에 접근이 가능하다. 
매소드의 경우 부모 클래스의 메소드가 재정의되있지 않다면 부모 클래스의 메소드를 호출하고
재정의 되어잇다면 그 메소드를 호출한다.

 

 

● 부모 객체와 자식 객체 중 누가 먼저 생성되나?

부모 클래스를 상속 받은 자식 클래스가 있다. 자식 클래스의 객체를 생성할 때 부모와 자식 객체 중 어떤 객체가 먼저 생성될까?

 

public class Child extends Parent {
	Child() {
    	super();
    }
}

위 코드를 보면 자식 클래스의 생성자의 가장 첫줄에는 super() 키워드가 작성되어있으며 만약 이 코드를 작성하지 않는다면 컴파일러에 의해 묵시적으로 생성된다. 

super()는 부모 클래스의 기본 생성자를 호출하는 부분으로 만약 부모 클래스의 기본 생성자가 존재하지 않는다면 직접 부모 클래스의 생성자를 super(...)이와 같이 호출해줘야한다. 

public class Child extends Parent {
	Child() {
    	super("Moon", "22");	
    }
}

위 코드와 같이 부모 클래스의 생성자를 호출해 자식 객체가 생성되기전 부모 클래스의 객체를 먼저 생성시킨다. 

 

 

● 오버라이딩

오버라이딩은 자식 클래스에서 상속받은 부모 클래스의 메소드를 자식 클래스에서 재정의하는 것이다. 

우선 오버라이딩이 되지 않은 메소드에 대해서 자식 객체에서 부모 메소드를 호출한다면 부모 메소드가 그대로 호출된다. 

하지만 자식 객체에서 부모 객체의 메소드를 재정의하여 자식 객체에 알맞는 내용으로 메소드를 재정의할 수 있다. 

이 경우라면 생성된 자식 객체로 부모 메소드(오버라이딩된)를 호출하면 원본인 부모 메소드가 호출되는 것이 아니라 재정의된 자식 클래스의 메소드가 호출된다. 

 

 

● super 키워드 

super키워드란 this키워드와 다른데 자식 클래스 내부에서 사용할 수 있는 키워드 있다.

부모 객체의 존재하는 필드 메소드를 호출 할 때 사용되는 것이다.
super.(필드)  >> 부모 클래스의 필드를 호출
super.(메소드) >> 부모 클래스의 메소드를 호출

 

 

● this 키워드

자식 클래스 내부에서 자기 자신을 의미한다
만약 부모 클래스에 int a;가 선언되어 있고 자식 클래스에는 a가 선언되있지 않다면 this.a는 부모 클래스의 a를 호출한다. 따라서 만약 자식 클래스 부모 클래스 둘 다 필드 a가 선언되어 있다면 
this를 통해 자식 필드를 호출, super키워드를 통해 부모 필드를 호출 할 수 있다. 

 

 

 

2) final클래스, final메소드, protected 접근 제한자

● final 클래스

public final class A {}

 

public final class ~~~{}
이 클래스는 다른 클래스에서 상속할 수 없다.

● final 메소드

protected final method1() {}

 

이 메소드가 부모 클래스의 메소드라면 상속받은 자식 클래스에서 이 메소드를 재정의할 수 없다. 

● protected 접근 제한자
필드 생성자 메소드에 붙는다. 같은 패키지에서는 public 역할을 다른 패키지에서는 자식 클래스만 접근 할 수 있다. 즉 다른 패키지의 자식 클래스에서는 public키워드 다른 패키지의 일반 클래스는 default역할을 한다.
A패키지에서 클래스를 선언하고(public) 그 클래스 맴버들은 접근 제한자 protected를 가진다. 
B패키지에서 클래스를 선언하고 위의 클래스를 상속한다.
아래 클래스는 자식 클래스이므로 부모클래스의 맴버인 protected맴버에 접근이 가능하다.

하지만 B패키지의, 다른 클래스는 A패키지의 이 맴버에 접근할 수 없다. 

 

 

 

3) 자동타입변환

public class Parent {
	public void parent_method1() {}
}
public class Child1 extends Parent {
	@Override
	public void parent_method1() {}
    
	public viod child1_method1() {}
}
public class Child2 extends Parent {
	@Override
	public void parent_method1() {}
    
	public viod child2_method1() {}
}

위와 같은 자식 클래스와 부모 클래스가 있다고 하자.

 

public class MainClass {
	public static void main(String ar[]) {
    	Parent C1 = new Child1(); //1
        Child1 C2 = new Child1(); //2
        
    }
}

위 두 1,2 객체의 차이점을 정리하면 

우선 생성된 객체는 Child1 이라는 객체는 맞다.

하지만 객체의 타입이 1번 Parent이며 2번은 Child1이다. 

1번 객체는 부모 클래스의 메소드와 필드만을 사용할 수 있다. 즉 자식 클래스의 메소드와 필드는 사용할 수 없다. (객체의 타입이 Parent이므로) 

이런 경우는 Child1 내부에서 부모 메소드를 재정의하게 되는데 자식 클래스에서 재정의된 부모 메소드를 호출하면 부모 메소드가 아닌 자식 클래스의 재정의 메소드가 호출되게 된다. 

 

2번의 경우는 Child1의 타입으로 Child1의 객체를 생성한 것이므로 Child1의 필드와 메소드 모두 접근이 가능하며 또한 Parent를 상속받았기 때문에 부모 클래스의 필드와 메소드 모두 접근이 가능하다. 

 

 

● 정리..
다형성에 들어가기전 까지 상속은 : 부모 클래스를 상속 받은 자식 클래스가 있다.
자식 클래스 변수에 자식 클래스 객체를 반환하였다고 해보자. 변수로 부모클래스의 필드
, 메소드를 호출 할 수 있다. 또한 변수로 자식 클래스의 필드, 메소드도 접근할 수 있다. 
재정의 된 메소드는 부모 클래스의 메소드를 호출하지 않고 자식 클래스의 메소드를 
호출하게 된다. 
하지만 이 방법은 다형성을 구현할 수 없다. 다형성을 구현하는 것은 하나의 타입에
여러가지 객체를 대입해 같은 코드로 다양한 실행결과를 얻는 것을 말하는데 
이런 상속 방법에는 자식 클래스 변수에 자식 객체를 넣는것, 그리고 자식 클래스 변수로 
선언했기에 자식 클래스의 메소드도 호출할수 있다. 이런 코드는 다양성을 실현할 수 없다.
그 변수에 그 자식 객체 이외에는 다른 객체를 대입할 수 없을 뿐더러 그 자식 클래스를
상속한 클래스가 아닌 이상 코드에서 자식 클래스의 메소드를 호출한 부분은 다형성을 실현시
킬수 없게 한다.
반면 부모 클래스의 상위 클래스 변수에 자식 객체를 대입하는 방법은 자식 클래스의
메소드, 필드를 호출할 수 없는 단점이 있지만 변수에 여러 객체르 대입 시킬 수 있고 
코드를 수정하지 않고 다양한 실행결과를 얻을 수 있다. 즉 다양성을 실현하기 위해서이다,.
하지만 이런 방식은 같은 코드로 다양한 결과를 얻은 이점을 주었지만 부모클래스의 필드와 
메소드(재정의된 메소드)만 사용할 수 있기 때문에 자식 클래스의 순수한 메소드는
사용할 수가 없다. 이것을 위해 강제 타입 변환이란 개념이 존재하는 것이다.

 

 

 

4) 다형성

● 다형성 비유

Tire 라는 부모 클래스가 있다고 가정해 하고 Bus와 Taxi는 자식 클래스라고 해보자
Tire의 run()이라는 메소드를 자식 클래스들은 각각 재정의 하고 있다. 
메인 함수에서 Tire tire =new Bus(); 이 필드와 Tire tire =new Taxi(); 이렇게 필드를 선언했다.
tire.run(); 이 코드는 위의 두 개 중 어떤 필드를 사용하느냐에 따라 상황이 다른 자식 객체의 run()이 실행된다. 즉 tire.run()이라는 코드를 변경하지 않고 필드 값만 바꾸어 주어서 다른 실행 결과를 얻을 수 있다. 

이것이 다형성의 하나의 예시이다. 

 

다형성을 구현하기 위해 상속의 개념이 필요한 것이었다. 

부모 클래스는 여러 자식 클래스들에게 상속될 수 있는데 각기 다른 자식 클래스에서는 자신에 입맛(?)에 맞게 부모 클래스의 메소드를 재정의하면 되는 것이다. 

예를 들어 Animal이라는 부모 클래스의 sound() 라는 메소드가 있고 자식 클래스 dog, cat 이 있다면 dog클래스에선 sound()를 '멍멍'(멍멍이라고 출력)하면 되고 cat은 '야옹' 이라고 출력한다면 sound()라는 동일한 메소드가 어떤 자식 클래스를 만나냐에 따라 서로 다른 소리를 가지게 된다. 

 

부모 클래스와 자식 클래스 관계의 다형성의 주제는 이후 정리하는 인터페이스와 매우 유사한 개념이다. 

위 동물의 예시를 보면 Animal이라는 부모 클래스는 그 자식 클래스(dog, cat)들이 지켜야할 규격 일종의 인터페이스 역할을 하는 셈이다. 

 

 

5) 강제 타입 변환

부모 타입 변수에 자식 객체를 선언하였다. 
그 변수로 부모 클래스의 메소드의 재정의 된 자식 클래스의 메소드를 잘 호출하며 사용하다가
어느 순간 그 자식 객체가 가지고 있는 메소드를 사용해야 할 경우가 생길 수 있다. 
이때 강제 타입 변환 코드 Child child=(Child) parent;이런 코드만 작성해 주면 된다.

 

강제 타입 변환은 다형성을 구현하기 위해 부모 타입에 자식 객체가 선언된 형태에서 자식 객체의 고유 메소드를 사용해야하는 경우 부모 타입을 -> 자식(본인)의 타입으로 변환하는 것을 말한다. 

 

 

 

6) 추상클래스

abstract class A {
    int a;
    static int b;
    void func1() {}
    static void func2() {}
    abstract void func3();
}

추상 클래스는 위와같이 일반적인 클래스에서 선언하는 필드와 메소드를 모두 선언할 수 있다.

abstract키워드를 가진 메소드가 선언되게 되는데 위 메소드는 실행 구간인 {}가 없다. 

 

추상 클래스의 특징1)

본인을 객체로 만들 수 없다.

추상 클래스는 본래 목적이 추상 클래스를 상속하는 자식 클래스에서 상속되어 인터페이스의 역할을 하기 위함이기 때문이다. 

 

추상 클래스의 특징2)

추상 클래스를 상속받은 자식 클래스에서는 abstract 키워드가 있는 부모 클래스의 메소드는 반드시 재정의해야한다. 

 

 

추상 클래스르 정리해보면 부모 클래스와 자식 클래스 간의 다형성을 구현하기 위해서 부모 클래스에서 인터페이스적 속성을 좀 더 강화한 느낌이다. 

자식 클래스에서 강제적으로 부모 클래스의 추상 메소드를 재정의 하도록 요구한다. 


 

 

 

2. 익명 자식 객체

1) 필드에서 익명 자식 객체

public class AnonymousClass {
	Parent parent = new Parent(10, "hello") {
    	int b = super.a;
    	@Override
        void func1() {}
        
        void func2() {}
    }
}

class Parent {
	int a;
    String str;
    
    Parent(int a, String str) {
    	this.a = a;
        this.str= str;
    }
    
    void func1() {}
}

익명 자식 객체란 필드에서 익명 자식 객체를 생성하여 할당할 수 있는데 

익명 자식 객체는 생성자를 가질 수 없다. 필드와 메소드만 가질 수 있다. 
익명자식객체도 부모 클래스 타입 필드에 선언된 것이므로 익명 자식 객체의 메소드나 필드는 호출할 수 없으며 재정의된 메소드만 호출이 가능하다.

그럼 익명 자식 객체에서 func2() 와 같은 메소드는 어따 써먹냐?-> 익명 자식 클래스의 {} 블록(렉시컬 환경) 내에서 사용 가능하다. 

 

2) 매개변수에서 익명 자식 객체

위 필드에서 익명 자식 객체를 선언한 것과 같이 매개변수에서도 익명 자식 객체를 전달할 수 있다. 

public class AnonymousClass {
    void func(Parent parent) {
    	parent.func1();
    }
    
    void func2() {
    	func(new Parent(10, "hello") {
            @Override
            void func1() {
            	
            }
        }
    }
}

class Parent {
	void func1() {}
}
public class MainClass {
	public static void main(String ar[]) {
    	AnonymousClass ac = new AnonymousClass();
        ac.func2();
    }
}

AnonymousClass 객체의 func2메소드에서는 func메소드를 호출하고 func의 매개변수로 Parent의 익명 자식 객체를 전달한다. 

그 자식 객체에서는 익명 자식 객체의 부모인 Parent 객체를 생성시키는데 그 생성자의 매개변수로 10과 "hello"를 주게 된다. 

익명 자식 객체에서는 Parent의 메소드인 func1일 재정의하게 된다. 

 

3) 지역 변수에서 익명 자식 객체

public class AnonymousClass {
	void func() {
    	Parent parent = new Parent(10, "hello") {
        	int b = super.a;
            @Override
            void func1() {}
            
            void func2() {}
        }
    }
}

메소드의 지역 변수에서도 역시 익명 자식 객체를 할당하 수 있다. 


 

 

※ 헷갈릴 내용 정리

1) 부모 클래스 변수에 자식 객체를 선언하면 그 변수는 자식 객체를 가르킨다.

 

2) 아래와 같이 선언된 c1, c2는 모두 동일한 자식 객체를 가리킨다. 

Child c1 = new Chid1();
parent c2 = c1;

자식 클래스 변수를 먼저 선언하고 그 변수를 부모 클래스 변수에 대입하면 두 변수는 
동일한 자식 객체를 가르킨다.

하지만 자식 클래스 변수에 선언된 자식 객체 c1은 자식 클래스의 모든 필드와 메소드를 사용하며
부모 클래스에 대입된 자식 객체 c2는 부모 클래스의 필드, 재정의된 메소드만 사용가능하다. 

 

'Java' 카테고리의 다른 글

10. 중첩 클래스  (0) 2022.02.06
9. 인터페이스  (0) 2022.02.06
7. 클래스 탐구  (0) 2022.01.20
6. 클래스 정리3 : 패키지와 imoprt  (0) 2022.01.20
5. 클래스 정리2 : final, 접근 제한자,  (0) 2022.01.20