인터페이스와 추상 클래스

gustjtmd·2022년 1월 31일
0

Java

목록 보기
12/40

인터페이스의 기본과 그 의미

추상 메소드만 담고 있는 인터페이스

인터페이스는 다음과 같이 생겼다
interface Printable{
	public void print(String doc)	//추상 메소드
}

메소드의 몸체가 비어 있는 메소드를 가리켜 '추상 메소드'라 한다.
그리고 인터페이스를 대상으로는 인스턴스 생성이 불가능하다.

클래스가 인터페이스를 상속하는 행위는 '상속'이 아닌 '구현'이라 한다
문법 관계는 상속과 동일하지만 본질은 '구현'이기 때문이다.
그리고 클래스의 인터페이스 구현에는 다음과 같은 특징이 있다.

1) 구현할 인터페이스를 명시할때 키워드 implements를 사용한다
2) 한 클래스는 둘 이상의 인터페이스를 구현할 수 있다.
3) 상속과 구현은 동시에 가능하다

따라서 Robot 클래스가 Machine 클래스를 상속하면서 인터페이스 둘 이상을 구현한다면

class Robot extends Machine implements Movable, Runnable{...}
그리고 인터페이스는 두 가지 특징도 기억해야 한다

1) 인터페이스의 형을 대상으로 참조변수의 선언이 가능하다
2) 인터페이스의 추상 메소드를 이를 구현하는 메소드 사이에 오버라이딩 관계가 성립된다
	-> 따라서 어노테이션 @Override의 선언 가능
    
코드로 확인해보자

interface Printable{
    public void print(String doc);
}
class Printer implements Printable{ //Printable을 구현하는 Printer 클래스
    @Override
    public void print(String doc){  //오버라이딩 관계 성립
        System.out.println(doc);
    }
}

public class PrintableInterface {
    public static void main(String[] args) {
        Printable prn = new Printer();  //Printable형 참조변수 선언 가능
        prn.print("Hello Java");
    }
}


Hello Java

--------------------------------------------------------------------------

위 코드에서 보이듯이 Printable형 참조변수를 선언할 수 있따
Printable prn = new Printer();

그리고 다음과 같이 참조변수를 대상으로 Printable 인터페이스에 정의된 추상 메소드를 
호출할 수 있다
prn.print("Hello Java")
이 경우 메소드 오버라이딩이 적용딘다 따라서 Printer 클래스의 print메소드가 호출된다.
그리고 실수의 확률을 줄이기 위해서 어노테이션 @Override를 활용하자

인터페이스의 본질적 의미

'인터페이스'의 사진적 의미는 '연결점' 또는 '접점'으로 둘 사이를 연결하는 매개체를 뜻한다
예를 들어보자

"마이크로소프트의 윈도우 삼성과 LG의 프린터를 대상으로 출력을 진행할 수 있다."

자바 기준으로 이 일을 주도하는것은 마이크로소프트이다 따라서 다음과 같은 결정을 내린다

"인터페이스를 하나 만들어서 모든 프린터 업체에게 제공해야 하겠다"

interface Printable{
	public void print(String doc);
}

"이 인터페이스를 회사별로 각자 구현해서 가져오기 바랍니다"
"그러면 저희는 print 메소드를 호출하면서 출력할 문서의 정보를 인자로 전달하겠습니다"

마이크로소프트는 위의 클래스 이름만 알면 될뿐 내부적으로 구현이 어떻게 이뤄지는지는 
알 필요가 없다

코드로 확인해보자

interface Printable{    //MS가 정의하고 제공하는 인터페이스
    public void print(String doc);
}
class SprinterDriver implements Printable{  // S사가 정의한 클래스
    @Override
    public void print(String doc){
        System.out.println("From Samsung printer");
        System.out.println(doc);
    }
}
class LPrinterDriver implements Printable{
    @Override
    public void print(String doc){
        System.out.println("From LG printer");
        System.out.println(doc);
    }
}

public class PrinterDeriver {
    public static void main(String[] args) {
        String myDoc = "This is a report about...";

        //삼성 프린터로 출력
        Printable prn = new SprinterDriver();
        prn.print(myDoc);
        System.out.println();

        //LG 프린터로 출력
        prn = new LPrinterDriver();
        prn.print(myDoc);
    }
}


From Samsung printer
This is a report about...

From LG printer
This is a report about...

인터페이스의 문법 구성과 추상클래스

인터페이스에 존재할수 있는 메소드에는 추상 메소드, 디폴트 메소드, static 메소드가 있다. 그리고 인터페이스 간 상속도 가능하며 인터페이스의 형(type) 이름을 대상으로 instanceof 연산을 할 수도 있다. 많은 특성이 클래스와 유사하다.

인터페이스의 선언되는 메소드와 변수

interface Printable{
	void print(String doc);// == public void printf(...)
}

"인터페이스의 모든 메소드는 public 선언된 것으로 간주한다"
즉 별도의 선언이 없어도 public이 된다 때문에 인터페이스 정의에서 메소드 앞에 public을 
붙일 필요는 없다.

interface Printable{
	int PW = 70;	// == public static final int PW = 70;
    	int PH = 120; // == public static final int PH = 120;
    	void printf(String doc);	//public void printf(...)
}

이렇게 선언된 변수에는 다음의 특징이 있따

1) 반드시 선언과 동시에 값으로 초기화를 해야 한다.
2) 모든 변수는 public static final이 선언된 것으로 간주한다.

결론적으로 인터페이스 내에 선언된 변수는 상수이다. final 그리고 static이 자동 선언되니
다음과 같이 접근 가능한 상수이다.

System.out.println(Printable.PW);	//인터페이스 내에 위치한 상수의 접근

때문에 상수는 대문자로 이름을 짓는 관례에 따라 인터페이스 내에 위치한 변수(상수)의 이름은
대문자로 작성해야한다.

인터페이스 간 상속

인터페이스를 구현하는 클래스는 인터페이스에 존재하는 모든 '추상 메소드'를 구현해야 한다.
하나라도 구현하지 않으면 해당 클래스를 대상으로 인스턴스 생성이 불가능하다

만약에 MS사에서 Printable 인터페이스 디자인시에 모든 프린터가 흑백이였는데
갑자기 컬러 프린터가 등장했다고 해보자
이럴때 원래 있던 Printable 인터페이스에 컬러 메소드를 추가하면
구현하고 있는 클래스에도 전부 컬러 메소드를 추가해야 한다 
이러한 상황을 고려하여 자바에서는 인터페이스의 상속을 지원한다. 코드로 확인해보자

interface Printable{
    void print(String doc);
}
interface ColorPrintable extends Printable{ //Printable을 상속하는 인터페이스
    void printCMYK(String doc);
}

class Prn909Drv implements ColorPrintable{
    @Override
    public void print(String doc){  // 흑백 출력
        System.out.println("From MD-909 black & white ver");
        System.out.println(doc);
    }
    @Override
    public void printCMYK(String doc){ //컬러 출력
        System.out.println("From MD-909 CMYK ver");
        System.out.println(doc);
    }
}

public class PrinterDriver3 {
    public static void main(String[] args) {
        String myDoc = "This is a report about...";
        ColorPrintable prn = new Prn909Drv();
        prn.print(myDoc);
        System.out.println();
        prn.printCMYK(myDoc);
    }
}


From MD-909 black & white ver
This is a report about...

From MD-909 CMYK ver
This is a report about...

--------------------------------------------------------------------------

이로써 기존에 제작 및 배포가 되어 사용 중인 드라이버를 수정할 필요가 없게 되었다
인터페이스 간의 상속은 extends를 사용해야 하는데 정리해보자면
1) 두 클래스 사이의 상속은 extends로 명시한다
2) 두 인터페이스 사이의 상속도 extends로 명시한다
3) 인터페이스와 클래스 사이의 구현만 implements로 명시한다.

인터페이스의 디폴트 메소드

위처럼 간단한 상황이면 상관없지만 수백개의 인터페이스에 추상 메소드를 추가해야 하는 상황을
상속으로 해결하면 인터페이스는 두배로 늘어나게 된다
이러한 상황을 해결하기 위해 '디폴트 메소드'가 등장하게 되었다.

코드로 확인해보자

interface Printable{
    void print(String doc);
    default void printCMYK(String doc){}    //인터페이스의 디폴트 메소드
}
class Prn731Drv implements Printable{
    @Override
    public void print(String doc){
        System.out.println("From MD-731 printer");
        System.out.println(doc);
    }
}

class Prn909Drv implements Printable{
    @Override
    public void print(String doc){
        System.out.println("From MD-909 black & white ver");
        System.out.println(doc);
    }
    @Override
    public void printCMYK(String doc){
        System.out.println("From MD-909 CMYK ver");
        System.out.println(doc);
    }
}
public class PrinterDriver4 {
    public static void main(String[] args) {
        String myDoc = "This is a report about...";
        Printable prn1 = new Prn731Drv();
        prn1.print(myDoc);

        System.out.println();
        prn1 = new Prn909Drv();
        prn1.print(myDoc);
        prn1.printCMYK(myDoc);
    }
}

From MD-731 printer
This is a report about...

From MD-909 black & white ver
This is a report about...
From MD-909 CMYK ver
This is a report about...


----------------------------------------------------------------------------------
디폴트 메소드의 특징은 다음과 같다

1) 자체로 완전한 메소드이다(비록 인터페이스 내에 정의되어 있지만)
2) 따라서 이를 구현하는 클래스가 오버라이딩 하지 않아도 상관없다.

디폴트 메소드 추가로 상속을 하지 않아도 문제가 해결 되었다.
이렇듯 디폴트 메소드는 인터페이스에 추상 메소드를 추가해야 하는 상솽에서 이전에 개발해 놓은
코드에 영향을 미치지 않기 위해 등장한 문법이다.

인터페이스의 static 메소드(클래스 메소드)

코드로 확인해보자

interface Printable{
    static void printLine(String str){
        System.out.println(str);
    }
    default void print(String doc){
        printLine(doc);
    }
}
//인터페이스 Printable에는 구현해야 할 메소드가 존재하지 않는다
class Printer implements Printable{}
public class SimplePrinter {
    public static void main(String[] args) {
        String myDoc = "This is a report about...";
        Printable prn = new Printer();
        prn.print(myDoc);

        //인터페이스의 static 메소드 직접 호출
        Printable.printLine("end of line");
    }
}

This is a report about...
end of line


---------------------------------------------------------------------

코드에 정의된 Printable 인터페이스에는 구현해야 할 메소드가 존재하지 않는다
그러나 이를 대상으로는 인스턴스 생성이 불가능 하므로 구현하는 클래스를 정의하였다

class Printer implements Printable{}

이 상황에서 보통은 인터페이스의 default 메소드를 오버라이딩 하나 static 메소드를 확인
하는 것이 목적이니 오버라이딩 하지 않았다.
그리고 인터페이스의 static 메소드는 인터페이스의 다른 메소드들과 마찬가지로 public 선언된
것으로 간주한다.

인터페이스의 static 메소드에 대해서는 이 정도만 알면 충반하다
"인터페이스에도 static 메소드를 정의할 수 있다"
"인터페이스의 static 메소드 호출 방법은 클래스 static 메소드 호출 방법과 같다."

인터페이스 대상의 instanceof 연산

if(ca instanceof Cake)

참조 변수 ca가 참조하는 인스턴스가 Cake의 인스턴스 이거나 Cake를 상속하는 클래스의
인스턴스인 경우 true가 반환되는데 Cake는 인터페이스의 이름도 될 수 있다.

"Cake를 직접 혹은 간접적으로 구현한 클래스의 인스턴스인 경우"

코드로 확인해보자

interface Printab{
    void printLine(String str);
}
class SimplePrint implements Printab{ //Printab를 직접 구현함
    public void printLine(String str){
        System.out.println(str);
    }
}

class MultiPrint extends SimplePrint{ //Printab 간적 구현함
    public void printLine(String str){
        super.printLine("start of multi...");
        super.printLine(str);
        super.printLine("end of multi");
    }
}

public class InstanceofInterface {
    public static void main(String[] args) {
        Printab prn1 = new SimplePrint();
        Printab prn2 = new MultiPrint();

        if(prn1 instanceof Printab)
            prn1.printLine("This is a simple printer.");
        System.out.println();

        if(prn2 instanceof Printab)
            prn2.printLine("This is a multiful printer.");
    }
}


This is a simple printer.

start of multi...
This is a multiful printer.
end of multi

---------------------------------------------------------------------

위 코드에서 SimplePrintPrintab 인터페이스를 직접 
MultiPrintPrintab 인터페이스를 간접 구현하여 true 반환

인터페이스의 또 다른 사용 용도 : Maker Interface

인터페이스는 클래스에 특별한 표식을 다는 용도로 사용이 된다 그리고 이렇게 사용되는
인터페이스를 가리켜 '마커 인터페이스'라 하는데 마커 인터페이스에는 아무런 메소드도 존재하지
않는 경우가 많다 

코드로 확인해보자

interface Upper{}   //마커 인터페이스
interface Lower{}   //마커 인터페이스

interface Printable{
    String getContents();
}

class Report implements Printable,Upper{
    String cons;

    Report(String cons){
        this.cons = cons;
    }
    public String getContents(){
        return cons;
    }
}

class Printer{
    public void printContents(Printable doc){
        if(doc instanceof Upper){   //doc참조 인스턴스가 Upper 구현한다면
            System.out.println(doc.getContents().toUpperCase());
        }
        else if(doc instanceof Lower){
            System.out.println(doc.getContents().toLowerCase());
        }
        else
            System.out.println(doc.getContents());
    }
}

public class MakerInterface {
    public static void main(String[] args) {
        Printer prn = new Printer();
        Report doc = new Report("Simple Funny News~");

        prn.printContents(doc);
    }
}

SIMPLE FUNNY NEWS~


----------------------------------------------------------------------------------

위 코드에서 호출하는 메소드 toUpperCase와 toLowerCase는 String 클래스에
각각 문자열의 모든 문자를 대문자,소문자로 바꿔준다.

class Printer{
    public void printContents(Printable doc){
        if(doc instanceof Upper){   //doc참조 인스턴스가 Upper 구현한다면
            System.out.println(doc.getContents().toUpperCase());
        }
        else if(doc instanceof Lower){
            System.out.println(doc.getContents().toLowerCase());
        }
        else
            System.out.println(doc.getContents());
    }
}

이 클래스 정의를 보면서 다음과 같은 판단을 내릴수 있다.

"Printable을 구현한 클래스의인스턴스만 printContents 메소드의 인자가 될 수 있다"
	-> public void printContents(Printable doc){...}

"printContents에 전달된 인스턴스가 Upper 인스턴스를 구현하면 대문자 출력"
	-> if(doc instanceof Upper){...}
    
"printContents에 전달된 인스턴스가 Lower 인스턴스를 구현하면 소문자 출력"
	-> else if(doc instanceof Lower){...}
    
그리고 Printable 인터페이스를 구현하면서 동시에 Upper 인터페이스를 구현함으로써

class Report implements Printable,Upper

내용물이 대문자로 출력이 된다. 만약 UpperLower로 바꿔주면 소문자로 출력된다.

정리하면 인터페이스 UpperLower는 클래스에 붙이는 표식으로 사용되었다
Upper는 대문자로 출력하라는 표시 Lower는 소문자로 출력하라는 표시이다.

즉 두 인터페이스는 '마커 인터페이스'

추상 클래스: Abstract Class

하나 이상의 추상 메소드를 갖는 클래스를 가리켜 '추상 클래스'라 한다

public abstract class House{	//추상 클래스
	public void methodOne(){
    	System.out.println("method one");
    }
    public abstract void methodTwo();	//추상 메소드
}

이 클래스는 하나의 추상 메소드를 갖고 있으니 추상 메소드이다. 그리고 이러한 추상 클래스에는
클래스 선언부에 abstract 선언을 추가해야 한다 이러한 추상 클래스는 성격이 인터페이스와
유사하다.

추상 클래스를 대상으로 인스턴스 생성도 불가능하며 다른 클래스에 의해서 추상 메소드가
구현이 되어야한다.

그럼에도 불구하고 이는 클래스이다 따라서 다음과 같이 구현의 형태가 아닌 상속의
형태를 띤다

public class MyHouse extends House{
	@Override
    public void methodTwo(){
    	System.out.println("method two");
    }
}

정리하면 여느 클래스들과 마찬가지로 인스턴스 변수와 인스턴스 메소드를 가지고 있지만
이를 상속하는 하위 클래스에 의해서 구현되어야 할 메소드가 하나 이상 있는 경우
이를 '추상 메소드'라 한다.
profile
반갑습니다

0개의 댓글