JAVA

상속

hyebin Lee 2023. 1. 24. 15:39

상속 개념

상속은 부모가 자식에게 물려주는 행위를 말한다.

객체 지향 프로그램에서도 부모클래스의 필드와 메소드를 자식 클래스에게 물려줄 수 있다.

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

public class A{
	int field1;
    void method1(){
    ...
    }
}
public class B extends A{ //A를 상속해서 B를 만들겠다. 
	String field2;
    void method2(){...}
}
//A로부터 물려받은 필드와 메소드
B b = new B(); 
b.field1 = 10;
b.method1();

//B가 추가한 필드와 메소드
b.field2 = "홍길동";
method2();

클래스 상속

자식 클래스를 선언할 때 어떤 부모로부터 상속받을 것인지 결정하고 부모 클래서를 다음과 같이 extends 뒤에 기술한다.

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

자바는 다중 상속을 허용하지 않는다. 즉, 여러 개의 부모 클래스를 상속할 수 없다. 따라서 반드시 extends 뒤에는 단 하나의 부모 클래스만이 와야 한다.

부모 생성자 호출

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

모든 객체는 생성자를 호출해야 생성된다. 그렇다면 부모 객체의 생성자는 어디서 호출된걸까? 🤔

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

//자식 생성자 선언 시 
public 자식클래스 (...) {
	super(); //자식클래스의 위에 있는 부모의 생성자를 호출한다.
}

super()은 컴파일 과정에서 자동추가 되는데 이것은 부모의 기본 생성자를 호출한다.

만약 부모 클래스에서 기본 생성자가 없다면 자식 생성자 선언에서 컴파일 에러가 발생한다.

 

⭐️ super()를 명시적으로 작성해야할 경우

부모 생성자가 매개변수를 갖는 생성자만 있다면 직접 매개값 코드를 넣어야한다. 

package ch07.sec03.exam01;

public class Phone { 
	public String model;
    public String color;
    
    public Phone(){
    	System.out.println("생성자 실행");
    }
}
package ch07.sec03.exam01;

public class SmartPhone extends Phone {
	public SmartPhone(String model, String color){
    	super(); //생략 가능 (컴파일 시 자동 추가됨)
        this.model = model;
        this.color = color;
        System.out.println("SmartPhone 생성자 실행됨");
    }
}

 

메소드 재정의

부모 클래스의 모든 메소드가 자식 클래스가 사용하기에 적합하지 않을 수 있다. 이러한 메소드는 자식 클래스에서 재정의해서 사용해야한다. 이것을 오버라이딩이라고 한다.

메소드 오버라이딩

메소드 오버라이딩은 상속된 메소드를 자식 클래스에서 재정의하는 것을 말한다.

메소드가 오버라이딩 되었다면 해당 부모 메소드는 숨겨지고 자식 메소드는 우선적으로 사영된다.

메소드 오버라이딩을 할 때 다음과 같은 규칙에 주의해서 작성해야한다.

  • 부모 메소드의 선언부 (리턴 타입, 메소드 이름, 매개변수) 와 동일해야한다.
  • 접근 제한을 더 강하게 오버라이딩 할 수 없다. (public -> pricate 으로 변경 불가)
  • 새로운 예외를 throws 할 수 없다.

접근 제한자

package ch07.sec04.exam01;

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

public class Computer extends Calculator {
	@Override //컴파일 시 정확한 오버라이딩이 되었는지 체크해줌 (생략 가능)
    public double areaCircle(double r){
    	System.out.println("Computer 객체의 areaCircle()실행");
        return Math.PI * r * r;
    }
}

@Override어노테니션이라고 한다. 추가적인 기능을 제공하기 위해 필드나 메소드 생성자 클래스 선언 위에 붙인다. 

재정의 한 내용이 잘 작성이 되어있는지 컴파일러가 검사를 할 수 있도록 한다. 따라서 가능하면 재정의를 할 떄 붙여주는 것이 좋다. 

pakage ch06.sec04.exam01;

public class ComputerExample {
	public static void main(String[] args){
    	int r = 10;
        
        Calculator calculator = new Calculator();
        System.out.println("원 면적 " + calculator.areaCircle(r)); 
        
        Computer computer = new Computer();
        System.out.println("원 면적 " + calculator.areaCircle(r));
    }
}

부모 메소드 호출

메소드를 재정의 하면 부모 메소드는 숨겨지고 자식 메소드만 사용되기 때문에 비록 부모 메소드 일부만 변경된다 하더라도 중복된 내용을 자식 메소드도 가지고 있어야 한다. 

예를 들어 부모쪽에 100줄의 코드가 있고 자식 코드에서 마지막 코드만 수정하고 싶을 경우, 부모쪽에 선언된 하면 100줄을 다시 쓴 후 마지막 줄만 수정 하는 방식으로 하게 된다. 하지만  이는 비효율적인 방법이다.  

이 문제는 자식 메소드와 부모메소드의 공동 작업 처리 기법을 이용하면 매우 쉽게 해결된다.

자식 메소드 내에서 부모 메소드를 호출하는 것인데, super 키워드와 도트 연산자를 사용하면 숨겨진 부모 메소드를 호출할 수 있다. 

즉, 부모의 메소드도 실행하면서 추가적으로 필요한 작업해야하는 코드도 수행할 수 있다. 

package sec04.exam02_super;

public class Airplane {
	public void land() {
		System.out.println("착륙합니다.");
	}
	
	public void fly() {
		System.out.println("일반비행합니다.");
	}
	
	public void takeOff() {
		System.out.println("이륙합니다.");
	}
}
package sec04.exam02_super;
//자식 클래스
//부모 메소드 호출(super) 사용 예
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  {
			// Airplane 객체의 fly() 메소드를 호출
			super.fly();
		}
		
	}
}
package sec04.exam02_super;
// 부모 메소드 호출(super) 사용 예
public class SupersonicAirplaneExample {
	public static void main(String[] args) {
		SupersonicAirplane sa = new SupersonicAirplane();
		sa.takeOff();
		sa.fly();
		sa.flyMode = SupersonicAirplane.SUPERSONIC;
		System.out.println("초음속 비행 모드로 변경합니다.");
		sa.fly();
		sa.flyMode = SupersonicAirplane.NORMAL;
		System.out.println("일반 비행 모드로 변경합니다.");
		sa.fly();
		sa.land();
	}
}

final클래스와 final 메소드

필드 선언 시 final을 붙이면 초기값 설정 후 값을 변경할 수 없다. 그렇다면 클랫와 메소드에 final을 붙이면 어떤 효과가 일어날까?

final 클래스와 final메소드는 상속과 관련이 있다.

final 클래스

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

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

public final class Member{
}

protected 접근 제한자

protected는 상속과 관련이 있고, public과 default의 중간쯤에 해당하는 접근 제한을 한다.

기본적으로 같은 패키지 안에 쓸 수 있고, 다른 패키지라도 자식이라면 사용할 수 있게 해준다.

중요한 점은 다른 패키지에서  상속을 통해서만 사용이 가능하고, 직접 객체를 생성해서 사용하는 것은 불가하다.

접근 제한자 제한 대상 제한 범위
protected 필드, 생성자, 메소드  같은 패키지이거나, 자식 객체만 사용 가능

⭐️⭐️⭐️⭐️⭐️ 타입 변환

타입 변환이란 타입을 다른 타입으로 변환하는 것을 말한다. 기본 타입의 변환과 마찬가지로 클래스의 타입변환도 있는데, 클래스의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생한다.

자동 타입 변환

자동 타입 변환은 의미대로 자동적으로 타입변환이 일어나는 것을 말한다. 

부모타입 변수를 선언하고 자식 타입의 객체를 대입할 수 있다. 부모가 넓은 범위이고 자식은 좁은 범위이다. 

Cat cat = new Cat(); //자식 객체
Animal animal = cat; //자식 객체를 부모 타입에 넣을 수 있다.
cat == animal //true , 같은 cat 객체를 참조하게 된다.
// ==는 번지가 같은지, 같은 객체를 참조하는지를 비교하는 연산자이다.

바로 앞의 부모가 아니더라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있다. 

부모타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다.

비록 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로 한정된다.

그러나 자식 클래스에서 오버라이딩된 메소드가 있다면 부모 메소드 대신 오버라이딩된 메소드가 호출된다. 이것은 다형성과 관련있기 때문에 잘 알아두어야 한다. 

 

child 타입의 객체를 만들어서 변수로 선언하고 , child 변수 타입을 Parent 타입으로 타입 변환을 시킨다.  이때 parent 변수로 사용할 수 있는 메소드는 method1, method2 만 사용이 가능하다.

위와 같은 경우 왜 child 변수를 타입변환 시켰을 때 method3을 사용할 수 없을까? 

parent 변수의 타입은 Parent 클래스 타입이므로 Parent에 선언된 메소드만 사용할 수 있다.

하지만 parent.method2() 는 Child (자식클래스)에 오버라이딩 된 메소드이다. 이유는 child가 참조가는 번지가 결국 parent 변수에 저장이 되기 때문에 parent가 참조하는 객체는 여전히 child객체이다. 따라서 재정의 한 메소드가 실행된다. 

강제 타입 변환

자식 타입은 부모 타입으로 자동 변환되지만, 반대로 부모타입은 자식 타입으로 자동 변환되지 않는다. 

대신 캐스팅 연산자로 강제 타입 변환을 할 수 있다.

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

그렇다고 해서 부모 타입 객체를 자식타입으로 무조건 강제 변환할 수 있는 것은 아니다.

자식 객체가 부모 타입으로 자동 변환 된 후 다시 자식 타입으로 변환할 때 강제 타입 변환을 사용할 수 있다.

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

자식 객체가 부모 타입으로 자동 변환하면 부모 타입에 선언된 필드와 메소드만 사용이 가능하는 제약 사항이 따른다.

만약 자식 타입에 선언된 필드와 메소드를 꼭 사용해야한다면 강제 타입 변환을 해서 다시 자식 타입으로 변환해야한다.

하지만 주의할 점은 만들어 질 때의 객체와 다른 객체로 복원할 수는 없다. 

만약 child2 로 만들어져 부모타입으로 자동변환된 객체를 child로 강제 변환할 수 없다. 무조건 만들어질 때의 객체로만 변환이 가능하다.

다형성 (자동 타입 변환 + 메소드 오버라이딩)

다형성이란 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말한다.

프로그램을 구성하는 객체를 바꾸면 프로그램의 실행 성능이 다르게 나올 수 있다.

 

1) 자동 타입 변환 이해하기

쉽게 예를 들어보자. 자동차를 설계 할 때 Tire 타입으로 타이어를 설계를 했다.

그리고 자동차 객체를 만든 후 타이어 자리에 장착(대입) 을 하는 객체는 꼭 Tire 타입이 아니어도 된다. 

자식 객체를 만들어서 대입해도 된다. 타이어 자리에 한국타이어나, 금오타이어가 들어갈 때의 자동차의 성능이 달라질 수 있다. 

 

2) 메소드 오버라이딩 이해하기

객체 사용 방법이 동일하다는 것을 동일한 메소드를 가지고 있다는 뜻이다. 

한국 타이어와 금오 타이어는 모두 타이머를 상속하고 있다. 이는 부모의 메소드를 동일하게 가지고 있다고 말할 수 있다.

만약 한국 타이어와 금오 타이어가 부모 메소드를 오버라이딩 하고 있다면, 오버라이딩된 내용은 다르기 때문에 실행 결과가 다르게 나온다.

이것이 바로 다형성이다. 

가지고 있다는 뜻이다. 

면접 질문 ? 다형성을 구현하기 위해서는 어떤 기술들이 필요합니까 ? 

public class Tire{
	//method
    public void roll(){
    	System.out.println("회전 합니다");
    }
}
public class HanTire extends Tire{
	//Override method
    
    @Override 
    public void roll(){
    	System.out.println("한국 타이어 회전 합니다");
    }
}
public class kumhoTire extends Tire{
	//Override method
    
    @Override 
    public void roll(){
    	System.out.println("금호 타이어 회전 합니다");
    }
}
public class Car{
	//필드 선언
    public Tire tire;
    
    //method
    public void run(){
    	//tire 필드에 대입된 객체의 roll() 호출
        tire.roll();
	}

}
public class CarExample{
	public static void main(String[] args){
    	//car Object Create
        Car myCar = new Car();
        
        //tire 객체 장착
        mycar.tire = new Tire();
        mycar.run();
        
        //HanTire 객체 장착
        mycar.tire = new HanTire();
        mycar.run();
        
        //KumhoTire 객체 장착
        mycar.tire = new KumhoTire();
        mycar.run();        
    }
}

실행 결과 : 

회전 합니다.

한국 타이어 회전 합니다

금호 타이어 회전 합니다

매개변수 다형성

다형성은 필드보다는 메소드, 생성자를 호출할 때 많이 발생한다. 메소드 클래스 타입의 매개변수를 가지고 있을 경우, 호출할 때 동일한 타입의 객체를 제공하는 것이 정석이지만 자식 객체를 제공할 수 도 있다. 여기서 다형성이 발생한다.

즉, 매개변수로 들어오는 객체는 자식 객체가 들어올 수 있고(타입변환),  메소드 재정의를 하고있다면 재정의된 메소드가 실행이 된다.

public class Driver {
	public void drive(Vehicle vehicle){
    	vehicle.run();
    }
}

일반적으로 drive() 메소드를 호출하면 다음과 같다. 

Driver driver = new Driver();
Vehicle vehicle = new Vehicle();
driver.drive(vehicle);

그러나 매개값으로 자동 타입 변환으로 인해 자식 객체도 제공할 수 있다.

자식 객체가 run() 메소드를 재정의 하고 있다면 재정의된 메소드가 호출된다. 

따라서 어떤 자식 객체가 제공되느냐에 따라서 drive()의 실행 결과는 달라진다. 이것이 매개변수의 다형성이다. 

객체 타입 확인

매개변수의 다형성이 실제로 어떤 객체가 매개값으로 제공되었는 지 확인하는 방법이 있다.

꼭 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인하고자 할 때, instanceof 연산자를 사용할 수 있다.

boolean result = 객체 instanceof 타입; 
//앞의 객체가 뒤에 타입으로 만들어 졌는가?

v 변수가 참조하는 객체는 버스 타입으로 만든 객체인가 ? 결과 : true

public void method(Parent parent){
	if(parent instanceof Child){ //parent 매개변수가 참조하는 객체가 Child인지 조사
    	Child child = (Child)parent; 
    }
}

강제 타입 변환 하기 전에 매개값의 타입을 확인하여 강제 타입 변환이 되는지 확인한다. 

강제 타입 변환을 하는 이유는 Child 객체의 모든 멤버(필드, 메소드)에 접근하기 위해서이다. 

추상 클래스

추상은 사전적인 의미로 실체 간의 공통되는 특성을 추출한 것을 말한다. 

상속과 헷갈릴 수 있지만 다르다.  

상속은 이미 부모가 있고, 자식이 부모를 지정해서 부모가 가지고 있는 것을 상속받아서 추가적으로 작성한 뒤 빠르게 자식 클래스를 만들 목적이다.

하지만 추상은  상속을 고려하지 않고 만들다 보니 공통된 부분이 있어, 공통된 부분을 모아서 부모를 만들고 상속받는것이다. 

공통된 부분만 모아서 클래스를 만드는것은 객체를 만들어서 사용한다고 할 수는 없다. 

즉, 클래스들의 공통적인 필드나 메소드를 추출해서 자식에게 공통된 것을 전달하는 목적으로 사용하는 클래스를 추상클래스라고 한다. 

추상 클래스 선언

클래스 언언에서 abstract 키워드를 붙이면 추상클래스 선언이 된다.

추상 클래스는 new 연산자를 사용해서 객체를 직접 생성할 수 없고, 상속을 통해 자식 클래스만 만들 수 있다.

public abstract class 클래스명{
	//필드
    //생서자
    //메소드
}

예제

package sec08.exam01_abstract_class;

public abstract class Phone {
	// Field
	public String owner;
	
	// Constructor
	public Phone(String owner) { this.owner = owner; }
	
	// Method
	public void turnOn()	{ System.out.println("폰 전원을 켭니다."); }
	public void turnOff()	{ System.out.println("폰 전원을 끕니다."); }
}
package sec08.exam01_abstract_class;

public class SmartPhone extends Phone {
	//생성자 선언
	public SmartPhone(String owner) { 
    	super(owner);  //부모의 생성자 선언
    }
	//메소드 선언
	public void internetSearch() { System.out.println("ÀÎÅÍ³Ý °Ë»öÀ» ÇÕ´Ï´Ù."); }
}
package sec08.exam01_abstract_class;

public class PhoneExample {
	public static void main(String[] args) {
		// abstract class 이므로 실행 클래스에서 직접적으로 생성할 수 없다.
		//Phone phone = new Phone("홍길동"); X
		
		SmartPhone smartPhone = new SmartPhone("홍길동");
		smartPhone.turnOn();
		smartPhone.internetSearch();
		smartPhone.turnOff();
	}
}

추상 메소드와 재정의

자식 클래스들이 가지고 있는 공통 메소드를 뽑아내서 추상 클래스로 작성할 때, 메소드 선언부(리턴타입, 메소드명, 매개변수) 만 동일하고 실행 내용은 자식 클래스마다 달라야 하는 경우가 많다.

추상 메소드 sound() 는 메소드는 똑같지만, 내용을 결정할 수 없다.

abstract 리턴타입 메소드명(매개변수, ...);
public abstract class Animal {
	abstract void sound();
}

추상 메소드는 abstract 키워드가 붙고, 메소드 실행 내용인 중괄호가 없다.

추상 메소드는 자식 클래스의 공통 메소드라는 것만 정의할 뿐, 실행 내용을 가지지 않는다.  따라서 추상 메소드는 자식 클래스에서 반드시 재정의(오버라이딩)해서 실행 내용을 채워야 한다. 

package sec08.exam02_abstract_method;

public abstract class Animal {
	public String kind;
	
	public void breathe() { System.out.println("숨을 쉽니다."); }
	
	// Abstract Method는 중괄호({}) 없이 작성하며 세미콜론(;)을 붙인다.
	public abstract void sound();
}
package sec08.exam02_abstract_method;

public class Cat extends Animal {
	public Cat() { this.kind = "포유류"; }
	
	// Abstract Method는 반드시 하위 클래스에서 재정의(Override) 해야 한다.
	@Override
	public void sound() { System.out.println("야옹"); }
}
package sec08.exam02_abstract_method;

public class Dog extends Animal {
	public Dog() { this.kind = "포유류"; }
	
	// Abstract Method는 반드시 하위 클래스에서 재정의(Override) 해야 한다.
	@Override
	public void sound() { System.out.println("멍멍"); }
}
package sec08.exam02_abstract_method;

public class AnimalExample {
	public static void main(String[] args) {
		Dog dog = new Dog();
		Cat cat = new Cat();
		
		dog.sound();
		cat.sound();
		
		System.out.println("----------------");
		
		Animal animal = null;
		
		animal = new Dog();
		animal.sound();
		
		animal = new Cat();
		animal.sound();
		
		System.out.println("----------------");
		
		animalSound(new Dog());
		animalSound(new Cat());
	}
	
	public static void animalSound(Animal animal) { //Animal은 추상클래스 이므로 올 수 없다.
		animal.sound();
	}
}