2025. 1. 7. 20:29ㆍ개발 회고/TIL
😊오늘 배운 내용
오늘은 어제에 이어서 다형성, 추상클래스, 인터페이스를 배웠지만 상속부터 다시 개념을 정리해보려 한다.
상속이란?
부모가 자식에게 물려주는 행위를 말한다. 자바에서 상속은 부모 클래스 코드를 자식 클래스 코드에 물려주는 의미이다.
그럼 왜 상속을 사용할까?
상속을 사용하면 적은 양의 코드로 새로운 클래스를 작성할 수도 있고 공통적인 코드를 관리하여 코드의 추가와 변경이 쉬워진다.
상속을 어떻게 할까?
다음과 같이 상속을 할 수 있다.
public class 자식클래스 extends 부모클래스 {
}
extends 부모클래스 를 하면 부모클래스를 자식클래스가 상속받겠다는 의미이다.
예제코드로 살펴보겠다.
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");
}
}
자식클래스인 SprortsCar 클래스는 부모클래스인 Car 클래스를 상속받는다. 상속받게 되면 자식 클래스는 부모클래스의 멤버를 사용할 수 있게 된다.
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'));
}
}
상속관계와 포함관계
상속관계는 ~는 ~이다. 라고 정의할 수 있다. 고래는 포유류이다. 처럼
포함관계는 ~는 ~를 가지고 있다. 라고 정의할 수 있다. 만약 고래와 포유류를 포함관계로 정의한다면 이상해 질 것이다. 포유류는 고래를 가지고있다.와 같이 문장이 어색해진다.
포함관계는 자동차는 타이어를 가지고 있다. 자동차는 핸들을 가지고 있다. 와 같이 정의할 수 있다.
따라서 포함관계를 만들때는 상속을 사용하지 않고 하위 부품들을 만드는 각자의 클래스를 만든 다음
하위클래스를 사용하려는 클래스에서 사용하면된다.
public class Main {
public static void main(String[] args) {
// 자동차 객체 생성
Car car = new Car("GV80", "Black", 50000000);
// 자동차 부품 : 타이어, 차문, 핸들 선언
Tire[] tires = new Tire[]{
new Tire("KIA", 150000), new Tire("금호", 150000),
new Tire("Samsung", 150000), new Tire("LG", 150000)
};
Door[] doors = new Door[]{
new Door("LG", "FL"), new Door("KIA", "FR"),
new Door("Samsung", "BL"), new Door("LG", "BR")
};
Handle handle = new Handle("Samsung", "S");
// 자동차 객체에 부품 등록
car.setTire(tires);
car.setDoor(doors);
car.setHandle(handle);
// 등록된 부품 확인하기
for (Tire tire : car.tire) {
System.out.println("tire.company = " + tire.company);
}
System.out.println();
for (Door door : car.door) {
System.out.println("door.company = " + door.company);
System.out.println("door.location = " + door.location);
System.out.println();
}
System.out.println();
// 자동차 핸들 인스턴스 참조형 변수에 저장
Handle carHandle = car.handle;
System.out.println("carHandle.company = " + carHandle.company);
System.out.println("carHandle.type = " + carHandle.type + "\n");
// 자동차 핸들 조작해보기
carHandle.turnHandle("Right");
carHandle.turnHandle("Left");
}
}
여러개의 객체를 사용할 때 객체배열을 사용한다.
단일상속과 다중상속
자바에서는 다중상속을 허용하지 않는다. 하지만 객체들간의 다형성을 위해서 인터페이스라는 개념이 있다.
final
final이란 키워드는 앞으로 변하지 않겠다는 의미의 키워드이다. 클래스의 필드에 앞으로 변하지 않을 필드가 있으면 final 키워드로 명시해준다. 주로 setter로 접근하지 않거나 getter로만 접근하는 필드는 final 키워드를 써준다.
또한 더이상 상속을 하지 않겠다고 이 클래스가 마지막 클래스라고 정의하기 위해서 클래스에 final 키워드를 명시해준다.
Object
Object클래스는 자바의 모든 클래스의 최상위 부모 클래스이다.
오버라이딩
부모클래스로부터 상속받은 메서드를 자식 클래스에서 재정의하는 것을 오버라이딩이라고한다. @Override라는 어노테이션을 붙이고 메서드를 재정의한다.
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();
}
}
위의 클래스에서는 brakePedal()과 horn() 메서드가 재정의 되었다.
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()
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()를 이용해 불러와서 초기화해주었다. 항상 이 작업이 먼저 선행된다.
다형성이란?
다양한 형태를 유지할 수 있는 성질을 말한다.
다형성을 실현하기 위해서는?
다형성을 실현하기 위해서는 추상클래스와 인터페이스등을 이용한다. 추상클래스는 여러 클래스의 공통적인 부분들을 뽑아서 추상화 함으로써 어떠한 '뼈대'개념을 만들고 거기에서 오버라이딩 함으로써 '다양한'객체들이 생성될 수 있도록한다.
이렇게 다형성을 구현했다면 이를 이용하기 위해서는 타입변환이 필요하다.
주로 "부모타입 변수이름 = 자식클래스 객체;" 이며 이 경우에는 자동 변환이 이루어진다. 왜냐햐면 부모타입이 더 큰 개념이기 때문이다. 다만 주의할 점은 부모 타입 변수로 자식 객체의 멤버에 접근할 때는 부모 클래스에 선언된 즉, 상속받은 멤버만 접근할 수 있다.
"자식타입 변수이름 = (자식타입) 부모타입 객체;"인 경우로 있다. 이는 부모타입인 객체를 자식타입으로 강제 형변환 하는 경우이다.
다만 무조건 강제 타입 변환을 할 수 있는 것은 아니다. 자식 타입 객체가 부모 타입으로 자동 타입 변환된 후 다시 자식 타입으로 변환될 때만 강제 타입 변환이 가능하다. 부모 타입 변수로는 자식 타입 객체의 고유한 멤버를 사용할 수 없기 때문에 사용이 필요한 경우가 생겼을 때 강제 타입 변환을 사용한다.
// 자식타입객체가 자동 타입변환된 부모타입의 변수
Mammal mammal = new Whale();
mammal.feeding();
// 자식객체 고래의 수영 기능을 사용하고 싶다면
// 다시 자식타입으로 강제 타입변환을 하면된다.
Whale whale = (Whale) mammal;
whale.swimming();
여기까지는 이론적으로 이해할 수 있다. 그렇지만 근본적으로 드는 궁금증이 그럼 왜 형변환 해야하는데? 부모클래스로부터 상속받아서 오버라이딩함으로써 다양한 객체 생성 -> 그 후에는 각자 그 객체에 접근해서 이용하면 되는 것 아닌가? 라는 궁금증이 들었다.
튜터님께 질문하였고 다음과 같은 답을 얻을 수 있었다. 만약 파일 업로드 기능이 있고 이 기능이 원래 카카오에서만 돌아간다. 그런데 상사가 네이버에서도 돌아가게 만들어라고 지시한다면?
만약 서로 다른 객체로써 기능을 각각 만들게 되면 따로따로 네이버에서 돌아가도록, 카카오에서 돌아가도록 만들어서 접근해야 할 것이다. 그러나 인터페이스를 만들어 두면 공통되는 부분은 같고 서로 다르게 만들어주어야 할 부분만 개발자가 작성하면 될것이다. 그리고 나중에 이 기능을 사용할 때 인터페이스 타입으로 형변환 해줌으로서 인터페이스를 이용해 다양한 객체에 접근 가능하게 하도록 하면 훨씬 편리하게 다형성을 실현할 수 있다.
만약 형변환 하지 않는다면 각기 다른 객체를 생성하고 그 객체에서 접근할 수도 있지만 위와같은 방법으로 다형성을 실현한다면 구조가 보다 편해지고 논리적이게 정리할 수 있다.
코드로 이해해보자면 다음과 같이 부모클래스로 형변환 해준다.
Tire tire = new HankookTire("HANKOOK");
Tire tire = new KiaTire("KIA");
public Car(Tire tire) {
this.tire = tire;
}
...
Car car1 = new Car(new KiaTire("KIA"));
Car car2 = new Car(new HankookTire("HANKOOK"));
그리고 사용할 시에 Tire 클래스로 형변환 해주었기에 Car 객체를 만들때 다형성을 실현할 수 있게 된다. 만약 형변환을 해주지 않았더라면 Car객체를 만들때 한국타이어를 이용하여 차를 만드는 객체 생성자 따로, 기아타이어를 이용하여 차를 만드는 객체 생성자 따로 구성해주어야 했을것이다.
Tire getHankookTire() {
return new HankookTire("HANKOOK");
}
Tire getKiaTire() {
return new KiaTire("KIA");
}
...
Tire hankookTire = car1.getHankookTire();
KiaTire kiaTire = (KiaTire) car2.getKiaTire();
리턴타입으로 받아와서 형변환 하는 경우도 있다.
instance of
다형성 기능으로 인해 해당 클래스 객체의 원래 클래스명을 체크하는 것이 필요한데 이때 사용할 수 있는 명령어가 instance of 이다.
// 다형성
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 출력
}
}
추상클래스
클래스가 설계도라면 추상 클래스는 미완성된 설계도이다.
public abstract class 추상클래스명 {
}
위와 같이 해당 클래스가 추상클래스임을 정의할 수 있다. 추상클래스는 추상메서드가 없어도 정의할 수 있다.
추상메서드는 다음과 같이 정의한다.
public abstract class 추상클래스명 {
abstract 리턴타입 메서드이름(매개변수, ...);
}
추상메서드에는 {}로 쓰는 구현 부분이 없다.
추상클래스를 상속받는 클래스에서는 추상메서드를 오버라이딩하여 사용한다.
public class 클래스명 extends 추상클래스명 {
@Override
public 리턴타입 메서드이름(매개변수, ...) {
// 실행문
}
}
인터페이스
인터페이스는 여러객체를 연결해주는 다리 역할이다.
상속 관계가 없는 다른 클래스들이 서로 동일한 메서드를 구현해야 할 때 인터페이스는 구현 클래스들의 동일한 사용 방법과 행위를 보장해 줄 수 있다.
- 인터페이스는 스팩이 정의된 메서드들의 집합입니다.
- 인터페이스의 구현 클래스들은 반드시 정의된 메서드들을 구현해야 합니다.
- 따라서 구현 클래스들의 동일한 사용 방법과 행위를 보장해 줄 수 있습니다.
- 이러한 특징은 인터페이스에 다형성을 적용할 수 있게 만들어 줍니다.
중요한 점은 인터페이스를 구현하는 클래스들은 반드시 인터페이스에 정의된 메서드들을 구현해야한다.
인터페이스 선언
public interface 인터페이스명 {
}
인터페이스 구성
- 모든 멤버 변수는 public static final이어야 합니다.
- 생략 가능합니다.
- 모든 메서드는 public abstract이어야 합니다.
- 생략 가능합니다. (static 메서드와 default 메서드 예외)
- 생략되는 제어자는 컴파일러가 자동으로 추가해줍니다.
public interface 인터페이스명 {
public static final char A = 'A';
static char B = 'B';
final char C = 'C';
char D = 'D';
void turnOn(); // public abstract void turnOn();
}
인터페이스 구현
public class 클래스명 implements 인터페이스명 {
// 추상 메서드 오버라이딩
@Override
public 리턴타입 메서드이름(매개변수, ...) {
// 실행문
}
}
인터페이스의 추상 메서드는 구현될 때 반드시 오버라이딩 되어야 한다. 만약 인터페이스의 추상 메서드를 일부만 구현해야 한다면 해당 클래스를 추상 클래스로 변경해 주면 됩니다.
인터페이스 상속
인터페이스 간의 상속이 가능하다. 인터페이스 간의 상속은 implements 가 아니라 extends 키워드를 사용한다. 인터페이스는 클래스와는 다르게 다중 상속이 가능하다.
public class Main implements C {
@Override
public void a() {
System.out.println("A");
}
@Override
public void b() {
System.out.println("B");
}
}
interface A {
void a();
}
interface B {
void b();
}
interface C extends A, B { }
또한 인터페이스의 구현은 상속과 함께 사용될 수 있다.
public class Main extends D implements C {
@Override
public void a() {
System.out.println("A");
}
@Override
public void b() {
System.out.println("B");
}
@Override
void d() {
super.d();
}
public static void main(String[] args) {
Main main = new Main();
main.a();
main.b();
main.d();
}
}
interface A {
void a();
}
interface B {
void b();
}
interface C extends A, B {
}
class D {
void d() {
System.out.println("D");
}
}
default
디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드이다.
- 메서드 앞에 default 키워드를 붙이며 블럭{ }이 존재해야 합니다.
- default 메서드 역시 접근 제어자가 public이며 생략이 가능합니다.
- 추상 메서드가 아니기 때문에 인터페이스의 구현체들에서 필수로 재정의 할 필요는 없습니다.
public class Main implements A {
@Override
public void a() {
System.out.println("A");
}
public static void main(String[] args) {
Main main = new Main();
main.a();
// 디폴트 메서드 재정의 없이 바로 사용가능합니다.
main.aa();
}
}
interface A {
void a();
default void aa() {
System.out.println("AA");
}
}
static
인터페이스에서 static 메서드 선언이 가능하다.
- static의 특성 그대로 인터페이스의 static 메서드 또한 객체 없이 호출이 가능합니다.
- 선언하는 방법과 호출하는 방법은 클래스의 static 메서드와 동일합니다.
- 접근 제어자를 생략하면 컴파일러가 public을 추가해 줍니다.
public class Main implements A {
@Override
public void a() {
System.out.println("A");
}
public static void main(String[] args) {
Main main = new Main();
main.a();
main.aa();
System.out.println();
// static 메서드 aaa() 호출
A.aaa();
}
}
interface A {
void a();
default void aa() {
System.out.println("AA");
}
static void aaa() {
System.out.println("static method");
}
}
인터페이스의 다형성
위에서 설명했던것과 같이 인터페이스를 이용해서 다양한 클래스를 구현하고 인터페이스 타입으로 형변환을 하여 사용하려는 부분에서 훨씬 간단하게 다형성 실현이 가능하다.
추상클래스 Tv 클래스
package week03.inter;
public abstract class Tv {
private String company; // 티비 회사
private int channel = 1; // 현재 채널 상태
private int volume = 0; // 현재 볼륨 상태
private boolean power = false; // 현재 전원 상태
public Tv(String company) {
this.company = company;
}
public void displayPower(String company, boolean power) {
if(power) {
System.out.println(company + " Tv 전원이 켜졌습니다.");
} else {
System.out.println(company + " Tv 전원이 종료되었습니다.");
}
}
public void displayChannel(int channel) {
System.out.println("현재 채널은 " + channel);
}
public void displayVolume(int volume) {
System.out.println("현재 볼륨은 " + volume);
}
public String getCompany() {
return company;
}
public int getChannel() {
return channel;
}
public int getVolume() {
return volume;
}
public boolean isPower() {
return power;
}
public void setChannel(int channel) {
this.channel = Math.max(channel, 0);
}
public void setVolume(int volume) {
this.volume = Math.max(volume, 0);
}
public void setPower(boolean power) {
this.power = power;
}
}
추상클래스를 상속받으면서 인터페이스를 구현하는 SamsungTv 클래스와 LgTv클래스
package week03.inter;
public class SamsungTv extends Tv implements MultiRemoteController{
public SamsungTv(String company) {
super(company);
}
@Override
public void turnOnOff() {
setPower(!isPower());
displayPower(getCompany(), isPower());
}
@Override
public void channelUp() {
setChannel(getChannel() + 1);
displayChannel(getChannel());
}
@Override
public void channelDown() {
setChannel(getChannel() - 1);
displayChannel(getChannel());
}
@Override
public void volumeUp() {
setVolume(getVolume() + 1);
displayVolume(getVolume());
}
@Override
public void volumeDown() {
setVolume(getVolume() - 1);
displayVolume(getVolume());
}
}
package week03.inter;
public class LgTv extends Tv implements MultiRemoteController {
public LgTv(String company) {
super(company);
}
@Override
public void turnOnOff() {
setPower(!isPower());
displayPower(getCompany(), isPower());
}
@Override
public void channelUp() {
setChannel(getChannel() + 1);
displayChannel(getChannel());
}
@Override
public void channelDown() {
setChannel(getChannel() - 1);
displayChannel(getChannel());
}
@Override
public void volumeUp() {
setVolume(getVolume() + 1);
displayVolume(getVolume());
}
@Override
public void volumeDown() {
setVolume(getVolume() - 1);
displayVolume(getVolume());
}
}
인터페이스인 MultiRemoteController
package week03.inter;
public interface MultiRemoteController {
void turnOnOff();
void channelUp();
void channelDown();
void volumeUp();
void volumeDown();
// 매개변수와 반환타입 다형성 확인 메서드
default MultiRemoteController getTV(Tv tv) {
if(tv instanceof SamsungTv) {
return (SamsungTv) tv;
} else if(tv instanceof LgTv){
return (LgTv) tv;
} else {
throw new NullPointerException("일치하는 Tv 없음");
}
}
}
Main
package week03.inter;
public class Main {
public static void main(String[] args) {
//타입변환을 통해 리모콘으로 엘지티비와 삼성티비를 왔다갔다 할 수 있게 됨
//즉 타입변환을 인터페이스 타입으로 함으로써 다형성을 유지할 수 있게 된다.
// LG TV 구현체를 조작
MultiRemoteController mrc = new LgTv("LG");
mrc.turnOnOff();
mrc.volumeUp();
mrc.channelDown();
mrc.channelUp();
mrc.turnOnOff();
// 조작 대상을 Samsung TV로 교체
System.out.println("\n<Samsung TV로 교체>");
mrc = new SamsungTv("Samsung");
mrc.turnOnOff();
mrc.channelUp();
mrc.volumeDown();
mrc.volumeUp();
mrc.turnOnOff();
// 매개변수, 반환타입 다형성 체크
System.out.println("\n<매개변수, 반환타입 다형성 체크>");
MultiRemoteController samsung = mrc.getTV(new SamsungTv("Samsung"));
samsung.turnOnOff();
SamsungTv samsungTv = (SamsungTv) samsung;
samsungTv.turnOnOff();
System.out.println();
MultiRemoteController lg = mrc.getTV(new LgTv("LG"));
lg.turnOnOff();
LgTv lgTv = (LgTv) lg;
lgTv.turnOnOff();
}
}
[오늘 회고]
오늘은 헷갈렸던 다형성을 구현하는 것과 형변환의 필요성에 대해서 완전히 정리하였다. 그리고 특강을 들으며 과제코드를 다음과 같이 개선해보았다.
과제코드 개선하기
- 모든 필드는 private으로
- 접근 지정자 꼭 명시해주기
- 카멜케이스 맞추기
- 게터세터
'개발 회고 > TIL' 카테고리의 다른 글
[1/14] TIL - 🌟 객체지향 연습하기 - 클래스, 상속, 가변인자 (1) | 2025.01.14 |
---|---|
[1/13] TIL - 콘솔 출력 형식 지정, 계산기 코드 개선, 앞으로의 공부 다짐 😊 (0) | 2025.01.13 |
[1/6] TIL - 클래스변수 문제 해결, 객체와 클래스, 생성자, 오버로딩, 접근제어자, getter와 setter, this, super, 상속, 오버라이딩 (0) | 2025.01.06 |
[1/2] TIL - 📚 오늘의 학습 정리: 연산자, 제어문, 배열, 콜렉션 자료구조, 객체와 생성자 (0) | 2025.01.02 |
[12/31] TIL - Java 버퍼비우기, 동적배열 , 참조형 변수, 래퍼클래스 & Git branch 협업 (1) | 2024.12.31 |