본문 바로가기
멀티캠퍼스 풀스택 과정/Java의 정석

자바의 정석4-2 인터페이스(interface), 디폴트와 static 메서드

by 이쟝 2022. 1. 5.

인터페이스(interface)

- 추상 메서드의 집합

- 구현된 것이 전혀 없는 설계도. 껍데기(모든 멤버가 public)

- 원래 인퍼테이스의 모든 메서드는 추상메서드이지만 static메서드와 디폴트 메서드(default method)가 추가 되었음

 

추상클래스와 인터페이스와의 차이

추상클래스: 일반클래스인데 추상 메서드도 같이 가지고 있음

인터페이스: 아무것도 없는데(멤버변수x) 추상 메서드만 가지고 있음.(iv, cv xx, 상수만)

추상클래스 -> 미완성 설계도, 인터페이스 -> 기본 설계도

 

인터페이스의 작성

 

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

 

모든 멤버변수(상수) 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;  // public static final int HEART = 2;
	int CLOVER = 1;        // public static final int CLOVER = 1;
	
	String getCardNumber(); //public abstract String getCardNumber();
}

인터페이스의 상속

 

- 인터페이스의 조상은 인터페이스만 가능(Object가 최고 조상 아님)

- 다중 상속이 가능(추상메서드는 충돌해도 문제 없음)

(자바가 단일 상속인 이유가 선언부가 다르면 둘다 상속받으면 그만이지만, 선언부가 같고 내용{}이 다르면 어느 쪽을 상속받을 지 결정할 수 없기 때문이다.)

 

interface Movable {
	/* 지정된 위치(x,y)로 이동하는 기능의 메서드 */
	void move(int x, int y);
}
interface Attackable {
	/* 지정된 대상(u)를 공격하는 기능의 메서드 */ 
	void attack(Unit u);
}
interface Fightable extends Movable, Attackable { }

 

-> 자손인터페이스(Fightable)은 조상 인터페이스(Movable, Attackable)에 정의된 멤버를 모두 상속받기 때문에 Fightable 자체에는 정의된 멤버가 없지만 Fightable은 상속받은 두개의 추상 메서드(move, attack)을 멤버로 갖게 됨


인터페이스의 구현

- 인터페이스에 정의된 추상 메서드를 완성하는 것(미완성 설계도 완성하기)

- 추상클래스extends를 사용해서 상속을 통해 추상메서드를 완성하는 것처럼 인터페이스 implements를 사용해서 추상메서드를 완성한다.

 

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

 

-> Fighter클래스는 Fightable 인터페이스를 구현한다!(Fightable 인터페이스에 있는 추상메서드의 몸통을 만들어줬다)

 

abstract class Fighter implements Fightable {
	public void move(int x, int y) {}
}
-> 일부만 구현하는 경우, 클래스 앞에 abstract를 붙여야 함(추상 클래스와 동일함)

추상클래스의 구현과 인터페이스 구현

 

추상클래스의 구현
class AudioPlayer extends Player { 
	void move(int x, int y){}
	void stop() {}
}
-> AudioPlayer 완성된 클래스(설계도) ,Player(미완성된 설계도)

인터페이스의 구현
class Fighter implements Fightable { 
	public void move(int x, int y){ }
	public void attack(Unit u) {}
}
-> Fighter 완성된 인터페이스(설계도), Fightable(미완성된 설계도)

추상클래스와 인터페이스의 공통점과 차이점

- 공통점: 추상메서드(미완성 설계도)를 가지고 있다. 

- 차이점: 추상클래스는 일반클래스 + 추상메서드 / 인터페이스는 추상메서드만!(iv를 가질 수 없음)

 

추상클래스 = iv + 생성자 + im + 추상메서드 인터페이스 = 추상메서드 + 상수 + (static 메서드 + default 메서드)

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

(1) 인터페이스도 구현 클래스의 부모이다.

 

class Fighter extends Unit implements Fightable { 
	public void move(int x, int y){ }
	public void attack(Unit u) {}
}
-> 상속과 구현을 동시에 할 수 있음!

Unit u      = new Fighter();  // 조상클래스
Fightable f = new Fighter();  // 인터페이스

 

더보기

-> Fighter 클래스로 Fightable 인터페이스를 구현했으니까 Fightable 참조변수로 Fighter클래스를 참조 하는게 가능함(다형성)

 

(2) 인터페이스 타입 매개변수는 인터페이스를 구현한 클래스의 객체만!!

 

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

 

interface Fightable {
	void move(int x, int y);
	void attack(Fightable f); // Fightable의 인퍼테이스를 구현한 클래스의 인스턴스만 가능!
}	

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

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

인퍼테이스는 다음과 같이 메서드의 매개변수 타입으로 사용될 수 있다. 
void attack(Fightable f) {
...
}

f.attack(new Fighter());와 같이 할 수 있음!

 

-> attack 메서드를 호출할 때는 매개변수로 Fightable 인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야 한다.!

 

(3) 인터페이스를 메서드의 리턴타입으로 지정할 수 있다.

 

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

 

-> 리턴타입인터페이스이면? 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 의미

-> Fightable은 리턴타입(반환타입) -> Fightable 인터페이스를 구현한 클래스의 인스턴스를 반환(return new Fighter();)

-> 메서드의 반환타입이 Fightable(인터페이스)이라면 안에 매개변수도 Fightable이 되어야 함 (메서드의 기본형처럼)

-> Fightable method의 문장을 한 문장으로 바꾸면 return new Fighter( );로 할 수 있음

 

public class FighterTest {
	public static void main(String[] args) {
		Unit2 u = new Fighter();
		Fable f = new Fighter();   //Fighter f = new Fighter();도 가능
		
		u.move(100,200);
		u.stop();
//		u.attack();  Unit에는 attack( )이 없어서 호출불가
		
		f.move(100, 200);
		Fighter f2 = new Fighter(); // 한줄로 줄이면 f.attack(new Fighter());
		f.attack(f2);
//		f.stop();   Fightable에는 stop()이 없어서 호출불가

	}
}

abstract class Unit2 {   // Unit2 추상클래스 생성
	int x, y;
	abstract void move(int x, int y);              // 정수 값만큼 이동하는 메서드
	void stop() {System.out.println("멈춥니다.");}  
}
	
interface Fable {    // Fable 인터페이스 생성
	void move(int x, int y);  // public abstract 생략
	public abstract void attack(Fable f);
}

class Fighter extends Unit2 implements Fable { // Unit2와 Fable을 상속 구현해 메서드를 완전하게 만듦)
	// 오버라이딩 규칙: 조상보다 접근제어자의 범위가 좁으면 안된다.( Fable의 범위는 public, default -> public )
	
	public void move(int x, int y) { 
		System.out.println("["+x+","+y+"]로 이동");
	}
	public void attack(Fable f) { 
		System.out.println(f + "를 공격");
	}
}

인터페이스의 장점

 

1) 두 대상(객체) 간의 연결, 대화, 소통을 돕는 중간 역할을 한다.

) GUI(Graphic User Interface, 윈도우)

 

2) 선언(설계)[껍데기]와 구현[알맹이]을 분리시킬 수 있게 한다. (변경이 유리함)

 

// 알맹이
class B { 
	public void method() {
		System.out.println("methodInB");
	}
}

// 껍데기
interface I { // classB에서 껍데기 부분(선언부)만 빼내서 새로운 인터페이스 생성
	public void method();
}

// 껍데기와 알맹이 분리
class B implements I {
	public void method() {
		System.out.println("methodInB");
	}
}

 

더보기

-> 껍데기 + 알맹이 -> 유연하지 않고 변경에 불리

-> 인터페이스를 새로 구현해서 껍데기와 알맹이를 분리시켰다.

 

강한 결합(빠르고 직접적)

-> A B를 사용(AB에 의존) 하지만 만약 AC를 사용하고 싶다면? 코드를 전체를 바꿔야 할 것이다. 하지만 인터페이스를 사용한다면?

 

느슨한 결합(느리지만 변경에 유리)

-> 인터페이스 덕분에 B가 변경되어도 A는 안 바꿀 수 있게 된다.(느슨한 결합)

-> A클래스는 B와 직접적인 관계가 없고 인터페이스와 관련 있다. (알맹이만 바꾸면 되고 껍데기는 똑같이!)

 

직접적인 관계의 두 클래스(A-B)

A클래스가 B클래스의 메서드를 호출(사용)

-> 직접적인 관계의 두 클래스는 한 쪽(Provider)이 변경되면 다른 한 쪽(User)도 변경되어야 한다.

 

간접적인 관계의 두 클래스(A-I-B)

기존의 B클래스를 인터페이스 I (껍데기), 클래스 B (알맹이)로 분리

-> B클래스method를 추상메서드로 갖는 인터페이스 작성

 

class A { 
	public void methodA(I i) {
		i.methodB();
	}
}

class C implements I {       //C 클래스로 I 인터페이스를 구현
	public void methodB() {
		System.out.println("methodB() in C");
	}	
}

 

-> A클래스도 B클래스의 메서드를 사용하는 것이 아니라 인터페이스 I를 사용!(A는 더 이상 B와 관계 X)

-> BC클래스로 변경해도 A클래스는 변경하지 않아도 된다! 새로 만들 수 있다!

 

public class InterfaceTest {

	public static void main(String[] args) {
		A a = new A();
		B a1 = new B();  // 한 줄로 줄이면 a.method(new B());
		a.method(a1);  // A가 B를 사용(의존)
		a.method(new C()); 
	}
}
// B클래스의 선언과 구현을 분리. method의 구현
class B implements I {
	public void method() {
		System.out.println("B클래스의 메서드");
	}
}
// method의 선언 
interface I {
	void method() ;
}

class A {
	public void method(I i) {  // 인터페이스 I를 구현한 클래스만 들어와라
		i.method();
	}
}

// C클래스 추가
class C implements I {
	public void method() {
		System.out.println("C클래스의 메서드");
	}
}

 

3) 개발시간을 단축할 수 있다.

- 메서드를 호출하는 쪽에서는 선언부만 알면 되기 때문에 일단 인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다.

- 동시에 인터페이스를 구현하는 클래스를 작성하게 되면, 양쪽에서 동시에 개발을 진행할 수 있다.

- AB를 완성하려면 B를 완성할 때까지 A는 기다려야 하지만 인터페이스 I가 있다면 추상메서드는 호출 가능하기 때문에 A클래스는 I를 이용해서 코드를 먼저 작성할 수 있다. 그리고 B클래스는 구현하기만 하면 된다.

 

4) 변경에 유리한 유연한 설계가 가능하다.

- A -> I -> B or C ㅇ(앞의 예제처럼)

 

5) 표준화가 가능하다.

- 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음. 개발자들에게 인터페이스를 구현해 프로그램을 작성하도록 하게 해서 보다 일관되고 정형화된 프로그램의 개발이 가능하다. (JDBC: 인터페이스 집합)

 

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

- 서로 아무런 관계 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어줄 수 있다.

 

삼층계층도

Unit클래스, GroundUnit 지상 유닛, AriUnit 공중 유닛,

인간: Marine 병사, 로봇:SCV, Tank 탱크, Dropship 수송선(비행기)

 

예제) 기계 수리 메서드 작성!(SCV, Tank, Dropship)

 

interface Repairable() {}

class SCV extends GroundUnit implements Repairable() { }
class Tank extends GroundUnit implements Repairable() { }
class Drophsip extends AirUnit implements Repariable() { }

void repair(Repairable r) { //Repairable 인터페이스를 구현한 클래스만!
	if(r instanceof Unit) {	}
}

 

Repairable 인터페이스로 묶어주게 된다면 Repairable을 구현했다는 공통점이 생겨서 Repairable의 자손이 된 것!

-> 상속계층도에서 공통점을 찾기 어려울 때는 인터페이스를 구현하게 바꿔준다.

-> Repariable 인터페이스를 구현한 클래스: SCV, Tank, Dropship


디폴트 메서드와 static 메서드

- 원래 인터페이스에 추상 메서드만 선언할 수 있었는데, JDK1.8부터 디폴트 메서드와 static메서드도 추가 가능

 

디폴트 메서드

- 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니어서 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.

- 인터페이스에 새로운 메서드(추상 메서드)를 추가하기 어려웠기 때문에 디폴트 메서드가 생겨난 것!! 

- 앞에 키워드 default를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통 { }이 있어야 한다. 접근제어자는 public이며, 생략 가능하다.

 

interface MyInterface {
	void method();
	void newMethod();  // 추상메서드
}
위의 코드를 아래 코드로 변경!
interface MyInterface {
	void method();
	default void newMethod() {} // 디폴트 메서드
}
-> 조상 클래스에 새로운 메서드를 추가한 것과 동일해짐

 

더보기
- 새로 추가된 디폴트 메서드가 기존의 메서드와 충돌할 때의 해결책

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

2) 디폴트 메서드와 조상 클래스의 메서드 간의 충돌: 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시

-> 그냥 직접 오버라이딩 하면 해결됨

 

예제) Parent 클래스에 method2 / MyInterface 클래스에 method1, method2, staticMethod / MyInterface 클래스에 method1, staticMethod  / Parent 클래스를 상속받고 MyInterface, MyInterface2를 구현하는 Child 클래스 생성하고 Child 클래스에 method1 추가!

 

class Child extends Parent implements MyInterface, MyInterface2 {
	public void method1() {
		System.out.println("method1() in Child"); // 오버라이딩
	}
}

class Parent {
	public void method2() {
		System.out.println("mthod2() in Parent");
	}
}

interface MyInterface {
	default void method1() {
		System.out.println( "method1() in MyInterface");
	}
	default void method2() {
		System.out.println("method2() 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("staticMehtod() in MyInterface2");
	}
}

public class interfaceTest2 {

	public static void main(String[] args) {
		Child c = new Child();
		c.method1();  //method1() in Child 인터페이스의 메서드가 아닌 child클래스의 메서드
		c.method2();  //method2() in Parent 안터페이스의 메서드가 아닌 parent클래스의 메서드
		MyInterface.staticMethod();  // staticMethod() in MyInterface
		MyInterface2.staticMethod(); // staticMethod() in MyInterface2

	}
}