이것이 자바다 6일차 - Chapter8 인터페이스

Seo-Faper·2023년 1월 16일
0

이것이 자바다

목록 보기
8/20

인터페이스 역할

인터페이스(Interface)는 다형성 구현의 주된 기술로 상속을 통한 다형성 구현보다 인터페이스를 활용하여 다형성을 구현하는 경우가 더 많다. 인터페이스의 사전적 정의는 두 장치를 연결하는 접속기로, 자바에서 인터페이스는 두 객체를 연결하는 역할을 한다.

객체 A가 인터페이스의 메소드를 호출하면 인터페이스는 객체 B의 메소드를 호출하고 그 결과를 객체 A로 전달해 준다. 객체 B 뿐만 아니라 인터페이스 뒤에는 여러 다형성을 구현하는 객체가 올 수 있다. 예를 들어 객체 B의 메소드와 객체 C의 메소드 실행 결과가 다르다면 객체 A는 객체 교체로 인해 다른 결과를 얻게 된다.

인터페이스와 구현 클래스 선언

인터페이스는 *.java 형태의 파일로 작성하고 컴파일 시 *.class 형태로 되기 때문에 물리적인 형태는 클래스와 같지만 소스를 작성할 땐 선언하는 방법과 구성 멤버가 클래스와는 다르다.

인터페이스 선언

인터페이스 선언은 class 키워드 대신 interface 키워드를 사용한다.

interface 인터페이스명 { ... } 		 // default 접근 제한
public interface 인터페이스명 { ... }  // public 접근 제한

접근 제한자는 클래스와 동일하게 default와 public이 있다.

중괄호 안에는 인터페이스가 가지는 멤버들을 선언할 수 있다.

public interface 인터페이스명
{
	public 상수 필드
    public 추상 메소드
    public 디폴트 메소드
    public 정적 메소드
    private 메소드
    private 정적 메소드 
}

추상 메소드란 이렇게 선언부만 있고 실행부인 중괄호가 없는 메소드를 말한다.
RemoteContorl.java (interface)

package ch08.sec02;

public interface RemoteControl
{
    public void turnOn();
}

구현 클래스 선언

이런 구조로 인터페이스가 돌아가는데, 객체 A가 인터페이스의 추상 메소드를 호출하면 인터페이스는 객체 B의 메소드를 실행한다. 그 때 객체 B는 인터페이스에 선언된 추상 메소드와 동일한 선언부를 가진 (즉, 재정의를 해야 한다.) 메소드를 가지고 있어야 한다.
이 때 객체 B를 인터페이스를 구현한(implement) 객체라고 부른다.

public class B implements 인터페이스명 { ... }

이렇게 implements 키워드를 통해 구현체를 만들 수 있다.
기렇게 implements가 선언된 클래스는 해당 클래스가 인터페이스를 통해 사용될 수 있다는 표시이며, 인터페이스의 추상 메소드를 재정의(Overriding) 해줘야 한다.

Television.java

package ch08.sec02;

public class Television implements RemoteControl{

    @Override
    public void turnOn() {
        System.out.println("TV를 켭니다.");
    }
}

변수 선언과 구현 객체 대입

인터페이스도 하나의 타입이므로 변수의 타입으로 사용 할 수 있다. 인터페이스는 참조 타입에 속하므로 null을 대입할 수 있다.

RemoteControl rc;
RemoteControl rc = null;

인터페이스를 통해 구현 객체를 사용하려면 인터페이스 변수에 구현 객체를 대입해야 한다.
정확히는 Heap영역에 존재하는 구현 객체의 주소값을 인터페이스 변수에 대입하는 것 이다.
다음은 Television 객체를 생성하고 주소값을 대입하는 코드이다.

rc = new Television();

만약 Television이 implements RemoteControl로 선언되지 않았다면 RemoteControl 타입으로 선언할 수 없다. 인터페이스 변수에 구현 객체가 대입이 되어 있다면 변수를 통해 인터페이스의 추상 메소드를 호출할 수 있게 된다.

package ch08.sec02;

public class RemoteControlExample
{
    public static void main(String[] args) {
        RemoteControl rc;
        rc = new Television();
        rc.turnOn();
    }
}

이 rc 변수에는 RemoteControl을 구현한 그 어떠한 객체든 대입할 수 있다. 예시로 또 다른 객체인 Audio객체를 만들고 호출해 보자.

package ch08.sec02;

public class Audio implements RemoteControl{
    @Override
    public void turnOn() {
        System.out.println("Audio를 켭니다.");
    }
}
package ch08.sec02;

public class RemoteControlExample
{
    public static void main(String[] args) {
        RemoteControl rc;
        // rc 변수에 Television 객체를 대입
        rc = new Television();
        rc.turnOn();

		// rc 변수에 Audio 객체를 대입 (교체시킴)
        rc = new Audio();
        rc.turnOn();
    }
}

출력 결과는 TV를 켭니다, Audio를 켭니다가 순서대로 출력된다.

상수 필드

인터페이스에는 public static final로 불변의 상수 필드를 멤버로 가질 수 있다.
인터페이스에서 선언된 필드는 모두 상수의 특성을 가지기 때문에 앞의 수식어를 붙이지 않아도 컴파일 과정에서 자동으로 public static final이 붙게 된다. 즉, 인터페이스는 컴파일 과정 시 정적 영역으로 모든 필드를 넘기는 특징이 있다.

package ch08.sec03;

public interface RemoteControl {
    int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;
}
package ch08.sec03;

public class RemoteControlExample {
    public static void main(String[] args) {
        System.out.println("리모콘 최대 볼륨 : "+RemoteControl.MAX_VOLUME);
        System.out.println("리모콘 최소 볼륨 : "+RemoteControl.MIN_VOLUME);
    }
}

추상 메소드

인터페이스는 구현 클래스가 재정의 해야 하는 public abstract 으로 시작하는 추상 메소드를 멤버로 가질 수 있다. 메소드의 선언부만 작성하면 된다.
이것 역시 필드 처럼 컴파일 과정에서 자동으로 public abstract이 붙게 된다.

package ch08.sec04;

public interface RemoteControl {
    int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;
    
    void turnOn();
    void turnOff();
    void setVolume(int volume); // 메소드 선언부만 작성 
}

이렇게 추상 메소드를 구현할 수 있다.

package ch08.sec04;


public class Television implements RemoteControl {
    private int volume;


    @Override
    public void turnOn() {
        System.out.println("TV를 켭니다.");
    }

    @Override
    public void turnOff() {
        System.out.println("TV를 끕니다.");
    }

    @Override
    public void setVolume(int volume) {
        if(volume>RemoteControl.MAX_VOLUME){
            this.volume = RemoteControl.MAX_VOLUME;
        }else if(volume < RemoteControl.MIN_VOLUME){
            this.volume = RemoteControl.MIN_VOLUME;
        } else {
            this.volume = volume;
        }
        System.out.println("현재 TV 볼륨 : "+volume);
    }
}

그리고 추상 메소드를 구체화 하는 코드는 implements를 받는 객체에서 재정의로 만들어 주면 된다.

디폴트 메소드

인터페이스에는 완전한 실행 코드를 가진 디폴트 메소드를 선언할 수 있다.
추상 메소드와 다른 점은 실행부 (중괄호)가 존재한다는 점이다.

[public] default 리턴타입 메소드명(매개변수,...) { ... }

이런 형태로 구현 할 수 있다.

package ch08.sec05;

public interface RemoteControl
{
    int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;

    void turnOn();
    void turnOff();
    void setVolume(int volume);

    default void setMute(boolean mute){
        if(mute){
            System.out.println("무음 처리합니다.");
            setVolume(MIN_VOLUME);
        } else {
          System.out.println("무음 해제 합니다. ");
        }
    }
}

default로 선언된 setMute()는 인터페이스임에도 실행부를 가진다. 그래서 인터페이스를 구현한 구현체에 없는 메소드지만 인터페이스 변수에 담았을 때 메소드 호출이 가능하다.

package ch08.sec05;

public class RemoteControlExample {
    public static void main(String[] args) {
        RemoteControl rc;
        rc = new Television();
        rc.turnOn();
        rc.setVolume(5);
        rc.setMute(true); // Television.java에 존재하지 않는 메소드
        rc.setMute(false); //Television.java에 존재하지 않는 메소드 
    }
}

또한 이 디폴트 메소드는 구현 클래스에서 재정의 할 수 있는데, 이 때는 default 키워드를 생략 하고 반드시 public 접근 제한자를 붙여야 한다.

package ch08.sec05;

public class Audio implements RemoteControl{
    private int volume;
    @Override
    public void turnOn() {
        System.out.println("Audio를 켭니다.");
    }

    @Override
    public void turnOff() {
        System.out.println("Audio를 끕니다.");
    }

    @Override
    public void setVolume(int volume) {
        if(volume>RemoteControl.MAX_VOLUME){
            this.volume = RemoteControl.MAX_VOLUME;
        }else if(volume < RemoteControl.MIN_VOLUME){
            this.volume = RemoteControl.MIN_VOLUME;
        } else {
            this.volume = volume;
        }
        System.out.println("현재 TV 볼륨 : "+volume);
    }

    private int memoryVolume;
    
    @Override
    public void setMute(boolean mute) {
        if(mute){
            this.memoryVolume = this.volume;
            System.out.println("무음 처리합니다.");
            setVolume(RemoteControl.MIN_VOLUME);
        } else {
            System.out.println("무음 해제합니다.");
            setVolume(this.memoryVolume); // 원래 볼륨을 복구 하는 코드
        }
    }


}
package ch08.sec05;

public class RemoteControlExample {
    public static void main(String[] args) {
        RemoteControl rc;
        rc = new Television();
        rc.turnOn();
        rc.setVolume(5);
        
        //기존 인터페이스에 존재하는 디폴트 메소드
        rc.setMute(true);
        rc.setMute(false);

        System.out.println();

        rc = new Audio();
        rc.turnOn();
        rc.setVolume(5);

        //재정의한 디폴트 메소드 호출
        rc.setMute(true);
        rc.setMute(false);
    }
}

이렇게 원래 볼륨을 복구하는 코드로 재정의 할 수 있다.

정적 메소드

또한 인터페이스는 정적 메소드도 선언이 가능하다. 추상 메소드와 디폴트 메소드는 구현 객체가 필요하지만 정적 메소드는 구현 객체가 없어도 인터페이스만으로 호출할 수 있다. 선언 방법은 클래스 정적 메소드와 완전 동일하다. 단, public을 생략하더라도 자동으로 컴파일 과정에서 붙는다.

[public | private] static 리턴타입 메소드명(매개변수, ...) { ... }
package ch08.sec06;

public interface RemoteControl
{
	//상수 필드
    int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;

	//추상 메소드 
    void turnOn();
    void turnOff();
    void setVolume(int volume);

    //디폴트 메소드
    default void setMute(boolean mute){
		... 
    }
    
    // 정적 메소드 
    static void changeBattery(){
        System.out.println("리모콘 건전지를 교환합니다.");
    }
}

이 정적 메소드는 상수 필드를 호출하는 것 처럼 쓸 수 있다.

    public static void main(String[] args) {
        
        RemoteControl rc;
        
        rc = new Television();
        rc.turnOn();
        rc.setVolume(5);
        rc.setMute(true);
        rc.setMute(false);

        System.out.println();

        rc = new Audio();
        rc.turnOn();
        rc.setVolume(5);

        //재정의한 디폴트 메소드 호출
        rc.setMute(true);
        rc.setMute(false);
		
        //정적 메소드 호출 
        RemoteControl.changeBattery();
    }

private 메소드

인터페이스의 상수 필드, 추상 메소드, 디폴트 메소드, 정적 메소드는 모두 public 접근 제한을 갖기 때문에 외부에서 접근이 가능하다. 그래서 인터페이스에도 private로 외부에서 접근할 수 없는 private 메소드 선언이 가능하다.

private 메소드의 용도는 디폴트와 정적 메소드들의 중복 코드를 줄이기 위함이다.
즉 인터페이스 내부에서 동작하는 메소드가 있을 때 사용한다.

package ch08.sec07;

public interface Service
{
    default void defaultMethod1(){
        System.out.println("defaultMethod1 종속 코드");
        defaultCommon();
    }
    default void defaultMethod2(){
        System.out.println("defaultMethod2 종속 코드");
        defaultCommon();
    }
    // private 메소드
    private void defaultCommon(){
        System.out.println("defaultMethod 중복 코드 A");
        System.out.println("defaultMethod 중복 코드 B");
    }

    static void staticMethod1(){
        System.out.println("staticMethod1 종속 코드");
        staticCommon();
    }
    static void staticMethod2(){
        System.out.println("staticMethod2 종속 코드");
        staticCommon();
    }
    private static void staticCommon(){
        System.out.println("staticMethod 중복 코드 C");
        System.out.println("staticMethod 중복 코드 D");
    }
}
package ch08.sec07;

public class ServiceImpl implements Service{ }
package ch08.sec07;

public class ServiceExample {
    public static void main(String[] args) {
        Service service = new ServiceImpl();

        service.defaultMethod1();
        System.out.println();
        service.defaultMethod2();
        System.out.println();

        Service.staticMethod1();
        System.out.println();
        Service.staticMethod2();
        System.out.println();
    }
}

출력하면

defaultMethod1 종속 코드
defaultMethod 중복 코드 A
defaultMethod 중복 코드 B

defaultMethod2 종속 코드
defaultMethod 중복 코드 A
defaultMethod 중복 코드 B

staticMethod1 종속 코드
staticMethod 중복 코드 C
staticMethod 중복 코드 D

staticMethod2 종속 코드
staticMethod 중복 코드 C
staticMethod 중복 코드 D

이렇게 나온다.

다중 인터페이스 구현

구현 객체는 여러개의 인터페이스를 implements 할 수 있다.
구현 클래스는 선언부에 implements 뒤에 쉼표로 구분된 인터페이스명으로 여러개의 인터페이스를 받을 수 있다. 그리고 여러개의 모든 인터페이스의 추상 메소드를 재정의 하면 된다.

public class 구현클래스명 implements 인터페이스A, 인터페이스B
인터페이스A 변수 = new 구현클래스명(...);
인터페이스B 변수 = new 구현클래스명(...);

이렇게 구현 객체가 어떤 인터페이스 변수에 대입되느냐에 따라 변수를 통해 호출할 수 있는 추상 메소드가 정해진다.

package ch08.sec08;

public interface RemoteControl {
    void turnOn();
    void turnOff();
}
package ch08.sec08;

public interface Searchable {
    void search(String url);
}
package ch08.sec08;

public class SmartTelevision implements RemoteControl, Searchable{
    @Override
    public void turnOn() {
        System.out.println("TV를 켭니다.");
    }

    @Override
    public void turnOff() {
        System.out.println("TV를 끕니다.");
    }

    @Override
    public void search(String url) {
        System.out.println(url+"을 검색합니다.");
    }
}

이렇게 2개의 인터페이스에 대한 모든 추상 메소드를 재정의 한다.

package ch08.sec08;

public class MultiInterfaceImplExample {
    public static void main(String[] args) {
        RemoteControl rc = new SmartTelevision();
        rc.turnOn(); //RemoteControl 인터페이스에 선언된 추상 메소드만 호출 가능
        rc.turnOff(); //RemoteControl 인터페이스에 선언된 추상 메소드만 호출 가능

        Searchable searchable = new SmartTelevision();
        //Searchable 인터페이스에 선언된 추상 메소드만 호출 가능
        searchable.search("https://www.youtube.com");
    }
}

그 후 사용할 때는 어떤 인터페이스 변수에 대입할지 결정한 후 사용하면 된다.

인터페이스 상속

인터페이스도 다른 인터페이스를 상속할 수 있다. (어질..)
심지어 클래스와는 달리 다중 상속을 허용한다.
extends 키워드 뒤에 상속할 인터페이스들을 나열하면 된다.

public interface 자식인터페이스 extends 부모인터페이스1, 부모인터페이스2 { ... }

자식인터페이스를 구현한 구현 클래스는 자식 인터페이스의 메소드 뿐만 아니라 상속받는 모든 부모인터페이스의 추상 메소드를 재정의 해야 한다. 그리고 그 구현 객체는 자식 및 부모 인터페이스 변수에 대입될 수 있다.

자식인터페이스 변수 = new 구현클래스(...);
부모인터페이스1 변수 = new 부모인터페이스1(...);
부모인터페이스2 변수 = new 부모인터페이스2(...);

자식인터페이스를 담는 변수는 부모와 자식의 모든 추상 메소드를 호출할 수 있지만 부모인터페이스를 담는 변수는 해당 부모인터페이스에 선언된 추상 메소드만 호출할 수 있다.

package ch08.sec09;

public interface InterfaceA {
    void methodA();
}
package ch08.sec09;

public interface InterfaceB {
    void methodB();
}
package ch08.sec09;

public interface InterfaceC extends InterfaceA, InterfaceB
{
    void methodC();
}

InterfaceC는 두 부모 인터페이스를 상속 받는다.

package ch08.sec09;

public class InterfaceCImpl implements InterfaceC{


    @Override
    public void methodA() {
        System.out.println("InterfaceCImpl-methodA() 실행");
    }

    @Override
    public void methodB() {
        System.out.println("InterfaceCImpl-methodB() 실행");
    }

    @Override
    public void methodC() {
        System.out.println("InterfaceCImpl-methodC() 실행");
    }

}

그리고 이 상속 카르텔(?)의 구현 클래스에는 모든 추상 메소드에 대한 재정의가 있어야 한다.

package ch08.sec09;

public class ExtendsExample
{
    public static void main(String[] args) {
        InterfaceCImpl impl = new InterfaceCImpl();

        InterfaceA ia = impl;
        ia.methodA();
        //ia.methodB(); InterfaceA와 InterfaceB는 상속관계가 아니므로 불가능

        InterfaceB ib = impl;
        //ia.methodA(); InterfaceA와 InterfaceB는 상속관계가 아니므로 불가능
        ib.methodB();
        
        InterfaceC ic = impl;
        ic.methodA();
        ic.methodB();
        ic.methodC();
    }
}

타입 변환

상속이 있으니 당연히 타입변환도 있다. 인터페이스의 타입 변환은 인터페이스와 구현 클래스 간에 발생한다. 인터페이스 변수에 구현 객체를 대입하면 구현 객체는 인터페이스 타입으로 자동 변환된다. 그리고 상속과 마찬가지로 변환된 인터페이스 타입을 구현 클래스 타입으로 바꿀려면 강제 타입 변환이 필요하다.

인터페이스 변수 = 구현객체;

부모 클래스가 인터페이스를 구현하고 있다면 자식 클래스도 인터페이스 타입으로 자동 타입 변환될 수 있다.

package ch08.sec10.exam02;

public interface Vehicle {
    void run();
}
package ch08.sec10.exam02;

public class Bus implements Vehicle{

    @Override
    public void run() {
        System.out.println("버스가 달립니다.");
    }

    public void checkFare(){
        System.out.println("승차요금을 체크합니다.");
    }
}

이렇게 인터페이스와 구현 클래스를 만들어두고

package ch08.sec10.exam02;

public class CastingExample {
    public static void main(String[] args) {
        Vehicle vehicle = new Bus();

        vehicle.run();

        Bus bus = (Bus) vehicle;
        bus.run();
        bus.checkFare();
    }
}

이렇게 강제 타입 변환도 할 수 있다.
인터페이스인 vechile을 구현 클래스 타입인 Bus로 강제 타입 변환이 일어난 것이다.

다형성

다형성이란 사용 방법은 동일하지만 다양한 결과가 나오는 성질을 뜻한다.
그리고 인터페이스는 앞서 배운 상속보다 현업에서 더 많이 다형성을 구현하기 위해 사용된다.

필드의 다형성

package ch08.sec11.exam01;

public interface Tire {
     void roll();
}

이렇게 하나의 인터페이스에

package ch08.sec11.exam01;

public class HankookTire implements Tire{
    @Override
    public void roll() {
        System.out.println("한국 타이어가 굴러갑니다.");
    }
}
package ch08.sec11.exam01;

public class KumhoTire implements Tire{
    @Override
    public void roll() {
        System.out.println("금호 타이어가 굴러갑니다.");
    }
}

서로 다른 구현체가 있다고 가정하면

package ch08.sec11.exam01;

public class Car {
    Tire tire1 = new HankookTire();
    Tire tire2 = new HankookTire();

    void run(){
        tire1.roll();
        tire2.roll();
    }
}

초기값은 한국타이어로 정해져 있지만

package ch08.sec11.exam01;

public class CarExample {
    public static void main(String[] args) {
        Car myCar = new Car();

        myCar.run();

        myCar.tire1 = new KumhoTire();
        myCar.tire2 = new KumhoTire();

        myCar.run();
    }
}

필드의 다형성에 의해 금호타이어로 바꿔 다른 결과를 출력시킬 수 있다.

메개변수의 다형성

package ch08.sec11.exam02;

public interface Vehicle {
    void run();
}
package ch08.sec11.exam02;

public class Driver {
    void drive(Vehicle vehicle){
        vehicle.run();
    }
}

Driver 클래스에 인터페이스인 Vehicle를 메개변수로 가지는 drive() 메소드를 만든다.
그 때 Bus와 Taxi가 Vechicle을 구현하는 구현 클래스라면 객체를 생성해서 전달하는 것이 가능하다.

package ch08.sec11.exam02;

public class Bus implements Vehicle{
    @Override
    public void run() {
        System.out.println("버스가 달립니다.");
    }
}
package ch08.sec11.exam02;

public class Taxi implements Vehicle{

    @Override
    public void run() {
        System.out.println("택시가 달립니다.");
    }
}
package ch08.sec11.exam02;

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

}

객체 타입 확인

상속 때랑 똑같다. instanceof 연산자로 확인 할 수 있다.

    public static void ride(Vehicle vehicle){
        if(vehicle instanceof Bus bus){
            bus.checkFare();
        }
        vehicle.run();
    }

봉인된 인터페이스

상속 파트랑 Dejavu를 느끼고 있는데, 암튼 똑같다.

public sealed interface InterfaceA permits InterfaceB { ... }

무분별한 자식 인터페이스 생성을 방지하기 위해 만들어 졌다.
저렇게 쓰면 InterfaceA의 자식 인터페이스는 InterfaceB만 가능하고 그 외에는 불가능해진다.

public non-sealed interface InterfaceB extends InterfaceA { ... }

그리고 InterfaceB에서는 non-sealed 키워드로 선언해 오직 상속 받는 부모 인터페이스는 InterfaceA임을 명시하면 된다.

연습문제

1번 인터페이스는 클래스가 아니기 때문에 new로 생성할 수 없습니다.

3번 할 수 있습니다. 정적으로 만들어 놓은 것들만이 컴파일 과정에 메소드 영역으로 넘어가 고정이 되어 버리지요.

4번 애초에 만들 때 강제 타입 변환을 할 필요 없이 InterfaceA ia = new 구현객채(); 이렇게 받죠..? 그리고 3번 같은 경우는 변수에 구현 객체의 메모리 주소가 담기는 거라 배열로도 가능합니다. 그냥 인터페이스 타입의 변수가 여러 개 있는 것과 동일하다.

제일 조상 격인 인터페이스 A를 가지고 있기 때문에 모두 가능하다.

public class TV implements Remocon{
	@Override
    public void powerOn() { System.out.println("TV를 켰습니다.");
    ...
}

재정의를 통해 만들면 되죠.

public class Cat implements Soundable(){
	@override
    public String sound(){
    	return "야옹";
    }
}
public class Dog implements Soundable(){
	@override
    public String sound(){
    	return "멍멍";
    }
}

package ch08.homework;

public interface DataAccessObject
{
    void select();
    void insert();
    void update();
    void delete();
}
package ch08.homework;

public class MysqlDao implements DataAccessObject{
    @Override
    public void select() {
        System.out.println("MySql DB에서 검색");
    }

    @Override
    public void insert() {
        System.out.println("MySql DB에서 삽입");
    }

    @Override
    public void update() {
        System.out.println("MySql DB를 수정");
    }

    @Override
    public void delete() {
        System.out.println("MySql DB에서 삭제");
    }
}
package ch08.homework;

public class OracleDao implements DataAccessObject{
    @Override
    public void select() {
        System.out.println("Oracle DB에서 검색");
    }

    @Override
    public void insert() {
        System.out.println("Oracle DB에서 삽입");
    }

    @Override
    public void update() {
        System.out.println("Oracle DB를 수정");
    }

    @Override
    public void delete() {
        System.out.println("Oracle DB에서 삭제");
    }
}

다형성은 신인가?


a instanceof C c 죠 자바가 12버전 이상이면!

profile
gotta go fast

0개의 댓글