신용권 님의 ''이것이 자바다'' 8장 공부 기록
책을 보면서 내용을 정리했다.
인터페이스(interface)는 객체의 사용방법을 정의한 타입이다. 인터페이스는 객체의 교환성을 높여주기 때문에 다형성을 구현하는 매우 중요한 역할을 한다. 또한 개발 코드와 객체가 서로 통신하는 접점 역할을 한다. 개발 코드가 인터페이스의 메소드를 호출하면 인터페이스는 객체의 메소드를 호출한다. 그래서 개발 코드는 객체의 내부 구조를 알 필요 없이 인터페이스의 메소드만 알면 된다.
인터페이서는 여러 객체들과 사용이 가능하므로 어떤 객체를 사용하느냐에 따라 실행 내용과 리턴값이 다를 수 있다. 따라서 개발 코드 측면에선 코드 변경 없이 실행 내용과 리턴값을 다양화할 수 있다는 장점을 가지게 된다.
인터페이스는 ".java" 형태의 소스 파일로 작성되고 컴파일러를 통해 ".class" 형식으로 컴파일 되기 때문에 물리적 형태는 클래스와 동일하다. 차이점은 소스를 작성할 때 선언하는 방법이 다르다.
[public] interface 인터페이스명{...}
interface 인터페이스명{
//상수
타입 상수명 = 값;
//추상 메소드
타입 메소드명 (매개변수, ..);
//디폴트 메소드
default 타입 메소드명(매개변수, ..);
//정적 메소드
static 타입 메소드명(매개변수){..};
}
인터페이스는 데이터를 저장할 수 없으므로 데이터를 저장할 인스턴스 또는 정적 필드를 선언할 수 없다. 대신 상수 필드만 선언할 수 있다.
인터페이스에서 선언된 필드는 모두 public static final의 특성을 갖는다. 이를 생략해도 컴파일 과정에서 자동적으로 붙게 된다. 상수명은 모두 대문자나 언더바로 작성한다.
public interface sample{
public int MAX = 10;
}
[public] default 리턴타입 메소드명(매개변수, ...){...}
[public] static 리턴타입 메소드명(매개변수, ..){..}
개발 코드가 인터페이스 메소드를 호출하면 인터페이스는 객체의 메소드를 호출한다. 객체는 인터페이스에서 정의된 추상 메소드와 동일한 메소드 이름, 매개타입, 리턴타입을 가진 실체 메소드를 가지고 있어야 한다. 이러한 객체를 인터페이스의 구현(implement) 객체라고 하고, 구현 객체를 생성하는 클래스를 구현 클래스라고 한다.
public class 구현클래스명 implements 인터페이스명 {
//인터페이스에 선언된 추상 메소드의 실체 메소드 선언
}
그리고 인터페이스에 선언된 추상 메소드의 실체 메소드를 선언해야 한다. 이 때 주의할 점은 인터페이스의 모든 메소드는 기본적으로 public 접근 제한을 가지므로 이보다 더 낮은 접근 제한으로 작성할 수 없다. public 생략 시 "Cannot reduce the visibility of the inherited method(상속된 메소드의 가시성을 줄일 수 없다)" 컴파일 에러가 발생한다.
추상 메소드에 대응하는 실체 메소드를 작성하지 않으면 해당 클래스는 자동으로 추상 클래스가 되며, 이 때는 클래스 선언부에 abstrac 키워드를 추가해야 한다.
public abstract calss Tv implements Rc{
public void turnOn(){...}
//public void turnOff(){..} -> 실체 메소드 작성하지 않음.
}
구현 클래스의 실체 메소드에 @Override 어노테이션을 붙이면 컴파일러가 실체 메소드인지를 체크한다.
구현 클래스가 작성되면 new 연산자로 객체를 생성하여 인터페이스에 대입할 수 있다. 인터페이스 변수는 참조 타입이므로 구현객체의 번지를 저장한다.
인터페이스 변수;
변수 = new 구현객체; //인터페이스 변수 = new 구현객체;
구현 클래스를 만들어 사용하는 것은 일반적이고 클래스를 재사용할 수 있기 때문이 편리하지만 일회성의 구현 객체를 만들기 위해 소스파일을 만들고 클래스를 선언하는 것은 비효율적이다. 이에 소스파일을 만들지 않고도 구현객체를 만들 수 있는 방법을 제공하는데, 이것이 익명구현 객체이다.
인터페이스 변수 = new 인터페이스(){
//인터페이스에 선언된 추상 메소드의 실체 메소드 선언
}; //반점 있음!
중괄호에는 인터페이스에 선언된 모든 추상 메소드들의 실체 메소드를 작성해야 한다. 그렇지 않으면 컴파일 에러가 발생한다.
추가적으로 필드와 메소드 선언이 가능하나, 익명 객체 안에서만 사용할 수 있고 인터페이스 변수로 접근할 수 없다.
모든 객체는 클래스로부터 생성된다. 익명 구현 객체의 소스코드를 컴파일하면 컴파일러에 의해 "클래스명$1.class"의 이름으로 파일이 생성된다.
public class 구현클래스명 implements 인터페이스A, 인터페이스B {
//인터페이스A에 선언된 추상 메소드의 실체 메소드 선언
//인터페이스B에 선언된 추상 메소드의 실체 메소드 선어
}
인터페이스로 구현 객체를 사용하려면 인터페이스 변수를 선언하고 구현 객체를 대입해야 한다. 인터페이스 변수는 참조 타입이므로 구현 객체가 대입될 경우 구현 객체의 번지를 저장한다.
RC rc = new Tv();
rc.turnOn();//Tv의 실체 메소드 실행
RC rc = new tv();
rc.turnOff();
public class ex {
public static void main(String[]args){
RC.go();//인터페이스로 바로 호출
}
}
요즘은 상속보다도 인터페이스를 통해 다형성을 구현한다. 상속에서 부모 타입에 어떤 자식 객체를 대입하느냐에 따라 실행 결과가 달라지는 것처럼, 인터페이스 타입에 어떤 구현 객체를 대입하느냐에 따라 실행 결과가 달라진다.
상속은 같은 종류의 하위 클래스를 만드는 기술이고, 인터페이스는 사용 방법이 동일한 클래스를 만드는 기술이다.
프로그램 소스 코드는 변함이 없는데, 구현 객체를 교체함으로써 프로그램의 실행 결과가 다양해지는 것을 인터페이스의 다형성이라고 부른다.
클래스에 문제가 있어 다른 클래스를 만들 때는 같은 메소드를 사용한다면 메소드 선언부가 동일해야 한다. 인터페이스를 추상 메소드를 작성하고 구현 클래스로 해당 메소드를 작성할 시에 이러한 문제를 해결할 수 있다. 처음부터 메소드 선언부가 동일하기 때문이다.
인터페이스는 메소드의 매개 변수로 많이 등장하는데, 해당 매개 값으로 여러 종류의 구현 객체를 줄 수 있으므로 메소드 실행 결과가 다양하게 나온다. 이것이 인터페이스 매개 변수의 다형성이다.
구현 객체가 인터페이스 타입으로 변환되는 것은 자동 타입 변환(promotion)에 해당한다. 자동 타입 변환은 프로그램 실행 도중에 자동적으로 타입 변환이 일어나는 것을 말한다.
인터페이스 변수 = 구현객체;//구현객체가 자동 타입 변환됨
Tire[] tires ={
new HTire();
new HTire();
new HTire();
new HTire();
}
void run(){
for(Tire tire : tires){
tire.roll();
}
}
//Driver 클래스
public class Driver{
public void drice(Vehicle vehicle){
vehicle.run();
}
}
//Vehicle 인터페이스 타입
public interface Vehicle{
public void run();
}
//구현 객체 Bus
public class Bus implements Vehicle{
@Override
public void run(){
System.out.println("버스가 달립니다");
}
}
//매개 변수의 다형성 테스트
public class DriverEx{
public static void main(String[] args){
Driver driver = new Driver()
Bus bus = new Bus();
driver.drive(bus);//bus ->자동 타입 변환됨
}
}
구현 클래스 변수 = (구현 클래스) 인터페이스 변수;//강제 타입 변환
if(vehicle instanceof Bus){
Bus bus = (Bus) vehicle; //안심하고 변환 가능
}
public interface 하위인터페이스 extends 상위인터페이스1, 하위인터페이스2 {...}
하위인터페이스 변수 = new 구현클래스(...);
상위인터페이스1 변수 = new 구현클래스(...);
상위인터페이스2 변수 = new 구현클래스(...);
인터페이스에서 디폴트 메소드를 허용한 이유는 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위해서이다. 기존 인터페이스의 이름과 추상 메소드의 변경 없이 디폴트 메소드만 추가할 수 있기 때문에 이전에 개발한 구현 클래스를 그대로 사용할 수 있으면서 새롭게 개발하는 클래스는 디폴트 메소드를 활용할 수 있다.
//부모 인터페이스
public interface ParentInterface{
public void method1();
public default void method2(){/*실행문*/}
}
//자식 인터페이스1
public interface ChildInterface1 extends ParentInterface {
public void method3;
}
//자식 인터페이스1을 구현하는 클래스는 method1()과 method3()의 실체 메소드를 가지고 있어야 하며 method2()를 호출할 수 있다.
ChildInterface1 ci1 = new ChildInterface1(){ //익명 구현 객체
@Override
public void method1() {/*실행문*/}
@Override
public void method3() {/*실행문*/}
};
ci1.method1();
ci1.method2();//ParentInterface의 method2() 호출
//자식 인터페이스2
public interface ChildInterface2 extends ParentInterface {
@Override
public default void method2() {/*실행문*/} //재정의
public void method3();
}
//자식 인터페이스1 구현 클래스와 유사함.
ChildInterface2 ci2 = new ChildInterface2(){ //익명 구현 객체
@Override
public void method1() {/*실행문*/}
@Override
public void method3() {/*실행문*/}
};
ci2.method1();
ci2.method2();//ParentInterface의 method2() 호출
//자식 인터페이스3
public interface ChildInterface3 extends ParentInterface {
@Override
public void method2() //추상 메소드로 재선언
public void method3();
}
//자식 인터페이스3을 구현하는 클래스는 method1(), method2(), method3()의 실체 메소드를 모두 가지고 있어야 한다.
ChildInterface2 ci3 = new ChildInterface3(){ //익명 구현 객체
@Override
public void method1() {/*실행문*/}
@Override
public void method2() {/*실행문*/}
@Override
public void method3() {/*실행문*/}
};
ci3.method1();
ci3.method2();//ChildInterface3 구현 객체의 method2() 호출