8.1 인터페이스 역할
인터페이스(interface)는 사전적인 의미로 두 장치를 연결하는 접속기를 말한다. 여기서 두 장치를 서로 다른 객체로 본다면, 인터페이스는 이 두 객체를 연결하는 역할을 한다.
객체 A가 인터페이스의 메소드를 호출하면, 인터페이스는 객체 B의 메소드를 호출하고 그 결과를 받아 객체 A로 전달해준다. 인터페이스 사용하는 이유는 다음과 같다.
객체 A는 인터페이스의 메소드만 사용하므로 다른 객체가 교체되는 것에 관심이 없다. 객체 A가 인터페이스의 메소드를 호출한다면 실제로 실행되는 것은 객체 B 또는 객체 C의 메소드이다.
만약 인터페이스 없이 객체 A가 객체 B를 직접 사용한다면 객체 A의 소스 코드를 객체 B에서 객체 C로 변경하는 작업이 추가로 필요할 것이다. 객체 B의 메소드 실행 결과와 객체 C의 실행 결과가 다르다면, 객체 A는 객체 교체로 인해 다른 결과를 얻게 된다.
이 특징으로 인해 인터페이스는 다형성 구현에 주된 기술로 이용된다. 상속보다 인터페이스를 이용해서 다형성을 구현하는 경우가 더 많다.
8.2 인터페이스와 구현 클래스
인터페이스는 '~.java' 형태의 소스 파일로 작성되고 '~.class' 횽태로 컴파일되기 때문에 물리적 형태는 클래스와 동일하다. 단, 소스를 작성할 때 선언하는 방법과 구성 멤버가 클래스와 다르다.
인터페이스 선언
class 키워드 대신 interface 키워드를 사용한다. 접근 제한자로는 default, public을 붙일 수 있다.
interface 인터페이스명 { ··· } // default 접근 제한
public interface 인터페이스명 { ··· } // public 접근 제한
public interface 인터페이스명 {
// public 상수 필드
// public 초상 메소드
// public 디폴트 메소드
// public 정적 메소드
// private 메소드
// private 정적 메소드
}
구현 클래스 선언
객체 A가 인터페이스의 추상 메소드를 호출하면 인터페이스는 객체 B의 메소드를 실행한다. 그렇다면 객체 B는 인터페이스에 선언된 추상 메소드와 동일한 선언부를 가진(재정의된) 메소드를 가지고 있어야 한다.
여기서 객체 B를 인터페이스를 구현한(implement) 객체라고 한다. 인터페이스에 정의된 추상 메소드에 대한 실행 내용이 구현(작성)되어 있기 때문이다.
객체 B와 같은 구현 객체는 다음과 같이 인터페이스를 구현하고 있음을 선언부에 명시해야 한다.
public class B implements 인터페이스명 { ··· }
implements 키워드는 해당 클래스가 인터페이스를 통해 사용할 수 있다는 표시이며, 인터페이스의 추상 메소드를 재정의한 메소드가 있다는 뜻이다.
변수 선언과 구현 객체 대입
인터페이스도 하나의 타입이므로 변수의 타입으로도 사용할 수 있다. 인터페이스는 참조 타입에 속하므로 인터페이스 변수에는 객체를 참조하고 있지 않다는 뜻으로 null을 대입할 수 있다.
인터페이스 변수;
인터페이스 변수 = null;
인터페이스를 통해 구현 객체를 사용하려면, 인터페이스 변수에 구현 객체의 번지를 대입해야 한다.
변수 = new 구현객체();
만약 구현 객체가 implements 인터페이스로 선언되지 않았다면 인터페이스 타입의 변수에 대입할 수 없다. 인터페이스 변수에 구현 객체가 대입이 되었다면 변수를 통해 인터페이스의 추상 메소드를 호출할 수 있게 된다.
변수.메소드();
인터페이스 변수를 통해 메소드가 호출되면, 실제로 실행되는 것은 구현 객체에서 재정의된 메소드이다.
8.3 상수 필드
인터페이스는 public static final 특성을 갖는 불변의 상수 필드를 멤버로 가질 수 있다.
[ public static final ] 타입 상수명 = 값;
인터페이스에 선언된 필드는 모두 public static final 특성을 갖기 때문에 public static final를 생략하더라도 자동적으로 컴파일 과정에서 붙게 된다. 상수명은 대문자로 작성하되, 서로 다른 단어로 구성되어 있을 경우 에는 언더바(_)로 연결하는 것이 관례이다.
8.4 추상 메소드
인터페이스는 구현 클래스가 재정의해야 하는 public 추상 메소드(abstract method)를 멤버로 가질 수 있다. 추상 메소드는 리턴 타입, 메소드명, 매개변수만 기술되고 중괄호 { }를 붙이지 않는 메소드를 말한다. public abstract를 생략하더라도 컴파일 과정에서 자동으로 붙게 된다.
[ public abstract ] 리턴타입 메소드명(매개변수, ···);
추상 메소드는 객체 A가 인터페이스를 통해 어떻게 메소드를 호출할 수 있는지 방법을 알려주는 역할을 한다. 인터페이스 구현 객체 B는 추상 메소드의 실행부를 갖는 재정의된 메소드가 있어야 한다.
구현 클래스에서 추상 메소드를 재정의할 때 주의할 점은 인터페이스의 추상 메소드는 기본적으로 public 접근 제한을 갖기 때문에 public보다 더 낮은 접근 제한으로 재정의할 수 없 때문에 재정의되는 메소드에는 모두 public이 추가되어 있다.
8.5 디폴트 메소드
인터페이스에는 완전한 실행 코드를 가진 디폴트 메소드를 선언할 수 있다. 추상 메소드는 실행부(중괄호 { })가 없지만, 디폴트 메소드는 실행부가 있다. 선언 방법은 클래스 메소드와 동일한데, 차이점은 default 키워드가 리턴 타입 앞에 붙는다. 디폴트 메소드의 실행부에는 상수 필드를 읽거나 추상 메소드를 호출하는 코드를 작성할 수 있다.
[public] default 리턴타입 메소드명(매개변수, ···) { ··· };
구현 클래스는 디폴트 메소드를 재정의해서 자신에 맞게 수정할 수도 있다. 재정의 시 주의할 점은 public 접근 제한자를 반드시 붙여야 하고, default 키워드를 생략해야 한다.
8.6 정적 메소드
인터페이스에는 정적 메소드도 선언이 가능하다. 추상 메소드와 디폴트 메소드와 반대로 정적 메소드는 구현 객체가 없어도 인터페이스만으로 호출할 수 있다. 단, public을 생략하더라도 자동으로 컴파일 과정에서 붙는 것이 차이점이다.
[ public | private ] static 리턴타입 메소드명(매개변수, ···) { ··· }
정적 메소드의 실행부(중괄호 { })를 작성할 때 주의할 점은 상수 필드를 제외한 추상 메소드, 디폴트 메소드, private 메소드 등을 호출할 수 없다는 것이다. 이 메소드는 구현 객체가 필요한 인스턴스 메소드이기 때문이다.
8.7 private 메소드
public 접근 제한을 갖는 인터페이스의 상수 필드, 추상 메소드, 디폴트 메소드, 정적 메소드를 public을 생략해서 선언해도 항상 외부에서 접근이 가능하다.
구분 | 설명 |
private 메소드 | 구현 객체가 필요한 메소드 디폴트 메소드 안에서만 호출 가능 |
private 정적 메소드 | 구현 객체가 필요 없는 메소드 디폴트 메소드뿐만 아니라 정적 메소드 안에서도 호출 가능 |
private 메소드의 용도는 디폴트와 정적 메소드들의 중복 코드를 줄이기 위함이다.
8.8 다중 인터페이스
구현 객체가 인터페이스 A와 인터페이스 B를 구현하고 있다면, 인터페이스 A와 인터페이스 B를 implements하여 각각의 인터페이스를 통해 구현할 수 있다.
구현 클래스는 다음과 같이 인터페이스 A와 인터페이스 B를 implement 뒤에 쉼표로 구분해서 작성해 모든 인터페이스가 가진 추상 메소드를 재정의해야 한다.
public class 구현클래스명 implements 인터페이스A, 인터페이스B {
// 모든 추상 메소드 재정의
}
인터페이스 A와 인터페이스 B를 구현한 객체는 다음과 같이 두 인터페이스 타입의 변수에 각각 대입될 수 있다.
인터페이스A 변수 = new 구현클래스명(···);
인터페이스B 변수 = new 구현클래스명(···);
8.9 인터페이스 상속
인터페이스는 클래스와는 달리 다중 상속을 허용한다. 다음과 같이 extends 키워드 뒤에 상속할 인터페이스를 나열한다.
public interface 자식인터페이스 extends 부모인터페이스1, 부모인터페이스2 { ··· }
자식 인터페이스의 구현 클래스는 자식 인터페이스의 메소드와 부모 인터페이스의 모든 추상 메소드를 재정의해야 한다. 그리고 구현 객체는 다음과 같이 자식 및 부모 인터페이스 변수에 대입될 수 있다.
자식인터페이스 변수 = new 클래스(···);
부모인터페이스1 변수 = new 클래스(···);
부모인터페이스2 변수 = new 클래스(···);
구현 객체가 자식 인터페이스 변수에 대입되면 자식 및 부모 인터페이스의 추상 메소드를 모두 호출할 수 있으나, 부모 인터페이스 변수에 대입되면 부모 인터페이스에 선언된 추상 메소드만 호출 가능하다.
8.10 타입 변환
인터페이스의 타입 변환은 인터페이스와 구현 클래스 간에 발생한다. 인터페이스 변수에 구현 객체를 대입하면 구현 객체는 인터페이스 타입으로 자동 타입 변환된다. 반대로 인터페이스 타입을 구현 클래스 타입으로 변환시킬 수 있는데, 이때는 강제 타입 변환이 필요하다.
자동 타입 변환
자동 타입 변환(promotion)은 의미 그대로 자동으로 타입 변환이 일어나는 것을 말한다. 자동 타입 변환은 다음과 같은 조건에서 일어난다.
부모 클래스가 인터페이스를 구현하고 있다면 자식 클래스도 인터페이스 타입으로 자동 타입 변환될 수 있다.
B b = new B();
C c = new C();
D d = new D();
E e = new E();
A a;
a = b; (가능)
a = c; (가능)
a = d; (가능)
a = e; (가능)
강제 타입 변환
강제 타입 변환은 캐스팅(casting) 기호를 사용해서 인터페이스를 타입을 구현 클래스 타입으로 변환시키는 것을 말한다.
구현 객체가 인터페이스 타입으로 자동 변환되면, 인터페이스에 선언된 메소드만 사용 가능하다.
8.11 다형성
인터페이스는 상속과 마찬가지로 다형성을 구현하는 주된 기술로 사용된다. 현업에서는 상속보다는 인터페이스를 통해서 다형성을 구현하는 경우가 많다.
다형성 (多形性)이란 사용 방법은 동일하지만 다양한 결과가 나오는 성질을 말한다. 구현 객체 B와 구현 객체 C 둘 중 어느 객체가 인터페이스에 대입되었느냐에 따라 객체 A의 메소드 호출 결과는 달라질 수 있다.
상속의 다형성과 마찬가지로 인터페이스 역시 다형성을 구현하기 위해 재정의와 자동 타입 변환 기능을 사용한다.
인터페이스의 추상 메소드는 내용이 다른 각 구현 클래스마다 재정의를 해야 한다. 구현 객체는 인터페이스 타입으로 자동 타입 변환이 되고, 인터페이스 메소드 호출 시 구현 객체의 재정의된 메소드가 호출되어 다양한 실행 결과를 얻을 수 있다.
필드의 다형성
상속과 마찬가지로 인터페이스 클래스 타입에 자식 객체를 대입해서 다형성을 보여준다. 인터페이스는 부모 타입이 클래스 타입이 아닌 인터페이스라는 점이 상속과 다르다.
매개변수의 다형성
매개변수 타입을 인터페이스로 선언하면 메소드 호출 시 다향한 구현 객체를 대입할 수 있다. 상속에서 자동 타입 변환 때문에, 메소드 호출 시 매개값을 다양화하기 위해 매개변수 타입을 부모 타입으로 선언하고 호출할 때에는 다양한 자식 객체를 대입하는 것과 비슷한 원리이다.
8.12 객체 타입 확인
객체 타입을 확인하기 위해 상석에서 사용한 instanceof 연산사를 인터페이스에서도 사용할 수 있다.
boolean result = 객체 instanceof 타입;
메소드의 매개변수가 인터페이스 타입일 경우, 메소드 호출 시 매개값은 해당 인터페이스르 구현하는 모든 객체가 될 수 있다. 매개값이 특정 구현 객체일 경우에만 강제 타입 변환을 하고 싶다면 instanceof 연산자로 검사해야 한다.
public void method( 인터페이스타입 변수1 ) {
if ( 변수1 instanceof 자식클래스타입 ) {
자식클래스타입 변수2 = (자식클래스타입) 변수1;
// 변수2 사용
}
}
Java 12부터는 instance 연산의 결과가 true일 경우, 우측 타입 변수를 사용할 수 있기 때문에 강제 타입 변환이 필요 없다.
if ( 변수1 instanceof 자식클래스타입 ) {
// 변수2 사용
}
8.13 봉인된 인터페이스
Java 15부터는 무분별한 자식 인터페이스 생성을 방지하기 위해 봉인된(sealed) 인터페이스를 사용할 수 있다.
InterfaceA의 자식 인터페이스는 InterfaceB만 가능하고, 그 이외는 자식 인터페이스가 될 수 없도록 다음과 같이 InterfaceA를 봉인된 인터페이스로 선언할 수 있다. sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 인터페이스를 지정해야 한다.
public sealed interface InterfaceA permits InterfaceB { ··· }
봉인된 InterfaceA를 상속하는 InterfaceB는 non-sealed 키워드로 다음과 같이 선언하거나, sealed 키워드를 사용해서 또 다른 봉인 인터페이스로 선언해야 한다.
public non-sealed interface InterfaceB extends InterfaceA { ··· }
non-sealed는 봉인을 해제한다는 뜻이므로 InterfaceB는 다른 자식 인터페이스를 만들 수 있다.
public interface InterfaceC extends InterfaceB { ··· }
'JAVA 개념 > Part 02; 객체 지향 프로그래밍' 카테고리의 다른 글
[이것이 자바다] CHAPTER 05. 참조 타입 (1) | 2024.06.16 |
---|---|
[이것이 자바다] CHAPTER 11. 예외 처리 (1) | 2024.06.11 |
[이것이 자바다] CHAPTER 09. 중첩 선언과 익명 객체 │ 개념 (0) | 2024.06.08 |
[이것이 자바다] CHAPTER 07. 상속 │ 개념 (0) | 2024.06.08 |