개발/부트캠프

본캠프 : Java 문법 종합반 3주차_2

EJ EJ 2025. 1. 7. 20:55

1. 상속

클래스 간의 상속은 extends 키워드를 사용하여 정의할 수 있다.

public class 자식클래스 extends 부모클래스 {
}

  • 부모 클래스에 새로운 필드와 메서드가 추가되면 자식 클래스는 이를 상속받아 사용할 수 있다.
  • 자식 클래스에 새로운 필드와 메서드가 추가되어도 부모 클래스는 어떠한 영향도 받지 않는다.
  • 따라서 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.
  • Java는 다중 상속을 허용하지 않는다.
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(); // 엔진 Orion 부앙~

        // 자식 클래스 객체에서 부모 클래스 멤버 사용
        sportsCar.company = "GENESIS";
        sportsCar.setModel("GV80");
        System.out.println("sportsCar.company = " + sportsCar.company); // sportsCar.company = GENESIS
        System.out.println("sportsCar.getModel() = " + sportsCar.getModel()); // sportsCar.getModel() = GV80
        System.out.println();

        sportsCar.horn(); // 빵빵
        System.out.println(sportsCar.changeGear('D')); // D
    }
}

 

*final 키워드를 클래스와 메서드에 선언하면 어떻게 될까?

클래스에 final 키워드를 지정하여 선언하면 최종적인 클래스가 됨으로 더 이상 상속할 수 없는 클래스가 됩니다.

    public final class Car {}
    ......................................
    public class SportsCar extends Car{} // 오류가 발생한다.

 

메서드에 final 키워드를 지정하여 선언하면 최종적인 메서드가 됨으로 더 이상 오버라이딩할 수 없는 메서드가 됩니다.

    public class Car {
       public final void horn() {
            System.out.println("빵빵");
        }
    }
    ......................................
    public class SportsCar extends Car{
        public void horn() { // 오류가 발생합니다.
            super.horn();
        }
    }

 

▶ Object

객체”를 의미하는 단어이며 보통, Object 클래스를 의미한다.

  • Object 클래스는 Java 내 모든 클래스들의 최상위 부모 클래스이다.
  • 따라서, 모든 클래스는 Object의 메서드를 사용할 수 있다.
  • 또한 부모 클래스가 없는 자식 클래스는 컴파일러에 의해 자동으로 Object 클래스를 상속받게 된다.

🧑‍💻 Object 클래스의 메서드를 몇 가지 소개

  • Object clone() : 해당 객체의 복제본을 생성하여 반환함.
  • boolean equals(Object object) : 해당 객체와 전달받은 객체가 같은지 여부를 반환함.
  • Class getClass() : 해당 객체의 클래스 타입을 반환함.
  • int hashCode() : 자바에서 객체를 식별하는 정수값인 해시 코드를 반환함.
  • String toString() : 해당 객체의 정보를 문자열로 반환함. & Object 클래스에서는 클래스이름 @해쉬코드값 리턴함.

▶ 오버라이딩

부모 클래스로부터 상속받은 메서드의 내용을 재정의 하는 것을 오버라이딩이라고 한다.

  • 부모 클래스의 메서드를 그대로 사용 가능하지만 자식 클래스의 상황에 맞게 변경을 해야 하는 경우 오버라이딩을 사용한다.
  • 오버라이딩을 하기 위해서는 아래 조건들을 만족해야 한다.
  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 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 부앙~
        System.out.println(sportsCar.speed); // 100
    }
}

 

▶ super 와 super()

super는 부모 클래스의 멤버를 참조할 수 있는 키워드

  • 객체 내부 생성자 및 메서드에서 부모 클래스의 멤버에 접근하기 위해 사용될 수 있다.
  • 자식 클래스 내부에서 선언한 멤버와 부모 클래스에서 상속받은 멤버와 이름이 같을 경우 이를 구분하기 위해 사용된다.

super(…)는 부모 클래스의 생성자를 호출할 수 있는 키워드

  • 객체 내부 생성자 및 메서드에서 해당 객체의 부모 클래스의 생성자를 호출하기 위해 사용될 수 있다.
  • 자식 클래스의 객체가 생성될 때 부모 클래스들이 모두 합쳐져서 하나의 인스턴스가 생성된다.
  • 이때 부모 클래스의 멤버들의 초기화 작업이 먼저 수행이 되어야 한다.
    • 따라서 자식 클래스의 생성자에서는 부모 클래스의 생성자가 호출된다.
    • 또한 부모 클래스의 생성자는 가장 첫 줄에서 호출이 되어야 한다.
public class Car {

    String company; // 자동차 회사
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    public Car(String model, String color, double price) {
        this.model = model;
        this.color = color;
        this.price = price;
    }

    public String getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }

    public double getPrice() {
        return price;
    }

    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 SportsCar(String model, String color, double price, String engine) {
        // this.engine = engine; // 오류 발생
        super(model, color, price);
        this.engine = engine;
    }

    public void booster() {
        System.out.println("엔진 " + engine + " 부앙~\n");
    }

    @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) {
        // 자식 클래스 스포츠카 객체를 생성합니다.
        SportsCar sportsCar = new SportsCar("Lamborghini", "Red", 400000000, "V12");
        sportsCar.brakePedal(); // 스포츠카에 브레이크란 없다
        sportsCar.horn(); // 엔진 V12 부앙~

        // 자식 클래스의 생성자를 통해 부모 클래스의 생성자가 호출되어 필드값이 초기화 되었는지 확인
        System.out.println("sportsCar.getModel() = " + sportsCar.getModel()); // Lamborghini
        System.out.println("sportsCar.getColor() = " + sportsCar.getColor()); // Red
        System.out.println("sportsCar.getPrice() = " + sportsCar.getPrice()); // 4.0E8

    }
}

 

3. 다형성

▶ 참조 변수의 타입 변환

-자동 타입 변환

부모 타입 변수 = 자식 타입 객체; 는 자동으로 부모 타입으로 변환이 일어난다.

  • 자식 객체는 부모 객체의 멤버를 상속받기 때문에 부모와 동일하게 취급될 수 있다.
  • 다만 주의할 점은 부모 타입 변수로 자식 객체의 멤버에 접근할 때는 부모 클래스에 선언된 즉, 상속받은 멤버만 접근할 수 있다.
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();
mammal.feeding();

// 자식객체 고래의 수영 기능을 사용하고 싶다면 다시 자식타입으로 강제 타입변환을 하면된다.
Whale whale = (Whale) mammal;
whale.swimming();

다만 무조건 강제 타입 변환을 할 수 있는 것은 아니다.

  • 자식 타입 객체가 부모 타입으로 자동 타입 변환된 후 다시 자식 타입으로 변환될 때만 강제 타입 변환이 가능하다.
  • 부모 타입 변수로는 자식 타입 객체의 고유한 멤버를 사용할 수 없기 때문에 사용이 필요한 경우가 생겼을 때 강제 타입 변환을 사용한다.

Mammal newMammal = new Mammal();
Whale newWhale = (Whale) newMammal; // ClassCastException 발생

  • 이렇게 자동 타입 변환된 부모 타입 변수가 아닌 부모 객체를 자식 타입의 변수로 강제 타입 변환하려고 하면 오류가 발생한다.

▶ 다형성

다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미

  • 예를 들어 자동차의 핸들을 교체하면 핸들링이 부드러워지고 바퀴를 교체하면 승차감이 좋아진다.
  • 소프트웨어 또한 구성하고 있는 객체를 바꿨을 때 소프트웨어의 실행 성능 및 결과물이 다르게 나올 수 있다.
  • 일전에 배운 참조 변수 타입 변환을 활용해서 다형성을 구현할 수 있다.
public class Tire {
    String company; // 타이어 회사

    public Tire(String company) {
        this.company = company; 
    }

    public void rideComfort() {
        System.out.println(company + " 타이어 승차감은?");
    }
}
public class KiaTire extends Tire{

    public KiaTire(String company) {
        super(company);
    }

    @Override
    public void rideComfort() {
        System.out.println(super.company + " 타이어 승차감은 " + 60);
    }
}
public class HankookTire extends Tire {

    public HankookTire(String company) {
        super(company);
    }

    @Override
    public void rideComfort() {
        System.out.println(super.company + " 타이어 승차감은 " + 100);
    }
}
public class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;
    }

    Tire getHankookTire() {
        return new HankookTire("HANKOOK");
    }

    Tire getKiaTire() {
        return new KiaTire("KIA");
    }
}
public class Main {
    public static void main(String[] args) {
        // 강제 형변환!
        Tire kiaSampleTire = new KiaTire("KIA");
        Tire hankookSampleTire = new HankookTire("HANKOOK");

        // 매개변수 다형성 확인!
        Car car1 = new Car(kiaSampleTire);
        Car car2 = new Car(hankookSampleTire);

        // 반환타입 다형성 확인!
        Tire hankookTire = car1.getHankookTire();
        KiaTire kiaTire = (KiaTire) car2.getKiaTire();

        // 오버라이딩된 메서드 호출
        car1.tire.rideComfort(); // KIA 타이어 승차감은 60
        car2.tire.rideComfort(); // HANKOOK 타이어 승차감은 100
    }
}

 

▶ 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 출력

    }
}

 

4. 추상 클래스

클래스가 설계도라면 추상 클래스는 미완성된 설계도.

abstract 키워드를 사용하여 추상 클래스를 선언할 수 있다.

      public abstract class 추상클래스명 { }

  • 추상 클래스는 추상 메서드를 포함할 수 있다.
  • 추상 메서드가 없어도 추상 클래스로 선언할 수 있다.
  • 추상 클래스는 자식 클래스에 상속되어 자식 클래스에 의해서만 완성될 수 있다.
  • 추상 클래스는 여러 개의 자식 클래스들에서 공통적인 필드나 메서드를 추출해서 만들 수 있다.

▶ 추상 메서드

아직 구현되지 않은 미완성된 메서드.

abstract 키워드를 사용하여 추상 메서드를 선언할 수 있다.

      public abstract class 추상클래스명 {

            abstract 리턴타입 메서드이름(매개변수, ...);
      }

  • 추상 메서드는 일반적인 메서드와는 다르게 블록{ }이 없습니다.
  • 즉, 정의만 할 뿐, 실행 내용은 가지고 있지 않습니다.

 추상 클래스 상속

extends 키워드를 사용하여 클래스에서 상속.

상속받은 클래스에서 추상 클래스의 추상 메서드는 반드시 오버라이딩 되어야 한다.

      public class 클래스명 extends 추상클래스명 {
      @Override
          public 리턴타입 메서드이름(매개변수, ...) {
             // 실행문
          }
      }

 

▶ 예제

BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에만 차이가 존재한다.

이외는 공통된 필드와 메서드이다.

따라서 horn() 메서드를 추상 메서드로 선언하여 자식 클래스에서 재정의 될 수 있도록 한다.

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 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 GenesisCar extends Car {

    @Override
    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(); // Benz 빵빵

        Car car2 = new AudiCar();
        car2.horn();
        System.out.println(); // Audi 빵빵

        Car car3 = new GenesisCar();
        car3.horn(); // Zenesis 빵빵
    }
}

 

5.인터페이스의 역할

  • 인터페이스는 두 객체를 연결해 주는 다리 역할을 해준다.
    • 사람과 삼성티비, 엘지티비 객체가 존재한다고 생각해 보겠다.
    • 사람 객체는 멀티 리모컨 인터페이스를 통해서 삼성티비 객체의 채널을 변경할 수 있다.
    • 이때 삼성티비가 아니라 엘지티비로 객체가 교체된다고 해도 채널을 변경할 수 있다.
  • 상속 관계가 없는 다른 클래스들이 서로 동일한 행위 즉, 메서드를 구현해야 할 때 인터페이스는 구현 클래스들의 동일한 사용 방법과 행위를 보장해 줄 수 있다.
    • 인터페이스는 스팩이 정의된 메서드들의 집합이다.
    • 인터페이스의 구현 클래스들은 반드시 정의된 메서드들을 구현해야 한다.
    • 따라서 구현 클래스들의 동일한 사용 방법과 행위를 보장해 줄 수 있다.
    • 이러한 특징은 인터페이스에 다형성을 적용할 수 있게 만들어 준다.

▶ 인터페이스 선언

인터페이스는 클래스와 마찬가지로 public, default 접근 제어자를 지정할 수 있다.

    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();
    }

 

 인터페이스 구현

추상 클래스와 마찬가지로 직접 인스턴스를 생성할 수 없기 때문에 클래스에 구현되어 생성된다.

  • implements 키워드를 사용하여 인터페이스를 구현할 수 있다. 
  • 인터페이스의 추상 메서드는 구현될 때 반드시 오버라이딩 되어야 한다.
  • 만약 인터페이스의 추상 메서드를 일부만 구현해야 한다면 해당 클래스를 추상 클래스로 변경해 주면 된다.

      public class 클래스명 implements 인터페이스명 { 
          // 추상 메서드 오버라이딩
          @Override
           public 리턴타입 메서드이름(매개변수, ...) {
                // 실행문
        }
    }

 

▶ 인터페이스 상속

인터페이스 간의 상속이 가능하다.

  • 인터페이스 간의 상속은 implements 가 아니라 extends 키워드를 사용한다.
  • 인터페이스는 클래스와는 다르게 다중 상속이 가능하다.
  • 인터페이스 C는 아무것도 선언되어 있지 않지만 인터페이스 A, B를 다중 상속받았기 때문에 추상 메서드 a, b를 갖고 있는 상태이다.
  • 따라서 Main 클래스에서 인터페이스 C가 구현될 때 a, b 추상 메서드가 오버라이딩된다.
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");
    }
}

 

6. 디폴트 메서드와 static 메서드

▶ 디폴트 메서드

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드이다.

  • 메서드 앞에 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");
    }
}

 

7. 다형성

▶ 자동 타입 변환

인터페이스 변수 = 구현객체;는 자동으로 타입 변환이 일어난다.

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

        // A 인터페이스에 구현체 B 대입
        A a1 = new B();

        // A 인터페이스에 구편체 B를 상속받은 C 대입
        A a2 = new C();

    }
}

interface A { }
class B implements A {}
class C extends B {}

 

▶ 강제 타입 변환

구현 객체 타입 변수 = (구현 객체 타입) 인터페이스 변수;

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

        // A 인터페이스에 구현체 B 대입
        A a1 = new B(); // a1은 인터페이스 A 타입이기 때문에, a() 메서드만 가지고 있음!
        a1.a(); // 출력 B.a()
        // a1.b(); // 불가능

        System.out.println("\nB 강제 타입변환"); // B클래스의 메서드를 사용하고자 강제 타입변환 시킴!
        B b = (B) a1;
        b.a(); // 출력 B.a()
        b.b(); // 출력 B.b() : 강제 타입변환으로 사용 가능
        System.out.println();

        // A 인터페이스에 구편체 B를 상속받은 C 대입
        A a2 = new C();
        a2.a(); // 출력 B.a()
        //a2.b(); // 불가능
        //a2.c(); // 불가능

        System.out.println("\nC 강제 타입변환");
        C c = (C) a2;
        c.a(); // 출력 B.a()
        c.b(); // 출력 B.b() : 강제 타입변환으로 사용 가능
        c.c(); // 출력 C.c() : 강제 타입변환으로 사용 가능
    }
}


interface A {
    void a();
}


class B implements A {
    @Override
    public void a() {
        System.out.println("B.a()");
    }

    public void b() {
        System.out.println("B.b()");
    }


}
class C extends B {
    public void c() {
        System.out.println("C.c()");
    }
}

 

 

▶ 인터페이스의 다형성

  • 멀티 리모컨 인터페이스 변수 = TV 구현 객체;를 선언하여 자동 타입 변환된 인터페이스 변수를 사용하여 TV 구현 객체의 기능을 조작할 수 있다.
  • TV 구현 객체를 교체해도 멀티 리모컨 인터페이스 변수는 전혀 수정 작업 없이 그대로 기능을 호출할 수 있다.
  • 다형성은 ‘여러 가지 형태를 가질 수 있는 능력’이라고 배웠다.
  • 사용 방법은 동일하지만 다양한 특징과 결과를 가질 수 있는 것이 바로 다형성이다.
    • 즉, 멀티 리모컨으로 티비를 사용하는 방법은 동일하지만 어떤 TV 구현 객체가 대입되었느냐에 따라 실행 결과가 다르게 나옴을 통해 다형성이 적용되었음을 확인할 수 있다.
  • 또한 인터페이스도 마찬가지로 매개변수와 반환 타입에서 다형성이 적용될 수 있다.
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;
    }
}
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());
    }
}
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());
    }
}
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 없음");
        }
    }
}
public class Main {
    public static void main(String[] args) {

        // LG TV 구현체를 조작, 자동형변환
        MultiRemoteController mrc = new LgTv("LG");
        mrc.turnOnOff(); // LG Tv 전원이 켜졌습니다.
        mrc.volumeUp(); // 현재 볼륨은 1
        mrc.channelDown(); // 현재 채널은 0
        mrc.channelUp(); // 현재 채널은 1
        mrc.turnOnOff(); // LG Tv 전원이 종료되었습니다.

        // 조작 대상을 Samsung TV로 교체
        System.out.println("\n<Samsung TV로 교체>"); // <Samsung TV로 교체>
        mrc = new SamsungTv("Samsung");
        mrc.turnOnOff(); // Samsung Tv 전원이 켜졌습니다.
        mrc.channelUp(); // 현재 채널은 2
        mrc.volumeDown(); // 현재 볼륨은 0
        mrc.volumeUp(); // 현재 볼륨은 1
        mrc.turnOnOff(); // Samsung Tv 전원이 종료되었습니다.

        // 매개변수, 반환타입 다형성 체크
        System.out.println("\n<매개변수, 반환타입 다형성 체크>");

        MultiRemoteController samsung = mrc.getTV(new SamsungTv("Samsung"));
        samsung.turnOnOff(); // Samsung Tv 전원이 켜졌습니다.

        SamsungTv samsungTv = (SamsungTv) samsung;
        samsungTv.turnOnOff(); // Samsung Tv 전원이 종료되었습니다.


        System.out.println();
        MultiRemoteController lg = mrc.getTV(new LgTv("LG"));
        lg.turnOnOff(); // LG Tv 전원이 켜졌습니다.

        LgTv lgTv = (LgTv) lg;
        lgTv.turnOnOff(); // LG Tv 전원이 종료되었습니다.
    }
}