상속
📌 상속의 사전적 정의는 부모가 자식에게 물려주는 행위를 말합니다.
- 객체 지향 프로그램에서도 부모 클래스의 필드와 메서드를 자식 클래스에게 물려줄 수 있습니다.
- 상속을 사용하면 적은 양의 코드로 새로운 클래스를 작성할 수도 있고 공통적인 코드를 관리하여 코드의 추가와 변경이 쉬워질 수도 있습니다.
- 이러한 특성 때문에 상속을 사용하면 코드의 중복이 제거되고 재사용성이 크게 증가하여 생산성과 유지 보수성에 매우 유리해집니다.
= 자바 객체 지향의 중요한 기능 중 하나로, 한번 변수나 메서드 선언하고 그걸 다른 이들이 빌려쓰는 형태로 만들어, 매번 변수, 메서드를 반복해서 선언하는 것을 방지한 방식이다.
- 위의 그림처럼 개발자, 댄서, 가수 모두 인간의 특징인 이름,나이,학습,일등의 특징을 가질 수 밖에 없다. 이 인간으로서의 특징을 개발자, 댄서, 가수에 각각 변수로 선언하면 같은 내용이 3배가 될 뿐이니, 상위 클래스(부모)를 생성하여 부모에서 인간의 특징을 정하고 다른 하위 클래스(자식)에게 정보를 공유하는게 효과적이다.
public class 자식클래스 extends 부모클래스 {
}
- 상속은 위와 같이 선언하게 되는데, 잘 보면 extends라는 키워드가 존재한다.
- 말 그대로 확장이란 의미인데, 상속이 이 확장의 개념과 동일하다
- 부모 클래스에 새로운 필드와 메서드가 추가되면 자식 클래스는 이를 상속받아 사용할 수 있다.
- 자식 클래스에 새로운 필드와 메서드가 추가되어도 부모 클래스는 어떠한 영향도 받지 않는다.
- 따라서 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.
‘부모가 자식보다 큰 사람이니까 부모 클래스도 마찬가지로 자식 클래스 보다 큰 범위겠지?’ 라고 생각하면 망한다. 여기서는 '청출어람'이라는 느낌으로 자식이 부모보다 더 많은 데이터를 가지고 있다. 단 위의 설명처럼 자식은 부모에게 영향을 주지 못하지만 부모는 자식에게 영향을 줄 수 있다.
< 1980년대 일반적인 가정을 생각하면 편하다 >
- 1. 자식이 인터넷 세상에 태어나면서 자연스럽게 부모보다 많은 데이터를 가지게 되었다.
- 2. 보통 부모님이 자식에게 끼치는 영향이 크다.
- 3. 부모의 수보다 자식 수가 보통 많다.
<예제>
public class Car {
String company; // 자동차 회사
private String model; // 자동차 모델
private String color; // 자동차 색상
private double price; // 자동차 가격
double speed; // 자동차 속도 , km/h
char gear = 'P'; // 기어의 상태, P,R,N,D
boolean lights; // 자동차 조명의 상태
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public char changeGear(char type) {
gear = type;
return gear;
}
public boolean onOffLights() {
lights = !lights;
return lights;
}
public void horn() {
System.out.println("빵빵");
}
}
public class SportsCar extends Car{
String engine;
public void booster() {
System.out.println("엔진 " + engine + " 부앙~\n");
}
}
public class Main {
public static void main(String[] args) {
// 부모 클래스 객체에서 자식 클래스 멤버 사용
Car car = new Car();
// car.engine = "Orion"; // 오류
// car.booster(); // 오류
// 자식 클래스 객체 생성
SportsCar sportsCar = new SportsCar();
sportsCar.engine = "Orion";
sportsCar.booster();
// 자식 클래스 객체에서 부모 클래스 멤버 사용
sportsCar.company = "GENESIS";
sportsCar.setModel("GV80");
System.out.println("sportsCar.company = " + sportsCar.company);
System.out.println("sportsCar.getModel() = " + sportsCar.getModel());
System.out.println();
sportsCar.horn();
System.out.println(sportsCar.changeGear('D'));
}
}
클래스 간의 관계
📌 클래스 간의 관계를 분석하여 관계 설정을 해줄 수 있다.
- 상속관계 : is - a (”~은 ~(이)다”)
- 포함관계 : has - a (”~은 ~을(를) 가지고 있다”)
상속관계 | 포함관계 |
|
|
1. 포함(Composition)
포함은 "has-a" 관계를 표현합니다.
한 클래스가 다른 클래스를 속성(멤버 변수)으로 가지는 형태입니다.
특징
- 유연성: 포함된 객체는 독립적이며, 필요에 따라 다른 객체로 교체할 수 있습니다.
- 코드 재사용: 여러 클래스에서 포함된 클래스를 재사용할 수 있습니다.
- 의존성 낮음: 포함된 객체의 내부 구현은 외부 클래스에 영향을 주지 않습니다.
class Circle{
int x;
int y;
int r;
}
----------------------------
class Circle{
Poiont c = new Point();
int r;
}
class Point {
int x;
int y;
}
2. 상속(Inheritance)
상속은 "is-a" 관계를 표현합니다.
하위 클래스(Subclass)가 상위 클래스(Superclass)의 속성과 메서드를 물려받는 구조입니다.
특징
- 코드 재사용: 공통 기능을 상위 클래스에서 정의하고, 하위 클래스에서 상속받아 사용.
- 유연성 낮음: 상속받은 클래스는 상위 클래스와 강한 결합을 가지며, 부모 클래스의 변경이 하위 클래스에 영향을 줌.
- 다형성 지원: 부모 타입의 참조로 자식 객체를 사용할 수 있음.
3. 포함 vs 상속의 차이점
구분 | 포함(Composition) | 상속(Inheritance) |
관계 | "has-a" 관계 | "is-a" 관계 |
결합도 | 낮음 (유연성 높음) | 높음 (부모 클래스에 강하게 의존) |
코드 재사용성 | 객체를 포함하여 재사용 가능 | 공통된 동작을 부모 클래스에서 정의 |
확장성 | 포함된 객체를 교체하여 기능 확장 가능 | 부모 클래스 설계 변경 시 제한적 |
다형성 지원 | 다형성 제공하지 않음 | 다형성(Polymorphism) 지원 |
목적 | 객체의 조립, 독립적인 관계 유지 | 객체의 계층 구조 정의, 공통 기능 상속 |
4. 포함과 상속의 사용 시점
포함(Composition)이 적합한 경우:
- 두 클래스가 명확한 "has-a" 관계일 때.
- 한 클래스가 다른 클래스의 특정 기능만 필요할 때.
- 코드 재사용과 독립성을 유지하면서 기능 확장이 필요할 때.
- 설계 변경 가능성이 높아 유연성이 중요한 경우.
상속(Inheritance)이 적합한 경우:
- 두 클래스가 명확한 "is-a" 관계일 때.
- 하위 클래스가 상위 클래스의 대체 가능성을 가져야 할 때(다형성).
- 공통 기능을 상위 클래스에 정의하고, 하위 클래스에서 추가적으로 확장할 때.
5. 실무에서의 선호
- 포함이 더 자주 사용됩니다.
이유: 유연성, 낮은 결합도, 유지보수 용이성 때문. - 상속은 사용이 간단하지만, 잘못된 설계로 인해 강한 결합이나 불필요한 상속 계층 구조를 초래할 수 있습니다.
따라서, 상속보다는 포함을 우선적으로 고려하고, 정말 필요한 경우에만 상속을 사용합니다.
단일 상속과 다중 상속
📌 Java는 클래스의 다중 상속을 허용하지 않습니다…
- 다중 상속을 허용하면 복잡한 소프트웨어의 기능을 구현할 때 여러 개의 클래스를 상속받아 쉽게 구현할 수 있다는 장점이 있는데.. 왜? 허용하지 않을까요?
- 왜냐하면 다중 상속을 허용하면 클래스 간의 관계가 복잡해지는 문제가 생기기 때문입니다.
- 만약 자식 클래스에서 상속받는 서로 다른 부모 클래스들이 같은 이름의 멤버를 가지고 있다면?
- 자식 클래스에서는 이 멤버를 구별할 수 있는 방법이 없다는 문제가 생깁니다.
= 자식이 부모를 여럿 골라도 되는데(?), 부모 입장에서 자식 2명이 이름이 같으면 구분을 못한다.
final 클래스와 final 메서드
📌 클래스에 final 키워드를 지정하여 선언하면 최종적인 클래스가 됨으로 더 이상 상속할 수 없는 클래스가 됩니다.
public class Car {
public final void horn() {
System.out.println("빵빵");
}
}
...
public class SportsCar extends Car{
public void horn() { // 오류가 발생합니다.
super.horn();
}
}
- final은 마치 채권처럼 모든 것을 처음 상태로 동결한다. 채권이 구매 시의 세금,가격,이자로 모든 변수를 상수로 지정해버리는 것과 같다 = 절대로 무엇도 바꿀 수 없다. 그저 그 모양그대로 가져와서 써야한다
Object
📌 Object는 말 그대로 “객체”를 의미하는 단어이며 보통, Object 클래스를 의미합니다.
- Object 클래스는 Java 내 모든 클래스들의 최상위 부모 클래스입니다.
- 따라서, 모든 클래스는 Object의 메서드를 사용할 수 있습니다.
- 또한 부모 클래스가 없는 자식 클래스는 컴파일러에 의해 자동으로 Object 클래스를 상속받게 됩니다.
- Object 클래스는 프로그래머가 만드는 클래스가 아니라 자체적으로 제공되는 최상위 클래스이다.
오버라이딩
🐳 부모 클래스로부터 상속받은 메서드의 내용을 재정의 하는 것을 오버라이딩이라고 합니다.
- 부모 클래스의 메서드를 그대로 사용 가능하지만 자식 클래스의 상황에 맞게 변경을 해야 하는 경우 오버라이딩을 사용합니다.
- 오버라이딩을 하기 위해서는 아래 조건들을 만족해야 합니다.
- 선언부가 부모 클래스의 메서드와 일치해야 합니다.
- 접근 제어자를 부모 클래스의 메서드 보다 좁은 범위로 변경할 수 없습니다.
- 예외는 부모 클래스의 메서드 보다 많이 선언할 수 없습니다
public class Car {
String company; // 자동차 회사
private String model; // 자동차 모델
private String color; // 자동차 색상
private double price; // 자동차 가격
double speed; // 자동차 속도 , km/h
char gear = 'P'; // 기어의 상태, P,R,N,D
boolean lights; // 자동차 조명의 상태
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public char changeGear(char type) {
gear = type;
return gear;
}
public boolean onOffLights() {
lights = !lights;
return lights;
}
public void horn() {
System.out.println("빵빵");
}
}
public class SportsCar extends Car{
String engine;
public void booster() {
System.out.println("엔진 " + engine + " 부앙~\n");
}
public SportsCar(String engine) {
this.engine = engine;
}
@Override
public double brakePedal() {
speed = 100;
System.out.println("스포츠카에 브레이크란 없다");
return speed;
}
@Override
public void horn() {
booster();
}
}
public class Main {
public static void main(String[] args) {
// 부모 클래스 자동차 객체 생성
Car car = new Car();
car.horn(); // 경적
System.out.println();
// 자식 클래스 스포츠카 객체 생성
SportsCar sportsCar = new SportsCar("Orion");
// 오버라이딩한 brakePedal(), horn() 메서드 호출
sportsCar.brakePedal();
sportsCar.horn();
}
}
- 오버라이딩에서 자식 클래스에서 부모 클래스의 메서드를 바꾼다고 해서 부모 클래스의 메서드가 바뀌는 것은 아닙니다.
- 자식 클래스에서 그 메서드를 새로운 방식으로 재정의하는 것입니다. = "엄마"를 자식이 "착한 엄마"라고 자신이 원하는(필요한) 대로 재해석한다는 말. 부모는 "엄마"라는건 바뀌지 않지만 자식에 한해서는 "착한 엄마"가 됨
- 그래서 아래와 같은 답이 나온다.
빵빵
스포츠카에 브레이크란 없다
엔진 Orion 부앙~
- 이름 비슷하지만 엄연히 오버로드와 오버라이딩은 다른 개념이다.
오버로드 | 같은 메서드 이름을 다른 매개변수를 사용하여 여러 번 정의하는 것입니다. 반환 타입은 동일하거나 달라질 수 있습니다. |
오버라이딩 | 상속 관계에서 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것입니다. 메서드 이름, 매개변수, 반환 타입이 부모 클래스와 정확히 일치해야 합니다. |
super 와 super()
🐳 super는 부모 클래스의 멤버를 참조할 수 있는 키워드입니다.
- 객체 내부 생성자 및 메서드에서 부모 클래스의 멤버에 접근하기 위해 사용될 수 있습니다.
- 자식 클래스 내부에서 선언한 멤버와 부모 클래스에서 상속받은 멤버와 이름이 같을 경우 이를 구분하기 위해 사용됩니다.
public void setCarInfo(String model, String color, double price) {
super.model = model; // model은 부모 필드에 set
super.color = color; // color는 부모 필드에 set
this.price = price; // price는 자식 필드에 set
}
- 말 그대로 자식 클래스에서 부모 클래스의 맴버에 접근하기 위해 필요한 기능
- super = 부모 , this = 만들어질 객체
- model, color는 부모 클래스의 매개변수가 바뀐다. price는 생성될 객체의 price값이 바뀐다.
🐳 super()는 부모 클래스의 생성자를 호출할 수 있는 키워드입니다.
- 객체 내부 생성자 및 메서드에서 해당 객체의 부모 클래스의 생성자를 호출하기 위해 사용될 수 있습니다.
- 자식 클래스의 객체가 생성될 때 부모 클래스들이 모두 합쳐져서 하나의 인스턴스가 생성됩니다.
- 이때 부모 클래스의 멤버들의 초기화 작업이 먼저 수행이 되어야 합니다.
- 따라서 자식 클래스의 생성자에서는 부모 클래스의 생성자가 호출됩니다.
- 또한 부모 클래스의 생성자는 가장 첫 줄에서 호출이 되어야 합니다.
// 자식 클래스 SportsCar 생성자
public SportsCar(String model, String color, double price, String engine) {
// this.engine = engine; // 오류 발생
super(model, color, price);
this.engine = engine;
}
- 자식 클래스 객체를 생성할 때 생성자 매개변수에 매개값을 받아와 super(…)를 사용해 부모 생성자의 매개변수에 매개값을 전달하여 호출하면서 부모 클래스의 멤버를 먼저 초기화합니다.
- 오버로딩된 부모 클래스의 생성자가 없다고 하더라도 부모 클래스의 기본 생성자를 호출해야 합니다.
- 따라서 눈에 보이지는 않지만 컴파일러가 super();를 자식 클래스 생성자 첫 줄에 자동으로 추가해 줍니다.
- 간단히 말해, 자식 클래스에서 객체를 생성하면
- 위 처럼 자식이 부모 클래스의 필드, 메서드를 포함함으로 자식의 객체에도 부모의 필드, 메서드가 포함되어야한다.
- 당연히 객체가 새로 생성되는 만큼 부모 필드도 초기화를 해줘야하는데
- 위의 코드에서 super(model, color, price);로 초기화를 따로 해줬지만, 안써도 자동으로 컴파일러가 초기화 해준다는 말
- 쉽게 말하면, 자식 클래스라는 붕어빵 틀(자식 클래스) 안에 부모 클래스 호두과자 틀(부모 클래스)이 또 있다면 당연히 붕어빵을 만들기 전에 붕어빵 틀만이 아니라 호두과자 틀도 청소해야한다.(초기화)
참조 변수의 타입 변환
자동 타입 변환
🧩 부모 타입 변수 = 자식 타입 객체; 는 자동으로 부모 타입으로 변환이 일어납니다.
- 자식 객체는 부모 객체의 멤버를 상속받기 때문에 부모와 동일하게 취급될 수 있습니다.
- 예를 들어 포유류 클래스를 상속받은 고래 클래스가 있다면 포유류 고래 = 고래 객체; 가 성립될 수 있습니다.
- 왜냐하면 고래 객체는 포유류의 특징인 모유 수유 행위를 가지고 있기 때문입니다.
- 다만 주의할 점은 부모 타입 변수로 자식 객체의 멤버에 접근할 때는 부모 클래스에 선언된 즉, 상속받은 멤버만 접근할 수 있습니다.
class Mammal {
// 포유류는 새끼를 낳고 모유수유를 한다.
public void feeding() {
System.out.println("모유수유를 합니다.");
}
}
class Whale extends Mammal {
// 고래는 포유류 이면서 바다에 살며 수영이 가능하다.
public void swimming() {
System.out.println("수영하다.");
}
@Override
public void feeding() {
System.out.println("고래는 모유수유를 합니다.");
}
}
public class Main {
public static void main(String[] args) {
// 고래는 포유류이기 때문에 포유류 타입으로 변환될 수 있습니다.
Mammal mammal = new Whale();
// 하지만 포유류 전부가 바다에 살고 수영을 할 수 있는 것은 아니기 때문에
// 수영 하다 메서드는 실행 불가
// 즉, 부모 클래스에 swimming이 선언되어있지 않아서 사용 불가능합니다.
// mammal.swimming(); // 오류 발생
// 반대로 모든 포유류가 전부 고래 처럼 수영이 가능한 것이 아니기 때문에 타입변환이 불가능합니다.
// 즉, 부모타입의 객체는 자식타입의 변수로 변환될 수 없습니다.
// Whale whale = new Mammal(); // 오류 발생
mammal.feeding();
}
}
- Mammal mammal = new Whale();은 자식 클래스인 Whale의 객체를 만들었지만, 이를 부모 클래스 타입 변수인 mammal에 할당했다.
- 원래 안될 것 같은 이 행위가 되는 이유는, 자식이 부모의 일부분의 데이터를 공유하기 때문이다.
- 즉 위 처럼 부모 클래스 Mammal이 가지고 있는 메서드나 필드가 아닌 요청은 오류가 나고 나머지는 가능한것
강제 타입 변환
🧩 자식 타입 변수 = (자식 타입) 부모 타입 객체;
- 부모 타입 객체는 자식 타입 변수로 자동으로 타입 변환되지 않습니다.
- 이럴 때는 (자식 타입) 즉, 타입 변환 연산자를 사용하여 강제로 자식 타입으로 변환할 수 있습니다.
class Animal {
void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Woof Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // 부모 타입 변수에 자식 타입 객체 할당
animal.makeSound(); // Animal 클래스의 메서드 호출
// 강제 타입 변환
Dog dog = (Dog) animal; // Animal 타입을 Dog 타입으로 강제 변환
dog.bark(); // Dog 클래스의 메서드 호출
}
}
설명:
- Animal animal = new Dog();에서 자식 객체인 Dog를 부모 타입인 Animal 변수에 할당합니다.
- animal.makeSound();는 부모 클래스인 Animal의 메서드를 호출합니다.
- 그 후, animal 변수는 Dog 객체를 참조하고 있기 때문에, 강제 타입 변환을 통해 Animal 타입 변수 animal을 Dog 타입 변수 dog로 변환합니다.
- 강제 타입 변환 (Dog) animal을 통해 이제 dog 변수는 Dog 클래스에서 정의한 bark() 메서드를 호출할 수 있게 됩니다.
- "Woof Woof!"가 결과로 나옵니다.
- 다만 무조건 강제 타입 변환을 할 수 있는 것은 아닙니다.
- 자식 타입 객체가 부모 타입으로 자동 타입 변환된 후 다시 자식 타입으로 변환될 때만 강제 타입 변환이 가능합니다.
- 부모 타입 변수로는 자식 타입 객체의 고유한 멤버를 사용할 수 없기 때문에 사용이 필요한 경우가 생겼을 때 강제 타입 변환을 사용합니다.
Mammal newMammal = new Mammal();
Whale newWhale = (Whale) newMammal; // ClassCastException 발생
public class Main {
public static void main(String[] args) {
// 자동 타입변환된 부모타입의 변수의 자식 객체
Mammal mammal = new Whale();
mammal.feeding();
// 자식객체 고래의 수영 기능을 사용하고 싶다면
// 다시 자식타입으로 강제 타입변환을 하면된다.
Whale whale = (Whale) mammal;
whale.swimming();
Mammal newMammal = new Mammal();
// Whale newWhale = (Whale) newMammal;
}
}
- Mammal mammal = new Whale();이걸로 자동 타입 변환 한 상태면, Whale whale = (Whale) mammal;로 다시 자식타입으로 강제로 되돌릴 수 있다.
- 되돌리는게 된다는 것이기 때문에, 부모를 자식의 변수에 할당하는 것은 불가능하다.
- 용암을 5칸(자식)짜리 틀을 들고왔는데, 3칸(부모)에 부워버릴 수 없다. 다 넘친ㄷ;;;
- 그러니까 저 말은 3칸을 5칸으로 늘렸던 걸 다시 3칸으로 줄인다는 말이다.
다형성
🧩 다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미합니다.
- 예를 들어 자동차의 핸들을 교체하면 핸들링이 부드러워지고 바퀴를 교체하면 승차감이 좋아집니다.
- 소프트웨어 또한 구성하고 있는 객체를 바꿨을 때 소프트웨어의 실행 성능 및 결과물이 다르게 나올 수 있습니다.
- 일전에 배운 참조 변수 타입 변환을 활용해서 다형성을 구현할 수 있습니다.
public Car(Tire tire) {
this.tire = tire;
}
...
Car car1 = new Car(new KiaTire("KIA"));
Car car2 = new Car(new HankookTire("HANKOOK"));
- 위에서 본것 처럼 객체도 된다. 지금 위 예시는 자식 객체를 선언하고 그 자식 객체를 부모 타입 객체로 다시 재 선언한 코드이다.
Tire getHankookTire() {
return new HankookTire("HANKOOK");
}
Tire getKiaTire() {
return new KiaTire("KIA");
}
...
Tire hankookTire = car1.getHankookTire();
KiaTire kiaTire = (KiaTire) car2.getKiaTire();
- 심지어는 매개변수도 된다... 여기서도 마찬가지로 반환 타입이 부모 타이어이기 때문에 자식 타이어 객체들을 반환값으로 지정할 수 있다. 당연히 이 자동 타입 변환 후라면 다시 강제 타입 변환도 된다......
- 아마 위의 설명만으로는 이걸 왜 하는지 이해가 안될거다. 간단히 설명하면
// Tire 클래스 (부모 클래스)
class Tire {
String brand;
public Tire(String brand) {
this.brand = brand;
}
public void getTireInfo() {
System.out.println("Tire Brand: " + brand);
}
}
// KiaTire와 HankookTire는 Tire를 상속받는 자식 클래스들
class KiaTire extends Tire {
public KiaTire(String brand) {
super(brand);
}
}
class HankookTire extends Tire {
public HankookTire(String brand) {
super(brand);
}
}
// Car 클래스
class Car {
Tire tire;
public Car(Tire tire) {
this.tire = tire; // Tire 타입 객체를 받는다
}
public void showTireInfo() {
tire.getTireInfo(); // Tire 객체의 메서드 호출
}
}
public class Main {
public static void main(String[] args) {
// 다양한 Tire 타입을 받아 Car 객체 생성 가능
Car car1 = new Car(new KiaTire("KIA"));
Car car2 = new Car(new HankookTire("HANKOOK"));
car1.showTireInfo(); // Tire Brand: KIA
car2.showTireInfo(); // Tire Brand: HANKOOK
}
}
왜 이렇게 해야 할까?
- 자동차의 타이어는 여러 종류일 수 있습니다. KiaTire, HankookTire 등 다양한 브랜드의 타이어가 있을 수 있는데, Car 클래스는 이 타이어를 구체적인 종류와 관계없이 받아들일 수 있어야 합니다.
- 타이어 브랜드가 늘어난다고 해서 Car 클래스를 매번 수정하는 것보다는, 부모 클래스인 Tire 타입으로 받아들여 자식 클래스에서 Tire 객체를 사용하게 하면, 확장성이 좋아지고 변경에 강한 코드가 됩니다.
- 말이 어려운데, 자 여기서 Car를 만들고자 한다. 자동차는 타이어라는 자식 클래스를 포함한다.
- 그런데 타이어에 A,B 종류가 있다면?
- Car가 tire의 종류를 파악하려면 , car -> 요청 -> tire -> 요청 -> A의 과정을 거친다
- 만약 tire가 A의 정보를 바로 접속해서 이미 들고 있다면? car -> 요청 -> tire(자식 클래스 A에서 정보 이미 받음)
- 더 빠르다.
변형 X | car -> 요청 -> tire -> 요청 -> A |
변형 O | car -> 요청 -> tire(자식 클래스 A에서 정보 다이렉트로 받음) |
(변형 없이 접근)
// 부모 클래스 : Tire
class Tire {
String brand;
public Tire(String brand) {
this.brand = brand;
}
public void getTireInfo() {
System.out.println("Tire Brand: " + brand);
}
}
// 자식 클래스 : KiaTire
class KiaTire extends Tire {
public KiaTire(String brand) {
super(brand);
}
public void getKiaTireInfo() {
System.out.println("Kia Tire Information: " + brand);
}
}
// 자식 클래스 : HankookTire
class HankookTire extends Tire {
public HankookTire(String brand) {
super(brand);
}
public void getHankookTireInfo() {
System.out.println("Hankook Tire Information: " + brand);
}
}
// 자동차 클래스 : Car
class Car {
Tire tire;
public Car(Tire tire) {
this.tire = tire;
}
// 변형 없이 타이어 종류에 맞는 메서드 직접 호출
public void showTireInfo() {
if (tire instanceof KiaTire) {
KiaTire kiaTire = (KiaTire) tire;
kiaTire.getKiaTireInfo();
} else if (tire instanceof HankookTire) {
HankookTire hankookTire = (HankookTire) tire;
hankookTire.getHankookTireInfo();
} else {
tire.getTireInfo();
}
}
}
public class Main {
public static void main(String[] args) {
// 다양한 타이어 종류를 Car에 할당
Car car1 = new Car(new KiaTire("KIA"));
car1.showTireInfo(); // Kia Tire Information: KIA
Car car2 = new Car(new HankookTire("HANKOOK"));
car2.showTireInfo(); // Hankook Tire Information: HANKOOK
}
}
(다형성 접근)
// 부모 클래스 : Tire
class Tire {
String brand;
public Tire(String brand) {
this.brand = brand;
}
public void getTireInfo() {
System.out.println("Tire Brand: " + brand);
}
}
// 자식 클래스 : KiaTire
class KiaTire extends Tire {
public KiaTire(String brand) {
super(brand);
}
@Override
public void getTireInfo() {
System.out.println("Kia Tire Information: " + brand);
}
}
// 자식 클래스 : HankookTire
class HankookTire extends Tire {
public HankookTire(String brand) {
super(brand);
}
@Override
public void getTireInfo() {
System.out.println("Hankook Tire Information: " + brand);
}
}
// 자동차 클래스 : Car
class Car {
Tire tire;
public Car(Tire tire) {
this.tire = tire;
}
// 다형성 활용, 부모 타입으로 자식 클래스 메서드를 호출
public void showTireInfo() {
tire.getTireInfo(); // Tire 타입으로 메서드 호출
}
}
public class Main {
public static void main(String[] args) {
// 다양한 타이어 종류를 Car에 할당
Car car1 = new Car(new KiaTire("KIA"));
car1.showTireInfo(); // Kia Tire Information: KIA
Car car2 = new Car(new HankookTire("HANKOOK"));
car2.showTireInfo(); // Hankook Tire Information: HANKOOK
}
}
- 이게 더 효율적이다.... 정말 자바는 말 그대로 효율의 극한, 중복을 정말 정말 정말 극혐한다.....
- 위 방식으로 굳이 새로운 정보가 필요할 때마다 클래스를 수정할 필요 없이 Main에서 다형성 코딩만 진행하면되고 관리 유지, 오류 방지에도 탁월하다.... (만든 사람이 진짜 변태인가 보다...)
instanceof
🧩 다형성 기능으로 인해 해당 클래스 객체의 원래 클래스명을 체크하는 것이 필요한데 이때 사용할 수 있는 명령어가 instance of입니다.
- 이 명령어를 통해서 해당 객체가 내가 의도하는 클래스의 객체인지 확인할 수 있습니다.
- {대상 객체} instance of {클래스 이름} 와 같은 형태로 사용하면 응답값은 boolean입니다.
- 다형성 설명 보면 알겠지만, 상당이 코드가 복잡해진다.
- 이로 인해 프로그래머가 코드를 짜면서도 이 객체(붕어빵)가 어디 클래스(설계도)를 바탕으로 만들어 진건지 헷갈리는 경우가 있는데
- 이때 해당 객체가 어느 클래스의 객체인지 확인하기 위해 있는 기능이다.
// 다형성
class Parent { }
class Child extends Parent { }
class Brother extends Parent { }
public class Main {
public static void main(String[] args) {
Parent pc = new Child(); // 다형성 허용 (자식 -> 부모)
Parent p = new Parent();
System.out.println(p instanceof Object); // true 출력
System.out.println(p instanceof Parent); // true 출력
System.out.println(p instanceof Child); // false 출력
Parent c = new Child();
System.out.println(c instanceof Object); // true 출력
System.out.println(c instanceof Parent); // true 출력
System.out.println(c instanceof Child); // true 출력
}
}
추상 클래스
🎵 클래스가 설계도라면 추상 클래스는 미완성된 설계도입니다.
- abstract 키워드를 사용하여 추상 클래스를 선언할 수 있습니다.
public abstract class 추상클래스명 {
abstract 리턴타입 메서드이름(매개변수, ...);
}
- 추상 클래스는 추상 메서드를 포함할 수 있습니다.
- 추상 메서드가 없어도 추상 클래스로 선언할 수 있습니다.
- 추상 클래스는 자식 클래스에 상속되어 자식 클래스에 의해서만 완성될 수 있습니다.
- 추상 클래스는 여러 개의 자식 클래스들에서 공통적인 필드나 메서드를 추출해서 만들 수 있습니다.
- 추상 메서드는 일반적인 메서드와는 다르게 블록{ }이 없습니다. 즉, 정의만 할 뿐, 실행 내용은 가지고 있지 않습니다.
추상 클래스는 왜 쓰는가?
- 공통된 기능을 구현: 추상 클래스는 여러 자식 클래스가 공통적으로 가지는 기능을 제공하는데 유용합니다. 예를 들어, 여러 종류의 자동차가 있지만, 모든 자동차는 움직이는 기능이 필요하고, 이 기능을 추상 클래스에 구현할 수 있습니다.
- 구체적인 구현과 공통의 계약을 결합: 추상 클래스는 구체적인 메서드 구현을 제공하고, 일부는 추상 메서드로 남겨두어 자식 클래스가 구현하도록 할 수 있습니다. 이를 통해, 자식 클래스는 기본 구현을 상속받고, 필요한 부분만 구체화합니다.
- 상태(필드) 공유: 추상 클래스는 **상태(필드)**를 가질 수 있기 때문에, 자식 클래스에서 공통적으로 사용하는 값을 선언하고, 이를 자식 클래스에서 상속받아 사용할 수 있습니다.
- 일관성 유지: 추상 클래스를 사용함으로써, 모든 자식 클래스가 공통적으로 특정 메서드를 구현하도록 강제할 수 있습니다. 이를 통해 일관된 동작을 보장할 수 있습니다.
추상 클래스 상속
public class 클래스명 extends 추상클래스명 {
@Override
public 리턴타입 메서드이름(매개변수, ...) {
// 실행문
}
}
- 상속받은 클래스에서 추상 클래스의 추상 메서드는 반드시 오버라이딩 되어야 합니다.
< 예제 >
public abstract class Car {
String company; // 자동차 회사 : GENESIS
String color; // 자동차 색상
double speed; // 자동차 속도 , km/h
public double gasPedal(double kmh) {
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public abstract void horn();
}
public class BenzCar {
String company; // 자동차 회사 : GENESIS
String color; // 자동차 색상
double speed; // 자동차 속도 , km/h
public double gasPedal(double kmh) {
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public void horn() {
System.out.println("Benz 빵빵");
}
}
public class AudiCar {
String company; // 자동차 회사 : GENESIS
String color; // 자동차 색상
double speed; // 자동차 속도 , km/h
public double gasPedal(double kmh) {
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public void horn() {
System.out.println("Audi 빵빵");
}
}
public class ZenesisCar {
String company; // 자동차 회사 : GENESIS
String color; // 자동차 색상
double speed; // 자동차 속도 , km/h
public double gasPedal(double kmh) {
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public void horn() {
System.out.println("Zenesis 빵빵");
}
}
public class Main {
public static void main(String[] args) {
Car car1 = new BenzCar();
car1.horn();
System.out.println();
Car car2 = new AudiCar();
car2.horn();
System.out.println();
Car car3 = new ZenesisCar();
car3.horn();
}
}
- BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에 차이가 존재합니다.
- 따라서 horn() 메서드를 추상 메서드로 선언하여 자식 클래스에서 재정의 될 수 있도록 합니다.
- BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에 차이가 존재합니다.
- 즉 위의 자식 클래스에서 horn()메서드만 내용이 제각각이라는거
- 각자 메서드 선언을 하면 3번 해야하니
- 추상 클래스를 선언해서 메서드 선언을 1번으로 줄이고
- 자식들이 내용만 채우라는것....
- 아래 참고...
public class BenzCar extends Car {
@Override
public void horn() {
System.out.println("Benz 빵빵");
}
}
public class AudiCar extends Car {
@Override
public void horn() {
System.out.println("Audi 빵빵");
}
}
public class AudiCar extends Car {
@Override
public void horn() {
System.out.println("Audi 빵빵");
}
}
public abstract class Car {
String company; // 자동차 회사
String color; // 자동차 색상
double speed; // 자동차 속도 , km/h
public double gasPedal(double kmh) {
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public abstract void horn(); // 추상클래스~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
public class Main {
public static void main(String[] args) {
Car car1 = new BenzCar();
car1.horn();
System.out.println();
Car car2 = new AudiCar();
car2.horn();
System.out.println();
Car car3 = new ZenesisCar();
car3.horn();
}
}
< 일단 정리 >
- 다형성은 데이터 접근이 많을 때 그를 단순화 하기 위해 사용 ( 부모가 자식에게 손자의 정보를 달라고 할때)
- 추상 클래스는 여러 자식들 중 메서드가 다 다른 애가 있으면 그걸 추상 클래스의 메서드로 만들어서 자식들이 내용만 채우게 (추상 : 그리기 / 자식들 : 원, 삼각형 동그라미 등)
- 다형성은 다양한 데이터나 객체를 하나의 부모 타입으로 처리해야 할 때 유용하게 사용됩니다. 예를 들어, 여러 자식 클래스의 객체들을 부모 클래스 타입으로 다루면서, 각 자식 클래스의 고유한 기능을 호출하는 상황에서 사용됩니다.
- 추상 클래스는 여러 자식 클래스가 공통된 기능을 가지고 있어야 할 때 유용합니다. 부모 클래스에서 추상 메서드를 정의해 두고, 각 자식 클래스가 그 메서드를 어떻게 구현할지 결정하게 만들 수 있습니다. 즉, 자식들이 부모 클래스의 특정 기능을 "구현"하게 강제하는 경우입니다.
< 요약 >
- 추상 클래스는 공통적인 기능을 정의하지만, 세부 구현은 자식 클래스에서 하도록 강제합니다.
- 다형성은 부모 타입으로 여러 자식 객체를 다룰 수 있게 해주며, 각 자식 클래스의 고유 메서드를 호출할 수 있도록 합니다.
- 오버라이딩은 부모 클래스에서 정의한 메서드를 자식 클래스에서 구체적으로 구현하여 각 자식 클래스에 맞는 동작을 하도록 만듭니다.
오늘의 문제
다음은 자동차와 관련된 클래스 구조입니다. Car 클래스를 부모 클래스로 하고, 이를 상속받은 Sedan과 SUV 클래스를 만들어보세요. 각 차종은 drive라는 메서드를 오버라이딩하여 다르게 동작해야 합니다. 이때 Car는 추상 클래스이며, drive 메서드는 추상 메서드로 정의되어야 합니다. 또한, 다형성을 활용하여 Car 타입의 배열을 만들어 다양한 차들을 처리하는 방법을 구현하세요.
요구사항
- Car 클래스는 추상 클래스이며, drive()라는 추상 메서드를 가지고 있습니다.
- Sedan 클래스와 SUV 클래스는 Car 클래스를 상속받으며, 각자의 drive() 메서드를 구현해야 합니다.
- Main 클래스에서 Car 타입의 배열을 이용해 여러 종류의 자동차 객체를 생성하고, 다형성을 이용해 각 차종의 drive() 메서드를 호출하는 코드 작성.
// 1. 부모 클래스 Car
abstract class Car {
String model;
public Car(String model) {
this.model = model;
}
// 2. 추상 메서드 drive()
public abstract void drive();
}
// 3. 자식 클래스 Sedan
class Sedan extends Car {
public Sedan(String model) {
super(model);
}
// 4. 오버라이딩된 drive() 메서드
@Override
public void drive() {
System.out.println(model + " 세단은 부드럽게 달립니다.");
}
}
// 5. 자식 클래스 SUV
class SUV extends Car {
public SUV(String model) {
super(model);
}
// 6. 오버라이딩된 drive() 메서드
@Override
public void drive() {
System.out.println(model + " SUV는 험한 길도 잘 달립니다.");
}
}
// 7. Main 클래스
public class Main {
public static void main(String[] args) {
// 8. Car 타입 배열 선언
Car[] cars = new Car[2];
// 9. 다양한 차종 생성
cars[0] = new Sedan("아반떼");
cars[1] = new SUV("투싼");
// 10. 다형성 활용하여 각 차의 drive 메서드 호출
for (Car car : cars) {
car.drive(); // 각 차종의 구체적인 drive 메서드가 호출됨
}
}
}
해설
- 추상 클래스 Car: Car 클래스는 abstract로 선언되어 있으며, drive()라는 추상 메서드를 정의하고 있습니다. 추상 메서드는 자식 클래스에서 반드시 구현해야 하므로, Car 클래스는 직접 drive()를 구현하지 않고, 자식 클래스들이 이 메서드를 구현하도록 강제합니다.
- 자식 클래스 Sedan과 SUV: Sedan과 SUV는 Car 클래스를 상속받고, 각자의 drive() 메서드를 구현합니다. 이때 drive() 메서드는 Car 클래스에서 정의된 추상 메서드를 오버라이딩하여 각자의 방식으로 자동차가 달리는 방식을 다르게 구현합니다.
- 다형성: Main 클래스에서 Car 타입의 배열을 만들고, 그 배열에 Sedan과 SUV 객체를 각각 저장합니다. 이후 Car 타입으로 선언된 변수 car를 통해 Sedan과 SUV 객체를 다룰 수 있습니다. 이때 각 객체는 다형성에 의해 자신의 drive() 메서드를 호출합니다. 즉, Sedan과 SUV는 부모 클래스 Car의 타입을 가지지만, 각자 다른 구현을 가진 drive() 메서드를 호출하게 됩니다.
- 오버라이딩: Sedan과 SUV 클래스에서 Car 클래스의 drive() 메서드를 오버라이딩하여, 각자 다른 방식으로 자동차의 운전 기능을 구현합니다. 이 메서드는 부모 클래스의 메서드를 자식 클래스에서 수정한 것으로, 해당 자식 객체에 맞는 동작을 하게 됩니다.
<결과>
아반떼 세단은 부드럽게 달립니다.
투싼 SUV는 험한 길도 잘 달립니다.
'Back-End (Web) > JAVA' 카테고리의 다른 글
[JAVA] 오류 및 예외에 대한 이해 (1) | 2024.11.12 |
---|---|
[JAVA] 인터페이스 (2) | 2024.11.12 |
[JAVA] package와 import (0) | 2024.11.12 |
[JAVA] 접근 제어자 (1) | 2024.11.12 |
[JAVA] This와 This() (0) | 2024.11.12 |