반응형
05-16 06:23
Today
Total
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

[Java] 상속 본문

Languages/Java

[Java] 상속

조용한고라니 2021. 3. 8. 09:36
반응형

상속이라는 단어는 아마 대부분 알 것이라고 생각된다. Java에서의 상속은 무엇일까? 자식 클래스가 상속받고 싶은 부모 클래스를 선택해서 물려받는다. 이때 상속받는 클래스 = 자식/하위/서브 클래스, 상속을 해주는 클래스 = 부모/상위/슈퍼 클래스 라고 한다. Java에서는 'extends' 키워드를 사용해 상속을 선언한다.

 

자식 클래스가 상속을 하게되면 부모 클래스의 필드메서드를 물려받는다(능력 및 기능을 제공받는다). 하지만 부모 클래스 멤버의 접근 제어자가 'private', 'default'라면 상속은 받을 수 있지만 접근은 어렵다.

 

그리고 자식 클래스가 여러 부모로부터 다중 상속받는 것은 불가(단일 상속)하다. 반대로 부모 클래스는 여러 자식 클래스에게 상속을 해주는 것이 가능하다.

 

Has A 상속

[A] -> [B] : B가 A의 멤버 또는 부품으로 사용될 경우

 

객체가 다른 객체를 자신의 멤버로 Has 하고 그것을 이용하면 그것을 Has A 상속이라고 한다. 남의 객체를 자신의 부품으로 이용하고, 그 부품의 기능을 자신이 구현할 필요가 없다. (extends라는 키워드를 사용해서 다른 클래스를 상속받지 않아도 멤버 변수로 다른 객체를 갖는다면 Has A 관계가 성립)

 

만약 다른 객체를 멤버로써 받아들이고 사용하지 않는다면, 그것은 상속이 아니다.

class A{
	private B b; // Has A 상속
    
    public A(B b){
    	this.b = b;
    }
    
    public func1(){
    	b.print();
    }
}

class B{
	int age;
    String name;
    
    public B(){}
}

Is A 상속

[A] -> [B] : B가 A의 부모 클래스나 기반으로 사용될 경우.

 

Has A가 부품으로 사용하는 상속이었다면, Is A는 부모 클래스를 기반으로 새로운 클래스를 만들려고 할 때 사용되는 상속이다. 다시말해 Is A는 동일한 부품을 만드는데 그것보다 더 개선되거나, 추가되는 클래스를 만들기 위해 사용한다. 'extends'라는 키워드를 사용해서 상속받는다. 

 

생성자를 정의하는 부분에서 객체가 자기 자신의 생성자를 가리킬때는 this()를 사용했다면, Is A 상속을 받은 객체가 부모 클래스의 생성자를 가리킬 때는 super()를 사용한다.

 

만약 부모클래스에 gen()이라는 "A"를 출력해주는 메서드가 있는데, 자식 클래스가 gen()이라는 "B"를 출력해주는 메서드를 갖고있다고 할 때, 자식클래스.gen()을 호출하면 "A"가 출력될까, "B"가 출력될까. 바로 "B"가 출력된다. 자식의 메서드가 우선순위를 더 높이 갖고있다.

이를 Override라고 한다.


● 상속의 장점

  • 클래스 사이의 멤버 중복 선언 불필요 - 클래스의 간결화
  • 클래스들의 계층적 분류로 클래스 관리 용이
  • 클래스 재사용과 확장을 통한 소프트웨어의 생산성 향상
class Parent{
  int age;
  String name;
}

class Child extends Parent{
  int grade;
}

 

● 부모 클래스 멤버에 대한 접근 지정

 

  private default protected public
같은 패키지
자식 클래스
X O O O
다른 패키지
자식 클래스
X X O O
같은 패키지
클래스
X O O O
다른 패키지
클래스
X X X O

 

● 상속과 생성자

 

자식 클래스와 부모 클래스는 모두 생성자를 갖는데, 자식 클래스의 객체가 생성될 때, 자식 클래스의 생성자와 부모 클래스의 생성자가 모두 실행될까? 아니면 자식 클래스의 생성자만 실행될까?

 

답은 '둘 다 생성된다'이다. 그럼 둘 중 누가 먼저 실행될까? 생각했던 것과 다르게 부모 클래스의 생성자가 먼저 실행된다. 그도 당연한 것이 부모 클래스가 먼저 초기화된 후 이를 활용하는 자식 클래스가 초기화되어야 하기 때문이다.

class Parent{
	int age;
	String name;
	
	public Parent() {
		System.out.println("# Parent");
	}
}
class Child extends Parent{
	int grade;
	
	public Child() {
		System.out.println("# Child");
	}
}

public class Inherit {

	public static void main(String[] args) {
		Child child = new Child();

	}
}
//# Parent
//# Child

● 자식 클래스에서 부모 클래스의 생성자 선택

 

부모 클래스는 여러 생성자를 갖을 수 있기 때문에 자식 클래스의 생성자와 함께 사용될 부모 클래스의 생성자를 결정해야한다. 따로 지정하지 않으면 부모 클래스의 기본 생성자를 호출하도록 컴파일한다. 이때 부모 클래스가 기본 생성자를 갖고 있다면 문제가 되지 않지만, 만약 부모 클래스가 다른 생성자를 갖는데 기본 생성자가 없다면 오류가 발생할 것이다.

 

부모 클래스의 기본 생성자가 아닌 다른 생성자를 호출하고싶다면 어떻게 해야할까. 'super()'라는 것을 사용한다.

이는 자식 클래스의 생성자에서 부모 클래스의 생성자를 명시적으로 선택할 수 있다. super()는 부모 클래스의 생성자 호출을 의미한다. super()안에 인자를 주어 매개 변수를 가진 생성자를 호출할 수도 있다.

 

super()는 생성자 정의 부분에서 반드시 맨 위에 있어야 한다.

class Parent{
	int age;
	String name;
	
	public Parent() {
		System.out.println("# Parent");
	}
	public Parent(int age, String name) {
		System.out.println("# Parent");
		this.age = age;
		this.name = name;
	}
}
class Child extends Parent{
	int grade;
	
	public Child(int age, String name) {
		super(age, name);
		System.out.println("# Child");
	}
}

public class Inherit {

	public static void main(String[] args) {
		
		Child child = new Child(12, "gorany");
	}
}

 

업캐스팅과 instanceof 연산자

 

캐스팅(casting)이란 타입 변환을 말한다. 자바에서 클래스에 대한 캐스팅은 업캐스팅과 다운캐스팅으로 나뉜다.

업캐스팅

자식 클래스는 부모 클래스의 속성을 상속받으므로, 자식 클래스는 부모 클래스로도 취급될 수 있다. Parent도 할머니/할아버지의 Child일 수 있기에 Parent도 Child가 될 수 있는 것이다. 즉 자식 클래스의 객체가 부모 클래스의 타입으로 변환되는 것을 업캐스팅 이라고 한다.

		Parent parent;
		Child child = new Child(12, "gorany");
		
		parent = child;
               //parent = (Parent) child;

다만 주의할 점이 있다. parent 변수는 Child객체를 가리키지만, Parent 변수에만 접근할 수 있다. 즉 grade 라는 변수는 접근할 수 없다. parent는 Parent 타입이기 때문에 Child의 멤버 변수인 grade에는 접근할 수 없는 것이다.

다운캐스팅

그러면 업캐스팅된 자식 클래스의 객체는 자신의 고유한 속성을 잃어버리는 것인가? 그렇지 않다. 잠시 가려져있을 뿐 업캐스팅한 것을 다시 돌려놓는다면 사용할 수 있다. 이를 '다운캐스팅'이라 한다.

 

다운캐스팅은 업캐스팅과 다르게 반드시 명시해주어야 한다.

		Parent parent;
		Child child = new Child(12, "gorany");
		
		//업캐스팅
		parent = child;
		
		//다운캐스팅
		Child children = (Child) parent;

 

 

객체와 참조 형식의 개수

Parent person = new Parent(); // O
Child person = new Child();   // O

Parent person = new Child()   // O
Child person = new Parent()   // X

person이 참조기 때문에 Parent라는 타입을 참조할 수 있는가?

Parent person = new Child()에서, person은 Child의 멤버나 메서드를 접근 할 수 없다. 이는 업캐스팅이다.

 

위 그림에서 Exam e = new NewExam()으로 했는데, 이 둘의 관계는 Exam -> NewExam이다. Exam의 크기만큼 메모리가 생기고, NewExam만큼 필요한 메모리를 더 확장 시키는데, Exam e는 Exam만큼만 참조하게 된다. 

 

아래를 메모리라고 했을 때, Exam e = new NewExam()으로 생성한다면 파란색 부분만 참조하게 되는 것이다.

Exam e = |||||||||||||||||||||||||||||||||||||||||| + ||||||||||

                            [ Exam ]                             [NewExam]

 

그러나 Exam의 total()이라는 메서드가 있고, NewExam의 total()을 오버라이드 했다고 하자.

public class NewExam extends Exam{

	private int com;

	@Override
	public int total() {
		return super.total() + com;
	}
}

public class Exam {

	private int kor, eng, math;

	public Exam() {
		kor = 10;
		eng = 10;
		math = 10;
		System.out.println("Exam");
	}

	public int total() {
		
		return kor + eng + math;
	}
}

이제 Exam exam = new NewExam(); 으로 exam.total()을 하면 Exam의 total()이 호출될까? NewExam의 total()이 호출될까?

답은 NewExam의 Override한 total()이 호출된다.

메서드 호출에 있어 그 객체의 참조 타입(참조 형)보다 객체의 타입에 우선 순위를 둔다.

 

다음은 참조 타입에 따른 메서드 접근에 관한 것이다. 다음 예제를 보자.

부모 클래스인 Exam은 avg()가 없고, 자식 클래스인 NewExam은 avg()가 있다.

public class NewExam extends Exam{

    private int com;

    @Override
    public int total() {
        return super.total() + com; 
    }
    
    public double avg(){
        return total() / 4;
}

public class Exam {

	public int kor, eng, math;

	public int total() {
		
		return kor + eng + math;
	}
}

/* main() */
Exam exam = new NewExam();

double avg = exam.avg();

맨 마지막 줄은 아무 문제가 없을까? 답은 No이다. exam의 참조 타입은 Exam이므로 NewExam이 갖는 공간에 접근할 수 없다. 그렇다고 NewExam이 갖는 avg()와 int com은 존재하지 않는 것일까?

 

그것 또한 No이다. Exam 타입으로는 접근할 수 없을 뿐이지 분명 존재한다. 그럼 이를 해결할 수 있는 방법은 "형 변환"이다.

double avg = ((NewExam) exam).avg();

동적 바인딩

객체는 일반적으로 갖는 변수의 크기만큼 메모리 공간을 갖는다고 알고있으나, 실제로 그렇지 않다. 그 이유는 객체가 갖는 메서드들의 주소를 갖는 공간이 있다. (4 Byte)

즉, 객체는 자신이 갖는 메서드들에 대한 주소를 갖고 있는 것이다. 그래서 print(Exam exam)이 실행될 때, Exam의 total()이 호출될지, NewExam의 total()이 호출될 지 여부는 컴파일 시에 결정( 정적 바인딩 )되는 것이 아니다.

메서드의 실행 도중 인자로 받아온 객체의 참조 타입이 아닌, 객체의 타입에 따라 결정되며, 이를 동적 바인딩 이라고 한다.

그럼 궁금증이 하나 생긴다. 모든 객체는 자신이 갖는 메서드들에 대한 주소를 갖는다고 했는데, 주소를 갖는 공간은 제각각 갖고있다고 해도, 그럼 메서드의 주소는 여러 개일까? 하나일까?

 

클래스가 정의되어 컴파일 될 때 메서드를 가리키는 주소 테이블이 형성된다. 그리고 객체가 생성될 때 메서드의 주소 공간이 만들어지면 이는 클래스의 메서드 주소 테이블을 가리킨다. 즉, 메서드들의 주소는 하나이며, 이를 공유하는 구조이다.

반응형

'Languages > Java' 카테고리의 다른 글

[Java] 파일 입력과 EOF  (0) 2021.03.10
[Java] Shuffle 메서드  (0) 2021.03.10
[Java] 클래스와 객체  (0) 2021.03.01
[Java] 우선순위 큐(Priority Queue)  (0) 2021.01.28
[Java] 람다식  (0) 2021.01.26
Comments