07 상속

winA·2025년 7월 1일

BE/Java

목록 보기
9/16
post-thumbnail

💛 상속 개념

부모가 자식에게 물려주는 행위

이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여 개발 시간을 단축시킨다.

자식클래스에서 모든 코드를 새로 작성하는 것보다는 부모 클래스에게 상속받고, 필드2와 method2만 추가 작성하는 것이 효율적인 코드 방식이라고 할 수 있다.

public class A{
	필드1, method1
}
public class B extends A{
	필드2, method2
}

클래스B에는 필드2와 method2만 정의가 돼있지만 실제 사용할 때는 클래스B가 필드1과 method1을 가지고 있는 것처럼 작동한다.

클래스A에서 필드1과 method1을 수정하면 클래스B에서는 별도의 작업없이 수정된 필드1과 method1에 접근할 수 있다.

💛 클래스 상속

자식이 부모를 선택한다. 자식 클래스를 선언할 때 어떤 부모로부터 상속받을 것인지를 결정한다.

public class 자식클래스 extends 부모클래스{
}

자바는 다중 상속을 허용하지 않는다. extends 뒤에는 단한개의 부모 클래스만 서술할 수 있다.

💛 부모 생성자 호출

자식 객체를 생성하면 부모 객체가 먼저 생성된 다음에 자식 객체가 생성된다.

  1. 자식 객체 호출
  2. 부모 객체 생성
  3. 자식 객체 생성

자식클래스 변수 = new 자식클래스();

자식 객체만 호출했는데, 부모 객체가 생성된 이유

모든 객체는 생성자를 호출해야만 생성된다. 개발자는 자식객체만 생성했는데, 어떻게 부모객체도 생성이 된것일까?

부모 생성자는 자식 생성자의 맨 첫 줄에 숨겨져 있는 super()에 의해 호출된다.

public 클래스(){
	super();
}

super()

컴파일 과정에서 자동 추가된다. 이것이 부모의 기본 생성자를 호출한다. 부모의 기본 생성자를 호출하는 것이기 때문에, 만약 부모 클래스에 기본 생성자가 없다면 자식 생성자를 선언할 때 컴파일 에러가 발생한다.

만약 부모클래스에 기본 생성자가 없고, 매개변수가 포함된 생성자만 있다면 개발자가 직접 super(매개값)의 형태로 자식 클래스의 첫줄에 호출해줘야 한다.

public 자식클래스(){
	super(매개값, ...);
}

예시

매개값 없는 생성자일 때

부모클래스

package ch07.sec03_parentconstructor.exam01;

public class Phone {
	
	//필드 선언
	public String model;
	public String color;
	
	//기본 생성자 선언
	**public Phone()** {
		System.out.println("Phone() 생성자 실행");
	}
}

자식클래스

package ch07.sec03_parentconstructor.exam01;

public class SmartPhone extends Phone {
	
	//자식 생성자 선언
	public SmartPhone(String model, String color) {
		//super(); //생략가능
		this.model = model;
		this.color = color;
		System.out.println("SmartPhone(String model, String color) 생성자 실행됨");
	}
}

매개값 있는 생성자일 때

부모클래스

package ch07.sec03_parentconstructor.exam02;

public class Phone {
	
	//필드 선언
	public String model;
	public String color;
	
	//매개변수를 갖는 생성자 선언
	public Phone(**String model, String color**) {
		this.model = model;
		this.color = color;
		System.out.println("Phone(String model, String color) 생성자 실행");
	}
}

자식클래스

package ch07.sec03_parentconstructor.exam02;

public class SmartPhone extends Phone {
	
	//자식 생성자 선언
	public SmartPhone(String model, String color) {
		**super(model, color);**
		System.out.println("SmartPhone(String model, String color) 생성자 실행됨");
	}
}

💛 method 재정의

부모 클래스의 method가 자식 클래스에게 적당한 method가 아닐 때는 오버라이딩을 위해 자식 클래스에게 맞는 method로 재정의한다.

method 오버라이딩

상속된 method를 자식 클래스에서 재정의하는 것

method가 오버라이딩됐다면 해당 부모 method는 숨겨지고 자식 method가 우선적으로 사용된다.

오버라이딩 규칙

  1. 부모 method의 선언부(리턴타입, method 이름, 매개변수)와 동일해야 한다.
  2. 접근 제한을 더 강하게 오버라이딩할 수 없다(public→private으로 변경 불가)
  3. 새로운 예외를 throws할 수 없다
package ch07.sec04_override.exam01;

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

자바에서는 어노테이션 기능이 있다. @Override는 정확히 오버라이딩이 되었는지 컴파일 단계에서 확인하고, 문제가 있다면 컴파일 에러를 생성한다. 이는 생략은 가능하다.

부모 method 호출

만약 부모의 method와 동일하지만 마지막에 한줄만 추가해서 자식 method를 구성하고 싶다면? 오버라이드를 하는 순간 부모의 method는 숨겨지기 때문에 부모 method의 코드를 모두 복사붙여넣기 해서 넣어줘야 한다. 이런 불필요한 상황을 방지하기 위해서 부모의 코드를 그대로 사용하고자할 때 super.method명();을 사용해서 오버라이드한 method에 부모의 코드를 그대로 넣을 수 있다.

이를 통해 부모 method를 재사용함으로써 자식 method의 중복 작업 내용을 없앨 수 있게 됐다. super의 위치는 자식 method 어디에 오든 관계 없다.

부모 클래스

package ch07.sec04_override.exam02;

public class Airplane {
	
	//method 선언
	public void land() {
		System.out.println("착륙합니다.");
	}
	
	public void fly() {
		System.out.println("일반 비행합니다.");
	}
	
	public void takeOff() {
		System.out.println("이륙합니다.");
	}
}

자식 클래스

package ch07.sec04_override.exam02;

public class SupersonicAirplane extends Airplane {
	
	//상수 선언
	public static final int NORMAL = 1;
	public static final int SUPERSONIC = 2;
	//상태 필드 선언
	public int flyMode = NORMAL;
	
	//method 재정의
	@Override
	public void fly() {
		if (flyMode == SUPERSONIC) {
			System.out.println("초음속 비행합니다.");
		} else {
//Airplane 객체의 fly() method 호출
			**super.fly();**
		}
	}
}

💛 final 클래스, final method

최종적으로 선언된 것, 재정의 불가 선언

final 클래스

final class는 최종적인 클래스이므로 더 이상 상속할 수 없는 클래스가 된다.

final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다.

대표적인 예시로 String클래스는 public final class String{}의 형태를 가졌기 때문에 우리는 String을 상속받아서 새로운 NewString 클래스를 만들 수 없다.

final method

오버라이딩할 수 없는 method가 된다. 부모 클래스를 상속해서 자식 클래스를 선언할 때, 부모 클래스에 선언된 final method는 자식 클래스에서 재정의할 수 없다.

💛 protected 접근 제한자

protected는 상속과 관련이 있다.

protected는 같은 패키지 안에서는 default처럼 접근이 가능하지만, 다른 패키지에서는 자식 클래스만 접근을 허용한다. protected는 필드와 생성자, method 선언에 사용될 수 있다.

예시

package ch07.sec06_protected.package1;

public class A {
	
	//필드 선언
	protected String field;
	
	//생성자 선언
	protected A() {
	}
	
	//method 선언
	protected void method() {
	}
}
package ch07.sec06_protected.package2;

public class C {
	
	//method 선언
	public void method() {
//A a = new A(); //x
//a.field = "value"; //x
//a.method(); //x
	}
}

만약 C클래스를 package1에서 선언했다면 문제없이 접근이 가능하다. 하지만 다른 패키지인 package2에서 선언된 C클래스는 A의 필드, 생성자, method에 접근할 수 없다.

package ch07.sec06_protected.package2;

import ch07.sec06_protected.package1.A;

public class D **extends A** {
	
	public D() {
//A() 생성자 호출
		super(); //o
	}
	
	public void method1() {
//A 필드값 변경
		this.field = "value"; //o
//A method 호출
		this.method(); //o
	}
	
	//method 선언
	public void method2() {
//A a = new A(); //x
//a.field = "value"; //x
//a.method(); //x
	}
}

하지만 A 클래스를 상속받은 다른 패키지에 있는 D는 자식 클래스이기 때문에 protected로 선언된 A의 필드, 생성자, method에 접근할 수 있다.

new 연산자를 사용해서 생성자를 직접 호출할 수는 없고, 자식 생성자에서 super()A 생성자를 호출할 수 있다.

숨기고는 싶은데 자식은 사용하게 하고 싶은 클래스를 작성하고 싶다면 protected 접근 제한자를 붙여주는 것이 좋다.

💛 타입 변환

클래스의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생한다. 아무 관계가 없는 클래스 간의 타입 변환은 불가능하다.

자동 타입 변환(Upcasting)

자식은 부모의 특징과 기능을 상속받기 때문에 부모 타입으로 자동 변환될 수 있다. 예를 들어 고양이가 동물의 특징과 기능을 상속받았다면 ‘고양이는 동물이다’가 성립하게 된다.

부모타입 변수 = 자식타입객체;
Cat cat = new Cat();
Animal animal = cat; //자동 타입 변환

// Animal animal = new Cat();

자식 타입 객체를 부모 타입 변수에 대입하는 경우, 형변환 연산자 없이 자동으로 타입이 변환된다. cat과 animal 변수는 타입만 다를 뿐, 동일한 Cat 객체를 참조한다. 같은 곳을 참조하기 때문에 cat==animal 연산 결과는 true를 반환한다.

바로 위의 부모 클래스가 아니더라도 상속 계층에서의 모든 상위 클래스 타입 관계에서는 자동 타입 변환이 가능하다.

자동 타입 변환 이후의 접근 제한

자동 타입 변환된 자식 객체는 부모 타입으로 참조되기 때문에, 부모가 가지고 있는 필드와 method만 접근이 가능하다. 자바가 참조 변수의 타입을 기준으로 멤버에 접근하기 때문이다. 우리가 호출한 것은 자식 객체라고 해도 참조 타입이 부모라면 부모가 알고 있는 범위 내에서만 접근이 가능하다.

자식만 알고 있는 method는 부모 타입으로는 참조하고 있는 정보 자체가 없기 때문에, 자식 method로의 접근할 수 있는 경로가 없기 때문에 컴파일 오류가 발생한다.

자식 클래스에서 오버라이딩된 method가 있다면 부모 method 대신 오버라이딩된 method가 호출된다.

강제 타입 변환(Downcasting)

부모 타입은 자식 타입으로 자동 변환되지 않는다. 만약 타입변환을 하고자 한다면 캐스팅 연산자()를 사용해서 강제 타입 변환을 할 수 있다.

자식타입 변수 = (자식타입) 부모타입객체;

강제 타입 변환은 항상 할 수 있는 것은 아니다. 자식 객체가 부모 타입으로부터 자동 변환된 후 다시 자식 타입으로 변환할 때 강제 타입 변환을 사용할 수 있다.

//자동 타입 변환
Parent parent = new Child();
//강제 타입 변환
Child child = (Child) parent;

강제 타입 변환은 부모 타입으로 자동 변환은 했지만 반드시 자식 객체에만 있는 method를 사용하고자 할 때 다시 자식 타입으로 강제변환하여 자식타입의 method를 사용할 수 있다.

예시

부모 클래스

package ch07.sec07_type.exam02;

public class Parent {
	
	public void method1() {
		System.out.println("Parent-method1()");
	}
	
	public void method2() {
		System.out.println("Parent-method2()");
	}
}

자식 클래스

package ch07.sec07_type.exam02;

public class Child extends Parent {
	
	//method 오버라이딩
	@Override
	public void method2() {
		System.out.println("Child-method2()");
	}
	
	//method 선언
	public void method3() {
		System.out.println("Child-method3()");
	}
}

예시

package ch07.sec07_type.exam03;

public class ChildExample {
	
	public static void main(String[] args) {
//객체 생성 및 자동 타입 변환
		Parent parent = new Child();
//Parent 타입으로 필드와 method 사용
		parent.field1 = "data1";
		parent.method1();
		parent.method2();
		
/*
parent.field2 = "data2"; //(불가능)
parent.method3(); //(불가능)
*/

//강제 타입 변환
		Child child = (Child) parent;
//Child 타입으로 필드와 method 사용
		child.field2 = "data2"; //(가능)
		child.method3(); //(가능)
	}
}

만약 parent.method3()의 주석을 풀게되면 위와 같은 오류가 발생하게 된다.

만약 method3에 접근하고 싶다면 다시 Child로 타입변환을 진행해줘야 한다.

기본 타입과 참조 타입

기본 타입은 값 자체를 저장하는 구조이다. 기본 타입에서 자동변환이 일어나기 위해서는 왼쪽 타입(대상 타입)이 더 큰 크기여야 한다. 그래야 정밀도나 범위 손실 없이 안전하게 저장할 수 있다. 기본 타입의 변환은 값의 복사가 전제이고, 크기가 기준이다.

참조 타입은 값이 아닌 객체의 주소를 저장한다. 자식 객체를 부모 타입으로 참조하게 되면 같은 객체를 부모 타입 시각으로 바라보는 것이다. 참조 타입의 변환은 크기가 아니라 타입 간의 상속 관계를 기준으로 발생하게 된다. 자식은 부모의 모든 것을 포함하고 있다. 그래서 자식 객체는 부모 타입이 요구하는 모든 것을 충족할 수 있다.

💛 다형성

사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질

같은 부모를 가지고 있어도 자식 클래스에서 어떻게 오버라이드 하느냐에 따라서 프로그램의 실행 결과(성능)이 바뀔 수 있다. 이를 다형성이라고 한다.

다형성 구현 요소

  • 자동 타입 변환
  • method 오버라이딩

필드 다형성

필드타입은 동일하지만(사용 방법은 동일하지만), 대입되는 객체가 달라져서 실행결과가 다양하게 나올 수 있는 것

부모 클래스

package ch07.sec08_poly.exam01;

public class Tire {
	
	//method 선언
	public void roll() {
		System.out.println("회전합니다.");
	}
}

자식 클래스1

package ch07.sec08_poly.exam01;

public class HankookTire extends Tire {
	
	//method 재정의(오버라이딩)
	@Override
	public void roll() {
		System.out.println("한국 타이어가 회전합니다.");
	}
}

자식 클래스2

package ch07.sec08_poly.exam01;

public class KumhoTire extends Tire {
	
	//method 재정의(오버라이딩)
	@Override
	public void roll() {
		System.out.println("금호 타이어가 회전합니다.");
	}
}

예시

package ch07.sec08_poly.exam01;

public class CarExample {
	
	public static void main(String[] args) {
//Car 객체 생성
		Car myCar = new Car();

//Tire 객체 장착
		myCar.tire = new Tire();
		myCar.run();

//HankookTire 객체 장착
		myCar.tire = new HankookTire();
		myCar.run();

//KumhoTire 객체 장착
		myCar.tire = new KumhoTire();
		myCar.run();
	}
}

어떤 타입 객체를 장착해주냐에 따라서 method의 실행결과는 달라지게 된다. 이는 자식 클래스가 공통의 method를 서로 다르게 오버라이딩하고 있기 때문이다.

객체 사용 방법이 동일하다는 것은 동일한 method를 갖고 있다라는 의미이다. 자식 객체를 바꾸어도 사용하는 코드에는 수정이 발생하지 않는다. 이는 객체지향 설계의 주요원칙인 OCP를 만족하는 조금의 변화가 생겼을 때, 변화된 부분만 수정하고 다른 부분에는 영향을 주지 않아야 한다.

매개변수 다형성

다형성은 method를 호출할 때 많이 발생한다. method가 클래스 타입의 매개변수를 가지고 있을 경우, 호출할 때 동일한 타입의 자식 객체를 제공할 수 있다. 어떤 자식 객체가 제공되느냐에 따라서 method의 실행 결과가 달라진다.

drive() method는 매개값으로 전달받은 vehiclerun() method를 호출한다. drive의 매개값으로 제공할 수 있는 것은 Vehicle뿐만 아니라 Vehicle과 상속 관계에 있는 자식 객체도 모두 제공할 수 있다. 이는 상속 구조에서 자동 타입 변환을 지원하기 때문에 별도의 캐스팅 오류없이 실행이 가능하다.

부모 클래스

package ch07.sec08_poly.exam02;

public class Vehicle {
	
	//method 선언
	public void run() {
		System.out.println("차량이 달립니다.");
	}
}

자식 클래스1

package ch07.sec08_poly.exam02;

public class Bus extends Vehicle {
	
	//method 재정의(오버라이딩)
	@Override
	public void run() {
		System.out.println("버스가 달립니다.");
	}
}

자식 클래스2

package ch07.sec08_poly.exam02;

public class Taxi extends Vehicle {
	
	//method 재정의(오버라이딩)
	@Override
	public void run() {
		System.out.println("택시가 달립니다.");
	}
}

예시

package ch07.sec08_poly.exam02;

public class Driver {
	
	//method 선언(클래스 타입의 매개변수를 가지고 있음)
	public void drive(**Vehicle vehicle**) {
		vehicle.run();
	}
}
package ch07.sec08_poly.exam02;

public class DriverExample {
	
	public static void main(String[] args) {
//Driver 객체 생성
		Driver driver = new Driver();

//매개값으로 Bus 객체를 제공하고 driver() method 호출
		Bus bus = new Bus();
		driver.drive(bus); // driver.drive(new Bus()); 와 동일

//매개값으로 Taxi 객체를 제공하고 driver() method 호출
		Taxi taxi = new Taxi();
		driver.drive(taxi); // driver.drive(new Taxi()); 와 동일
	}
}

필드 다형성 vs 매개변수 다형성

필드 다형성은 객체의 멤버로 유지되기 때문에 장착된 객체가 계속 사용된다. 매개변수 다형성은 객체를 전달만 받고 일시적으로 사용한다. 그래서 매번 다른 객체를 넘겨받아서 사용할 수 있다.

매개변수 다형성은 필드 다형성과 같은 원리의 다형성을 일시적, 전달용으로 적용한 방식이다.

💛 객체 타입 확인

매개변수의 다형성에서 실제로 어떤 객체가 매개값으로 제공되었는지에 대해서 확인하는 방법이 있다. 매개변수가 아니더라도 참조하는 객체의 타입에 대해서는 모두 instanceof 연산자를 사용하여 확인할 수 있다.

boolean result = 객체 instanceof 타입;

좌항의 객체가 우항의 타입이면 true, 아니면 false를 반환한다.

if(parent instanceof Child child){
	//child 변수 사용
}

instanceof 연산의 결과가 true일 경우, 우측 타입 변수를 바로 사용할 수 있다. 조건에 맞을 때만 안전하게 다운캐스팅하여 자식 객체의 기능을 사용할 수 있도록 한다.

public class AnimalTrainer {

    public void train(Animal animal) {
        animal.sound(); // 오버라이딩된 method 실행

        if (animal **instanceof** Dog dog) {
            dog.wagTail(); // Dog만의 method
        } else if (animal **instanceof** Cat cat) {
            cat.scratch(); // Cat만의 method
        } else {
            System.out.println("알 수 없는 동물입니다.");
        }
    }
}
public class AnimalExample {

    public static void main(String[] args) {
        AnimalTrainer trainer = new AnimalTrainer();

        Animal a1 = new Dog();
        Animal a2 = new Cat();

        trainer.train(a1); // 멍멍 + 꼬리 흔들기
        trainer.train(a2); // 야옹 + 할퀴기 공격!
    }
}

💛 abstract 클래스

실체 클래스들의 공통적인 필드나 method를 추출해서 선언한 클래스

abstract 클래스는 실체 클래스의 부모 역할을 한다. abstract 클래스는 새로운 실체 클래스를 만들기 위한 규격을 정의한 부모 클래스로만 사용되기 때문에 new 연산자를 사용해서 abstract 클래스 객체를 직접 생성할 수는 없고, extends 뒤에만 올 수 있다.

abstract 클래스 선언

클래스 선언에 abstract 키워드를 붙인다.

public **abstract** class 클래스명{
 //필드
 //생성자
 //method
}

객체로 선언되지 못할 뿐 생성자, 필드, method를 모두 선언할 수 있다.

abstract 클래스로부터 상속받은 클래스들은 일반 클래스의 역할로 new 연산자로 객체를 만들 수 있다. 부모 abstract클래스의 method도 모두 호출할 수 있다.

abstract method와 재정의

자식 클래스들이 해당 method를 모두 사용하지만 그 안에서의 동작은 모두 다를 때, 우리는 abstract method를 선언한다. abstract method는 공통 method라는 것을 정의할 뿐, 실행 내용을 가지지는 않는다.

abstract 리턴타입 method명(매개변수, ...);

따라서 abstract method에는 중괄호로 이루어진 실행문은 존재하지 않는다. 이는 abstract method는 abstract 클래스 안에서만 정의할 수 있다.

자식클래스는 abstractmethod로 정의된 함수를 자식클래스에서 반드시 재정의해야 한다.

tip: option+Enter

아직 구현되지 않은 method 위에 커서를 두고 option+Enter 단축키를 누르면 자동으로 필요한 method 틀을 오버라이딩 형태로 생성해준다.

Implement methods
@Override
public void run() {
    // TODO: 구현
}

💛 봉인된 클래스

final 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 하지만 무분별한 자식 생성은 오류를 낳을 수 있다. 그래서 Java15부터 sealed 클래스가 도입됐다.

이는 부모 클래스에서 만들 수 있는 자식 클래스를 지정함으로써 이 외에는 자식 클래스로 생성되지 못하도록 제한한다.

public sealed class Person permits Employee, Manager{}

상속 가능 지정받은 자식 클래스는 선언할 때 다음 키워드 중 하나를 반드시 선언해줘야 한다.

  • final: 더 이상 상속할 수 없다.
    public final class Employee extends Person{}
  • non-sealed: 봉인을 해제한다.
    public non-sealed class Manager extends Person{}

0개의 댓글