자바에서 인터페이스는 객체의 사용 방법을 정의한 타입입니다. 인터페이스는 객체의 교환성을 높여주기 때문에 다형성을 구현하는 매우 중요한 역할을 합니다.
인터페이스
< 상수 필드 >
인터페이스는 객체 사용 설명서이므로 런타임 시 데이터를 저장할 수 있는 필드를 선언할 수 없습니다. 그러나 상수 필드는 선언이 가능합니다. 상수는 인터페이스에 고정된 값으로 런타임 시에 데이터를 바꿀 수 없습니다. 상수를 선언할 때에는 반드시 초기값을 대입해야 합니다.
< 추상 메소드 >
추상 메소드는 객체가 가지고 있는 메소드를 설명한 것으로 호출할 떄 어떤 매개값이 필요하고, 리턴 타입이 무엇인지만 알려준다.
< 디폴트 메소드 >
디폴트 메소드는 인터페이스에 선언되지만 사실은 객체(구현 객체)가 가지고 있는 인스턴스 메소드라고 생각해야 합니다.
< 정적 메소드 >
디폴트 메소드와는 달리 객체가 없어도 인터페이스만으로 호출이 가능합니다.
예제를 보면서 이해해 봅시다.
package com.company.Interface;
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("무음 처리합니다.");
} else {
System.out.println("무음 해제합니다.");
}
}
public static void changeBattery() {
System.out.println("건전지를 교환합니다.");
}
}
package com.company.Interface;
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);
}
@Override
public void setMute(boolean mute) {
RemoteControl.super.setMute(mute);
}
}
구현 클래스에서 인터페이스의 추상 메소드들에 대한 실체 메소드를 작성할 떄 주의할 점은 인터페이스의 모든 메소드는 기본적으로 public접근 제한을 갖기 때문에 public보다 더 낮은 접근 제한으로 작성할 수 없다. public을 생략하면 "Cannot reduce the visibility of the inherited method"라는 컴파일 에러를 만나게 된다. 만약 인터페이스에 선언된 추상 메소드에 대응하는 실체 메소드를 구현 클래스가 작성하지 않으면 구현 클래스는 자동적으로 추상 클래스가 된다. 그렇기 때문에 클래스 선언부에 abstract 키워드를 추가해주어야 한다.
public abstract class Television implements RemoteControl {
public void turnOn() { ... };
public void turnOff() { ... };
// setVolume()실체 메소드가 없다 ( 일부만 구현 )
}
구현 클래스가 작성되면 new 연산자로 객체를 생성할 수 있다.
Television 변수에 대입한다고 인터페이스를 사용하는 것이 아니다.
Television tv = new Television();
인터페이스로 구현 객체를 사용하려면 다음과 같이 인터페이스 변수를 선언하고 구현 객체를 대입해야 한다. 인터페이스 변수는 참조 타입이기 때문에 구현 객체가 대입될 경우 구현 객체의 번지를 저장한다.
인터페이스 변수;
변수 = 구현객체
or
인터페이스 변수 = 구현객체
package com.company.Interface;
public class RemoteControllerExample {
public static void main(String[] args) {
RemoteControl rc;
rc = new Television();
rc.turnOn();
rc.turnOff();
}
}
//TV를 켭니다.
//TV를 끕니다.
익명 구현 객체
구현 클래스를 만들어 사용하는 것이 일반적이고, 클래스를 재사용할 수 있기 때문에 편하지만, 일회성의 구현 객체를 만들기 위해 소스 파일을 만들고 클래스를 선언하는것은 비효율적이다. 자바는 소스 파일을 만들지 않고도 구현 객체를 만들 수 있는 방법을 제공하는데, 그것이 익명 구현 객체이다.
자바는 UI프로그래밍에서 이벤트를 처리하기 위해, 그리고 임시 작업 스레드를 만들기 위해 익명 구현 객체를 많이 활용한다. 자바 8에서 지원하는 람다식은 인터페이스의 익명 구현 객체를 만들기 때문에 익명 구현 객체의 패턴을 잘 익혀 두어야 한다. 또한 이는 실행문이기 때문에 마지막에 ; ( 세미콜론 )을 붙여 주어야 한다.
package com.company.Interface;
import sun.jvm.hotspot.debugger.cdbg.Sym;
public class RemoteControllerExample {
public static void main(String[] args) {
RemoteControl rc = new RemoteControl() {
private int volume;
@Override
public void turnOn() {
System.out.println("turnOn the TV");
}
@Override
public void turnOff() {
System.out.println("turnOff the 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;
}
}
@Override
public void getVolume() {
System.out.println("volume >> " + volume);
}
};
rc.setVolume(8);
rc.getVolume();
}
}
// volume >> 8
다중 인터페이스 구현 클래스
package com.company.Interface;
public class SmartTelevision implements RemoteControl, Searchable{
private int volume;
public SmartTelevision(int volume) {
this.volume = volume;
}
@Override
public void turnOn() {
System.out.println("TV를 켭니다.");
}
@Override
public void turnOff() {
System.out.println("TV를 끕니다.");
}
@Override
public void getVolume() {
System.out.println("volume >> " + volume);
}
@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;
}
}
@Override
public void setMute(boolean mute) {
RemoteControl.super.setMute(mute);
}
@Override
public void search(String url) {
System.out.println(url + "을 검색합니다.");
}
public static void main(String[] args) {
RemoteControl rc = null;
rc = new SmartTelevision(5);
rc.getVolume();
Searchable searchable = null;
searchable = new SmartTelevision(5);
searchable.search("http://localhost:4000");
}
}
위와 같이 RemoteControl, Searchable의 추상 메서드를 모두 구현해야 한다. 만약 하나라도 구현을 안한다면 추상 클래스로 선언해야 합니다.
디폴트 메소드 사용
디폴트 메소드는 인터페이스에 선언되지만, 인터페이스에서 바로 사용할 수 없습니다. 디폴트 메소드는 추상 메소드가 아닌 인스턴스 메소드이므로 구현 객체가 있어야 사용할 수 있습니다. 예를 들어 RemoteControl인터페이스는 setMute()라는 디폴트 메소드를 가지고 있지만, 이 메소드를 다음과 같이는 호출 할 수는 없습니다.
RemoteControl.setMute(true);
setMute()메소드는 RemoteControl 구현 객체가 필요한데, 다음과 같이 Television 객체를 인터페이스 변수에 대입하고 나서 setMute()를 호출할 수 있다
RemoteControl rc = new Television();
rc.setMute(true);
디폴트 메소드는 인터페이스의 모든 구현 객체가 가지고 있는 기본 메소드라고 생각하면 됩니다. 그러나 어떤 구현 객체는 디폴트 메소드의 내용이 맞지 않아 수정이 필요할 수도 있습니다. 구현 클래스를 작성할 때 디폴트 메소드를 오버라이딩 해서 자신에게 맞게 수정하면 디폴트 메소드가 호출 될 때 재정의한 메소드가 호출됩니다.
다음 예시를 봅시다.
package com.company.Interface;
import java.rmi.Remote;
public class Audio implements RemoteControl{
private int volume;
private boolean mute;
@Override
public void turnOn() {
System.out.println("Audio를 켭니다.");
}
@Override
public void turnOff() {
System.out.println("Audio를 끕니다.");
}
@Override
public void getVolume() {
System.out.println("volume >> " + volume);
}
@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("현재 Audio 볼륨: " + volume);
}
@Override
public void setMute(boolean mute) {
this.mute = mute;
if(mute) {
System.out.println("Audio 무음 처리 합니다.");
} else {
System.out.println("Audio 무음 해재합니다.");
}
}
public static void main(String[] args) {
RemoteControl rc = null;
rc = new Television();
rc.turnOn();
rc.turnOff();
rc = new Audio();
rc.turnOn();
rc.turnOff();
rc.setMute(true);
}
}
//TV를 켭니다.
//TV를 끕니다.
//Audio를 켭니다.
//Audio를 끕니다.
//Audio 무음 처리 합니다.
타입 변환과 다형성
인터페이스는 메소드의 매개 변수로 많이 등장한다. 인터페이스 타입으로 매개변수를 선언하면 메소드 호출 시 매개값으로 여러 가지 종류의 구현 객체를 줄 수 있기 때문에 메소드 실행 결과가 다양해 진다.
package com.company.Interface.Car;
public interface Tire {
public void roll();
}
package com.company.Interface.Car;
public class HankookTire implements Tire{
@Override
public void roll() {
System.out.println("한국 타이어가 굴러갑니다.");
}
}
package com.company.Interface.Car;
public class KumhoTire implements Tire{
@Override
public void roll() {
System.out.println("금호 타이어가 굴러갑니다.");
}
}
package com.company.Interface.Car;
public class Car {
Tire[] tires = {
new HankookTire(),
new HankookTire(),
new HankookTire(),
new HankookTire(),
};
void run() {
for(Tire tire : tires) {
tire.roll();
}
}
public static void main(String[] args) {
Car myCar = new Car();
myCar.run();
myCar.tires[0] = new KumhoTire();
myCar.tires[1] = new KumhoTire();
myCar.run();
}
}
//한국 타이어가 굴러갑니다.
//한국 타이어가 굴러갑니다.
//한국 타이어가 굴러갑니다.
//한국 타이어가 굴러갑니다.
//금호 타이어가 굴러갑니다.
//금호 타이어가 굴러갑니다.
//한국 타이어가 굴러갑니다.
//한국 타이어가 굴러갑니다.
매개 변수의 다형성도 이와 비슷하므로 그냥 넘어가겠다.
인터페이스의 상속
또한 인터페이스의 상속에 대한 간단한 예를 보여드리겠습니다.
package com.company.Interface.inheritanceInterface;
public interface InterfaceA {
public void methodA();
}
package com.company.Interface.inheritanceInterface;
public interface InterfaceB {
public void methodB();
}
package com.company.Interface.inheritanceInterface;
public interface InterfaceC extends InterfaceA, InterfaceB{
public void methodC();
}
package com.company.Interface.inheritanceInterface;
public class ImplementaionC implements InterfaceC{
@Override
public void methodA() {
System.out.println("ImplementationC-methodA() 실행");
}
@Override
public void methodB() {
System.out.println("ImplementationC-methodB() 실행");
}
@Override
public void methodC() {
System.out.println("ImplementationC-methodC() 실행");
}
}
package com.company.Interface.inheritanceInterface;
public class Example {
public static void main(String[] args) {
ImplementaionC impl = new ImplementaionC();
InterfaceA ia = impl;
ia.methodA();
System.out.println();
InterfaceB ib = impl;
ib.methodB();
System.out.println();
InterfaceC ic = impl;
ic.methodA();
ic.methodB();
ic.methodC();
}
}
//ImplementationC-methodA() 실행
//
//ImplementationC-methodB() 실행
//
//ImplementationC-methodA() 실행
//ImplementationC-methodB() 실행
//ImplementationC-methodC() 실행
디폴트 메소드의 필요성
기존에 MyInterface라는 인터페이스와 이를 구현한 MyClassA라는 클래스가 있었다. 시간이 흘러 MyInterface에 기능을 추가해야할 필요성이 생겼다. 그래서 MyInterface에 추상 메소드를 추가했는데, 엉뚱하게도 MyClassA에서 추상 메소드에 대한 실체 메소드가 없기 떄문에 오류를 내보낸다. MyClassA를 수정할 여건이 안된다면 결국 MyInterface에 추상 메소드를 추가할 수 없다. 그래서 MyInterface에 디폴트 메소드를 선언하는 것이다. 디폴트 메소드는 추상 메소드가 아니기 때문에 구현 클래스에서 실체 메소드를 작성할 필요가 없다.
수정된 MyInterface를 구현한 새로운 클래스인 MyClassB는 method1()의 내용은 반드시 채워야 하지만, 디폴트 메소드인 method2()는 MyInterface에 정의된 것을 사용해도 되고 필요에 따라 메소드 오버라이딩을 해도 된다.
package com.company.Interface.defaultInterface;
public interface ParentInterface {
public void method1();
public default void method2() {
System.out.println("ParentInterface-method2()");
}
}
package com.company.Interface.defaultInterface;
public interface ChildrenInterface extends ParentInterface{
public void method3();
}
package com.company.Interface.defaultInterface;
public interface ChildrenInterface2 extends ParentInterface{
@Override
default void method2() {
System.out.println("ChildrenInterface2-method2()");
}
public void method3();
}
package com.company.Interface.defaultInterface;
public interface ChildrenInterface3 extends ParentInterface{
@Override
public void method2();
public void method3();
}
package com.company.Interface.defaultInterface;
public class Example {
public static void main(String[] args) {
ChildrenInterface ci1 = new ChildrenInterface() {
@Override
public void method3() {
System.out.println("ChildInterface-method3()");
}
@Override
public void method1() {
System.out.println("ChildInterface-method1()");
}
};
ci1.method1();
ci1.method2();
ci1.method3();
System.out.println();
ChildrenInterface2 ci2 = new ChildrenInterface2() {
@Override
public void method3() {
System.out.println("ChildInterface2-method3()");
}
@Override
public void method1() {
System.out.println("ChildInterface2-method1()");
}
};
ci2.method1();
ci2.method2();
ci2.method3();
System.out.println();
ChildrenInterface3 ci3 = new ChildrenInterface3() {
@Override
public void method2() {
System.out.println("ChildInterface3-method2()");
}
@Override
public void method3() {
System.out.println("ChildInterface3-method3()");
}
@Override
public void method1() {
System.out.println("ChildInterface3-method1()");
}
};
ci3.method1();
ci3.method2();
ci3.method3();
}
}
//ChildInterface-method1()
//ParentInterface-method2()
//ChildInterface-method3()
//
//ChildInterface2-method1()
//ChildrenInterface2-method2()
//ChildInterface2-method3()
//
//ChildInterface3-method1()
//ChildInterface3-method2()
//ChildInterface3-method3()
다음과 같이 ChildInterface3는 ParentInterface를 상속하고 ParentInterface의 디폴트 메소드인 method2()를 추상 메소드로 재선언한다. 그리고 자신의 추상 메소드인 method3()를 선언한다
이 경우 method1(), method2(), method3()를 모두 구현해야 한다. ( 익명 구현 객체 사용 )
'School > Java Programming' 카테고리의 다른 글
Java Programming - Exception (0) | 2022.03.17 |
---|---|
Java Programming - 중첩 클래스 & 중첩 인터페이스 (0) | 2022.03.17 |
Java Programming - Inheritance (0) | 2022.03.09 |
Java Programming - Class (0) | 2022.03.09 |
Java Programming - Array (0) | 2022.03.09 |