[Java] 5. 상속 & 다형성

Kyunghwan Ko·2022년 9월 26일
0

Java

목록 보기
8/14

01. 상속(inheritance)

💡 상속을 통해 유지 보수가 쉽고, 중복을 방지하며, 프로그램의 수정 및 추가가 유연한 프로그램을 만들 수 있다.

  • 사전적으로 상속은 친족 관계에서 한 사람이 다른 사람에게 재산에 관한 권리와 의무의 일체를 이어주고, 이어받는 것이다.
  • 실생활에서는 부모님이 자식들에게 재산을 상속한다.
  • 프로그램에서는 클래스 간에 상속이 일어난다. B 클래스가 A 클래스를 상속받으면 B 클래스는 A 클래스의 필드와 메서드를 사용할 수 있다.

클래스의 상속

  • 상속을 간단히 표현하면 위와 같다.
  • 상속하는 클래스는 "상위 클래스”(=부모 클래스)라고 부른다. 상속받는 클래스는 “하위 클래스”(=자식 클래스)라고 부른다.
  • 클래스의 관계는 동물 클래스(부모) → 사자 클래스(자식), 개 클래스(자식), 고양이 클래스(자식) 와 같이 부모 클래스추상적이고 자식 클래스구체적이어야 한다.
  • 자바에서 상속을 할 때는 예약어 “extends”를 활용한다.
  • 상속을 받는 자식 클래스 뒤에 extends 를 사용하고 부모 클래스를 적어주면 된다. 예를 들어, 클래스 A를 클래스 B가 상속받을 때 그 코드는 다음과 같다.

class 자식 클래스명 extends 부모 클래스명 { ... }

class A {
	...
}

class B extends A {
	...
}

클래스 상속의 예제

💡 클래스 상속의 활용과 이점을 예제를 통해 알아보자.

  • Car, Truck, Bus 클래스를 만들어, 다음 출력문을 출력하는 프로그램을 만든다고 가정하자.
    • 클래스에는 speed를 설정하는 setSpeed(), String을 반환하는 drive() 메서드가 포함되어야 한다.

시속 100Km로 승용차를 운전합니다.
시속 80Km로 화물차를 운전합니다.
시속 60Km로 버스를 운전합니다.

상속을 사용하지 않을 때

  • Car, Truck, Bus 클래스를 각각 만든다면, 클래스마다 speed, type 등을 선언해야 하고 setSpeed(), drive() 메서드 등을 적어야 한다.
  • 이러한 방법을 사용하면 클래스마다 중복되는 코드가 생기며, 공통된 기능을 추가하고 싶을 때 역시 각 클래스마다 기능을 추가해야 하므로 번거롭다.
  • 예제코드
    class Car {
    	
    	private int speed;
    	private String type;
    	
    	public Car () {
    		setSpeed(100);
    		type = "승용차";
    	}
    	
    	public void setSpeed(int speed) {
    		this.speed = speed;
    	}
    	
    	public String drive() {
    		return "시속 " + speed + "Km로 " + type + "를 운전합니다.";
    	}
    	
    }
    
    class Truck {
    	
    	private int speed;
    	private String type;
    	
    	public Truck () {
    		setSpeed(80);
    		type = "화물차";
    	}
    	
    	public void setSpeed(int speed) {
    		this.speed = speed;
    	}
    	
    	public String drive() {
    		return "시속 " + speed + "Km로 " + type + "를 운전합니다.";
    	}
    }
    
    class Bus {
    	
    	private int speed;
    	private String type;
    	
    	public Bus () {
    		setSpeed(60);
    		type = "버스";
    	}
    	
    	public void setSpeed(int speed) {
    		this.speed = speed;
    	}
    	
    	public String drive() {
    		return "시속 " + speed + "Km로 " + type + "를 운전합니다.";
    	}
    }
    
    public class VehicleTest2 {
    
    	public static void main(String[] args) {
    		
    		Car myCar = new Car();
    		Truck myTruck = new Truck();
    		Bus myBus = new Bus();
    		
    		System.out.println(myCar.drive());
    		System.out.println(myTruck.drive());
    		System.out.println(myBus.drive());		
    	}
    }

상속을 사용할 때

  • Vehicle이라는 부모 클래스를 만들어 변수와 메서드를 한번만 작성하고, Car, Truck, Bus 클래스에서 Vehicle의 필드와 메서드를 활용할 수 있다.
  • 예제코드
    class Vehicle{
    	
    	protected int speed;
    	protected String type;
    	
    	public Vehicle () {}
    	
    	public void setSpeed(int speed) {
    		this.speed = speed;
    	}
    	
    	public String drive() {
    		return "시속 " + speed + "Km로 " + type + "를 운전합니다.";
    	}
    }
    
    class Car extends Vehicle {
    	
    	public Car () {
    		speed = 100;
    		type = "승용차";
    	}
    }
    
    class Truck extends Vehicle {
    	public Truck () {
    		speed = 80;
    		type = "화물차";
    	}
    }
    
    class Bus extends Vehicle {
    	public Bus () {
    		speed = 60;
    		type = "버스";
    	}
    }
    
    public class VehicleTest {
    
    	public static void main(String[] args) {
    		
    		Car myCar = new Car();
    		Truck myTruck = new Truck();
    		Bus myBus = new Bus();
    		
    		System.out.println(myCar.drive());
    		System.out.println(myTruck.drive());
    		System.out.println(myBus.drive());		
    	}
    }

상속을 사용하는 이유

💡 상속은 다양한 장점을 갖는다.

  • 부모 클래스의 코드를 재사용할 수 있어서 중복되는 코드를 줄일 수 있다.
  • 이미 검증된 소프트웨어를 재사용할 때 유용하며, 그러한 소프트웨어를 손쉽게 개발, 유지 및 보수하기에 유리하다.
  • 자식 클래스에서 super예약어를 통해 부모 클래스의 필드와 메서드를 쉽게 활용할 수 있다.

상속과 생성자

  • 자식 객체를 위해, 부모 클래스의 생성자 호출이 먼저 완료되어야 한다.
  • 생성자 호출 순서
    • 부모 클래스의 생성자 → 자식 클래스의 생성자
  • 즉, 자식 객체를 호출하면 자동으로 부모 생성자가 먼저 호출이 됨.
  • 자식 생성자로 super(); 를 사용

명시적인 생성자 호출

public class Parent{
	public Parent() { // 기본 생성자: 파라미터가 없는 생성자(NoAurgument Constructor)
		System.out.println("부모 생성자");
	}
}

public class Child extends Parent{

	public Parent(){
		super(); //명시적으로 생성자를 호출함
		System.out.println("부모 생성자");
	}
}

묵시적인 생성자 호출

public class Parent{
	public Parent() {
		System.out.println("부모 생성자");
	}
}

public class Child extends Parent{

	public Child(){
		// super(); 묵시적으로 기본 생성자를 호출함, 컴파일러가 default로 호출
		System.out.println("부모 생성자");
	}
}
  • 오류가 발생하는 경우
    • 기본 생성자가 없는데 사용할 경우.
public class Parent{
	private int age;
    
	public Parent(int age) {
    	this.age = age;
		System.out.println("부모 생성자, 나이: " + this.age);
	}
}

public class Child extends Parent{

	public Child(){
    	/* 컴파일 에러 발생 */
		// super(); defatul로 기본생성자 호출되는대 부모클래스에 기본생성자가 없기 때문
		System.out.println("부모 생성자");
	}
}

따라서 코드를 리팩토링(refactoring)하면 다음과 같습니다.

public class Child extends Parent{

	public Child(){
    	/* 컴파일 에러 해결 */
		super(50);
		System.out.println("부모 생성자");
	}
}

오버로딩 (Overloading)

메서드 오버로딩 (Method Overloading)

함수의 파라미터를 달리해서 함수를 작성하는 것이고
대표적인 예로 System.out.println() 이 있습니다.

→ 즉, 매개변수의 갯수데이터 타입은 다르고, 함수명은 동일하게 메서드를 정의하는 것

class Calculator {
	// 예 1
	int sum(int x, int y){
		return x + y;
	}
	// 예2 2
	double sum(double x, double, y){
		return x + y;	
	}
	// 예 3 → error
	int sum(int x, double y){
		return x + (int)y;
	}
	// 예 4 → error
	double sum(int x, double y){
		return x + y;
	}
}

오버로딩의 조건

  • 메서드의 이름이 같아야 한다.
    • 예1, 예2, 예3, 예4 모두 이름이 같다.
  • 메서드의 매개변수 갯수데이터 타입이 달라야 한다.
  • 메서드의 매개변수 수와 타입이 같고 리턴타입이 다른 경우, 오버로딩이 성립하지 않는다.
    • 예3와 예4는 오버로딩이 성립되지 않는다.

오버라이딩

메서드 오버라이딩(Method Overriding)

💡 자식클래스가 부모클래스의 메소드를 자신의 필요에 맞추어 재정의한다.
단, "함수 바디"만 달리 하여야한다.

class Shape{ public void draw() { System.out.println("Shape"); } }

class Circle extends Shape {
	 @Override
	 public void draw() { System.out.println("Circle을 그립니다."); }
}

class Rectangle extends Shape {
	 @Override
	 public void draw() { System.out.println("Rectangle을 그립니다."); }
}

class Triangle extends Shape {
	 @Override
	 public void draw() { System.out.println("Triangle을 그립니다."); }
}

Shape 클래스를 부모로 가지는 Circle, Rectangle, Triangle 클래스들은 Shape의 draw() 메서드를 활용가능하지만, 위의 표기처럼 같은 이름, 같은 매개변수와 결과 값을 가진 새로운 메소드로 재정의 가능하며 자식을 객체로 불러왔을 때, 오버라이딩된 자식의 메소드로 사용된다.

오버로딩 vs 오버라이딩 차이

02.다형성(Polymorphism)

다형성 = 상속 + 오버라이딩

다형성이란?

  • 하나의 객체가 여러 가지 타입을 가질 수 있는 것.
  • 상위 클래스가 동일한 메시지로 하위 클래스들을 서로 다르게 동작 시키는 객체 지향 원리이다.
  • 다음 예시를 보자

class Shape{
	public void draw() {
		System.out.println("도형을 그립니다.");
	}
}
class Rectangle extends Shape{
	@Override
	public void draw() {
		System.out.println("사각형을 그립니다.");
	}
    public void onlyRecFunc(){
    	System.out.println("오직 사각형클래스의 함수입니다.");
    }
}
class Triangle extends Shape {
	@Override
	public void draw() {
		System.out.println("삼각형을 그립니다.");
	}
}
public class ShapeTest {
	public static void main(String[] args) {
		// 1번
		Shape shape = new Shape();
		shape.draw();

		// 2번
		Rectangle rectangle = new Rectangle();
		rectangle.draw();

		// 3번
		Triangle triangle = new Triangle();
		triangle.draw();
	}
}

이와 같이 상속 관계인 상위 클래스의 함수, 변수를 덮어쓰는 것이 다형성이다.

  • 상위클래스의 함수를 하위 클래스에서 메서드 오버라이딩을 하며, 여러 가지 결과를 만들어낼 수 있다.

다형성의 장점

  1. 유지보수가 쉽다.
    • 여러 객체를 하나의 타입으로 관리가 가능하기 때문에 코드 관리가 편리해 유지보수가 용이하다.
  2. 재사용성이 증가 한다.
    • 객체를 재사용하기 쉬워지기 때문에 개발자의 코드 재사용성이 높아진다.
  3. 안정성이 높아진다.
    • 클래스간의 의존성이 줄어들어 결합도가 낮아지며 확장성이 높아져 안정성이 높아집니다.

다형성의 필수 조건

  1. 상속관계
    • 다형성을 활용하기 위해서는 부모-자식 간 클래스 상속이 필수로 이루어져야 한다.
  2. 오버라이딩 - 메서드 재정의
    • 다형성이 보장되기 위해서는 하위 클래스 메소드가 반드시 재정의 되어 있어야 한다.
  3. 업캐스팅 (upCasting) - 부모클래스의 타입으로 형변환
    • 부모 = 자식;
    • 부모 타입(Shape)으로 자식클래스(Rectangle)를 업캐스팅하여 객체를 생성해야 한다.
    • Shape s = new Rectangle(); // (Shape) new Rectangle();

💡 캐스팅 : 형변환을 의미

사전적 의미

  • 업캐스팅 (UpCasting) : 자손타입의 참조변수를 조상타입의 참조변수로 변환하는 것
  • 다운캐스팅 (DownCasting) : 조상타입의 참조변수를 자손타입의 참조변수로 변환하는 것

일반적 의미

  • 업캐스팅: 다형성을 적용한 것
  • 다운캐스팅: 다형성 적용으로 잃어버린 특성을 복구시키기 위해 원래 상태로 되돌리는 것

💡 다형성 적용으로 잃어버린 특성이란?

앞선 Shape-Rectangle 관계를 표현한 코드에서

컴파일 에러ver - by 다형성적용 즉, 업캐스팅

Shape s = new Rectangle();
/* 컴파일 에러 */
s.onlyRecFunc(); // Rectangle클래스에만 선언된 함수인 onlyRecFunc()호출 불가! 
// -> 다형성 적용으로 인해 Rectangle클래스의 특성잃어버림

컴파일 에러해결ver - by 다운캐스팅

Shape s = new Rectangle();
/* 컴파일 에러해결 */
Reactangle r = (Rectangle) s;
r.onlyRecFunc(); // Rectangle클래스에만 선언된 함수인 onlyRecFunc() 호출 가능!
// -> 다운캐스팅을 통해 잃어버린 Rectangle클래스의 특성 복구 완료!

다형성 구현 방법

1. 상속 클래스 구현

  • 부모 자식 간 상속 클래스를 구현
  • 부모클래스 : Book, 자식클래스 : Novel
  • 기본 생성자 외 파라미터를 필요로하는 생성자를 추가해 생성자를 중복 정의
    package Book;
    
    public class Book {
            private String name;
            private String publisher;
            // 기본생성자
            public Book(){
                this.name = "";
                this.publisher = "";
            }
            // 파라미터 필요로하는 생성자
            public Book(String name, String publisher){
                this.name = name;
                this.publisher = publisher;
            }
            public void print(){
                System.out.println("print : Book");
            };
    }
  1. 메서드 오버라이딩
  • 자식 클래스 Novel에 부모 메서드를 오버라이딩하여 재정의
    class Novel extends Book{
        private String name;
        private String publisher; 
        public Novel(String name, String publisher){
            super(name, publisher); // Book(String name, String publisher) 호출
        }
        @Override
        public void print(){
            System.out.println("print : Novel"); // Body만 변경
        }
    }
  1. 업캐스팅하여 객체 선언
  • 업캐스팅(자식클래스 객체를 부모 클래스로 형변환)하여 객체 선언(부모 = 자식;)

    Book b = new Novel("메타버스 소설", "출판사(IT)"); // (Book) new Novel("메타버스 소설", "출판사(IT)")
  1. 부모 클래스 객체로 자식 메소드 호출
  • 부모클래스(Book)으로 생성된 객체의 멤버 함수를 호출

    Book b = new Novel("소설","소설출판사");
     b.print(); // 출력=> print : Novel

참고 (업캐스팅과 다운 캐스팅 설명)

(좌- 스택 영역, 우-힙 영역)

upCasting 예시

부모 클래스를 참조 변수로 하여 자식 클래스 객체 생성한 것을 가리킬 수 있다.

다음 그림은 업캐스팅 시 스택(좌), 힙(우) 메모리 영역을 보여준다.
참조변수 a 가 가리키고 있는 위치에서 부모 클래스(A)에 할당된 영역만큼 접근할 수 있다.
그러니 그 외의 부분 B에 종속된 함수(printB())를 사용할 수 없는 것이다.

downCasting 예시

  • 자식클래스 참조변수 = 부모클래스 객체 생성 → 컴파일 에러 발생!

데이터 타입이 B 클래스이고, 만약, B의 기능(함수)을 사용하려고 하는데
참조변수인 b가 참조하는 곳에는 클래스 A의 멤버(필드, 메서드) 밖에 없기 때문에 추후 b에서 클래스 B에만 할당된 맴버에 접근하는 것을 미연에 방지하고자 컴파일 에러를 발생시킵니다.

현재 힙 영역에 클래스 A의 맴버만 올라가 있는 이유는
new A(); 했기 때문이다.

  • 명시적 형변환 시 사용 가능
    • 조건 : 자식 클래스(B)의 객체가 힙 영역에 이미 올라와 있을 경우 명시적 형변환으로 다운캐스팅이 가능합니다.
    A a = new B();
    B b = (A) a;

profile
부족한 부분을 인지하는 것부터가 배움의 시작이다.

0개의 댓글