개발새발

[Java] 상속(inheritance)과 super 본문

Java/개념을 Java

[Java] 상속(inheritance)과 super

칸쵸. 2023. 9. 27. 00:33
728x90

오늘은.. 상속에 관해 글을 써보겠어요

처음에는 그냥 'extends로 클래스 확장해주고, 뭐 super로 받으면 되는거 아닌가?' 하고 쉽게 생각했는데 ..플래그

이게 공부를 할 수록 아리송하고 생각보다 제대로 알아야 할 개념이 많아서 조금 애먹었습니다

100% 이해한 것은 아니겠지만 그래도 나름대로 정리를 해보겠어요

 


1. 상속(Inheritance)이란

한 클래스가 다른 클래스의 멤버를 물려받아 사용하는 것

상속 관계에 있는 클래스는 부모(상위 클래스)와 자식(하위 클래스)으로 나뉘게 된다.

이때 자식 클래스는 부모의 모든 멤버를 상속 받게되고(생성자 및 초기화 블럭 제외), 그렇기에 필연적으로 자식 클래스의 멤버 개수는 부모의 멤버 개수보다 적을 수 없게된다. 또한, 자손의 변경(ex 멤버추가)은 부모 클래스에 영향을 미치지 못한다.

 

클래스 상속 문법과 함께 예를 들어보자!

class Parent{
	//부모 클래스의 멤버변수 및 메서드 정의
}

class Child extends Parent{
	//자식 클래스의 멤버변수 및 메서드 정의
}

위와 같이, 자바 문법으로 상속을 구현할 때는 extends 예약어를 사용한다. 이는 부모 클래스인 Parent가 가지고 있는 속성이나 기능을 확장하여 Child 클래스를 구현한다는 뜻이 된다.

 

그림으로 나타낸 구조

 

예를 들어서.. 포켓몬의 정보를 이름과 도감번호를 통해서 정의하는 포켓몬 도감, Pokedex 클래스가 있다고 하자.

class Pokedex{
	//멤버변수
	String pokemonName; //포켓몬 이름
    int pokedexNum; //포켓몬 도감번호
}

그런데 누군가는 여기에 만족하지 못하고, 포켓몬의 타입까지 넣어서 정보를 나타내고자 한다. 

그렇다면 어떻게 해야할까?

class Pokedex{
	String pokemonName;
    int pokedexNum;
    String pokemonType; //멤버변수로 포켓몬 타입 추가
}

지금까지 배운 방법으로는 이렇게 기존 클래스에 필요한 멤버변수를 추가해서 업그레이드 버전을 만들었을 것이다.

하지만 상속을 쓰면 어떻게 될까?

class NewPokedex extends Pokedex {
	String pokemonType;
}

NewPokedex라는 업그레이드 된 도감 클래스를 새로 만들고, 기존 구버전의 Pokedex를 상속해서 포켓몬의 타입을 나타내는 멤버변수 하나만 추가했다! 이렇게 되면 새로운 클래스에는 구버전에도 있는 멤버변수를 선언할 필요가 없어지고(멤버변수를 상속 받게 되니까), 새롭게 필요한 멤버변수만 추가함으로써 의도한 바를 달성할 수 있다.

즉, NewPokedex클래스에는 보이지 않지만 부모클래스의 멤버변수 pokemonName과 pokedexNum, 그리고 자식 클래스에서 선언한 type까지 모두 세 개의 멤버변수를 갖게 되는 것이다.

 


2. 모든 클래스의 최고 조상 Object class

상속을 말할 때, 빼먹을 수가 없는 클래스가 있는데 그것은 모든 클래스의 최고 조상 지존짱인 Object 클래스이다~

그래서 부모가 없는 클래스는 컴파일러를 통해 자동으로 Object 클래스 상속을 받게 된다.

 

위의 Parent 클래스의 경우, 따로 존재하는 부모 클래스가 없기 때문에

class Parent extends Object{
	//클래스의 멤버변수 및 메서드 정의
}

사실상 이렇게 'extends Object'가 숨겨져있다고 봐도 될 듯하다.

그리고 Object 클래스를 상속받게되는 모든 클래스는, 당연하게도 Object 클래스에 정의된 메서드를 모두 상속받게 되는데... (예를 들어 toString(), hashCode() ...등등) 구체적인 메서드 종류에 대해서는 글을 따로 다루는 것이 좋을 듯.

아무튼 그렇습니다.


3. 참조변수 super

상속을 말 할 때, 또 빼먹을 수 없는 놈이 있는데.. 그것은 바로 참조변수 super, 그리고 생성자 호출 메서드 super()

어째 this 와 this()가 생각나는 모양새다.

그런데 이것들은 실제로 this와 상당히 유사한 기능을 갖추었다. 이왜진

 

마찬가지로 super 와 super()는 이름만 같지 뜻은 완전히 다른 놈이기 때문에 둘을 따로 다루어보겠다.

일단 참조변수 super 부터

 

super : 객체 자신을 가리키는 참조변수로, 인스턴스메서드(생성자) 내에서만 존재

 

이렇게 말하면 진짜 this. 랑 다를게 뭐야? 싶겠지만 둘은 여기서 차이가 난다.

 

this : 지역변수와 인스턴스변수를 구별할 때 사용

super : 부모클래스의 멤버변수와, 자식클래스의 멤버변수를 구별할 때 사용

 

예를 들어보자!

public class SuperMain{
	public static void main(String[] args){
    	Child c = new Child();
        c.superMethod();
    }
}

class Parent{
	int x = 10;
}

class Child extends Parent{
	int x = 20; //this.x
    
    void superMethod() {
    	System.out.println("x = " + x); //Child의 멤버변수 x
        System.out.println("this.x = " + this.x); //Child의 멤버변수 x
        System.out.println("super.x = " + super.x); //Parent 클래스에서 상속받은 변수 x
    }
}

출력결과

위와 같이 Parent 클래스에서 선언한 멤버변수 x와 Child 클래스에서 선언한 멤버변수 x는 그 이름이 같음에도,

super를 통해서 구별이 가능해졌다!!!

 

** 참고

만약 Child 클래스에서 x 변수를 따로 선언하지 않았다면,  Parent 클래스의 x가 Child 클래스에서의  this.x 이자, super.x가 될 수 있다

 


4. 조상 생성자 호출 메서드 super()

이 친구도 this 때와 마찬가지로, 참조변수였던 super와는 사실상 무관하다고 볼 수 있다.

그렇다면 자식 클래스가 대체 부모 클래스의 생성자를 호출 할 일이 대체 뭐가 있나? 할텐데.. 일단 상속을 받고나면 생각보다 부를 일이 많다!!! 그 이유를 알아보자

 

앞서 말했듯이 클래스 상속을 받게 되면 자식 클래스는 부모 클래스의 모든 멤버를 다 물려받게 되지만 딱 두 개, 상속 받지 못하는 요소가 있었다. 바로 생성자와 초기화 블록.

 

아까의 포켓몬 도감 Pokedex 클래스로 생각해보자.

이전에는 Pokedex 객체의 속성(멤버변수)만을 선언했지만, 이번에는 클래스의 생성자까지 만들어보겠다.

class Pokedex{
	String pokemonName;
	int pokedexNum;
	
	Pokedex(String pokemonName, int pokedexNum){
		this.pokemonName = pokemonName;
		this.pokedexNum = pokedexNum;
	}
}

그리고 Pokedex 클래스를 상속받는 NewPokedex 클래스의 생성자도 만들어보았다.

class NewPokedex extends Pokedex{
	String pokemonType;
	
	NewPokedex(String pokemonName, int pokedexNum, String pokemonType){
		this.pokemonName = pokemonName;
        this.pokedexNum = pokedexNum;
		this.pokemonType = pokemonType;
	}
}

하.. 티스토리 코드 자동 줄맞춤 저따구로 되는거 매번 킹받네..

어쨌든 위 코드를 보면.. 에러가 나지 않고 작동에는 문제가 없는 것으로 보일텐데, 사실 이는 바람직하지 못한 코드다!

 

왜? 자식 클래스에서 부모 클래스의 멤버 변수를 직접 초기화 하고있는 모양새이기 때문이다.

부모 클래스의 멤버변수는 부모 클래스 자체적으로 수정을 하는 편이 낫다. 자식 클래스는 pokemonType,

즉 본인이 선언한 변수에 대해서 직접 초기화를 진행하는 것이 바람직하다.

 

그렇다고 저 코드 두 줄을 지우자니 문제가 생기고, 어떻게 하는 것이 좋을까?

이럴 때 바로 super()를 써서 부모 클래스의 생성자를 호출하는 것이다.

class NewPokedex extends Pokedex{
	String pokemonType;
	
	NewPokedex(String pokemonName, int pokedexNum, String pokemonType){
        super(pokemonName, pokedexNum); 
		//pokemonName과 pokedexNum을 매개변수로 받는 부모클래스 Pokedex의 생성자 호출
		this.pokemonType = pokemonType;
	}
}

 

 

그럼 또 여기서 드는 근본적인 의문점 하나.

왜 자식 클래스에서 생성자를 만들 때, 부모 클래스 생성자를 호출하지 않으면 오류가 나는가?

 

여기서 아주아주아주아주 중요한 생성자 선언의 조건이 나오는 것이다!!!

 

생성자의 첫 줄에는 반드시 생성자를 호출 해야한다

 

이게 뭔 소리야? 난 그동안 그런거 호출한 적 없는데?

 

아니!!! 당신은 지금까지 항상 올웨이즈올데이에브리타임 생성자를 호출해왔다! 그럼 왜 몰랐냐?!

그동안 컴파일러가 항상 자동으로 호출을 해줬으니까!

 

.

.

.

 

진짜 이게 무슨 뚱딴지같은 소리냐 할텐데, 아까 NewPokedex 도감을 예로 들어보자

만약 super(pokemonName, pokedexNum) 를 통해 부모클래스의 생성자를 호출하지 않았다면?

class NewPokedex extends Pokedex{
	String pokemonType;
	
	NewPokedex(String pokemonName, int pokedexNum, String pokemonType){
		this.pokemonType = pokemonType;
	}
}

라잌 디스...

당연하게도 바아로 에러가 난다. 어떻게?

생성자 Pokedex()가 정의가 되지 않았다고 나온다! 그럼 이런 에러는 대체 왜 뜨는가?

바로 앞서 말했듯이 생성자 선언의 조건에 따라 컴파일러가 자동으로 부모클래스의 디폴트 생성자를 호출하기 때문이다.

**디폴트 생성자: 매개변수가 없는 생성자

 

그러니까 우리가 작성한 이 코드에 써있지 않아서 그렇지, 컴파일링을 거치는 순간

class NewPokedex extends Pokedex{
	super(); //컴파일러가 자동으로 호출하는 부모 클래스의 디폴트 생성자
    
    String pokemonType;
	
	NewPokedex(String pokemonName, int pokedexNum, String pokemonType){
		this.pokemonType = pokemonType;
	}
}

실제 코드는 이렇게 돌아가게 되는 것이다.

 

당연히 우리는 Pokedex 클래스에서 매개변수를 2개나 받는 생성자를 만들었기 때문에 부모 클래스의 디폴트 생성자는 존재하지도 않게 되었고, 자식 클래스인 NewPokedex의 생성자는 갈 길을 잃게 된 것이다.

없는데 어떻게 찾아요

이럴 때 해결 방법이 바로

  1. 부모 클래스에서 디폴트 생성자를 따로 선언해주기
  2. super()를 통해 원하는 생성자 직접 호출하기

...가 되는 것이다. 방법은 상황따라 알아서 잘 적용하면 될듯요.

 

그럼 또 여기서 드는 의문.

아니 그럼 그동안 뭐 상속 받은적도 없는 클래스는 생성자에서 대체 뭘 자동으로 호출한거야?

 

앞서 말했던 Object 클래스가 기억이 나십니까. 모든 클래스의 최고 조상 지존 존엄 킹갓짱 클래스.

상속 받은 부모가 없으면 뭐다? 자동으로 Object 클래스를 상속받게 된다!

그동안 그 생성자들은 Object()를 자동으로 호출하고 있었던 것이다~~~ 아주 명쾌해요 푸하하


5. 오버라이딩(Override)

드디어 상속의 마지막 이야기 오버라이딩 흑흑

이 오버라이딩은 무엇이냐? 단어 자체가 가진 의미를 통해 생각해보자.

Override : 덮어쓰기
이때 덮어쓰는 대상은 메서드로, 상속받은 부모의 메서드를 자신에 맞게 변경하는 것

결국 그냥 상속 받은 메서드를 가져와서 '아휴 이거.. 구려서 원.. 여기 클래스에서는 뭐 맞지가 않네 투덜투덜' 하면서 내용을 입맛대로 갖다 고치는 셈이다.

 

그리고 오버라이딩을 할 때의 규칙이 있는데, 바로.. 선언부(반환 타입, 메서드 이름, 매개변수 목록)는 그대로 유지하고,

오직 그 메서드의 내용만 자신의 클래스에 맞게 변경을 할 수 있다는 것.

 

예를 들어서 아까의 포켓몬 도감 코드를 계속 재탕을 해보면..

하.. 분명 이클립스에 신나게 우다다 할 때는 좋았는데 다시 떠듬떠듬 치려니까 귀찮네요

class Pokedex{
	String pokemonName;
	int pokedexNum;
	
	Pokedex(String pokemonName, int pokedexNum){
		this.pokemonName = pokemonName;
		this.pokedexNum = pokedexNum;
	}
	//포켓몬의 정보를 반환하는 메서드 getPokemonInfo()
	String getPokemonInfo() {
		return "포켓몬 이름: " + pokemonName + "\n도감번호: " + pokedexNum ;
	}
}

class NewPokedex extends Pokedex{
	String pokemonType;
	
	NewPokedex(String pokemonName, int pokedexNum, String pokemonType){
		super(pokemonName, pokedexNum);
		this.pokemonType = pokemonType;
	}
	
	@Override
	String getPokemonInfo() {
		return "포켓몬 이름: " + pokemonName + "\n도감번호: " + pokedexNum
				+ "\n포켓몬 타입: " + pokemonType;
	}
}

Pokedex 클래스에 포켓몬의 정보를 반환하는 getPokemonInfo() 메서드를 만들었고, NewPokedex에서 그 메서드를 상속받아 반환 내용을 클래스의 속성에 맞게 수정한 것을 확인할 수 있다.

 

메서드 상단의 저 @Override 표시는 어노테이션(annotation)의 일종으로,  '이 메서드 오버라이딩 한것임!' 하고 명시하는 용도일 뿐, 없다고 뭐  기능을 하지 못하고 그런건 아니다. (하지만 부모클래스에 동일한 이름의 메서드가 없을 시에는 저기서 에러가 나는 것을 볼 수 있음)


6. 오버라이딩(Overriding) vs 오버로딩(Overloading)

그리고 진짜진짜진짜진짜 마지막으로....

허구헌날 Override와 Overload를 헷갈리는 나를 위해서 쓰는 마지막 코너........

 

1) Override

상속 받은 메서드의 내용을 변경

 

2) Overload

이름이 같은 새로운 메서드를 정의 (상속과 전혀 관련 X)

 

애초에 Overload는 상속과는 관련도 없을 뿐더러 그냥 이름만 같은 새로운 메서드를 만드는 행위를 일컫는다. 

그냥 근본적으로 둘은 상관이 1.도.없으니까 헷갈리지 마시라 제발 (미래의 나에게...)

 


이렇게... 상속 이야기는 ㄹㅇ루다가 끝났습니다.

분명 다 알고있다고 생각했는데 직접 예시 코드를 짜면서 머리 속에 물음표가 백만개가 돌아다니기 시작했고,

그것들을 명쾌하게 해결하는 데 너무 힘들었어요....

뭐랄까 개념 하나하나는 알겠는데 흐름이 안잡혔달까

암튼 지금도 100% 상속에 대해 이해했다고는 못하겠지만 그래도 이전보다는 코드를 보고

머리가 팽팽 돌아가는게 느껴집니다. 와하하 진짜로 끝!!!