다형성 적용하기

다형성을 이용하면 다양한 타입을 하나의 타입으로 묶어서 코드를 더 간결하게 만들 수 있다.

 

  • 다형성 적용 전의 코드
public static void main(String[] args) {
    Dog dog = new Dog();
    Cat cat = new Cat();
    Cow cow = new Cow();

    System.out.println("동물 소리 테스트 시작"); dog.sound();
    System.out.println("동물 소리 테스트 종료");
    System.out.println("동물 소리 테스트 시작"); cat.sound();
    System.out.println("동물 소리 테스트 종료");
    System.out.println("동물 소리 테스트 시작"); cow.sound();
    System.out.println("동물 소리 테스트 종료");
}

 

  • 다형성 적용 후 코드
public static void main(String[] args) {
    Dog dog = new Dog();
    Cat cat = new Cat();
    Cow cow = new Cow();
    
    soundAnimal(dog);
    soundAnimal(cat);
    soundAnimal()cow;
}

//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(Animal animal) {
    System.out.println("동물 소리 테스트 시작"); animal.sound();
    System.out.println("동물 소리 테스트 종료");
}

Dog, Cat, Cow를 Animal의 자식 클래스로 만듦으로써 모두 Animal 타입으로 업캐스팅이 가능해졌고, soundAnimal(Animal animal)로 간단하게 표현할 수 있게 되었다.

 

 

  • 리팩토링
public static void main(String[] args) {
    Animal[] animalArr = {new Dog(), new Cat(), new Caw()};
    for (Animal animal : animalArr) {
    soundAnimal(animal);
    }
}

//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(Animal animal) {
    System.out.println("동물 소리 테스트 시작"); animal.sound();
    System.out.println("동물 소리 테스트 종료");
}

 

새로운 기능이 추가되었을 때 변하는 부분을 최소화하는 것이 잘 작성된 코드이다. 그러기 위해선 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하자.

 

남은 문제

다형성을 적용해 위 코드를 간결하게 만들었지만 두 가지 문제가 남아있다.

  1. Animal 클래스 인스턴스를 생성할 수 있는 문제
  2. Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성

 

1. Animal 클래스 인스턴스 생성할 수 있는 문제

개, 고양이, 소가 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제 존재하는 것은 이상하게 느껴진다. 실수로 new Animal()을 사용해 Animal 인스턴스를 만들면 제대로 수행하지 않는 인스턴스가 생성될 수 있다.

 

2. Animal의 하위 클래스에서 메서드 오버라이딩을 하지 않을 가능성

Animal을 상속한 하위 클래스에서 깜빡하고 메서드를 오버라이딩하지 않는다면 의도한 결과가 나오지 않을 수 있다. 예시로 pig.sound()를 호출하고 “꿀꿀”을 의도했는데, pig에 sound()가 없어서 Animal의 sound()가 호출이 될 수 있다.

이 두가지 문제를 해결하기 위해서는 추상 클래스와 추상 메서드를 사용하면된다.

 

추상 클래스

추상 클래스는 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스를 의미한다. 추상 클래스는 상속을 목적으로 부모 클래스 역할을 하는 인스턴스를 생성할 수 없는 클래스이다.

추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.

 

특징

abstract class AbstractAnimal {...}
  • class 앞에 abstract
  • 인스턴스 생성 불가능
  • 하나 이상의 추상 메서드 가지고 있음

 

추상 메서드

추상 메서드는 부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야하는 메서드이다.

  • 특징
    • abstract 키워드가 붙여져 있다
    • 메서드 바디가 없다
    • 상속받는 자식 클래스가 무조건 오버라이딩해서 사용해야 한다
    • 추상 클래스가 추상 클래스를 상속받으면 추상 메서드 오버라이딩 안 해도 됨

 

정리

  • 추상 클래스로 Animal 인스턴스 생성 문제를 근본적으로 방지해준다
  • 추상 메서드로 부모 메서드를 오버라이딩 하지 못할 문제를 방지해준다

 

 

순수 추상 클래스

순수 추상 클래스는 추상 메서드로만 이루어진 추상 클래스를 의미한다.

  • 특징
    • 인스턴스 생성 불가
    • 상속 시 자식은 모든 메서드를 오버라이딩 해야한다
    • 주로 다형성을 위해 사용된다

순수 추상 클래스를 상속하면 모든 추상 메서드를 오버라이딩해야하기 때문에 어떤 규격을 지켜서 구현해야 하는것처럼 느껴진다. 이런 순수 추상클래스를 자바가 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공한다.

 

 

인터페이스

인터페이스는 앞서 설명한 순수 추상 클래스와 동일하지만 약간의 편의 기능이 추가 되었다.

  • 특징
    • 모든 인터페이스 메서드는 public, abstract 이다
    • public abstract는 생략이 권장된다
    • 다중 구현(다중 상속)을 지원한다
    • public static final로 상수 선언한다
  • 인터페이스를 사용해야 하는 이유
    • 제약 : 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이승의 메서드를 반드시 구현하라는 규약(제약)을 주는 것이다. 순수 추상 클래스만 사용한다면 구현 함수를 추가할 수 있는 문제가 발생할 수 있는데, 인터페이스를 사용하면 이 문제를 사전에 원천 차단할 수 있다.
    • 다중 구현 : 상속은 다중 상속이 불가능한 반면에 인터페이스에는 추상 메서드로만 구성되어 있기 때문에 다이아몬드 문제가 발생하지 않아 다중 구현이 가능하다.

 

 

클래스, 추상 클래스, 인터페이스는 모두 똑같다

클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 동일하다. 모두 .java로 정의하고, .class로 다뤄진다.

 

 

Reference

인프런 '김영한의 실전 자바 - 기본편'

'Dev Language > Java' 카테고리의 다른 글

[자바/JDBC] 스프링-DB 1편 1. JDBC의 이해  (0) 2024.01.30
[자바/기본] 12. 다형성과 설계  (0) 2024.01.17
[자바/기본] 10. 다형성1  (0) 2024.01.17
[자바/기본] 9. 상속  (0) 2024.01.16
[자바/기본] 8. final  (0) 2024.01.16

다형성은 객체지향 프로그래밍의 꽃!

 

다형성(Polymorphism)이란?

다형성은 이름 그대로 다양한 형태, 여러 형태를 의미한다.

프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 의미한다.

보통 하나의 객체는 하나의 타입으로 고정되어 있는데, 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다.

 

  • 다형성을 이해하기 위해 알아야할 개념
    • 다형적 참조 : 부모는 자식을 품을 수 있다.
    • 메서드 오버라이딩

 

 

다형적 참조 : 부모는 자식을 품을 수 있다

// Parent가 Child이 부모 클래스일 때

// 가능 
// 부모 타입의 변수가 자식 인스턴스를 참조
Parent poly = new Child(); 

// 불가능, 컴파일 오류 발생
Child poly = new Parent();

위 코드처럼 Child 인스턴스를 Parent 타입으로 설정이 가능하고, poly.parentMethod() 실행시 아래와 같은 과정으로 함수가 호출된다.

 

 

자바에서 부모 타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이처럼 다양한 형태를 참조할 수 있다고 해서 다형적 참조라고 한다.

 

다형적 참조의 한계

Parent poly = new Child(); 인 상태에서 poly.childMethod()를 호출하면 컴파일에러가 발생한다. 왜냐하면 poly의 타입은 Parent이고, 여기에 찾는 메서드가 없다면 Parent의 부모를 탐색한다(상속 관계는 부모로만 찾아서 올라갈 수 있기 때문). 하지만 childMethod()는 Parent의 자식 클래스에 있는 메서드이므로 부모에서 자식으로 함수를 호출할 수 없다. 따라서 poly.childMethod()를 찾지 못해 컴파일 에러가 발생한다.

 

 

캐스팅

  • 업캐스팅 : 자식 타입 → 부모 타입 으로 형변환
  • 다운캐스팅 : 부모 타입 → 자식 타입 으로 형변환
((Child) poly).childMethod(); // 다운캐스팅을 통해 부모타입을 자식타입으로 변환 후 기능 호출
((Child) 참조값).childMethod(); // 참조값을 읽은 다음 자식 타입으로 다운캐스팅

이때 poly의 타입이 변하는 것이 아니다. poly의 타입은 Parent로 유지되고, poly의 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 된다.

 

 

업캐스팅은 생략할 수 있는 반면, 다운캐스팅은 생략할 수 없다.

이 개념은 잘 알아둬야 한다.

  • 다운캐스팅이 위험한 이유

이유는 다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있기 때문이다.

Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 다운 캐스팅 -> ClassCaseException 발생
child2.childMethod(); // 실행 불가

위 코드를 실행하면 런타임 오류인 ClassCastException이 발생한다. 이 예외가 발생하는 이유는 다음과 같다.

parent2 변수에 Parent 인스턴스의 참조값을 저장했기 때문에 해당 위치(참조)에는 Child 인스턴스가 없고, Parent 인스턴스만 있을 뿐이다. 이때 Child로 다운캐스팅 후 Child의 메서드를 호출한다고해도 Child 인스턴스 자체가 없기 때문에 ClassCastException(사용할 수 없는 타입으로 다운캐스팅할 때 발생) 예외가 발생한다.

 

→ 다운 캐스팅의 경우, 부모 타입은 모두 함께 생성되지만 자식타입의 인스턴스는 생성되지 않기 때문에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있다.

 

  • 업캐스팅이 안전한 이유

업캐스팅의 경우, 이런 문제가 절대로 발생하지 않는다. 왜냐하면 객체 생성시 해당 타입의 상위 부모 타입이 모두 함께 생성되기 때문이다. 따라서 메모리 상에 자식 인스턴스 포함 그의 모든 부모 인스턴스가 모두 존재하기 때문에 항상 안전해야 한다. 따라서 업캐스팅을 생략할 수 있다.

 

따라서, 웬만하면 다운캐스팅은 사용하지 말자!

 

instanceof

instanceof는 참조형 변수가 참조하는 대상이 다양할 때, 어떤 인스턴스를 참고하는지 확인하기 위한 키워드이다. 즉, 인스턴스의 타입을 확인할 때 사용한다.

// value 변수의 타입이 B 타입 혹은 B의 자식 타입 -> true
// B b = value(new A())가 성립하면 true
value instanceof B 

// 예시
new Parent() instanceof Parent // true
new Child instanceof Parent // true

 

 

메서드 오버라이딩

메서드 오버라이딩은 기존 기능을 덮어 새로운 기능으로 재정의한다는 뜻이다. 메서드 오버라이딩에서 꼭 기억해야하는 것은 다음과 같다.

오버라이딩 된 메서드가 항상 우선권을 가진다!

즉, 더 하위 자식에서 오버라이딩 된 메서드가 우선권을 갖는 것이다.

 

 

위 그림에서 처럼 Parent타입으로 method()를 호출해도, 그 자식 타입인 Child에서 method()를 오버라이딩 했다면 Child의 method()를 호출한다.

 

정리

다형성에서 기억해야할 것은 두가지다. 다형적 참조와 메서드 오버라이딩!

  • 다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
  • 메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의. 오버라이딩된 메서드가 우선권을 가진다.

 

 

Reference

인프런 '김영한의 실전 자바 - 기본편'

'Dev Language > Java' 카테고리의 다른 글

[자바/기본] 12. 다형성과 설계  (0) 2024.01.17
[자바/기본] 11. 다형성2  (0) 2024.01.17
[자바/기본] 9. 상속  (0) 2024.01.16
[자바/기본] 8. final  (0) 2024.01.16
[자바/기본] 7. 자바 메모리 구조와 final  (0) 2024.01.16

상속 관계

상속은 객체 지향 프로그래밍의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용 할 수 있게 해준다. 하나의 대상만 extends 키워드로 상속받을 수 있다.

  • 부모 클래스(슈퍼 클래스)
    • 상속으로 자신의 필드와 메서드 코드를 제공하는 클래스
    • 자식 클래스를 정보를 모르고, 접근이 불가능하다
  • 자식 클래스(서브 클래스)
    • 부모 클래스로부터 필드와 메서드를 상속받는 클래스
    • 부모 클래스 정보를 알고 있고, 접근이 가능하다

 

다중 상속

 

다중 상속이란 하나의 자식 클래스가 두 개 이상의 부모 클래스를 가질 수 있는 것을 의미한다. 하지만 자바에서 다중 상속은 다이아몬드 문제과 클래스 계층 구조이 복잡화? 때문에 불가능하다.

다이아몬드 문제는 같은 메서드 명을 가진 부모메서드 명이 중복될 때 어떤 것을 사용해야 할지 모르는 문제를 의미한다. 위에서 AirplaneCar는 Airplane의 move()를 오버라이드 해야할지, Car의 move()를 오버라이드 해야할지 정할 수 없다. 따라서 자바에서 자식 클래스는 하나의 부모 클래스를 가질 수밖에 없다.

 

상속과 메모리 구조

Car 클래스를 상속받는 ElectricCar가 있는 상황에서 메모리 구조는 다음과 같다.

 

new ElectricCar()를 호출하면 ElectricCar 뿐만 아니라 상속 관계에 있는 Car까지 함께 포함해서 인스턴스를 생성한다. 참조값 x001에 Car, ElectricCar 두가지 클래스 정보가 공존하고 있다.

→ 상속 관계를 사용하면 하나의 참조값에 자식/부모 클래스가 함께 생성되고, 공간도 각각 구분된다.

 

electricCar.charge() 호출

electricCar.charge()를 호출하면 electricCar는 ElectricCar의 인스턴스이므로, ElectricCar의 charge()를 선택해 호출한다.

이때 메서드는 호출하는 변수의 타입(클래스)을 기준으로 선택하는 것을 알 수 있다.

 

electricCar.move() 호출

electricCar.move()를 호출시 move()를 찾는 과정은 다음과 같다.

 

 

x001로 이동 → 호출 타입인 ElectricCar에서 move() 찾음 (없음) → 부모 클래스인 Car에서 move() 찾음 (있음) → Car의 move() 호출

 

핵심

  • 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식 인스턴스가 모두 생성된다.
  • 상속 관계의 객체를 호출할 때, 호출자의 타입(변수의 타입)으로 대상 타입을 찾는다.
    • ex. ElectricCar electricCar = new ElectricCar(); 에서 변수 electricCar의 타입인 ElectricCar가 타겟된다
  • 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다.
    • 기능을 찾지 못하면 컴파일 오류가 발생한다.

 

상속과 메서드 오버라이딩

메서드 오버라이딩(Method Overriding)은 부모에게서 상속받은 기능을 자식이 재정의 하는 것을 의미한다.

  • @Override
    • 상위 클래스의 메서드를 오버라이드하는 것을 나타내는 애노테이션(프로그램이 읽을 수 있는 특별한 주석)이다
    • 실수로 오버라이딩을 못하는 경우를 방지하고 코드의 명확성을 보장할 수 있다
    • 오버라이딩 규칙(메서드명, 오버라이딩 여부)을 지키지 않으면 컴파일 에러가 발생한다

 

메서드 오버라이딩 조건

  • 메서드 시그니처 : 메서드 시그니처가 동일해야 한다(이름, 매개변수 타입, 순서, 개수)
  • 반환 타입 : 반환 타입이 같아야 한다. 반환 타입이 하위 타입일 수 있다.
  • 접근 제어자 : 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다
  • 예외 : 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없다.
  • static, final, private : 키워드가 붙은 메서드는 오버라이딩 될 수 없다
  • 생성자 오버라이딩 : 생성자는 오버라이딩 할 수 없다

→ @Override 붙이고, 컴파일 에러 발생하지 않게만 작성하자!

 

super

super - 부모클래스에 대한 참조

부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어있으면, 자식 클래스의 필드와 메서드가 사용되기 때문에 자식에서 부모의 필드나 메서드를 호출할 수 없다. 이때 super 키워드를 사용하면 부모를 참조할 수 있다. 즉, super를 사용하면 부모의 필드, 메서드에 접근 가능하다.

 

super - 생성자

상속 관계의 인스턴스를 생성하면 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어진다. Child를 만들면 부모인 Parent까지 함께 만들어지는 것이다. 따라서 자식, 부모 각각의 생성자도 모두 호출되어야 한다.

규칙 : 상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다

  • 상속 관계의 생성자 호출은 결과적으로 부모 → 자식 순서로 실행된다. 부모 데이터를 먼저 초기화한 후 자식 데이터를 초기화한다.
  • 상속 관계에서 부모의 생성자를 호출할 때는 super(…)를 사용하면 된다. 이때 super는 생성자 첫줄에 위치해야한다(안그럼 컴파일 에러 난다).

 

 

Reference

인프런 '김영한의 실전 자바 - 기본편'

'Dev Language > Java' 카테고리의 다른 글

[자바/기본] 11. 다형성2  (0) 2024.01.17
[자바/기본] 10. 다형성1  (0) 2024.01.17
[자바/기본] 8. final  (0) 2024.01.16
[자바/기본] 7. 자바 메모리 구조와 final  (0) 2024.01.16
[자바/기본] 6. 접근 제어자  (0) 2024.01.16

 

final

final 키워드가 붙으면 값을 못 바꾼다고 생각하면 된다. 그래서 특정 변수의 값을 할당한 이후에 변경하지 않아야 할 때 final을 사용하면 된다.

  • final 지역변수
    • 지역변수에 final을 설정하면 최초 한 번만 할당할 수 있다.
    • 이후 값을 변경할 수 없다. 변경을 시도할 때 컴파일 에러가 발생한다
    • 매개변수에 final을 설정하면 메서드 내부에서 매개변수의 값을 변경할 수 없다
  • static final - 상수
    • 상수를 만들 때 사용한다
    • static final 변수명은 대문자에 _(언더 스코어)의 형태로 작성해야 한다. CONSTANT_VALUE **처럼 말이다
    • 상수는 공용으로 사용되므로 주로 public을 붙여 public static final 로 많이 사용한다.
    • 상수는 런타임에 변경할 수 없다. 그래서 변경하려면 프로그램 종료 후 코드를 변경한 다음 다시 실행해야 변경사항이 반영된다.
    • 중앙에서 값을 하나로 관리할 수 있다는 장점이 있다

 

final 대신 static final을 사용해야 하는 이유

 

final value = 10; 처럼 필드를 설정한다면 매 인스턴스마다 값이 10이 할당될 것이다. 그런데 변수가 final 이므로 변경도 못하는데 매 인스턴스마다 value = 10 이 할당되어 있으니 결국 메모리 낭비가 될 것이다. 따라서 final에 static 을 붙여서 공용으로 사용하는 값(MY_VALUE = 10)을 만들어 모든 인스턴스가 이 하나를 가지고 사용하면 메모리 낭비가 줄어들 것이다. 따라서 상수를 만들려면 웬만해선 static final로 만들자!

 

final과 상속/오버라이딩

final을 사용하면 클래스는 상속이 불가능하고, 메서드는 오버라이딩이 불가능하다.

 

 

Reference

인프런 '김영한의 실전 자바 - 기본편'

'Dev Language > Java' 카테고리의 다른 글

[자바/기본] 10. 다형성1  (0) 2024.01.17
[자바/기본] 9. 상속  (0) 2024.01.16
[자바/기본] 7. 자바 메모리 구조와 final  (0) 2024.01.16
[자바/기본] 6. 접근 제어자  (0) 2024.01.16
[자바/기본] 5. 패키지  (0) 2024.01.04

+ Recent posts