Preface
11월 중순에 시작했던 상속 파트를 이제서야 마쳤다.
상속 자체는 어려운 부분이 없었지만, 다형성을 코드로 구현하는 것이 조금 복잡했다.
책의 초반부에서 다형성에 관한 내용을 글로만 읽었을 땐 해당 특성이 무엇을 의미하는 것인지 정확히 알 수 없었는데
직접 코드로 구현해보니 다형성이 어떤 특성이고 왜 객체지향 프로그래밍의 장점으로 소개되는지 조금은 이해할 수 있었다.
1. 상속의 개념
- 부모 클래스는 상위 클래스, 자식 클래스는 하위 클래스 또는 파생 클래스라고 부른다.
- 부모 클래스에서 private 접근 제한을 갖는 필드와 메소드는 상속 대상에서 제외된다.
- 부모 클래스와 자식 클래스가 다른 패키지에 존재한다면 default 접근 제한을 갖는 필드와 메소드도 상속 대상에서 제외된다.
- 상속을 이용하면 부모 클래스의 수정으로 모든 자식 클래스들의 수정 효과를 가져올 수 있다.
- 자바는 다중 상속을 허용하지 않는다.
→ 여러 개의 부모 클래스를 상속할 수 없다.
- 상속 예제
package ch7;
class CellPhone {
// 필드
String model;
String color;
// 생성자
// 메소드
void powerOn() {
System.out.println("turn on");
}
void powerOff() {
System.out.println("turn off");
}
void bell() {
System.out.println("bell rings");
}
void sendVoice(String message) {
System.out.println("me: " + message);
}
void receiveVoice(String message) {
System.out.println("counterpart: " + message);
}
void hangUp() {
System.out.println("end call");
}
}
class DmbCellPhone extends CellPhone {
// 필드
int channel;
// 생성자
DmbCellPhone(String model, String color, int channel) {
this.model = model;
this.color = color;
this.channel = channel;
}
// 메소드
void turnOnDmb() {
System.out.println("channel " + channel + "번 DMB 방송 수신을 시작합니다.");
}
void changeChannelDmb(int channel) {
this.channel = channel;
System.out.println("채널 " + channel + "번으로 바꿉니다.");
}
void turnOffDmb() {
System.out.println("DMB 방송 수신을 멈춥니다.");
}
}
public class DmbCellPhoneExample {
public static void main(String[] args) {
// DmbCellPhone 객체 생성
DmbCellPhone dmbCellPhone = new DmbCellPhone("자바폰", "검정", 10);
// CellPhone으로부터 상속받은 필드
System.out.println("model: " + dmbCellPhone.model);
System.out.println("color: " + dmbCellPhone.color);
// DmbCellPhone의 필드
System.out.println("channel: " + dmbCellPhone.channel);
// CellPhone으로부터 상속받은 메소드 호출
dmbCellPhone.powerOn();
dmbCellPhone.bell();
dmbCellPhone.sendVoice("Hello");
dmbCellPhone.receiveVoice("Hi, this is Kim");
dmbCellPhone.sendVoice("Oh~ Hi Kim");
dmbCellPhone.hangUp();
// DmbCellPhone의 메소드 호출
dmbCellPhone.turnOnDmb();
dmbCellPhone.changeChannelDmb(12);
dmbCellPhone.turnOffDmb();
}
}
- 모든 객체는 클래스의 생성자를 호출해야 생성된다.
- 부모 생성자는 자식 생성자의 맨 첫 줄에서 호출된다.
- super( )는 부모의 기본 생성자를 호출한다.
- 부모 클래스에 기본 생성자가 없고 매개 변수가 있는 생성자만 존재한다면 자식 생성자에서 반드시 부모 생성자 호출을 위해 super(매개값, ...)를 명시적으로 호출해야 한다.
→ super(매개값, ...)는 반드시 자식 생성자 첫 줄에 위치해야 한다.
package ch7;
class People {
public String name;
public String ssn;
public People(String name, String ssn) {
this.name = name;
this.ssn = ssn;
}
}
class Student extends People {
public int studentNo;
public Student(String name, String ssn, int studentNo) {
super(name, ssn);
this.studentNo = studentNo;
}
}
public class StudentExample {
public static void main(String[] args) {
Student student = new Student("KIM", "1234-5678", 1);
System.out.println("name: " + student.name);
System.out.println("ssn: " + student.ssn);
System.out.println("studentNo: " + student.studentNo);
}
}
- 메소드 오버라이딩(@Override): 상속된 메소드의 내용이 자식 클래스에 맞지 않을 경우, 자식 클래스에서 동일한 메소드를 재정의하는 것
→ 부모 객체의 메소드는 숨겨지므로 자식 객체에서 메소드를 호출하면 오버라이딩 된 자식 메소드가 호출된다.
- 메소드 오버라이딩 규칙
1) 부모의 메소드와 동일한 리턴 타입, 메소드 이름, 매개 변수 리스트를 가져야 한다.
2) 접근 제한을 약하게 할 순 있지만, 더 강하게 오버라이딩 할 순 없다.
3) 새로운 예외를 throws할 수 없다.
- 자식 클래스 내부에서 오버라이딩 된 부모 클래스의 메소드를 호출해야 한다면 명시적으로 super 키워드를 붙여 부모 메소드를 호출할 수 있다.
→ super는 부모 객체를 참조하므로 부모 메소드에 직접 접근할 수 있다.
super.부모메소드();
package ch7;
class Airplane {
public void land() {
System.out.println("착륙합니다.");
}
public void fly() {
System.out.println("일반비행합니다.");
}
public void takeOff() {
System.out.println("이륙합니다.");
}
}
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 {
super.fly();
// 부모 클래스의 메소드 호출
}
}
}
public class SupersonicAirplaneExample {
public static void main(String[] args) {
SupersonicAirplane sa = new SupersonicAirplane();
sa.takeOff();
sa.fly();
sa.flyMode = SupersonicAirplane.SUPERSONIC;
sa.fly();
sa.flyMode = SupersonicAirplane.NORMAL;
sa.fly();
sa.land();
}
}
- final 키워드: 해당 선언이 최종 상태이며 결코 수정될 수 없음을 나타낸다.
→ 클래스, 필드, 메소드 선언 시에 사용할 수 있다.
- final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다.
→ 최종적인 클래스이므로 상속할 수 없는 클래스가 되기 때문이다.
public final class 클래스명 {...}
- final 메소드는 최종적인 메소드이므로 오버라이딩 할 수 없다.
- protected 접근 제한자: 같은 패키지에서는 default와 같이 접근 제한이 없지만, 다른 패키지에서는 자식 클래스만 접근을 허용한다.
→ 다른 패키지에 있는 자식 클래스인 경우 new 연산자를 사용해서 부모 생성자를 직접 호출할 순 없지만, super( )로 부모 생성자를 호출하는 것은 가능하다.
- 다형성: 같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질
- 자바는 부모 클래스로 타입 변환이 가능하다.
→ 부모 타입에 모든 자식 객체가 대입될 수 있다.
- 클래스 타입 변환: 상속 관계에 있는 클래스 사이에서 발생
1) 자식 타입은 부모 타입으로 자동 타입 변환이 가능하다.
2) 바로 위의 부모가 아니더라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있다.
- 자동 타입 변환(Promotion): 자식은 부모의 특징과 기능을 상속받으므로 부모와 동일하게 취급될 수 있다는 것
부모클래스 변수 = 자식클래스타입;
Cat cat = new Cat();
Animal animal = cat; // Animal animal = new Cat();
// cat과 animal 변수는 타입만 다를 뿐 동일한 Cat 객체를 참조한다.
- 부모 타입으로 자동 타입 변환된 후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다.
→ 단, 메소드가 자식 클래스에서 오버라이딩 되엇다면 자식 클래스의 메소드가 대신 호출된다.
package ch7;
class Tire {
public int maxRotation;
public int accumulatedRotation;
public String location;
public Tire(String location, int maxRotation) {
this.location = location;
this.maxRotation = maxRotation;
}
public boolean roll() {
++accumulatedRotation;
if(accumulatedRotation<maxRotation) {
System.out.println(location + " Tire 수명: " + (maxRotation - accumulatedRotation) + "회");
return true;
} else {
System.out.println("*** " + location + " Tire 펑크 *** ");
return false;
}
}
}
class Car {
Tire frontLeftTire = new Tire("앞왼쪽", 6);
Tire frontRightTire = new Tire("앞오른쪽", 2);
Tire backLeftTire = new Tire("뒤왼쪽", 3);
Tire backRightTire = new Tire("뒤오른쪽", 4);
int run() {
System.out.println("[자동차가 달립니다.]");
if(frontLeftTire.roll()==false) { stop(); return 1; }
if(frontRightTire.roll()==false) { stop(); return 2; }
if(backLeftTire.roll()==false) { stop(); return 3; }
if(backRightTire.roll()==false) { stop(); return 4; }
return 0;
}
void stop() {
System.out.println("[자동차가 멈춥니다.]");
}
}
class HankookTire extends Tire {
public HankookTire(String location, int maxRotation) {
super(location, maxRotation);
}
@Override
public boolean roll() {
++accumulatedRotation;
if(accumulatedRotation<maxRotation) {
System.out.println(location + " HankookTire 수명: " + (maxRotation - accumulatedRotation) + "회");
return true;
} else {
System.out.println("*** " + location + " HankookTire 펑크 *** ");
return false;
}
}
}
class KumhoTire extends Tire {
public KumhoTire(String location, int maxRotation) {
super(location, maxRotation);
}
@Override
public boolean roll() {
++accumulatedRotation;
if(accumulatedRotation<maxRotation) {
System.out.println(location + " KumhoTire 수명: " + (maxRotation - accumulatedRotation) + "회");
return true;
} else {
System.out.println("*** " + location + " KumhoTire 펑크 *** ");
return false;
}
}
}
public class CarExample {
public static void main(String[] args) {
Car car = new Car();
for(int i=1; i<=5; i++) {
int problemLocation = car.run();
switch(problemLocation) {
case 1:
System.out.println("앞왼쪽 한국타이어로 교체");
car.frontLeftTire = new HankookTire("앞왼쪽", 15);
break;
case 2:
System.out.println("앞오른쪽 금호타이어로 교체");
car.frontRightTire = new KumhoTire("앞오른쪽", 13);
break;
case 3:
System.out.println("뒤왼쪽 한국타이어로 교체");
car.backLeftTire = new HankookTire("뒤왼쪽", 14);
break;
case 4:
System.out.println("뒤오른쪽 금호타이어로 교체");
car.backRightTire = new KumhoTire("뒤오른쪽", 17);
break;
}
System.out.println("---------------------------");
}
}
}
- 여러 개의 객체를 배열로 관리하면 인덱스로 표현할 수 있어 제어문 사용 시 더욱 효율적이다.
- 메소드를 호출할 때 매개 변수의 타입과 동일한 매개값을 지정하는 것이 정석이지만, 매개값을 다양화하기 위해 매개 변수에 자식 타입 객체를 지정할 수도 있다.
→ 매개 변수의 다형성
- 매개 변수의 타입이 클래스일 경우, 해당 클래스의 객체뿐만 아니라 자식 객체까지도 매개값으로 사용할 수 있다.
package ch7;
class Vehicle {
public void run() {
System.out.println("차량이 달립니다.");
}
}
class Driver {
public void drive(Vehicle vehicle) {
vehicle.run();
}
}
class Bus extends Vehicle {
@Override
public void run() {
System.out.println("버스가 달립니다.");
}
}
class Taxi extends Vehicle {
@Override
public void run() {
System.out.println("택시가 달립니다.");
}
}
public class DriverExample {
public static void main(String[] args) {
Driver driver = new Driver();
Bus bus = new Bus();
Taxi taxi = new Taxi();
driver.drive(bus);
driver.drive(taxi);
}
}
- 강제 타입 변환(Casting): 부모 타입을 자식 타입으로 변환하는 것
→ 자식 타입이 부모 타입으로 자동 변환된 후, 다시 자식 타입으로 변환하는 경우에만 강제 타입 변환을 사용할 수 있다.
자식클래스 변수 = (자식클래스) 부모클래스타입(자식 타입이 부모 타입으로 변환된 상태);
- instanceof 연산자: 어떤 객체가 어떤 클래스의 인스턴스인지 확인할 수 있는 연산자
→ 매개값의 타입을 조사할 때 주로 사용한다. (강제 타입 변환 가능 유무를 확인하기 위해)
boolean result = 좌항(객체) instanceof 우항(타입);
- 클래스의 종류
1) 실체 클래스: 객체를 직접 생성할 수 있는 클래스
2) 추상 클래스: 클래스들의 공통적인 특성을 추출해서 선언한 클래스
- 추상 클래스가 부모이고 실체 클래스가 자식으로 구현되어 실체 클래스는 추상 클래스의 모든 특성을 물려받고, 추가적인 특성을 가질 수 있다.
- 추상 클래스는 객체를 직접 생성해 사용할 수 없다.
→ new 연산자를 사용해서 인스턴스를 생성시킬 수 없다.
- 추상 클래스는 새로운 실체 클래스를 만들기 위해 부모 클래스로만 사용된다.
→ extends 뒤에만 올 수 있는 클래스이다.
- 추상 클래스의 용도
1) 실체 클래스들의 공통된 필드와 메소드 이름을 통일할 목적
2) 실체 클래스를 작성할 때 시간을 절약
- 추상 클래스를 선언할 땐 클래스 선언부에 abstract 키워드를 붙여야 한다.
1) new 연산자를 이용해 객체를 생성할 순 없고, 상속을 통해 자식 클래스만 만들 수 있다.
2) new 연산자로 직접 생성자를 호출할 수는 없지만, 자식 객체가 생성될 때 super(...)를 호출해서 추상 클래스 객체를 생성하므로 반드시 생성자가 있어야 한다.
package ch7;
abstract class Phone {
public String owner;
public Phone(String owner) {
this.owner = owner;
}
public void turnOn() {
System.out.println("turn on the phone");
}
public void turnOff() {
System.out.println("turn off the phone");
}
}
class SmartPhone extends Phone {
public SmartPhone(String owner) {
super(owner);
}
public void internetSearch() {
System.out.println("search for the internet");
}
}
public class PhoneExample {
public static void main(String[] args) {
// Phone phone = new Phone();
SmartPhone smartphone = new SmartPhone("Kim");
smartphone.turnOn();
smartphone.internetSearch();
smartphone.turnOff();
}
}
- 추상 메소드: 추상 클래스에서만 선언할 수 있는 메소드로, 메소드의 선언부만 있고 실행 내용인 중괄호가 없는 메소드
1) abstract 키워드가 붙어 있다.
2) 자식 클래스에서 추상 메소드를 필수로 재정의(오버라이딩)해서 실행 내용을 작성해야 한다.
public abstract class Animal {
public abstract void sound();
}
// 자식 클래스에서 sound() 메소드를 반드시 재정의해야 한다.
'Java > 이것이 자바다' 카테고리의 다른 글
이자바 8장(인터페이스) 확인문제 (0) | 2023.03.14 |
---|---|
인터페이스 (0) | 2023.03.14 |
이자바 6장(클래스) 확인문제 (0) | 2022.11.24 |
이자바 5장(참조 타입) 확인문제 (0) | 2022.11.23 |
이자바 4장(조건문과 반복문) 확인문제 (0) | 2022.11.23 |
댓글