7. 객체지향 프로그래밍(2)

메밀·2022년 11월 7일
0

자바의 정석

목록 보기
3/6

1. 상속(inheritance)

1) 상속의 정의와 장점

상속: 기존의 클래스를 재사용하여 새로은 클래스를 작성하는 것

class Child extends Parent{}

조상 클래스: 부모(parent)클래스, 상위(super)클래스, 기반(base)클래스
자손 클래스: 자식(child)클래스, 하위(sub)클래스, 파생된(derived)클래스

cf) 상속 계층도 → 벤다이어그램

만약 Parent 클래스에 age라는 정수형 변수를 멤버변수로 추가하면, 자손 클래스는 조상의 멤버를 모두 상속받기 때문에 child클래스에도 자동적으로 age라는 멤버변수가 추가된다.

그러나 Child 클래스에 새로운 메서드 등이 추가되어도 조상인 Parent 클래스엔 아무런 영향도 받지 않는다.

  • 생성자와 초기화블럭은 상속되지 않는다. 멤버만 상속된다.

  • 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.

  • 접근 제어자가 private 또는 default인 멤버들은 상속되지 않는다기보다, 상속은 받지만 자손 클래스로부터의 접근이 제한된다.

  • Parent 클래스를 상속받은 Child 클래스와 Child2 클래스 간에는 서로 아무런 관계도 성립되지 않는다. 한편, child 클래스를 상속받은 GrandChild 클래스가 있다고 가정하면, GrandChild 클래스는 Parent + Child 클래스의 멤버를 모두 상속 받는다.

  • 자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.

전체 프로그램을 구성하는 클래스들을 면밀히 설계 분석하여 클래스 간의 상속관계를 적절히 맺어주는 것이 객체지향 프로그래밍에서 가장 중요한 부분이다.

2) 클래스간의 관계 — 포함관계

상속 이외에도 클래스를 재사용하는 또 다른 방법 → 포함(Composite)
한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것

class Circle{
    int x;
    int y;
    int r;
}

class Point{
    int x;
    int y;
}

// ⭐️⭐️⭐️⭐️⭐️⭐️⭐️
class Circle{
    Point c = new Point(); // ⭐️
    int r;
}

3) 클래스 간의 관계 설정하기

상속: ~은 ~다. ex) 원은 점이다 → 어색
포함: ~은 ~를 가지고 있다. ex) 원은 점을 가지고 있다

class DeckTest{
    public static void main(String[] args) {
        Deck d = new Deck();
        Card c = d.pick();
        System.out.println(c);

        d.shuffle();
        c = d.pick(0); // 맨 위에 거 뽑기
        System.out.println(c);
    }
}

class Deck {
    final int CARD_NUM = 52;
    Card cardArr[] = new Card[CARD_NUM]; // Card와 포함관계

    Deck(){ // Deck의 카드를 초기화한다
        int i = 0;

        for(int k = Card.KIND_MAX; k > 0; k--){
            for(int n = 0; n < Card.NUM_MAX; n++){
                cardArr[i++] = new Card(k, n+1);
            }
        }
    }

    Card pick(int index){
        return cardArr[index];
    }

    Card pick(){
        int index = (int)(Math.random() * CARD_NUM);
        return pick(index);
    }

    void shuffle(){
        for(int i = 0; i < cardArr.length; i++){
            int r = (int)(Math.random() * CARD_NUM);

            Card tmp = cardArr[i];
            cardArr[i] = cardArr[r];
            cardArr[r] = tmp;
        }
    }
}

class Card{
    static final int KIND_MAX = 4; // 무늬의 수
    static final int NUM_MAX = 13; // 무늬 별 카드의 수

    static final int SPADE = 4;
    static final int DIAMOND = 3;
    static final int HEART = 2;
    static final int CLOVER = 1;
    int kind;
    int number;

    Card(){
        this(SPADE, 1);
    }
    
    Card(int kind, int number){
        this.kind = kind;
        this.number = number;
    }
    
    public String toString(){
        String[] kinds = {"", "CLOVER", "HEART", "SPADE"};
        String numbers = "0123456789XJQR"; // 숫자 10은 X로 표현

        return "kind: " + kinds[this.kind] + ", number: " + numbers.charAt(this.number);
    }
}

4) 단일 상속

자바는 오직 단일 상속만을 허용

5) Object 클래스 — 모든 클래스의 조상

Object 클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스

다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object 클래스로부터 상속받음

자바의 모든 클래스들은 Object 클래스의 멤버를 상속받기 때문에 Object클래스에 정의된 멤버들을 사용할 수 있다. 그동안 toString()이나 equals()와 같은 메서드를 따로 정의하지 않고도 쓸 수 있었던 것은 이 때문.

2. 오버라이딩

1) 오버라이딩이란?

조상클래스로부터 상속받은 메서드의 내용을 변경하는 것

2) 오버라이딩의 조건

자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와 이름, 매개변수, 반환타입이 같아야 한다
cf) 공변반환타입: 반환타입을 자손클래스 타입으로 변경하는 것 가능

— 조상클래스의 메서드를 자손 클래스에서 오버라이딩 할 때

  • 접근제어자를 조상클래스의 메서드보다 좁은 범위로 변경할 수 없다
  • 예외는 조상클래스의 메서드보다 많이 선언할 수 없다
    → 단순히 선언된 예외의 개수가 아니라 모든 예외의 최고 조상인 Exception 같은 게 X
  • 인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.
    cf) 조상클래스의 static 메서드를 자손 클래스에서 똑같은 이름으로 정의 가능.
    그러나 이건 각 클래스에 별개의 메서드를 정의한 것일 뿐 오버라이딩 X.

3) 오버로딩, 오버라이딩

생략

4) super

자손클래스에서 조상클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수
상속받은 멤버가 자신의 멤버와 이름이 같을 때 super를 붙여 구별
변수 뿐 아니라 메서드 역시 super를 써서 호출할 수 있다. 오버라이딩된 메서드를 호출할 때 유용

class Point{
    int x;
    int y;

    String getLocation(){
        return "x: " + x + "y: " + y;
    }
}

class Point3D extends Point{
    int z;
    String getLocation(){
        // return "x: " + x + "y: " + y + "z: " + z;
        return super.getLocation() + ", z: " + z; // ★★★
    }
}

5) super() — 조상 클래스의 생성자

조상클래스의 생성자 호출
첫줄에서 호출해야함 → 조상의 멤버들이 먼저 초기화되어 있어야 함

Object 클래스를 제외한 모든 클래스의 생성자 첫 줄에 this() 또는 super()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동적으로 super()를 생성자의 첫줄에 삽입한다.
인스턴스를 생성할 떈 클래스를 선택하는 것만큼 생성자를 선택하는 것도 중요하다.

class Point {
    int x;
    int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    String getLocation(int x, int y){
        return "x: " + x + ", y: " + y;
    }
}

class Point3D extends Point {
    int z;

    Point3D(int x, int y, int z){
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    String getLocation() {
        // return "x: " + x + "y: " + y + "z: " + z;
        return super.getLocation() + ", z: " + z;
    }
}

public class Lab {
    public static void main(String[] args) {

    }
}

컴파일 에러. Point3D클래스의 생성자에서 조상 클래스의 생성자 Point()를 찾을 수 없기 때문.

Point3D의 생성자 첫 줄이 생성자(this()든 super()든)를 호출하는 문장이 아니기에 컴파일러가 자동적으로 super();를 호출한다. 그러나 Point 클래스에 Point()가 정의되어 있지 않기 때문에 컴파일 에러

수정
Point 클래스에 Point()를 추가
Point3D(int x, int y, int z)의 첫줄에서 Point(int x, int y)를 호출 → super(x, y);

조상클래스의 멤버변수는 이처럼 조상의 생성자에 의해 초기화되도록 해야 한다.

3. package와 import

1) 패키지(package)

패키지: 클래스의 묶음. 패키지엔 클래스나 인터페이스를 포함시킬 수 있으며 서로 관련된 클래스들끼리 그룹 단위로 묶어서 클래스를 효율적으로 관리

클래스가 물리적으로 하나의 파일이듯(.class) 패키지는 물리적으로 하나의 디렉토리

하나의 소스파일엔 첫번째 문장으로 단 한 번의 패키지 선언만을 허용
모든 클래스는 반드시 하나의 패키지에 속해야 한다.
패키지는 점(.)을 구분자로 계층구조로 구성할 수 있다
패키지는 물리적으로 클래스파일(.class)을 포함하는 하나의 디렉토리

2) 패키지의 선언

클래스나 인터페이스의 소스파일(.java) 맨 위에

package 패키지명;

3) import문

클래스의 코드를 작성하기 전에 import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스 코드에 사용되는 클래스 이름에서 패키지명은 생략 가능

4) import문의 선언

import 패키지명.클래스명;
import 패키지명.*;
→ 지정된 패키지의 모든 클래스를 패키지 명 없이 사용 가능

5) static import문

static 멤버를 호출할 때 클래스 이름 생략 가능

4. 제어자(modifier)

1) 제어자란?

클래스, 변수, 메서드의 선언부에 사용되어 부가적인 의미를 부여한다.

접근 제어자: public, protected, default, private
그 외: static, final, abstract, native, transient, synchronized, volatile, strictfp

2) static — 클래스의, 공통적인

멤버 변수메서드
모든 인스턴스에 공통적으로 사용하는 클래스 변수가 된다
클래스 변수는 인스턴스를 생성하지 않고도 사용 가능하다
클래스가 메모리에 로드될 때 생성된다
인스턴스를 생성하지 않고도 호출이 가능한 static 메서드가 된다.
static 메서드 내에서는 인스턴스 멤버들을 직접 사용할 수 없다
cf) static 초기화블럭: 클래스가 메모리에 로드될 때 단 한 번만 수행, 주로 클래스 변수의 초기화에 사용

3) final — 마지막의, 변경될 수 없는

  • 클래스: 변경될 수 없는 클래스, 확장할 수 없는 클래스. 다른 클래스의 조상이 될 수 없음
  • 메서드: 오버라이딩 불가
  • 멤버변수, 지역변수: 값 변경이 불가능한 상수

생성자를 이용한 final 멤버의 초기화
final 변수는 상수이므로 일반적으로 선언과 초기화를 동시에 하지만, 인스턴스 변수의 경우 생성자에서 초기화 가능
→ 이 기능을 활용하면 각 인스턴스마다 final이 붙은 멤버변수가 다른 값을 갖도록 하는 것이 가능하다.

4) abstract — 추상의, 미완성의

  • 추상메서드: 메서드의 선언부만 작성하고 실제 수행 내용 구현X
  • 클래스에 사용되어 클래스 내에 추상메서드가 존재함을 알림
  • 추상클래스는 미완성이므로 인스턴스 생성 불가
  • 이 클래스 자체로는 쓸모가 없지만, 다른 클래스가 이 클래스를 상속 받아서 일부의 원하는 메서드만 오버라이딩 해도 된다는 장점이 있다.

5) 접근 제어자

제어자같은 클래스같은 패키지자손 클래스전체
publicOOOO
protectedOOO
(default)OO
privateO
  • 클래스: public, (default)
  • 메서드, 멤버변수: public, protected, (default), private

— 접근제어자를 이용한 캡슐화

⭐️ 클래스나 (특히)멤버에 접근 제어자를 사용하는 이유 ⭐️

  • 외부로부터 데이터 보호 → 데이터 감추기(data hiding), 객체 지향 개념의 캡슐화(encapsulation)
  • 외부엔 불필요한, 내부적으로만 사용되는 부분을 감추기 위해
  • 멤버변수를 private이나 protected로 제한하고 멤버변수의 값을 읽고 변경할 수 있는 public 메서드를 제공함으로서 간접적으로 멤버변수의 값을 다루는 것이 바람직
    - getter: get으로 시작하는 메서드는 단순히 멤버변수의 값을 반환
    - setter: set으로 시작하는 메서드는 매개변수에 지정된 값을 검사하여 조건에 맞는 값일 때만 멤버변수의 값을 변경
  • 만일 상속을 통해 확장될 것이 예상되는 클래스라면 private 대신 protected
public class TimeTest {
    public static void main(String[] args) {
        Time t = new Time(12, 35, 30);
        System.out.println(t);
        // t.hour = 13; // private 변수라 접근 불가
        t.setHour(t.getHour() + 1);
        System.out.println(t); //  System.out.println(t.toString()); 과 같다.
    }
}

class Time {
    private int hour, minute, second;

    Time(int hour, int minute, int second) {
        setHour(hour);
        setMinute(minute);
        setHour(second);
    }

    public int getHour() {
        return hour;
    }

    public void setHour(int hour) {
        if (hour < 0 || hour > 23) return;
        this.hour = hour;
    }

    public int getMinute() {
        return minute;
    }

    public void setMinute(int minute) {
        if (minute < 0 || minute > 59) return;
        this.minute = minute;
    }

    public int getSecond() {
        return second;
    }

    public void setSecond(int second) {
        if (second < 0 || second > 59) return;
        this.second = second;
    }

    public String toString() {
        return hour + ": " + minute + ": " + second;
    }
}

— 생성자의 접근 제어자(Singleton)

생성자의 접근제어자를 private으로 지정하면 외부에서 생성자에 접근할 수 없으므로 인스턴스를 생성할 수 없게 된다. 그래도 클래스 내부에선 인스턴스 생성 가능

대신 인스턴스를 생성해서 반환해주는 public 메서드를 제공함으로써 외부에서 이 클래스의 인스턴스를 사용하도록 할 수 있다. 이 메서드는 public static이어야 한다.

생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다. 왜냐면 자손 클래스의 인스턴스를 생성할 때 조상 클래스의 생성자를 호출해야 하는데, 조상 생성자의 접근제어자가 private이므로 호출 불가
→ final 클래스로 선언하여 상속 불가능한 클래스임을 알리는 것이 좋다.

cf) Math 클래스의 생성자가 private

public class SingletonTest {
    public static void main(String[] args) {
        // Singleton s = new Singleton();
        Singleton s = Singleton.getInstance();
    }
}

final class Singleton{
    private static Singleton s = new Singleton();

    private Singleton(){
        // ...
    }

    public static Singleton getInstance(){
        if(s == null)
            s = new Singleton();
        return s;
    }
}

6) 제어자의 조합

대상사용가능한 제어자
클래스public, (default), final, abstract
메서드모든 접근 제어자, final, abstract, static
멤버변수모든 접근 제어자, final, static
지역변수final

— 제어자를 조합 유의사항

  • 메서드에 static과 abstract를 함께 사용할 수 없다.
    — static 메서드는 몸통이 있는 메서드에만 사용할 수 있기 때문
  • 클래스에 abstract와 final을 동시에 사용할 수 없다.
    — final은 확장할 수 없는데 abstract는 상속을 통해 완성해야한다는 의미이므로 모순
  • abstract 메서드의 접근 제어자가 private일 수 없다.
    — abstract는 자손 클래스에서 구현해주어야 하는데 접근제어자가 private이면 자손클래스에서 접근할 수 없기 때문
  • 메서드에 private과 final을 같이 사용할 필요는 없다.
    — 접근제어자가 private인 메서드는 오버라이딩될 수 없기 때문에 둘 중 하나만 사용해도 의미가 충분

5. 다형성

1) 다형성이란?

다형성: 여러 형태를 가질 수 있는 능력 → 한 타입의 참조변수로 여러 타입의 객체 참조
⇒ 조상클래스 타입의 참조변수로 자손클래스의 인스턴스 참조 가능

CaptionTv c = new CaptionTv();
Tv t = new CaptionTv();

이 경우 실제 인스턴스가 CaptionTv 타입이라고 할지라도 참조변수 t로는 CaptionTv 인스턴스의 모든 멤버를 사용할 수 없다. Tv 타입의 참조변수로는 CaptionTv 인스턴스 중 Tv클래스의 멤버들만 사용 가능.
⇒ 같은 타입의 인스턴스라도 참조변수의 타입에 따라 사용 가능한 멤버의 개수가 달라진다.

자손타입의 참조변수로 조상타입의 인스턴스 참조는 불가능.

자손타입의 참조변수가 사용할 수 있는 멤버 개수가 더 많기 때문.

2) 참조변수의 형변환

상속 관계에 있는 클래스 사이에는 참조변수의 형변환이 가능

자손 → 조상: up-casting, 형변환 생략 가능
조상 → 자손: down-casting, 형변환 생략 불가 ⇒ 캐스트 연산자 사용

Car타입의 참조변수 c를 Car 타입의 조상인 Object 타입의 참조변수로 형변환하는 것은 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 게 분명하므로 형변환 생략 가능.

자손타입으로의 형변환은 생략 불가. instanceof 연산자를 사용해서 참조변수가 참조하고 있는 실제 인스턴스의 타입을 확인하는 것이 안전.

⇒ 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조하는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것.

class Car{
    String color;
    int door;

    void drive(){
        System.out.println("drive~~~");
    }

    void stop(){
        System.out.println("stop");
    }
}

class FireEngine extends Car{
    void water(){
        System.out.println("water!");
    }
}
class CastingTest{
    public static void main(String[] args) {
        Car car = new Car();
        Car car2 = null;
        FireEngine fe = null;
        
        car.drive();
        fe = (FireEngine)car; // 여기!
        fe.drive();
        
    }
}

주석 ‘여기’ 부분에서 에러 발생. (ClassCastException)

조상타입의 인스턴스를 자손타입의 참조변수로 참조 X

Car 인스턴스의 멤버 개수보다 FireEngine이 사용할 수 있는 멤버 개수가 더 많음.

cf) car2 = (Car)fe;는 가능

서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.

3) instanceof 연산자

참조변수가 참조하고 있는 인스턴스의 실제 타입 알기

if(c instanceof FireEngine)
return boolean

class Car{
    String color;
    int door;

    void drive(){
        System.out.println("drive~~~");
    }

    void stop(){
        System.out.println("stop");
    }
}

class FireEngine extends Car{
    void water(){
        System.out.println("water!");
    }
}

class Lab{
    public static void main(String[] args) {
        FireEngine fe = new FireEngine();

        if(fe instanceof FireEngine){
            System.out.println("this is a fire engine instance.");
        }

        if(fe instanceof Car){
            System.out.println("this is a Car instance.");
        }

        if(fe instanceof Object){
            System.out.println("this is a Object instance.");
        }

        System.out.println(fe.getClass().getName()); // 클래스의 이름을 출력
    }
}

출력 결과
this is a fire engine instance.
this is a Car instance.
this is a Object instance.
class FireEngine

실제 인스턴스와 같은 타입의 instanceof 연산 이외에 조상타입의 instanceof 연산에도 true

instanceof 연산의 결과가 true라는 것은 검사한 타입으로 형변환을 해도 아무런 문제가 없다는 뜻

4) 참조변수와 인스턴스의 연결

메서드의 경우 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드가 호출되지만 멤버변수의 경우 참조변수의 타입에 따라 달라진다.

cf) static 메서드는 static 변수처럼 참조변수의 타입에 영향을 받는다. 참조변수의 타입에 영향을 받지 않는 것은 인스턴스 메서드 뿐이다. 그래서 static 메서드는 반드시 참조변수가 아닌 클래스이름.메서드로 호출해야 한다.

멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 땐 자손 클래스에 선언된 멤버변수가 사용된다.

class Parent{
    int x = 100;
    void method(){
        System.out.println("Parent method");
    }
}

class Child extends Parent{
    int x = 200;

    void method(){
        System.out.println("Child Method");
    }
}

class Lab{
    public static void main(String[] args) {
        Parent p = new Child();
        Child c = new Child();

        System.out.println("p.x = " + p.x); // 100 (parent의 변수)
        p.method(); // child method

        System.out.println("c.x = " + c.x); // 200 (child의 변수)
        c.method(); // child method
    }
}

참조변수 p와 c 모두 Child 인스턴스를 참조

메서드인 method()의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인 Child 클래스에 정의된 메서드가 호출되지만, 인스턴스 변수인 x는 참조변수의 타입에 따라 달라진다.

class Parent{
    int x = 100;

    void method(){
        System.out.println("Parent Method");
    }
}

class Child extends Parent{}

class Lab{
    public static void main(String[] args) {
        Parent p = new Child();
        Child c = new Child();

        System.out.println("p.x = " + p.x); // 100
        p.method(); // parent method

        System.out.println("c.x =" + c.x); // 100
        c.method(); // parent method
    }
}

child에 멤버가 없고 단순히 조상으로부터 멤버만 상속하므로 참조변수의 타입에 관계없이 조상의 멤버를 사용

⇒ 참조변수의 타입에 따라 결과가 달라지는 경우는 조상 클래스의 멤버변수와 같은 이름의 멤버변수를 자손 클래스에 중복해서 정의한 경우 뿐

class Lab{
    public static void main(String[] args) {
        Parent p = new Child();
        Child c = new Child();

        System.out.println("p.x = " + p.x);
        p.method();
        System.out.println();
        System.out.println("c.x =" + c.x);
        c.method();
    }
}

class Parent{
    int x = 100;

    void method(){
        System.out.println("parent method");
    }
}

class Child extends Parent{
    int x = 200;

    void method(){
        System.out.println("x = " + x); // this.x와 같다
        System.out.println("this.x = " + x);
        System.out.println("super.x = " + super.x);
    }
}
p.x = 100 x = 200 // child의 method()가 호출되어서 this.x인 200이 출력 this.x = 200 super.x = 100

c.x =200 x = 200 this.x = 200 super.x = 100

멤버변수들은 주로 private으로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버변수에 접근할 수 있도록 하지, 이 예제처럼 다른 외부 클래스에서 참조변수를 통해 직접적으로 인스턴스 변수에 접근할 수 있게 하지 않는다.

5) 매개변수의 다형성

참조변수의 다형적인 특징은 메서드의 매개변수에도 적용

class Product를 상속받는 Tv, Computer 등의 클래스가 있다고 가정하면, 매개변수에 다형성을 적용하여 간단히 메서드를 작성할 수 있다.

void buy(Product p){}

cf) b.buy(new Tv());

6) 여러 종류의 객체를 배열로 다루기

조상타입의 참조변수 배열을 사용하면 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.

import java.util.Vector;

class Product{
    int price;
    int bonusPoint;

    Product(int price){
        this.price = price;
        bonusPoint = (int)(price / 10.0);
    }

    Product(){
        price = 0;
        bonusPoint = 0;
    }
}

class Tv extends Product{
    Tv() {
        super(100);
    }

    @Override
    public String toString() {
        return "Tv{" +
                "price=" + price +
                ", bonusPoint=" + bonusPoint +
                '}';
    }
}

class Computer extends Product{
    Computer(){super(200);}

    @Override
    public String toString() {
        return "Computer{" +
                "price=" + price +
                ", bonusPoint=" + bonusPoint +
                '}';
    }
}

class Audio extends Product{
    Audio(){super(50);}

    @Override
    public String toString() {
        return "Audio{" +
                "price=" + price +
                ", bonusPoint=" + bonusPoint +
                '}';
    }
}

class Buyer{
    int money = 1000;
    int bonusPoint = 0;
    Vector item = new Vector(); // 구입한 제품을 저장할 vector 객체

    void buy(Product p){
        if(money < p.price){
            System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }

        money -= p.price;
        bonusPoint += p.bonusPoint;
        item.add(p);
        System.out.println(p + "을/를 구입하셨습니다.");
    }

    void refund(Product p){
        if(item.remove(p)){
            money += p.price;
            bonusPoint -= p.bonusPoint;
            System.out.println(p + "을/를 반품하셨습니다.");
        } else {
            // 제거 실패
            System.out.println("구입하신 제품 중 해당 제품이 없습니다.");
        }
    }

    void summary(){
        int sum = 0;
        String itemList = "";
        
        if(item.isEmpty()){ // vector가 비어있는지 확인
            System.out.println("구입하신 제품이 없습니다.");
            return;
        }

        // 반복문을 이용해서 구입한 물품의 총 가격과 목록을 만든다
        for(int i = 0; i < item.size(); i++){
            Product p = (Product) item.get(i); // vector의 i번째에 있는 객체를 얻어온다.
            sum += p.price;
            itemList += (i==0) ? "" + p : ", " + p;
        }

        System.out.println("구입하신 물품의 총 금액은 " + sum + "만원입니다.");
        System.out.println("구입하신 제품은 " + itemList + "입니다.");
    }
}

class Lab{
    public static void main(String[] args) {
        Buyer b = new Buyer();
        Tv tv = new Tv();
        Computer com = new Computer();
        Audio audio = new Audio();

        b.buy(tv);
        b.buy(com);
        b.buy(audio);
        b.summary();

        System.out.println();

        b.refund(com);
        b.summary();
    }
}

6. 추상클래스(abstract class)

1) 추상클래스란?

  • 미완성 설계도
  • 멤버 개수에 관계된 것이 아니라 미완성 메서드(추상메서드)를 포함하고 있다는 의미
  • 상속을 통해 자손 클래스에 의해 완성
  • 새로운 클래스를 작성하는데 있어 바탕이 되는 조상클래스

2) 추상메서드(abstract method)

abstract 리턴타입 메서드이름();

추상클래스를 상속받는 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상메서드를 모두 구현해주어야 한다.

3) 추상클래스의 작성

여러 클래스에 공통적으로 사용되는 클래스를 바로 작성하기도 하고, 기존 클래스의 공통적인 부분을 뽑아 추상클래스로 만들어 상속하는 경우도 있다.

  • 상속: 자손 클래스를 만드는데 조상 클래스를 사용하는 것
  • 추상화: 기존 클래스의 공통 부분을 뽑아 조상 클래스를 만드는 것

추상메서드 대신 void stop(){} 이렇게 아무 내용 없는 메서드로 작성하는 것도 가능.

그러나 자손 클래스에서 추상메서드를 반드시 구현하도록 강요하기 위해 abstract를 붙임

abstract class Player{
    // cd "player", vcr"player"

    boolean pause; // 일시정지 상태를 저장하기 위한 변수
    int currentPos; // 현재 play 되고 있는 위치를 저장하기 위한 변수

    Player(){
        pause = false;
        currentPos = 0;
    }

    /* 지정된 위치(pos)에서 재생을 시작하는 기능 수행*/
    abstract void play(int pos);

    /* 재생을 즉시 멈추는 기능 수행 */
    abstract void stop();

    void play(){
        if(pause){
            // pause가 true일 때 pause가 호출되면
            pause = false;
            play(currentPos);
        } else {
            pause = true;
            stop();
        }
    }
}

class CDPlayer extends Player{
    void play(int currentPos){
        // 조상의 추상메서드를 구현, 내용 생략
    }

    void stop(){
        // 조상의 추상메서드를 구현, 내용 생략
    }

    // cdPlayer 클래스에 추가로 정의된 멤버
    int currentTrack;

    void nextTrack(){
        currentTrack++;
    }

    void preTrack(){
        if(currentTrack > 1)
            currentTrack--;
    }
}

기존 클래스의 공통된 부분을 뽑아내어 추상 클래스를 만드는 것도 가능

7. 인터페이스(interface)

1) 인터페이스란?

  • 일종의 추상클래스
  • 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버 변수를 구성원으로 가질 수 없다.
  • 오직 추상메서드와 상수만을 멤버로

2) 인터페이스의 작성

interface 인터페이스이름 {
  public static final 타입상수이름 =;
  public abstract 메서드이름(파라미터);
}

public 또는 default가 가능

제약사항

  • 모든 멤버변수는 public static final이어야 하며, 이를 생략 가능
  • 모든 메서드는 public static이어야 하며, 이를 생략 가능
  • JDK1.8부터 static 메서드와 default 메서드 추가 허용
  • 인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에 제어자 생략 가능
  • 컴파일러가 자동 추가

3) 인터페이스의 상속

인터페이스는 인터페이스로부터만 상속받을 수 있음
다중상속 가능
최고조상 X
자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.

4) 인터페이스의 구현 - implements

class Fighter implements Fightable{}

구현하려는 인터페이스의 메서드 중 일부만 구현한다면 abstract를 붙여 추상클래스로 선언해야 함.

abstract class Fighter implements Fightable {}

5) 인터페이스를 이용한 다중상속

두 조상으로부터 상속받는 멤버 중 멤버 변수의 이름이 같거나 메서드의 선언부가 일치하고 구현내용이 다르다면, 이 두 조상으로부터 상속받는 자손클래스는 어느 조상을 상속받는 것인지 알 수 없다.
→ 자바가 다중상속을 허용하지 않는 이유

자바도 인터페이스를 이용하면 다중 상속이 가능하다고 하는 것일 뿐 자바에서 인터페이스로 다중상속을 구현하는 경우는 거의 없다.

6) 인터페이스를 이용한 다형성

자손클래스의 인스턴스를 조상타입의 참조변수로 참조하는 것이 가능하다.

인터페이스도 마찬가지로, 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조 가능, 인터페이스 타입으로 형변환도 가능

// Fightable을 Fighter가 구현했을 때

Fightable f = (Fightable)new Fighter();
Fightable f = new Fighter();

Fightable 타입의 참조변수로는 인터페이스 Fightable에 정의된 멤버만 호출 가능

인터페이스는 메서드의 매개변수로 사용 가능

→ 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공하라

→ attack(new Fighter())

리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미. 이 문장은 외울 때까지 반복해야 한다.

interface Parseable{
    // 구문 분석 작업 수행
    public abstract void parse(String fileName);
}

class ParserManager{
    // 리턴타입이 Parseable 인터페이스다
    public static Parseable getParser(String type){
        if(type.equals("XML")){
            return new XMLParser();
        } else {
            Parseable p = new HTMLParser();
            return p;
            // return new HTMLParser();
        }
    }
}

class XMLParser implements Parseable{
    public void parse(String fileName){
        System.out.println(fileName + " -- XML parsing is completed");
    }
}

class HTMLParser implements Parseable{
    public void parse(String fileName){
        System.out.println(fileName + " -- HTML parsing is completed");
    }
}

class Lab{
    public static void main(String[] args) {
        Parseable parser = ParserManager.getParser("XML"); // ★★★★★
        parser.parse("document.xml");
        parser = ParserManager.getParser("HTML");
        parser.parse("document2.html");
    }
}

ParserManager 클래스의 getParser 메서드는 매개변수로 넘겨받는 type의 값에 따라 XMLParser인스턴스 또는 HTMLParser 인스턴스를 반환한다.

0개의 댓글