ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ch7 자바 객체] 추상(abstract) 클래스와 인터페이스(interface)
    프로그래밍 언어/JAVA 2022. 6. 22. 15:24
    추상 클래스(abstract class)란?

     

    클래스를 설계도에 비유한다면, 추상 클래는 미완성 설계도에 비유할 수 있다. 미완성 설계도란, 단어의 듯 그대로 완성되지 못한 채로 남겨진 설계도를 말한다. 클래스가 미완성이라는 것은 멤버의 개수에 관련된 것이 아니라, 단지 미완성 메서드(추상 메서드)를 포함하고 있다는 의미이다. 

    미완성 설계도로 완성된 제품을 만들 수 없듯이 추상 클래스로 인스턴스는 생성할 수 없다. 추상 클래스는 상속을 통해서 자손 클래스에 의해서만 완성될 수 있다. 

     

    추상 클래스 : 미완성 설계도, 인스턴스 생성 불가, 미완성 메서드(추상 메서드)를 포함하고 있는 클래스

     

     

    추상 클래스 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는 데 있어서 바탕이 되는 조상 클래스로서 중요한 의미를 갖는다. 새로운 클래스를 작성할 때 아무 것도 없는 상태에서 시작하는 것보다는 완전하지 못하더라도 어느 정도 틀을 갖춘 상태에서 시작하는 것이 나을 것이다. 

     

    추상 클래스는 키워드 'abstract'를 붙이기만 하면 된다. 이렇게 함으로써 이 클래스를 사용할 때, 클래스 선언부의 abstract를 보고 이 클래스에는 추상 메서드가 있으니 상속을 통해서 구현해주어야 한다는 것을 쉽게 알 수 있을 것이다. 

     

    abstract class 클래스 이름 { 
    ... 

     

    추상 클래스는 추상 메서드를 포함하고 있다는 것을 제외하고는 일반 클래스와 전혀 다르지 않다. 추상 클래스에도 생성자가 있으며, 멤버변수와 메서드도 가질 수 있다. 

     

     

    추상 메서드(abstract method)란?

     

    메서드는 선언부와 구현부(몸통)로 구성되어 있다. 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨 둔 것이 추상 메서드이다. 즉) 설계만 해 놓고 실제 수행될 내용은 작성하지 않았기 때문에 미완성 메서드인 것이다. 메스드를 이와 같이 미완성 상태로 남겨 놓은 이유는 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만을 작성하고, 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려 주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워두는 것이다. 

     

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

     

     

    추상클래스로부터 상속받은 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상메서드를 모두 구현해주어야 한다. 만일 조상으로부터 상속받은 추상메서드 중 하나라도 구현하지 않는다면, 자손클래스 역시 추상클래스로 지정해줘야 한다. 

     

    abstract class Player{
    	abstract void play(int pos);
    	abstract void stop();
    }
    
    class AudioPlayer extends Player{
    	void play(int pos) {} //추상세머드를 구현
    	void stop() {} //추상 메서드를 구현
    }
    
    abstract class AbstractPlayer extends Player{
    	void play(int pos) {}
    }

     

    위의 코드에서 AudioPlayer처럼 상속받은 Player의 메서드들을 모두 구현해주거나 일부만 구현해 줄 경우 AbstractPlayer처럼 추상클래스로 지정해주어야한다. 만일 추상클래스로 지정해주지 않으면 빨간줄로 에러가 뜨는데 마우스를 올려두면 위의 추천 해결방안으로 1. 구현되지 않은 메서드를 추가하거나 2. 추상 클래스로 만들거나 이렇게 2가지 방안을 추천해준다. 

     

     

    추상클래스의 작성

     

    여러 클래스에 공통적으로 사용될 수 있는 클래스를 바로 작성하기도 하고, 기존의 클래스의 공통적인 부분을 뽑아서 추상클래스로 만들어 상속하도록 하는 경우도 있다. 속이 자손 클래스를 만드는데 조상 클래스를 사용하는 것이라면, 이와 반대로 추상화는 기존의 클래스의 공통부분을 뽑아내서 조상 클래스를 만드는 것이라고 할 수 있다. 추상화를 구체화와 반대되는 의미로 이해하면 보다 쉽게 이해할 수 있을 것이다.

     

    상속계층도를 따라 내려갈수록 클래스는 점점 기능이 추가되어 구체화의 정도가 심해지며, 상속계층도를 따라 올라갈수록 클래스는 추상화의 정도가 심해진다고 할 수 있다. 즉, 상속계층도를 따라 내려 갈수록 세분화되며, 올라갈수록 공통요소만 남게된다. 

     

    class Marine{
    	int x,y;
    	void move(int x, int y){}
    	void stop() {}
    	void stimPack() {}
    }
    
    class Tank{
    	int x,y;
    	void move(int x, int y){}
    	void stop() {}
    	void changeMode() {}
    }
    
    class Dropship{
    	int x,y;
    	void move(int x, int y){}
    	void stop() {}
    	void load() {}
    	void unload() {}
    }

     

     

    위에 처럼 각자 나름대로 기능을 가지고 있는 유닛들을 클래스로 정리해 본 것인데 살펴보면 공통부분이 있다. 이런 부분들을 하나의 클래스로 만들고 상속 받도록 만들어보자.

     

     

    abstract class Unit{
    	int x,y;
    	abstract void move(int x, int y); //지정된 위치로 이동
    	void stop() {} // 현재 위치에서 멈춤
    }
    
    class Marine extends Unit{ //보병
    	void move(int x, int y){}
    	void stimPack() {/*스팀팩 사용*/}
    }
    
    class Tank extends Unit{ //탱크
    	void move(int x, int y){}
    	void changeMode() { /*공격 모드 변환*/}
    }
    
    class Dropship  extends Unit //수송선
    	void move(int x, int y){}
    	void load() {/*선택 대상 태운다 */} 
    	void unload() {} //선택된 대상 내린다
    }

     

     

    각 클래스의 공통부분을 뽑아내서 Unit 클래스를 정의하고 이로부터 상속받도록 하였다. 이들 클래스에서 stop메서드는 선언부, 구현부 모두 공통적이지만, Marine, Tank는 지상유닛이고 Dropship은 공중유닛이기 때문에 이동하는 방법이 서로 달라서 move 메서드의 실제 구현 내용이 다를 것이다. 그래도 move 메서드의 선언부는 같기 때문에 추상메서드로 정의할 수 있다.! 즉) move 메서드가 추상메서드로 선언된 것은, 앞으로 Unit 클래스를 상속받아서 작성되는 move메서드는 자신의 클래스에 맞게 알맞게 반드시 구현해야 한다는 의미가 담겨있다. 

     

     

    Unit [ ] group = new Unit[3];
    group[0] = new Marine();
    group[1] = new Tank();
    group[2] = new Dropship();

    for(int =0; i<group.length; i++)
       group[i].move(100,200);  // Unit배열의 모든 유닛을 좌표 (100, 200)의 위치로 이동시킨다.

     

     

    ※ 예제 1번  

     

    package ch7;
    
    public class Ex7_10 {
    
    	public static void main(String[] args) {
    		Unit [] group = {new Marine(), new Tank(), new Dropship()}; 
    		for(int i =0; i<group.length; i++) {
    			group[i].move(100, 200);
    		}
    	}
    
    }
    
    
    abstract class Unit{
    	int x,y;
    	abstract void move(int x, int y); //지정된 위치로 이동
    	void stop() {} // 현재 위치에서 멈춤
    }
    
    class Marine extends Unit{ //보병
    	void move(int x, int y){
    		System.out.println("Marine[x=" + x + ", y="+y+ "]");
    	}
    	void stimPack() {/*스팀팩 사용*/}
    }
    
    class Tank  extends Unit{ //탱크
    	void move(int x, int y){
    		System.out.println("Tank[x=" + x + ", y="+y+ "]");
    	}
    	void changeMode() { /*공격 모드 변환*/}
    }
    
    class Dropship  extends Unit{ //수송선
    	void move(int x, int y){
    		System.out.println("Dropship[x=" + x + ", y="+y+ "]");
    	}
    	void load() {/*선택 대상 태운다 */} 
    	void unload() {} //선택된 대상 내린다
    }

    예제 1번 출력 값

     

    예제 1번은 공조상인 Unit 클래스 타입의 객체 배열을 통해서 서로 다른 종류의 인스턴스를 하나의 묶음으로 다룰 수 있다는 것을 보여주기 위한 것이다. 다형성에서 알아봤듯이 조상 타입의 참조변수로 자손 타입의 인스턴스를 참조하는 것이 가능하기 때문에  Unit [] group = {new Marine(), new Tank(), new Dropship()};  이처럼  조상 타입의 배열에 자손 타입의 인스턴스를 담을 수 있는 것이다. 

     

     

    인터페이스 (interface)

     

    인터페이스는 일종의 추상클래스이다. 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 상수만을 멤버로 가질 수 있으며, 그 외에 어떠한 요소도 허용하지 않는다. 

     

    인터페이스도 추상클래스처럼 완성되지 않은 불완전한 것이기 때문에 그 자체만으로 사용되기 보다는 클래스를 작성하는데 도움 줄 목적으로 작성된다. 

     

    interface 인터페이스이름{
       public static final 타입 상수이름 = 값;
       public abstract 메서드이름(매개변수 목록);
    }

     

    interface의 접근제어자로 public 또는 default만 사용할 수 있다. 

     

    - 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.

    - 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. 

     

    interface PlayingCard{
    	public static final int SPADE =4;
    	final int DIAMOND =3; // public static final int DIAMOND=3;
    	static int HEART =2;
    	int CLOVER =1;
    	
    	public abstract String getCardNumber();
    	String getCardKind(); // public abstract String getCardKind();
    }

     

     

    인터페이스의 상속

     

    인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와 달리 다중상속, 즉 여러개의 인터페이스로부터 상속을 받는 것이 가능하다.

     

    interface Movable{
    	void move(int x, int y);
    }
    
    interface Attackable{
    	void attack(Unit u);
    }
    
    interface Fightable extends Movable, Attackable{}

     

    이넡페이스(Fightable)는 조상 인터페이스 (Movable, Attackable)에 정의된 멤버를 모두 상속 받는다. 그래서 Fightable 자체에는 정의된 멤버가 하나도 없지만 조상 인터페이스로부터 상속받은 두 개의 추상 메서드를 멤버로 갖게 된다.

     

     

     

    인터페이스의 구현

     

    인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상클래스가 상속을 통해 추상메서드를 완성하는 것처럼, 인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야 한다. 다만) 클래스는 확장한다는 의미의 키워드 'extends'를 사용하지만 인터페이스는 구현한다는 의미인 키워드 'implements'를 사용할 뿐이다.

     

    class 클래스이름 implements 인터페이스이름 { 
    // 인터페이스에 정의된 추상메서드를 모두 구현해야 한다.
    }
    class Fighter extends Unit implements Fightable{
    	 public void move(int x, int y) {}
    	 public void attack (Unit u) {}
    }

     

     

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

     

    인터페이스 Fightable을 클래스 Fighter가 구현했을 때, 다음과 같이 Fighter인스턴스를 Fightable 타입의 참조변수로 참조하는 것이 가능하다. 

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

     

    따라서 인터페이스는 다음과 같이 메서드의 매개변수 타입으로도 사용될 수 있다.

    void attack (Fightable f) {
    }

     

     

    인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다. 

    class Fighter extends Unit implements Fightable{
    public void move(int x, int y) {}
    public void attack (Fightable f) {}
    }

     

    위와 같이 Fightable인터페이스를 구현한 Fighter클래스가 있을 때, attack메서드의 매개변수로 Fighter인스턴스를 넘겨줄 수 있다. 즉) attack(new Fighter())와 같이 할 수 있다는 것이다. 그리고 아래처럼 메서드의 리턴 타입으로 인터페이스를 지정하는 것도 가능하다.

     

    Fightable method() {
    Fighter f = new Fighter(); 
    return f;  //이 두문장을 한 문장으로 바꾸면 return new Fighter();
    }

     

    리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다. 

    즉) 위의 코드에서는 method()의 리턴타입이 Fightable인터페이스이기 때문에 메서드의 return 문에서 Fightable 인터페이스를 구현한 Fighter클래스의 인스턴스 주소를 반환한다.

     

     

    인터페이스의 장점

     

    1. 개발시간을 단축시킬 수 있다.

     : 일단 인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다. 메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다. 그리고 동시에 다른 한 쪽에는 인터페이스를 구현하는 클래스를 작성하게 되면, 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고 양쪽에서 동시에 개발을 진행할 수 있다. 

     

    2. 표준화가 가능하다.

    : 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록함으로써 보다 일관되고 정형화된 프로그램 개발이 가능하다.

     

    3. 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.

     : 서로 상속관계에 있지도 않고, 같은 조상클래스를 가지고 있지 않은 서로 아무런 관계가 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어줄 수 있다.

     

    4. 독립적인 프로그래밍이 가능하다.

     : 인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.

     

     

     

    디폴트 메서드와 static 메서드

     

    조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스의 경우에는 보통 큰 일이 아니다. 인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해얗기 때문이다. 인터페이스가 변경되지 않으면 좋겠지만, 아무리 설계를 잘 해도 언젠가 변경은 발생하기 마련이다. 따라서 JDK 설계자들은 디폴트 메서드(default method)라는 것을 고안해내었다. 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다. 

     

     

    ※ 예제 2번 ※

     

    package ch7;
    
    public class Ex7_12 {
    
    	public static void main(String[] args) {
    		
    		Child3 c = new Child3();
    		c.method1();
    		c.method2();
    		MyInterface.staticMethod();
    		MyInterface2.staticMethod();
    
    	}
    
    }
    
    class Child3 extends Parent3 implements MyInterface, MyInterface2{
    	public void method1() {
    		System.out.println("method1() in Child3"); //오버라이딩
    	}
    }
    
    class Parent3{
    	public void method2() {
    		System.out.println("method2 in Parent3");
    	}
    }
    
    interface MyInterface{
    	default void method1() {
    		System.out.println("method1() in MyInterface");
    	}
    	
    	default void method2() {
    		System.out.println("method1() in MyInterface");
    	}
    	
    	static void staticMethod() {
    		System.out.println("staticMethod() in MyInterface");
    	}
    }
    
    interface MyInterface2{
    	default void method1() {
    		System.out.println("method1() in MyInterface2");
    	}
    	static void staticMethod() {
    		System.out.println("staticMethod() in MyInterface2");
    	}
    }

    예제 2번 출력 값

     

    1. 여러 인터페이스의 디폴트 메서드 간의 충돌 
    : 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.

    2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
    : 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다. 
    반응형

    댓글

Designed by Tistory.