개발자입니다
[Java] 예제 소스 정리 - 상속, 추상 클래스 본문
com.eomcs.oop.ex05
예제 소스 정리
상속
상속 - 사용 전
package com.eomcs.oop.ex05.a
public class Exam01 {
public static void main(String[] args) {
Car c1 = new Car();
c1.maker = "비트자동차";
c1.model = "티코";
c1.capacity = 5;
Car c2 = new Car("소나타", "비트자동차", 5);
}
}
public class Car {
public String model;
public String maker;
public int capacity;
public Car() {}
public Car(String model, String maker, int capacity) {
this.model = model;
this.maker = maker;
this.capacity = capacity;
}
public void run() {
System.out.println("달린다!");
}
}
public class Exam02 {
public static void main(String[] args) {
Calculator c1 = new Calculator();
c1.plus(5);
c1.minus(2);
System.out.println(c1.result);
}
}
public class Calculator {
public int result;
public void plus(int value) {
this.result += value;
}
public void minus(int value) {
this.result -= value;
}
}
public class Exam03 {
public static void main(String[] args) {
Score s = new Score();
s.name = "홍길동";
s.kor = 100;
s.eng = 100;
s.math = 100;
s.compute();
System.out.printf("%s: %d(%.1f)\n", s.name, s.sum, s.aver);
}
}
public class Score {
public String name;
public int kor;
public int eng;
public int math;
public int sum;
public float aver;
public void compute() {
this.sum = this.kor + this.eng + this.math;
this.aver = this.sum / 3f;
}
}
상속 - 상속하지 않고 기능 추가 I
새 프로젝트에서는 제조사, 모델명, 수용인원 외에 썬루프 장착여부, 자동변속 여부를 추가적으로 저장하고 싶다!
방법1) 기존의 Car 클래스를 변경한다
문제점:
=> 기존 소스를 변경하게 되면 기존의 Car를 사용해서 만든 프로그램들도
영향을 받는다.
=> 심각한 오류가 발생할 수 있다.
=> 그래서 기존의 소스 코드를 손대는 것은 매우 위험하다.
=> 그리고 계속 코드를 덧 붙이다 보면 누더기 코드가 될 가능성이 있다.
=> 쓰지도 않는 코드가 계속 누적되는 문제가 있다.
package com.eomcs.oop.ex05.b;
public class Exam01 {
public static void main(String[] args) {
Car c1 = new Car("비트자동차", "티코", 5, true, true);
}
}
# 기능 추가하기
1) 기존 클래스에 코드를 추가하는 방법
- 기존 코드를 변경하게 되면 원래 되던 기능도 오류가 발생할 수 있는 위험이 있다.
- 그래서 원래 코드를 손대는 것은 매우 위험한 일이다.
- 기존에 잘 되던 기능까지 동작이 안되는 문제가 발생할 수 있기 때문이다.
2) 기존 코드를 복사하여 새 클래스를 만드는 방법
- 장점
- 기존 코드를 손대지 않기 때문에 문제가 발생할 가능성은 줄인다.
- 단점
- 기존 코드의 크기가 큰 경우에는 복사 붙여넣기가 어렵다.
- 기존 클래스의 소스가 없는 경우에는 이 방법이 불가능하다.
엥? 다른 개발자가 배포한 라이브러리만 있는 경우를 말한다.
소스가 없는 다른 개발자가 만든 클래스에 기능을 덧 붙일 때는 이 방법이 불가능하다.
- 기존 코드에 버그가 있을 때 복사 붙여넣기 해서 만든 클래스도 영향을 받는다.
- 기존 코드를 변경했을 때 복사 붙여넣기 한 모든 클래스를 찾아 변경해야 한다.
3) 기존 코드를 상속 받아 기능을 추가하는 방법
- 장점
- 기존 클래스의 소스 코드가 필요 없다.
- 간단한 선언으로 상속 받겠다고 표시한 후 새 기능만 추가하면 된다.
- 단점
- 일부 기능만 상속 받을 수 없다.
- 쓰든 안쓰든 모든 기능을 상속 받는다.
public class Car {
public String model;
public String maker;
public int capacity;
// 필드를 추가한다.
public boolean sunroof;
public boolean auto;
public Car() {}
// 다른 예제에서 다음 생성자를 사용하기 때문에
// 이 생성자에 파라미터를 덧붙일 수는 없다.
public Car(String model, String maker, int capacity) {
this.model = model;
this.maker = maker;
this.capacity = capacity;
}
// 새로 생성자를 추가해야 한다.
public Car(String model, String maker, int capacity,
boolean sunroof, boolean auto) {
// 이 클래스의 다른 생성자를 먼저 호출할 수 있다.
// => 이때 this()를 사용한다.
this(model, maker, capacity);
this.sunroof = sunroof;
this.auto = auto;
}
}
public class Exam02 {
public static void main(String[] args) {
Calculator c1 = new Calculator();
c1.plus(5);
c1.multiple(2);
c1.minus(2);
c1.divide(4);
System.out.println(c1.result);
}
}
// 기존의 클래스에 코드를 추가한다.
package com.eomcs.oop.ex05.b;
public class Calculator {
public int result;
public void plus(int value) {
this.result += value;
}
public void minus(int value) {
this.result -= value;
}
// 새 기능을 기존 클래스에 추가한다.
//
public void multiple(int value) {
this.result *= value;
}
public void divide(int value) {
this.result /= value;
}
}
public class Exam03 {
public static void main(String[] args) {
Score s = new Score();
s.name = "홍길동";
s.kor = 100;
s.eng = 100;
s.math = 100;
s.music = 100;
s.art = 100;
s.compute();
System.out.printf("%s: %d(%.1f)\n", s.name, s.sum, s.aver);
}
}
// 기존의 클래스에 코드를 추가한다.
package com.eomcs.oop.ex05.b;
public class Score {
public String name;
public int kor;
public int eng;
public int math;
public int sum;
public float aver;
// 새 필드를 추가한다.
public int music;
public int art;
// 기존 코드를 변경한다.
public void compute() {
this.sum = this.kor + this.eng + this.math + this.music + this.art;
this.aver = this.sum / 5f;
}
}
상속 - 상속하지 않고 기능 추가 II
새 프로젝트에서는 제조사, 모델명, 수용인원 외에 썬루프 장착여부, 자동변속 여부를 추가적으로 저장하고 싶다!
방법2) 기존 코드를 복사하여 새 클래스(Car2)를 만든다.
문제점:
=> 같은 일을 하는 여러 클래스가 존재(코드가 중복됨)하게 되면 관리하기가 힘들다!
=> 만약 원본 코드에 버그가 있으면 버그도 복사하게 된다.
따라서 버그를 고칠 때는 복사한 모든 소스 파일을 찾아 고쳐야 한다.
package com.eomcs.oop.ex05.c;
public class Exam01 {
public static void main(String[] args) {
Car2 c = new Car2("비트자동차", "티코", 5, true, true);
}
}
// 기존 코드를 복사하여 새 클래스를 만든 후에 코드를 추가했다.
package com.eomcs.oop.ex05.c;
public class Car2 {
public String model;
String maker;
public int capacity;
public boolean sunroof;
public boolean auto;
public Car2() {}
public Car2(String model, String maker, int capacity) {
this.model = model;
this.maker = maker;
this.capacity = capacity;
}
public Car2(String model, String maker, int capacity,
boolean sunroof, boolean auto) {
this(model, maker, capacity);
this.sunroof = sunroof;
this.auto = auto;
}
}
public class Exam02 {
public static void main(String[] args) {
Calculator2 c1 = new Calculator2();
c1.plus(5);
c1.multiple(2);
c1.minus(2);
c1.divide(4);
System.out.println(c1.result);
}
}
// 기존 코드를 복사하여 새 클래스를 만든 후에 코드를 추가했다.
package com.eomcs.oop.ex05.c;
public class Calculator2 {
public int result;
public void plus(int value) {
this.result += value;
}
public void minus(int value) {
this.result -= value;
}
// 새 기능을 기존 클래스에 추가한다.
//
public void multiple(int value) {
this.result *= value;
}
public void divide(int value) {
this.result /= value;
}
}
public class Exam03 {
public static void main(String[] args) {
Score2 s = new Score2();
s.name = "홍길동";
s.kor = 100;
s.eng = 100;
s.math = 100;
s.music = 100;
s.art = 100;
s.compute();
System.out.printf("%s: %d(%.1f)\n", s.name, s.sum, s.aver);
}
}
public class Score2 {
public String name;
public int kor;
public int eng;
public int math;
public int sum;
public float aver;
// 새 필드를 추가한다.
public int music;
public int art;
// 기존 코드를 변경한다.
public void compute() {
this.sum = this.kor + this.eng + this.math + this.music + this.art;
this.aver = this.sum / 5f;
}
}
상속 - 상속 문법을 이용한 기능 추가
새 프로젝트에서는 제조사, 모델명, 수용인원 외에 썬루프 장착여부, 자동변속 여부를 추가적으로 저장하고 싶다!
방법3) 상속을 이용하여 기능을 추가한 클래스를 사용한다.
장점:
=> 기존 코드에 문제가 있으면 그 코드를 수정하는 순간 그 코드를 상속 받아 만든 모든 클래스에 자동으로 적용된다.
=> 기존 코드를 손대지 않기 때문에 새 기능을 추가하더라도 기존 기능에 문제가 발생할 가능성이 거의 없다.
=> 소스 코드의 유지보수가 쉽다.
package com.eomcs.oop.ex05.d;
public class Exam01 {
public static void main(String[] args) {
Sedan c = new Sedan("티코", "비트자동차", 5, true, true);
}
}
이것이 상속이라는 문법이 등장한 이유이다.
=> 즉 기존 코드의 수정을 최소화하면서 새 기능을 추가하는 방법!
=> 기존 기능을 재작성하지 않고 다시 사용할 수 있게 만드는 문법이다.
=> 코드 재사용성을 높인다.
// 기존의 클래스를 손대지 않고 새 기능만 추가한다.
// 어떻게? 상속 문법을 이용한다.
package com.eomcs.oop.ex05.d;
// 상속
// => 기존 클래스의 코드를 손대지 않고 기능을 확장하게 도와주는 문법이다.
// => 재사용할 기존 클래스를 지정한다.
// => 새 클래스에는 추가할 기능을 덧붙인다.
public class Sedan extends Car { // "Sedan을 통해서 Car의 기능을 사용할 수 있다"
// 인스턴스 변수 추가
public boolean sunroof;
public boolean auto;
public Sedan(String model, String maker, int capacity,
boolean sunroof, boolean auto) {
this.model = model;
this.maker = maker;
this.capacity = capacity;
this.sunroof = sunroof;
this.auto = auto;
}
}
public class Exam02 {
public static void main(String[] args) {
Calculator2 c1 = new Calculator2();
c1.plus(5); // Calculator의 메서드이다.
c1.multiple(2); // Calculator2의 메서드이다.
c1.minus(2); // Calculator의 메서드이다.
c1.divide(4); // Calculator2의 메서드이다.
System.out.println(c1.result);
}
}
// 기존의 클래스를 손대지 않고 새 기능만 추가한다.
// 어떻게? 상속 문법을 이용한다.
package com.eomcs.oop.ex05.d;
public class Calculator2 extends Calculator {
// 상속은 기존 코드를 자동으로 복사해오는 것이 아니다! 아니다! 아니다!
// 아니다! 아니다! 아니다! 아니다! 아니다! 아니다! 아니다!
/*
public int result;
public void plus(int value) {
this.result += value;
}
public void minus(int value) {
this.result -= value;
}
*/
// 새 기능을 추가한다.
//
public void multiple(int value) {
this.result *= value;
}
public void divide(int value) {
this.result /= value;
}
}
// 상속 - 상속 문법을 이용한 기능 추가
package com.eomcs.oop.ex05.d;
public class Exam03 {
public static void main(String[] args) {
Score2 s = new Score2();
s.name = "홍길동";
s.kor = 100;
s.eng = 100;
s.math = 100;
s.music = 100;
s.art = 100;
s.compute();
System.out.printf("%s: %d(%.1f)\n", s.name, s.sum, s.aver);
}
}
// 기존의 클래스를 손대지 않고 새 기능만 추가한다.
// 어떻게? 상속 문법을 이용한다.
package com.eomcs.oop.ex05.d;
public class Score2 extends Score {
// 새 필드를 추가한다.
public int music;
public int art;
// 기존 코드를 변경한다. => 기존의 메서드 재정의 => 오버라이딩(overriding)
@Override // 컴파일러에게 재정의를 제대로 했는지 검사해달라고 서비스를 요청하는 명령
public void compute() {
this.sum = this.kor + this.eng + this.math + this.music + this.art;
this.aver = this.sum / 5f;
}
}
상속의 실제적인 의미
public class Exam01 {
public static void main(String[] args) {
B obj = new B();
// B 인스턴스를 이용하여 B가 사용권을 획득한 A 클래스의 메서드를 호출할 수 있다.
obj.m1(); // A 클래스의 m1() 호출
obj.m2(); // B 클래스의 m2() 호출
}
}
실험:
bin/main/.../A.class 파일을 제거한 후 다시 실행하라!
=> 결과는?
A 클래스가 없다고 예외가 발생한다.
=> 의미?
상속 받는다는 것은 수퍼 클래스의 코드를 그대로 복제해 온다는 것이 아니다.
그냥 수퍼 클래스의 코드를 사용할 수 있는 권한을 획득한다는 것이다.
그래서 서브 클래스를 사용하려면
반드시 서브 클래스가 상속 받는 모든 조상 클래스가 있어야 한다.
public class A {
void m1() {
System.out.println("A.m1()");
}
}
public class B extends A {
// 상속이란? 수퍼 클래스의 사용권을 획득하는 것이다.
// 그래서 내 것처럼 사용하는 것이다.
public void m2() {
System.out.println("B.m2()");
}
}
public class Exam02 {
public static void main(String[] args) {
D obj = new D();
obj.m4(); // obj 레퍼런스의 클래스에서 m4()를 찾아 호출한다.
obj.m3(); // obj 레퍼런스의 클래스(D)에서 m3()를 찾아보고 없다면 수퍼 클래스에서 찾는다.
obj.m2(); // 만약 D의 수퍼 클래스에서도 못찾는다면 그 위의 클래스에서 찾아본다.
obj.m1(); // 그 위에 클래스에서도 없다면 더 위에 클래스에서 찾아본다.
// obj.m0(); // 더 위에 있는 클래스에서도 찾을 수 없다면 컴파일 오류이다!
}
}
즉 메서드를 호출할 때는 클래스 상속 관계에 따라 레퍼런스의 클래스에서 시작하여 상위 클래스로 찾아간다.
그래서 클래스를 사용하려면 그 클래스가 상속 받는 상위 클래스들이 모두 있어야 한다.
public class C extends B {
// 상속이란? 수퍼 클래스의 사용권을 획득하는 것이다.
// 그래서 내 것처럼 사용하는 것이다.
public void m3() {
System.out.println("C.m3()");
}
}
public class D extends C {
// 상속이란? 수퍼 클래스의 사용권을 획득하는 것이다.
// 그래서 내 것처럼 사용하는 것이다.
public void m4() {
System.out.println("D.m4()");
}
}
상속 - 클래스 로딩과 인스턴스 생성 과정
package com.eomcs.oop.ex05.f;
public class Exam01 {
public static void main(String[] args) {
// B 클래스의 설계도에 따라 Heap 영역에 변수를 준비한다.
// - B 클래스는 A 클래스에 기능을 덧붙인 것이라 선언했기 때문에
// A 클래스의 설계도에 따라 A 클래스에 선언된 인스턴스 변수도 함께 생성된다.
B obj = new B();
obj.v2 = 200; // B 클래스 설계도에 따라 만든 변수
obj.v1 = 100; // A 클래스 설계도에 따라 만든 변수
System.out.printf("v2=%d, v1=%d\n", obj.v2, obj.v1);
System.out.println("---------------------------------");
// 클래스는 오직 한 번만 로딩된다.
// - 그래서 static 블록도 위에서 한 번 실행되면 다시 실행하지 않는다.
//
B obj2 = new B();
obj2.v2 = 2000;
obj2.v1 = 1000;
System.out.printf("v2=%d, v1=%d\n", obj2.v2, obj2.v1);
// B 클래스의 인스턴스 생성 과정
// 1) B의 수퍼 클래스가 로딩되어 있지 않다면, 수퍼 클래스(A 클래스)를 먼저 로딩한다.
// - 스태틱 필드 생성한 후
// - 스태틱 블록 실행한다.
// 2) 그런 후 B 클래스를 로딩한다.
// - 스태틱 필드 생성한 후
// - 스태틱 블록 실행한다.
// 3) 인스턴스 필드 생성
// - 수퍼 클래스의 인스턴스 필드부터 생성한다.
// v1 | v2 : A의 v1 필드 생성, B의 v2 필드 생성
// 0 | 0 : 각 필드를 기본 값으로 설정한다.
// 100 | 0 : A 클래스의 생성자 수행
// 100 | 200 : B 클래스의 생성자 수행
//
// - B 클래스의 인스턴스는 수퍼 클래스의 인스턴스 필드도 포함한다.
//
// 인스턴스 생성 절차 정리!
// 1) 상속 받은 수퍼 클래스를 먼저 메모리에 로딩한다.
// 이미 로딩되어 있다면 다시 로딩하지는 않는다.
// 2) 그런 후 해당 클래스를 메모리에 로딩한다.
// 마찬가지로 이미 로딩되어 있다면 다시 로딩하지는 않는다.
// 3) 수퍼 클래스에 선언된 대로 인스턴스 변수를 Heap에 만든다.
// 4) 해당 클래스에 선언된 대로 인스턴스 변수를 Heap에 만든다.
// 5) 수퍼 클래스부터 생성자를 실행하며 해당 클래스까지 내려온다.
// 그래서 인스턴스를 생성할 때는 항상 상속 받아야 하는 클래스 파일이 모두 있어야 한다.
// 테스트 하는 방법?
// => A.class 파일을 제거하고 실행해 보라!
}
}
용어 정리!
상속(inheritance)
- 기존에 만든 클래스를 자신의 코드처럼 사용하는 기법이다.
- 보통 기존 코드를 손대지 않고 새 코드를 덧붙일 때 많이 사용한다.
수퍼클래스(super class) = 부모클래스(parents class)
- B 클래스 입장에서, B 클래스에게 상속 해주는 A 클래스를 말한다.
서브클래스(sub class) = 자식클래스(child class)
- A 클래스 입장에서, A 클래스가 상속해주는 B 클래스를 말한다.
즉 수퍼 클래스나 서브 클래스는 상대적인 개념이다.
public class A {
int v1;
static {
System.out.println("A클래스의 static{} 실행!");
}
}
public class B extends A {
int v2;
static {
System.out.println("B클래스의 static{} 실행!");
}
}
상속
- 재사용할 기존 클래스를 지정한다.
- 새 클래스에는 추가할 기능을 덧붙인다.
주의! 개발 입문자들이 가장 많이 착각하는 것!
- 상속이라는 단어 때문에 개발 입문자들이 많이 오해한다.
"B 클래스가 A 클래스를 상속했기 때문에, B 클래스는 A 클래스의 코드를 갖고 있을 것이다."
- 아니다!
B 클래스는 단지 A 클래스의 링크 정보만 갖고 있다. 따라서 B 클래스를 사용하려면 반드시 A 클래스가 있어야 한다.
상속 - 생성자 호출 순서
package com.eomcs.oop.ex05.g;
public class Exam01 {
public static void main(String[] args) {
C obj = new C();
System.out.printf("v1=%d, v2=%d, v3=%d\n", obj.v1, obj.v2, obj.v3);
}
}
생성자 호출 순서
1) C 클래스의 생성자를 호출하면, 그 생성자의 첫 번째 문장이 수퍼 클래스의 생성자를 호출하는 명령이다.
그래서 수퍼 클래스인 B 클래스의 생성자를 호출한다.
2) B 클래스의 생성자를 호출하면, 그 생성자의 첫 번째 문장이 수퍼 클래스의 생성자를 호출하는 명령이다.
그래서 수퍼 클래스 A의 생성자를 호출한다.
3) A 클래스의 생성자를 호출하면, 그 생성자의 첫 번째 문장이 수퍼 클래스의 생성자를 호출하는 명령이다.
그래서 수퍼 클래스 Object의 생성자를 호출한다.
4) Object 클래스의 생성자를 호출하면, 더이상 수퍼 클래스가 없기 때문에 Object() 생성자를 실행한다.
그리고 이 생성자를 호출한 A 클래스의 생성자로 리턴한다.
5) A 클래스의 생성자를 실행한 후 이 생성자를 호출한 B 클래스의 생성자로 리턴한다.
6) B 클래스의 생성자를 실행한 후 이 생성자를 호출한 C 클래스의 생성자로 리턴한다.
7) C 클래스의 생성자를 실행한다.
public class A /*extends Object*/ {
int v1;
A() {
// 수퍼 클래스의 어떤 생성자를 호출할지 지정하지 않으면 컴파일러는
// 다음과 같이 수퍼 클래스의 기본 생성자를 호출하라는 명령을
// 생성자의 첫 줄에 추가한다.
super(); // 즉 개발자가 붙이지 않으면 자동으로 붙인다.
// 헐.. 강사님, A 클래스의 수퍼 클래스는 없는데요?
// => 클래스를 정의할 때 수퍼 클래스를 지정하지 않으면,
// 컴파일러는 자동으로 수퍼 클래스를 java.lang.Object 클래스로 지정한다.
// => 그래서 자바의 모든 클래스는 반드시 수퍼 클래스가 있으며,
// 자바의 모든 클래스는 java.lang.Object의 자손 클래스가 된다.
System.out.println("A() 생성자!");
this.v1 = 100;
}
}
public class B extends A {
int v2;
B() {
// 수퍼 클래스의 어떤 생성자를 호출할지 지정하지 않으면 컴파일러는
// 다음과 같이 수퍼 클래스의 기본 생성자를 호출하라는 명령을
// 생성자의 첫 줄에 추가한다.
super(); // 즉 개발자가 붙이지 않으면 자동으로 붙인다.
System.out.println("B() 생성자!");
this.v2 = 200;
}
}
public class C extends B {
int v3;
C() {
// 수퍼 클래스의 어떤 생성자를 호출할지 지정하지 않으면 컴파일러는
// 다음과 같이 수퍼 클래스의 기본 생성자를 호출하라는 명령을
// 생성자의 첫 줄에 추가한다.
// super(); // 즉 개발자가 붙이지 않으면 자동으로 붙인다.
System.out.println("C() 생성자!");
this.v3 = 300;
// 만약 개발자가 수퍼 클래스의 생성자를 호출하는 명령을 작성할 때
// 그 전에 다른 코드가 있다면 컴파일러는 오류를 발생시킨다.
// super(); // 따라서 수퍼 클래스 생성자를 호출하는 명령은 반드시 첫 문장으로 와야 한다.
}
}
상속 - 수퍼 클래스에 기본 생성자가 없을 때!
package com.eomcs.oop.ex05.h;
public class Exam01 {
public static void main(String[] args) {
B obj = new B();
System.out.printf("v1=%d, v2=%d\n", obj.v1, obj.v2);
}
}
public class A /*extends Object*/ {
int v1;
// A 클래스는 기본 생성자가 없다.
// => int 값을 받는 생성자만 있다.
//
A(int value) {
this.v1 = value;
System.out.println("A(int) 생성자!");
}
}
public class B extends A {
int v2;
B() {
// 수퍼 클래스의 어떤 생성자를 호출할지 지정하지 않으면 컴파일러는
// 다음과 같이 수퍼 클래스의 기본 생성자를 호출하라는 명령을 붙인다.
//
// super(); //만약 수퍼 클래스에 기본 생성자가 없으면 컴파일 오류가 발생한다!
// 해결 방법?
// - 개발자가 직접 수퍼 클래스에 있는 생성자를 호출하라!
super(100);
System.out.println("B() 생성자!");
}
}
상속 - 다중 상속
만약 C 클래스가 A와 B를 모두 상속 받을 수 있다면, C 클래스의 인스턴스를 만들 때 v2 변수는 A 설계도에 따라 float 메모리를 만들어야 하는가? 아니면 B 설계도에 따라 int 메모리를 만들어야 하는가?
메서드의 경우도 마찬가지이다. 다중 상속이 가능하다고 가정하자. 두 개의 수퍼 클래스에 같은 이름의 메서드가 있을 때, 어떤 메서드를 호출해야 하는가?
package com.eomcs.oop.ex05.i;
public class Exam01 {
public static void main(String[] args) {
C obj = new C(); // ?
// 그래서 자바는 클래스의 다중 상속을 지원하지 않는다.
}
}
public class A /*extends Object*/ {
int v1;
float v2;
}
public class B /*extends Object */ {
int v2;
}
// 자바는 다중 상속을 지원하지 않는다.
public class C extends A, B {
int v3;
}
상속 : specialization
package com.eomcs.oop.ex05.j;
// 자동차 기능이 필요해서 처음에 Car 클래스를 정의하여 사용했다.
//
public class Exam01 {
public static void main(String[] args) {
Car tico = new Car();
tico.run();
Car porter = new Car();
porter.run();
}
}
public class Car {
public String model;
public String maker;
public int capacity;
public Car() {}
public Car(String model, String maker, int capacity) {
this.model = model;
this.maker = maker;
this.capacity = capacity;
}
public void run() {
System.out.println("달린다!");
}
}
Car 클래스를 만든 이후에 승용차나 트럭에 대해 좀 더 섬세하게 제어하기 위해서 Car 클래스를 상속 받아 Sedan, Truck 클래스를 추가로 정의하게 되었다.
이렇게 기존 클래스에 기능을 덧붙여 특별한 클래스를 만드는 것
- 수퍼 클래스를 상속 받아 서브 클래스를 만드는 것을 "전문화(specialization)"라 부른다.
public class Exam02 {
public static void main(String[] args) {
// 이전에 만들었던 Car 클래스를 사용할 수도 있고,
Car generalCar = new Car();
generalCar.run();
// 좀 더 전문화해서 만든 Sedan, Truck 클래스도 사용할 수 있다.
Sedan s = new Sedan();
Truck t = new Truck();
s.doSunroof(true);
t.dump();
}
}
// 기존의 Car 클래스를 상속 받아 특별한 기능을 덧붙인다.
// - specialization 이라 한다.
//
public class Sedan extends Car {
@Override
public void run() {
System.out.println("쌩쌩 달린다.");
}
public void doSunroof(boolean open) {
if (open) {
System.out.println("썬루프를 연다.");
} else {
System.out.println("썬루프를 닫는다.");
}
}
}
public class Truck extends Car {
@Override
public void run() {
System.out.println("덜컹 덜컹 달린다.");
}
public void dump() {
System.out.println("짐을 내린다.");
}
}
상속 - Generalization 수행 전
상속의 종류
1) specialization
=> 가장 많이 사용하는 방법으로 수퍼 클래스를 상속 받아 서브 클래스를 만드는 것이다.
=> 수퍼클래스에 새 특징을 추가하거나 새 기능을 추가하여 더 특별한 일을 수행하는 서브클래스를 만든다.
그래서 이런 상속을 "특수화/전문화(specialization)"이라 부른다.
2) generalization
=> 리팩토링 과정에 수행하는 방법이다.
=> 서브클래스들의 공통 분모를 추출하여 수퍼클래스를 정의하는 방법을 말한다.
=> 그래서 이런 상속을 "일반화/표준화(generalization)"이라 부른다.
Sedan 클래스와 Truck 클래스의 공통 분모를 추출하여 Car라는 클래스를 정의하고, 두 클래스는 이렇게 새로 만든 Car 클래스를 상속 받도록 한다.
package com.eomcs.oop.ex05.k;
public class Exam01 {
public static void main(String[] args) {
// 이렇게 Sedan과 Truck 클래스를 만들어 쓰다가 보니
// 두 클래스 사이에 공통 코드가 발견되었다.
// => 유지보수를 쉽게하기 위해 공통 코드를 추출하여 중복 코드를 없앨 필요가 있었다.
// => 다음 예제를 보라!
Sedan s = new Sedan();
Truck t = new Truck();
s.doSunroof(true);
t.dump();
}
}
public class Sedan {
public void start() {
System.out.println("시동 건다!");
}
public void shutdown() {
System.out.println("시동 끈다!");
}
public void run() {
System.out.println("쌩쌩 달린다.");
}
public void doSunroof(boolean open) {
if (open) {
System.out.println("썬루프를 연다.");
} else {
System.out.println("썬루프를 닫는다.");
}
}
}
public class Truck {
public void launch() {
System.out.println("시동 건다!");
}
public void stop() {
System.out.println("시동 끈다!");
}
public void go() {
System.out.println("덜컹 덜컹 달린다.");
}
public void dump() {
System.out.println("짐을 내린다.");
}
}
상속 - Generalization 수행 후
여러 클래스에 공통으로 들어 가는 기능이나 필드가 있다면 유지보수가 쉽도록 별도의 클래스로 추출한다. 그리고 상속 관계를 맺는다.
- Sedan과 Truck 사이에 공통 필드와 메서드가 있다.
공통 기능을 추출하여 별도의 클래스를 정의하는 것을 "일반화(generalization)"라 한다.
Sedan과 Truck의 공통 기능인 start(), shutdown(), run() 메서드를 추출하여 Car 클래스를 만들고 Sedan과 Truck은 이 클래스의 서브클래스가 된다.
start()와 shutdown() 은 Sedan이나 Truck 모두 같은 작업을 수행하기 때문에 상속 받은 것을 그대로 사용하며 되지만, run()은 Sedan과 Truck이 서로 다르게 작업하기 때문에 재정의(오버라이딩) 해야 한다.
package com.eomcs.oop.ex05.l;
public class Exam01 {
public static void main(String[] args) {
// Sedan과 Truck 두 클래스의 공통점을 추출하여 수퍼 클래스를 정의한 후에
// 두 클래스의 사용법이 크게 바뀌는 것은 아니다.
// 단지 유지보수가 좋아질 뿐이다.
Sedan s = new Sedan();
s.run();
Truck t = new Truck();
t.run();
// 이렇게 Car 클래스가 존재하면
// 어떤 개발자는 Car 클래스를 사용하려고 시도할 것이다.
// 문제는, Car 클래스의 목적이 소스 코드를 보다 쉽게 관리하기 위해 만든 클래스이지,
// 직접 사용하려고 만든 클래스가 아니다.
// 그럼에도 불구하고 다른 개발자가 다음과 같이 Car 클래스를 사용한다면
// 이를 막을 수 없다.
Car c = new Car();
// 사실 Car 클래스는 Sedan과 Truck에 공통으로 들어가는 코드를
// 좀 더 쉽게 관리하기 위해 추출하여 클래스로 만든 것이다.
// 이렇게 직접 사용하려고 만든 클래스가 아니다.
// 그럼에도 불구하고 위의 코드처럼
// Car 클래스의 인스턴스를 만드는 것을 막을 수가 없다.
// 이것을 막는 문법이 "추상클래스" 이다.
// 다음 패키지의 예제를 확인하라!
}
}
public class Car {
public Car() {
super();
}
public void start() {
System.out.println("시동 건다!");
}
public void shutdown() {
System.out.println("시동 끈다!");
}
public void run() {
System.out.println("달린다.");
}
}
public class Sedan extends Car {
public void run() {
System.out.println("쌩쌩 달린다.");
}
public void doSunroof(boolean open) {
if (open) {
System.out.println("썬루프를 연다.");
} else {
System.out.println("썬루프를 닫는다.");
}
}
}
public class Truck extends Car {
public void run() {
System.out.println("덜컹 덜컹 달린다.");
}
public void dump() {
System.out.println("짐을 내린다.");
}
}
추상클래스
상속 - 추상클래스
Car 클래스는 Sedan과 Truck의 공통 기능을 관리하기 위해 만든 클래스이다.
즉 Car 클래스는 직접 인스턴스를 만들어 사용하기 위해서 정의한 클래스가 아니라,
Sedan과 Truck에서 겹치는 코드를 공통 관리하기 위해 만든 클래스이다.
가능한 직접 Car 클래스를 사용하는 것을 피해야 한다.
package com.eomcs.oop.ex05.m;
public class Exam01 {
public static void main(String[] args) {
// => 다음과 같이 Car 클래스를 사용하는 것을 막기 위해 추상 클래스 문법이 등장하였다.
// => 만약 추상 클래스의 인스턴스를 만들려고 한다면 컴파일 오류가 발생할 것이다.
// Car c = new Car(); // 컴파일 오류!
// generalization 과정에서 정의한 수퍼 클래스에 대해
// 직접 사용하지 못하도록 추상클래스로 선언한다.
Sedan s = new Sedan();
Truck t = new Truck();
}
}
이렇게 Sedan과 Truck의 경우처럼 여러 클래스의 공통점을 추출하여 수퍼 클래스를 정의하는 경우, 그 수퍼 클래스의 목표는 서브 클래스의 공통 기능을 물려주는 것이다. 처음부터 Car를 먼저 만들어 쓰다가 더 특별한 클래스가 필요해서 Sedan이나 Truck을 만든 것이 아니라, 여러 클래스를 사용하다 보니 공통 분모가 보여서, 소스 코드의 유지보수가 쉽도록
한 클래스로 모아두기 위해 만든 경우는 해당 클래스를 직접 사용할 이유가 없다.
특히 generalization을 수행하여 만든 수퍼 클래스의 경우 직접 사용할 목적으로 만든 클래스가 아니다. 단지 서브 클래스에 공통 기능을 물려주기 위해 존재하는 클래스이다. 이런 클래스들은 직접 사용하지 못하게 막는 것이 좋다.
클래스를 직접 사용하지 못하게 막고 단지 서브 클래스를 만들어 사용하도록 제한하는 문법이 "추상 클래스" 이다.
추상 클래스
=> 서브클래스에게 공통 기능을 상속해주는 목적으로 만든 클래스이다.
=> 직접 사용하지 않는 클래스이다.
=> 즉 개발자에게 이 클래스를 상속 받아 새 클래스를 만들어 쓰라는 의미다!
=> 보통 '일반화(generalization)' 과정에서 생성되는 클래스를 추상 클래스로 만든다.
=> 문법:
abstract class 클래스명 {...}
public abstract class Car {
public Car() {
super();
}
public void start() {
System.out.println("시동 건다!");
}
public void shutdown() {
System.out.println("시동 끈다!");
}
public void run() {
System.out.println("달린다.");
}
}
public class Sedan extends Car {
public void run() {
System.out.println("쌩쌩 달린다.");
}
public void doSunroof(boolean open) {
if (open) {
System.out.println("썬루프를 연다.");
} else {
System.out.println("썬루프를 닫는다.");
}
}
}
public class Truck extends Car {
public void run() {
System.out.println("덜컹 덜컹 달린다.");
}
public void dump() {
System.out.println("짐을 내린다.");
}
}
여러 클래스에 공통으로 들어 가는 기능이나 필드가 있다면,
1) 유지보수가 쉽도록 별도의 클래스로 추출한다.
- Sedan과 Truck 사이에 공통 필드와 메서드가 있다.
- 공통 기능을 추출하여 별도의 클래스를 정의하는 것을 "일반화(generalization)"라 한다.
2) 그리고 상속 관계를 맺는다.
- 수퍼 클래스를 추출한 후 수퍼 클래스를 상속 받으면 된다.
상속 - 추상 메서드
Car 클래스의 start()와 shutdown()은 서브 클래스에서 그대로 받아 사용해도 된다.
그러나 run() 메서드는 서브 클래스마다 자신의 특징에 맞춰 재정의해야 한다.
그렇다면 굳이 수퍼 클래스에서 run() 메서드를 구현할 필요가 없지 않는가?
run()처럼 서브 클래스에서 무조건 재정의되어야 하는 메서드인 경우
즉 수퍼 클래스에서 정의하지 않고 서브클래스에 반드시 정의하도록 강제하는 문법이
"추상메서드(abstract method)"이다.
package com.eomcs.oop.ex05.n;
public class Exam01 {
public static void main(String[] args) {
Sedan car1 = new Sedan();
car1.run();
}
}
// 추상 클래스
// => 서브클래스에게 공통 기능을 상속해주는 목적으로 만든 클래스이다.
// => 직접 사용하지 않는 클래스이다.
// => 즉 개발자에게 이 클래스를 상속 받아 새 클래스를 만들어 쓰라는 의미다!
public abstract class Car {
public Car() {
super();
}
public void start() {
System.out.println("시동 건다!");
}
public void shutdown() {
System.out.println("시동 끈다!");
}
// 추상 메서드
// => 서브 클래스에서 재정의할 메서드라면 굳이 수퍼 클래스에서 구현하지 말라!
// => 또는 서브 클래스에서 반드시 구현하도록 강제하고 싶다면 추상 메서드로 선언한다.
// => 추상 메서드를 상속 받는 서브클래스는 반드시 구현해야 한다.
// 만약 구현하지 않으면 추상 메서드인채로 남아 있기 때문에
// 서브클래스도 추상클래스가 되어야 한다.
// 일반 클래스는 인스턴스를 생성하여 메서드를 호출하기 때문에
// 구현되지 않은 메서드를 갖는 것은 오류이다.
// 그래서 일반 클래스는 추상 메서드를 가질 수 없다.
// => 왜? 추상 메서드가 있다는 것은 해당 메서드를 실행할 수 없다는 것이고
// 실행할 수 없는 메서드를 갖는 클래스는
// 인스턴스를 생성해서는 안되기 때문에
// 추상메서드를 갖는 클래스는 반드시 추상클래스여야 한다.
// 일반 클래스는 추상메서드를 가질 수 없다.
//
public abstract void run();
}
public class Sedan extends Car {
@Override
public void run() {
System.out.println("쌩쌩 달린다.");
}
public void doSunroof(boolean open) {
if (open) {
System.out.println("썬루프를 연다.");
} else {
System.out.println("썬루프를 닫는다.");
}
}
}
public class Truck extends Car {
public void run() {
System.out.println("덜컹 덜컹 달린다.");
}
public void dump() {
System.out.println("짐을 내린다.");
}
}
'네이버클라우드 AIaaS 개발자 양성과정 1기 > Java' 카테고리의 다른 글
[비트캠프] 51일차(11주차1일) - 해커톤: 자유 주제 서비스 CRUD 만들기 (0) | 2023.01.16 |
---|---|
[비트캠프] 50일차(10주차5일) - Java(상속, specialization, generalization, 추상 클래스, 추상 메서드) (0) | 2023.01.13 |
[비트캠프] 49일차(10주차4일) - Java: (backend, frontend)-app-04~05, 상속, 생성자 (0) | 2023.01.12 |
[비트캠프] 48일차(10주차3일) - Java: (backend, frontend)-app-03 (0) | 2023.01.11 |
[비트캠프] 47일차(10주차2일) - Java: (backend, frontend)-app-02 (0) | 2023.01.10 |