인터페이스란?
인터페이스는 일종의 추상클래스이다. 추상클래스처럼 추상 메소드를 갖지만, 추상화 정도가 높아, 추상 클래스와 달리, 몸통을 갖춘 일반 메소드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상 메소드와 상수만 멤버로 가질 수 있다. 추상 클래스를 미완성 설계도로 본다면, 인터페이스는 기본 설계도라고 할 수 있다.
인터페이스 작성하기
인터페이스 작성은 클래스와 동일하다. 단, 키워드로 class가 아닌, interface를 작성한다. 인터페이스의 멤버들에는 일반적인 클래스, 멤버들과 달리 제약사항이 있다.
- 모든 멤버변수는 public static final이어야 하며(인터페이스는 멤버변수로 상수만 가질 수 있다) 이를 생략할 수 있다.
- 모든 메소드는 public abstract이어야하며, 이를 생략할 수 있다.
멤버변수, 메소드의 제어자 모두 생략할 수 있다. 이는, 생략하여도, 컴파일시 컴파일러에서 추가해주기 때문에 생략이 가능한 것이다.
인터페이스의 상속
인터페이스는 다른 인터페이스로부터만 상속받을 수 있다(클래스, 추상클래스 불가능). 인터페이스 상속을 할 때는 클래스 상속하듯 extends키워드를 사용한다. 그리고 일반적인 클래스와 다르게 다중상속이 가능하다. 자바에서 다중상속을 지원하지 않는 이유는 메소드 충돌, 멤버변수 충돌등의 이유가 있다 근데 왜 인터페이스는 다중상속을 지원할까? 이유는 이렇게 두가지를 볼 수 있다.
- 인터페이스에서 멤버변수 정의는 static변수만 정의가 가능하므로, 클래스 이름을 붙여 구분이 가능하다.
- 추상메소드는 실체가 없는 메소드이다, 그렇기때문에, 원하는 인터페이스에 대해서 상속을 받으면 되기 때문에, 문제가 되지않는다.
이로 인해, 다중 상속 구현을 위해 인터페이스를 사용한다고 하기도 한다. 아래 예시를 보면, movable과attackable인터페이스 두개를 상속받는 attackable 인터페이스를 볼 수 있다.
interface movable{
public static final int x = 10;
int y = 20; // 생략이 가능하므로. 사실상 public static final int y와 동일한 구문이다
public abstract void move(int x, int y);
}
interface attackable{
public static final int hit = 10;
String msg = "Attack";
public abstract void attack();
}
interface Fightable extends movable, attackable{}
인터페이스 구현하기
인터페이스도 추상클래스처럼 자신을 구현하는 클래스를 작성할 수 있다. 추상클래스는 extends를 통해서 구현하였다면, 인터페이스는 implements를 이용해서 구현한다. 만약에 구현한 인터페이스에서 일부 메소드만 구현한다면 abstract를 붙여서 추상클래스로 선언해야한다.
interface test11{
public abstract void hello();
public abstract void world();
}
abstract class interTest implements test11{
public void hello(){
System.out.println("Hello");
}
@Override
public void world() {}
}
class testing extends interTest{
void test(){
this.hello();
}
}
그리고 상속과 구현을 동시에 해줄 수 도 있다.
class Unit1{
private int x;
private int y;
private int hp;
Unit1(int x,int y,int hp){
this.x = x;
this.y = y;
this.hp = hp;
}
public int getX(){
return x;
}
public int getY(){
return y;
}
public int getHp(){
return hp;
}
public void setY(int y){
this.y = y;
}
public void setX(int x){
this.x = x;
}
public void setHp(int hp){
this.hp = hp;
}
}
class testingg extends Unit1 implements test11,attackable{
testingg(){
super(10,20,100);
}
@Override
public void hello() {
}
@Override
public void world() {
}
@Override
public void attack(int damage) {
}
}
인터페이스 이름에는 주로 'able'로 끝나는 네이밍이 많다. 이유는 이 인터페이스는 어떠한 기능, 행위를 하는데 필요한 메소드를 제공한다는것을 강조하기 위함이다. 인터페이스를 구현해 보자.
interface movable{
public static final int x = 10;
int y = 20; // 생략이 가능하므로. 사실상 public static final int y와 동일한 구문이다
public abstract void move(int x, int y);
}
interface test11{
public abstract void hello();
public abstract void world();
}
interface attackable{
public static final int hit = 10;
String msg = "Attack";
public abstract void attack(int damage);
}
interface Fightable extends movable, attackable{};
class Unit1{
private int x;
private int y;
private int hp;
Unit1(int x,int y,int hp){
this.x = x;
this.y = y;
this.hp = hp;
}
public int getX(){
return x;
}
public int getY(){
return y;
}
public int getHp(){
return hp;
}
public void setY(int y){
this.y = y;
}
public void setX(int x){
this.x = x;
}
public void setHp(int hp){
this.hp = hp;
}
}
class Fighter extends Unit1 implements Fightable{
Fighter(){
//default constructor if nothing value
super(10,20,100);
}
Fighter(int x, int y, int hp){
//if get value
super(x,y,hp);
}
@Override
public void move(int x, int y) {
System.out.println("Move to x : " + x + " y : " + y);
this.setX(x);
this.setY(y);
}
@Override
public void attack(int damage) {
this.setHp(this.getHp() - damage);
System.out.println("Attack Damage : " + damage);
System.out.println("Player's HP : " + this.getHp());
}
}
class interface1{
public static void main(String[] args){
Fighter a = new Fighter();
a.move(10,20);
a.attack(10);
a.attack(20);
if(a instanceof Unit1){
System.out.println("Instance of Unit1");
}
if(a instanceof Fightable){
System.out.println("Instance of Fightable");
}
if(a instanceof movable){
System.out.println("Instance of movable");
}
if(a instanceof attackable){
System.out.println("Instance of attackable");
}
if(a instanceof Object){
System.out.println("Instance of Object");
}
}
}
인터페이스를 이용한 다중상속
자바에서 인터페이스를 이용해서 다중상속을 해줄 수 있다. 다중 상속을 지원하지 않는 이유는, 상속받는 멤버중에서 멤버변수의 이름이 같거나, 메소드의 선언부가 일치하고, 구현되는 내용이 다른 경우, 자손 클래스는 어떤 조상 클래스의 메소드를 상속받는것인지 알 수 없기 때문이다. 반두개의 클래스를 상속받아야하는 상황에서 두 조상클래스 중 비중이 높은 쪽을 선택하고, 다른 한 쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나(사실 이 방법은 다중상속이라 하기에는 애매한 감이 있는거같다) 한쪽의 필요한 부분을 뽑아 인터페이스로 만든 다음 구현을 할 수 있다.
class radio{
protected boolean power;
protected int channel;
protected int volume;
public void power(){power = !power;}
public void channelUp(){channel++;}
public void channelDown(){channel--;}
public void volumeUp(){volume++;}
public void volumeDown(){volume--;}
}
class vcr{
protected int counter;
public void play(){System.out.println("Play vcr");}
public void stop(){System.out.println("Stop vcr");}
public void reset(){System.out.println("Reset vcr"); counter = 0;}
public int getCounter(){return counter;}
public void setCounter(int c){counter = c;}
}
// vcr 클래스에 정의된 메소드와 일치하는 추상 메소드를 갖는 인터페이스 작성
interface ivcr{
public abstract void play();
public abstract void stop();
public abstract void reset();
public abstract void getCounter();
public abstract void setCounter(int c);
}
public class interface2 extends radio implements ivcr{
vcr v = new vcr();
@Override
public void play() {
v.play();
}
@Override
public void stop() {
v.stop();
}
@Override
public void reset() {
v.reset();
}
@Override
public void getCounter() {
v.getCounter();
}
@Override
public void setCounter(int c) {
v.setCounter(c);
}
public static void main(String[] args){
}
}
인터페이스 활용해보기
package interfaceTest;
import java.util.Vector;
interface Targetable{}
interface Repairable {}
interface Liftable{
public abstract void liftOff();
public abstract void move(int x, int y);
public abstract void stop();
public abstract void land();
}
interface Attackable{
public abstract void attack(Targetable target,int damage);
}
abstract class GameObject{
int hitPoint;
final int MAX_HP;
GameObject(int MAX_HP){
this.MAX_HP = MAX_HP;
this.hitPoint = this.MAX_HP;
}
}
class Unit extends GameObject{
Unit(int MAX_HP){
super(MAX_HP);
}
}
class Building extends GameObject{
boolean lift;
Building(int MAX_HP){
super(MAX_HP);
this.lift = false;
}
}
class ImplementAttackable implements Attackable{
ImplementAttackable(){}
@Override
public void attack(Targetable target,int damage) {
GameObject t = (GameObject) target;
int previous = t.hitPoint;
t.hitPoint -= damage;
System.out.println(target + " health : " + previous + " -> " + t.hitPoint);
}
}
class ImplementLift implements Liftable{
@Override
public void land() {
System.out.println("Land");
}
@Override
public void stop() {
System.out.println("Stop");
}
@Override
public void move(int x, int y) {
System.out.println("Move");
}
@Override
public void liftOff() {
System.out.println("Lift Off");
}
}
class Tank extends Unit implements Repairable,Targetable,Attackable{
int damage;
Tank(){
super(400);
this.damage =50;
}
Attackable a = new ImplementAttackable();
public void attack(Targetable target,int damage){
a.attack(target,damage);
}
@Override
public String toString(){
return "Tank";
}
}
class Marine extends Unit implements Repairable,Targetable,Attackable{
int damage;
Marine(){
super(70);
this.damage = 10;
}
Attackable a = new ImplementAttackable();
public void attack(Targetable target,int damage){
a.attack(target,damage);
}
@Override
public String toString(){
return "Marine";
}
}
class DropShip extends Unit implements Repairable,Targetable{
DropShip(){
super(150);
}
Vector<Unit> units = new Vector<Unit>();
// 공격가능이 아니므로, ImplementAttackable멤버로 x
public void loadUnit(Unit u){
units.add(u);
System.out.println(u + "를 태웠습니다.");
}
public void unLoadUnit(Unit u){
units.remove(u);
}
@Override
public String toString(){
return "DropShip";
}
}
class SCV extends Unit implements Attackable, Repairable,Targetable{
int damage;
SCV(){
super(60);
this.damage = 5;
}
public void Repair(Repairable r){
GameObject g = (GameObject) r;
while(g.hitPoint != g.MAX_HP){
System.out.println(++g.hitPoint);
}
System.out.println("수리완료");
}
Attackable a = new ImplementAttackable();
@Override
public void attack(Targetable target, int damage) {
a.attack(target,damage);
}
@Override
public String toString(){
return "SCV";
}
}
class CommandCenter extends Building implements Liftable,Targetable{
CommandCenter(){
super(1000);
}
Liftable i = new ImplementLift();
@Override
public void move(int x, int y) {
i.move(x,y);
}
@Override
public void stop() {
i.stop();
}
@Override
public void land() {
i.land();
}
@Override
public void liftOff() {
i.land();
}
}
class test{
public static void main(String[] args){
Tank t = new Tank();
DropShip d = new DropShip();
Marine m = new Marine();
SCV s = new SCV();
t.attack(d,t.damage);
t.attack(d,t.damage);
m.attack(t,t.damage);
}
}
인터페이스의 이해
인터페이스의 본질적인 측면에 대해 보자. 인터페이스를 사용하기 위해서는 두가지 사항을 염두해야한다
- 클래스를 사용하는 쪽(User)과 클래스를 제공하는쪽(Provider)가 있다.
- 메소드를 사용하는 쪽에서는 사용하려는 메소드의 선언부만 알면 된다.(내용은 몰라도 된다)
class A{
public void playB(B b){
b.methodB();
}
public void playC(C c){
c.methodC();
}
}
class B{
public void methodB(){
System.out.println("method b");
}
}
class C{
public void methodC(){
System.out.println("method c");
}
}
class test{
public static void main(String[] args){
A a = new A();
B b = new B();
C c = new C();
a.playB(b);
a.playC(c);
}
}
이런 클래스가 있다고 가정하자. 이런 경우 A클래스는 B와 C가 작성되어있어야 하며 직접적인 관계로 되어있다. B,C의 선언부가 변경되면 이를 사용하는 클래스 A도 변경되어야 한다. 예를 들면 A클래스의 methodB,methodC의 내용에 서술된 B,C의 선언부 이름을 변경해 주어야되는것이다. 이와 같이 직접적인 관계의 클래스관계의 Provider과 User 모두 변경이 되야한다는 단점이 있다. 인터페이스를 매개체로하면 인터페이스를 통해 B,C메소드에 접근하도록 하면, B,C에 변경사항이 생기거나 클래스 B,C와 같은 기능의 다른 클래스로 대체되어도 클래스 A는 전혀 영향을 받지 않도록 하는것이 가능하다. 이와 같이 인터페이스를 이용해 변경해 줄 수 있습니다
package Interface2;
interface I{
public abstract void play();
}
class B implements I{
@Override
public void play() {
System.out.println("Class B");
}
}
class C implements I{
@Override
public void play() {
System.out.println("Class C");
}
}
class A{
void autoPlay(I i){
i.play();
}
}
public class intertest {
public static void main(String[] args){
A a = new A();
a.autoPlay(new B());
a.autoPlay(new C());
}
}
위와 같은 형태는 매개변수를 동적으로 제공받는 형태이다. 반대로 제 3의 클래스를 통해서 메소드를 동적으로 제공받을 수 도 있다.
package Interface2;
class intertest{
public static void main(String[] args){
}
}
interface I{
public abstract void methodB();
}
class A{
void methodA(){
I i = InstanceManager.getInstance();
i.methodB();
System.out.println(i.toString());
}
}
class B implements I{
public void methodB(){
System.out.println("Method B");
}
@Override
public String toString(){
return "Class B";
}
}
class InstanceManager{
public static I getInstance(){
return new B();
}
}
또 다른 방법으로 아래와 같이 제 3의 클래스를 통해서 메소드를 제공받을 수 도 있다.
package Interface2;
class intertest{
public static void main(String[] args){
A a = new A();
a.methodA();
}
}
interface I{
public abstract void methodB();
}
class A{
void methodA(){
I i = InstanceManager.getInstance();
i.methodB();
System.out.println(i.toString());
}
}
class B implements I{
public void methodB(){
System.out.println("Method B");
}
@Override
public String toString(){
return "Class B";
}
}
class InstanceManager{
public static I getInstance(){
return new B();
}
}
인터페이스의 default메소드와 static메소드
JDK 1.8부터는 디폴트 메소드와 , static메소드를 선언해 줄 수 있다.
우선 인터페이스의 static메소드는 접근제어가자 항상 public이며, 생략이 가능하다. 다만 static 메소드는 재정의(오버라이딩)이 불가능하다는 점을 알고있자. 그리고 static메소드 접근법과 동일하게, 인터페이스 이름으로 접근해야한다. 참조변수를 통한 접근은 불가능하다. 일반적인 인터페이스의 추상메소드와 다르게, static메소드는 구현을 해주어야한다.
package Interface2;
interface test1{
public static void printSentence(){
System.out.println("Hello world from test1");
}
}
interface test2{
public static void printSentence(){
System.out.println("Hello world from test2");
}
}
public class intertest{
public static void main(String[] args){
test1.printSentence();
test2.printSentence();
}
}
이번에는 디폴트 메소드를 알아보자.
디폴트 메소드가 생긴 이유는, 처음 인터페이스를 구현해도 언젠가는 변경을 하게 되는데, 이 과정에서 변경을 하면, 인터페이스를 구현한 모든 클래스가 영향을 받기 때문에, 이 영향을 조금 줄이기 위해 설계되었다고 한다. 디폴트 메소드는 추상 메소드의 기본적인 구현을 제공하는 메소드로, 추상메소드가 아니므로, 디폴트 메소드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다. 또한 디폴트 메소드도 static메소드와 동일하게, 구현을 해주어야한다. static메소드와 다른점은, 디폴트 메소드는 오버라이딩 하여 재정의가 가능한 성격을 가지고 있다.
package Interface2;
interface MyInstance{
void method();
void newMethod();
}
class test implements MyInstance{
@Override
public void method() {
}
@Override
public void newMethod() {
}
}
위 코드같은 경우 인터페이스 내에있는 메소드가 모두 추상메소드(public abstract)이므로 인터페이스를 상속받은 클래스는 모든 메소드를 구현해야하는것을 볼 수 있다.
package Interface2;
interface MyInstance{
void method();
default void newMethod(){
System.out.println("newMethod() from MyInstance");
};
}
class test implements MyInstance{
@Override
public void method() {
}
@Override
public void newMethod() {
MyInstance.super.newMethod();
}
}
반면 이렇게 newMethod()를 default 메소드로 바꿔주면, 구현하지 않아도 오류가 나지 않는것을 볼 수 있다. 인터페이스에 구현된 default메소드에 접근하기 위해서는, (인터페이스 이름).super.(메소드이름)()으로 접근해주면된다. super가 기본적으로 상속받은걸 접근할때 사용하는 키워드다 하지만, 이와 같이 접근하는 이유는, 클래스 상속같은 경우, 단일 상속을 하기 때문에, 조상 클래스가 명확하게 정의된다. 하지만, 인터페이스 같은 경우에는 다중상속이 되기 때문에, 모호해 질 수 있어서 위와같이 접근하는 방법을 사용한다.
'Language > Java' 카테고리의 다른 글
[Java]클래스간의 관계 결정하기 (0) | 2022.01.07 |
---|---|
[Java] 내부클래스 (0) | 2022.01.07 |
[Java] 추상화란? (0) | 2022.01.04 |
[Java] 다형성(polymorphism) (0) | 2022.01.04 |
[Java]제어자 (0) | 2021.12.29 |