Java Category/Java

[JAVA] 상속과 메소드 재정의(Overriding)

ReBugs 2023. 1. 7.

본 게시글은 혼자 공부하는 자바 (저자 : 신용권)의 책과 유튜브 영상을 참고하였고, 개인적으로 정리하는 글임을 알립니다.


현실에서 상속은 부모가 자식에게 재산, 유전 정보 등을 물려주는 행위를 말한다.

자식은 상속을 통해서 부모가 물려준 것을 이용할 수 있다.

객체 지향 프로그래밍에서도 부모 클래스의 멤버를 자식 클래스에게 물려줄 수 있다.

프로그램에서 부모 클래스를 상위 클래스라고 부르고, 자식 클래스를 하위 클래스 또는 파생 클래스라고 부른다.

 

상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여준다.

 

클래스 상속

자식 클래스를 선언할 때 어떤 부모 클래스를 상속받을 것인지 결정하고, 선택된 부모 클래스는 extends 뒤에 기술한다.

class derivedClass extends baseClass{
//필드
//생성자
//메소드
}

 

상속은 다음과 같은 특징을 가지고 있다.

  • 자식 클래스는 여러 개의 부모 클래스를 상속 받을 수 없다.(다중 상속 불허)
  • 부모 클래스에서 private 접근 제한을 갖는 필드와 메소드는 상속 대상에서 제외된다.
  • 부모 클래스와 자식 클래스가 다른 패키지에 존재한다면 default 접근 제한을 갖는 필드와 메소드도 상속 대상에서 제외된다.
  • 부모 클래스에서 protected 접근 제한을 갖는 멤버는 같은 패키지에서는 default 접근 제한과 같이 접근 제한이 없지만 다른 패키지에서는 자식 클래스만 접근을 허용한다.

아래 예제는 부모 클래스 baseClass 클래스와 부모 클래스를 상속받은 derivedClass 클래스가 있다.

부모 클래스는 일반적인 전화이고 자식 클래스는 스마트폰이다.

 

baseClass.java

package TestPackage;
public class baseClass {
		String model;
		String color;
		
		void powerOn() { System.out.println("전원 ON"); }	
		void powerOff() { System.out.println("전원 OFF"); }
		void bell() { System.out.println("Ring Ring"); }	
		void sendVoice(String message) { System.out.println("나 : " + message); }	
		void receiveVoice(String message) { System.out.println("상대 : " + message); }	
		void hangUp() { System.out.println("통화 종료"); }
}

 

derivedClass.java

package TestPackage;
public class derivedClass extends baseClass{
		String appName; //자식만 가지고 있는 필드

		derivedClass(String model, String color, String appName) { //생성자
			this.model = model; //부모로부터 상속받은 필드
			this.color = color; //부모로부터 상속받은 필드
			this.appName = appName;
		}

		void turnOnapp() {
			System.out.println("어플리케이션 " + appName + "을 실행");
		}	
		void changeappName(String appName) {
			this.appName = appName;
			System.out.println("어플리케이션 " + appName + "으로 실행 변경");
		}
		void turnOffapp() {
			System.out.println("어플리케이션 종료.");
		}
}

 

Test.java

package TestPackage;
public class Test {
	public static void main(String[] args) {
		//자식 객체 생성
		derivedClass smartphone = new derivedClass("갤럭시-Z플립-3", "블랙", "JAVA");
		
		//부모로부터 상속받은 필드
		System.out.println("모델 : " + smartphone.model);
		System.out.println("색상 : " + smartphone.color);
		
		//자식 고유의 필드
		System.out.println("실행중인 어플 이름 : " + smartphone.appName);
		
		//부모로부터 상속받은 메소드 호출
		smartphone.powerOn();
		smartphone.bell();
		smartphone.sendVoice("여보세요");
		smartphone.receiveVoice("네 여보세요 주호민입니다.");
		smartphone.sendVoice("응. 호민이형 왜?");
		smartphone.hangUp();
		
		//자식 고유의 메소드 호출
		smartphone.turnOnapp();
		smartphone.changeappName("C++");
		smartphone.turnOffapp();
	}
}
/*
 모델 : 갤럭시-Z플립-3
색상 : 블랙
실행중인 어플 이름 : JAVA
전원 ON
Ring Ring
나 : 여보세요
상대 : 네 여보세요 주호민입니다.
나 : 응. 호민이형 왜?
통화 종료
어플리케이션 JAVA을 실행
어플리케이션 C++으로 실행 변경
어플리케이션 종료.
 */

 

부모 생성자 호출

부모 없는 자식 없듯이, 부모 객체가 먼저 생성되고 그다음에 자식 객체가 생성된다.

 

 

모든 객체는 클래스의 생성자를 호출해야만 생성된다. 부모 객체도 예외는 아니다.

부모 객체는 어떻게 생성되는걸까?
부모 생성자는 자식 생성자에서 호출된다.
자식 생성자에서 부모 생성자가 명시적으로 선언되지 않았다면 컴파일러는 아래와 같은 부모 생성자를 호출하는 코드를 실행한다. (부모 생성자는 자식 생성자의 맨 첫 줄에서 호출된다.)
public class derivedClass extends baseClass{
    public derivedClass(){
    super();
    }
}​
super();는 부모의 기본 생성자를 호출한다.
생성자가 선언되지 않아도 컴파일러에 의해 기본 생성자가 만들어진다.

만약 직접 자식 생성자를 선언하고 명시적으로 부모 생성자를 호출하고 싶다면, 아래와 같이 작성하면 된다.

자식클래스 ( 매개변수 선언, ...){
 super( 매개값, ...);
...
}
  • super( 매개값, ...)는 매개값의 타입과 일치하는 부모 생성자를 호출한다. 만약 매개값의 타입과 일치하는 부모 생성자가 없을 경우 컴파일 에러가 발생한다.
  • super( 매개값, ...)가 생략되면 컴파일러에 의해 super()가 자동적으로 추가되기 때문에 부모의 기본 생성자가 존재해야 한다.
  • 부모 클래스에 기본 생성자가 없고 매개 변수가 있는 생성자만 있다면 자식 생성자에서 반드시 부모 생성자 호출을 위해  super( 매개값, ...)를 명시적으로 호출해야 한다.
  • super( 매개값, ...)는 반드시 자식 생성자 첫 줄에 위치해야 하며, 그렇지 않으면 컴파일 에러가 발생한다.

 

People.java

package TestPackage;
public class People {
	public String name;
	public String ssn;
	
	public People(String name, String ssn) {
		this.name = name;
		this.ssn = ssn;
	}
}

 

Student.java

package TestPackage;
public class Student extends People{
public int studentNo;
	public Student(String name, String ssn, int studentNo) { //자식 생성자 선언
		super(name, ssn); //부모 생성자 호출
		this.studentNo = studentNo;
	}
}

 

Test.java

package TestPackage;
public class Test {
	public static void main(String[] args) {
		Student student = new Student("홍길동", "123456-1234567", 1);
		System.out.println("name : " + student.name);
		System.out.println("ssn : " + student.ssn);
		System.out.println("studentNo : " + student.studentNo);
	}
}
/*
name : 홍길동
ssn : 123456-1234567
studentNo : 1
 */

 

메소드 재정의(Overriding)

부모 클래스의 모든 메소드가 자식 클래스에 맞게 설계되어 있다면 가장 이상적인 상속이지만, 어떤 메소드는 자식 클래스가 사용하기에 적합하지 않을 수도 있다. 이 경우 상속된 일부 메소드는 자식 클래스에서 다시 수정해서 사용해야한다. 자바는 이런 경우를 위해 메소드 재정의(오버라이딩)기능을 제공한다.

 

메소드 재정의는 자식 클래스에서 부모 클래스의 메소드를 다시 정의하는 것을 말한다. 메소드를 재정의 할 때는 다음과 같은 규칙에 주의해서 작성해야 한다.

  • 부모의 메소드와 동일한 시그니처(리턴 타입, 메소드 이름, 매개 변수 목록)를 가져와야 한다.
  • 접근 제한을 더 강하게 재정의할 수 없다.
  • 새로운 예외(Exception)를 throws할 수 없다.
접근 제한
접근 제한을 더 강하게 재정의 할 수 없지만, 더 약하게 재정의 할 수는 있다.

2023.01.06 - [Language/JAVA] - [JAVA] 접근 제한자(Access Modifier)

 

메소드가 재정의되었다면 부모 객체의 메소드는 숨겨지기 때문에, 자식 객체에서 메소드를 호출하면 재정의된 자식 메소드가 호출된다.

 

Calculator.java(부모 클래스)

package TestPackage;
public class Calculator {
	double areaCircle(double r) { 
		System.out.println("Calculator 객체의 areaCircle() 실행");
		return 3.14159 * r * r; 
	}
}

 

Computer.java(자식 클래스)

package TestPackage;
public class Computer extends Calculator{
	@Override //어노테이션
	double areaCircle(double r) { //오버라이딩
		System.out.println("Computer 객체의 areaCircle() 실행");
		return Math.PI * r * r; 
	}
}

 

Test.java

package TestPackage;
public class Test {
	public static void main(String[] args) {
		int r = 10;		
		Calculator calculator = new Calculator();
		System.out.println("원의 면적 : " + calculator.areaCircle(r));	//자식 객체의 메소드 호출	
		System.out.println();		
		Computer computer = new Computer();
		System.out.println("원의 면적 : " + computer.areaCircle(r)); //부모 객체의 메소드 호출
	}
}
/*
Calculator 객체의 areaCircle() 실행
원의 면적 : 314.159

Computer 객체의 areaCircle() 실행
원의 면적 : 314.1592653589793
 */

 

@Override(어노테이션)
Computer.java 에서 @Override는 어노테이션이라고 부른다.
어노테이션은 생략해도 상관없지만, 이것을 붙이면 메소드가 정확히 재정의된 것인지 컴파일러가 확인하기 때문에 개발자의 실수를 줄여준다.

 

자식 클래스에서 부모 클래스의 메소드를 재정의하게 되면, 부모 클래스의 메소드는 숨겨지고 재정의된 자식 메소드만 사용된다. 그러나 자식 클래스 내부에서 재정의된 부모 클래스의 메소드를 호출해야 하는 상황이발생한다면 명시적으로 super 키워드를 붙여서 부모 메소드를 호출할 수 있다.

 

Airplane.java

package TestPackage;
public class Airplane {
	public void land() {
		System.out.println("착륙");
	}	
	public void fly() {
		System.out.println("일반 비행모드");
	}	
	public void takeOff() {
		System.out.println("이륙");
	}	
}

 

SupersonicAirplane.java

package TestPackage;
public class SupersonicAirplane extends Airplane{
	public static final int NORMAL = 1;
	public static final int SUPERSONIC = 2;
	
	public int flyMode = NORMAL;
	
	@Override
	public void fly() {
		if(flyMode == SUPERSONIC) {
			System.out.println("초음속 모드");			
		} else {
			super.fly();//부모 객체의 fly()메소드 실행
		}
	}
}

 

Test.java

package TestPackage;
public class Test {
	public static void main(String[] args) {
		SupersonicAirplane sa = new SupersonicAirplane();		
		sa.takeOff(); //이륙
		sa.fly(); //비행
		sa.flyMode = SupersonicAirplane.SUPERSONIC; //비행모드 변경
		sa.fly();
		sa.flyMode = SupersonicAirplane.NORMAL; //비행모드 변경
		sa.fly();		
		sa.land(); //착륙
	}
}
/*
이륙
일반 비행모드
초음속 모드
일반 비행모드
착륙
*/

 

final 클래스와 final 메소드

클래스를 선언할 때 final 키워드를 class 앞에 붙이면 이 클래스는 최종적인 클래스이므로 상속할 수 없는 클래스가 된다.

즉, final 클래스는 부모 클래스가 될 수 없어서 자식 클래스를 만들 수 없다는 뜻이다.

위의 사진과 같이 final 클래스를 자식 클래스에서 부모 클래스로 상속받으려고 하면 에러가 뜨는 것을 볼 수 있다.

 

메소드도 마찬가지이다. 부모 클래스에 있는 final 메소드는 자식 클래스에서 오버라이딩 할 수 없다.

댓글