인터페이스는 본래 추상 메서드와 상수만으로 구성되어 클래스나 프로그램이 제공하는 기능을 명시적으로 선언해주는 역할을 한다. 일종의 설계도라고 볼 수도 있다.
인터페이스의 구현 즉, 추상 메서드의 구현은 클래스에서 이루어지는데, 클래스마다 다르게 구현했다고 하더라도 인터페이스에서 선언한 메서드의 형태만 파악할 수 있기 때문에 메서드의 구현부를 몰라도 사용할 수 있다. 이런 특성에 의해 인터페이스를 설계도로서 이용할 수 있게 된다.
package calculator;
public interface Calc {
/* final 예약어를 쓰지 않아도 컴파일 과정에서 자동으로 상수가 됨 */
double PI = 3.14;
int ERROR = -999999999;
/* abstract를 쓰지 않아도 컴파일 과정에서 자동으로 추상메서드가 됨 */
int add(int num1, int num2);
int sub(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
}
인터페이스는 기본적으로 상수와 함수의 선언으로만 이루어져있기 때문에 final
이나 abstract
를 쓰지 않아도 컴파일 과정에서 자동으로 상수와 추상 메서드로 변환되며, 아무것도 적지 않으면 메서드의 접근지정자는 public
이 된다.
구현부가 없는 추상 메서드로만 이루어져 있기 때문에 인스턴스 생성이 불가능 할 뿐만 아니라, 클래스와 달리 생성자의 선언조차 불가능하다.
인스턴스 생성이 불가능한 인터페이스를 사용하려면, 클래스에서 인터페이스를 구현하는 과정이 반드시 필요하다. 인터페이스를 구현한다는 것은 인터페이스에서 선언되었던 추상 메서드를 구현한다는 것을 뜻한다.
public class Calculator implements Calc {
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
@Override
public int sub(int num1, int num2) {
return num1 - num2;
}
@Override
public int times(int num1, int num2) {
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
if (num2 != 0) {
return num1 / num2;
}
else {
return Calc.ERROR;
}
}
}
상속할 때 extends
예약어를 사용했듯 구현하고자하는 인터페이스를 implements
예약어 뒤에 적어주면 된다. 인터페이스에 선언된 상수를 사용하고 싶다면 그 상수의 이름을 그대로 가져다 쓰거나, 인터페이스 명으로 참조(Calc.Error
)하여 사용한다.
만약 인터페이스의 모든 추상 메서드를 구현하고 싶지 않다면, 해당 클래스를 추상 클래스로 선언하고, 나머지는 하위 클래스에서 구현하면 된다. 하위 클래스에서만 사용하고 싶은 메서드를 인터페이스의 선언없이 따로 구현하여 사용하는 것 또한 가능하다.
Calculator
추상 클래스package calculator;
public abstract class Calculator implements Calc {
@Override
public int add(int num1, int num2) {
return num1 + num2;
}
@Override
public int sub(int num1, int num2) {
return num1 - num2;
}
}
MyCalculator
클래스package calculator;
public class MyCalculator extends Calculator {
@Override
public int times(int num1, int num2) {
return num1 * num2;
}
@Override
public int divide(int num1, int num2) {
if (num2 != 0) {
return num1 / num2;
}
else
return ERROR;
}
public void showInfo() {
System.out.println("This is MyCalculator.");
}
}
calculator
패키지 내 관계상속 관계에서 상위 클래스로의 묵시적 형 변환이 가능했던 것처럼, MyCalcultor
자료형은 Calculator
자료형이기도 하면서, Calc
자료형이기도 하므로, 상위 인터페이스로의 묵시적 형 변환이 가능하다. 묵시적 형 변환과 가상메서드의 원리에 의해 인터페이스에서도 다형성을 구현할 수 있다.
Calc calc = new MyCalculator();
MyCalculator myCalc = new MyCalculator();
변수의 자료형에 의하여, calc
는 add, sub, times, divide 함수만, myCalc
는 showInfo
함수까지도 추가로 사용이 가능하다.
자바7
까지는 인터페이스에서 오직 상수와 추상메서드만 사용할 수 있었다. 하지만 자바8
로 넘어오면서 인터페이스 내에서 구현까지 할 수 있는 default
메서드와 static
메서드가 추가되었고, 자바9
에서는 privat
메서드까지 활용할 수 있게 되었다.
default
메서드원래 인터페이스에서는 함수의 구현이 아예 불가능했기 때문에 인터페이스를 구현한 클래스들이 공통된 기능을 사용한다고 하더라도 각각의 클래스에서 구현해야했다. 하지만 디폴트 메서드가 추가되면서 인터페이스에서도 함수 구현이 가능해졌고, 인터페이스를 구현할 클래스들이 공통적으로 사용할 기능을 디폴트 메서드로 구현할 수 있게 되었다. 원한다면 디폴트 메서드도 클래스에서 오버라이딩해서 사용할 수 있다.
package calculator;
public interface Calc {
double PI = 3.14;
int ERROR = -999999999;
int add(int num1, int num2);
int sub(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
public default void description() {
System.out.println("정수 계산기를 구현합니다.");
}
}
디폴트 메서드는 함수명 앞에 default
예약어를 붙이고 함수를 구현하면 된다.
static
메서드인터페이스의 메서드를 사용하고 싶다면 일반적으로 클래스로 인터페이스를 구현하고, 클래스의 인스턴스를 생성한 다음 접근할 수 있었다. 그러나 정적 메서드가 추가되면서 클래스의 인스턴스 생성 여부와 상관없이 인터페이스 명으로 참조하여 사용할 수 있게 되었다.
package calculator;
public interface Calc {
double PI = 3.14;
int ERROR = -999999999;
int add(int num1, int num2);
int sub(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
public default void description() {
System.out.println("정수 계산기를 구현합니다.");
}
static int square(int num) {
return num * num;
}
}
static
예약어를 함수명 앞에 붙이고, 함수를 구현한다. 메소드를 사용할 때는 인스턴스명으로 참조하므로 Calc.square(10)
과 같이 사용하면 된다.
private
메서드private
메서드는 인터페이스 내에서만 사용 가능한 메서드로 주로 인터페이스에서 함수를 구현할 때 코드 재사용성을 높이기 위해 선언한다. 인터페이스 메서드의 구현부가 반복되는 부분이 많고, 그 부분을 외부에서 사용할 일이 없을 때 private
예약어와 함께 함수를 구현하면 된다.
private
메서드는 구현되어야 하므로, abstract
와 쓸 수는 없지만 static
과는 사용할 수 있다. privat static
메서드는 주로 static
메서드에서 호출하게 된다.
package calculator;
public interface Calc {
double PI = 3.14;
int ERROR = -999999999;
int add(int num1, int num2);
int sub(int num1, int num2);
int times(int num1, int num2);
int divide(int num1, int num2);
public default void description() {
privateMethod();
privateStaticMethod();
System.out.println("정수 계산기를 구현합니다.");
}
static int square(int num) {
// 정적 메서드에서는 private 메서드를 호출하면 오류 발생
privateStaticMethod();
return num * num;
}
private void privateMethod() {
System.out.println("private 메소드 입니다.");
}
private static void privateStaticMethod() {
System.out.println("private 정적 메소드 입니다.");
}
}
상속에서는 다중 상속이 불가능 했었다. 하지만 인터페이스의 경우, 한 클래스가 여러개의 인터페이스를 구현하는 것이 가능 하다.
위와 같은 그림을 코드로 표현하면 다음과 같다
public class C implements A, B {
}
이 경우, C
클래스를 추상 클래스로 사용하는 것이 아니라면, A
인터페이스와 B
인터페이스에 있는 모든 추상 메서드를 구현해야 한다.
A
인터페이스에는 methodA()
메서드가, B
인터페이스에는 methodB()
메서드가 각각 선언되어있다고 가정해보자.
A cToA = new C();
B cToB = new C();
만약 이렇게 상위 인터페이스로 형 변환이 되었다면, cToA
변수는 methodA
에만 접근이 가능하고, cToB
변수는 methodB
에만 접근이 가능하다.
이 경우 같은 이름의 메서드가 추상 메서드인 경우, 정적 메서드인 경우, 디폴트 메서드인 경우, private 메서드인 경우로 나눠 볼 수 있다.
추상 메서드의 경우 어차피 구현부가 C
클래스에 존재하기 때문에 C
클래스에서 재정의 된 메서드가 호출된다.
정적 메서드는 인터페이스명으로 참조해서 사용할 수 있으므로 이름이 같아도 크게 상관 없다. 이때 C
클래스에서 오버라이딩한다면 C
클래스 명으로도 참조할 수 있게 되지만, 그렇지 않다면 C
클래스 명으로 참조하면 오류가 발생한다.
무조건 C
클래스에서 재정의를 해주어야 오류가 나지 않는다. 재정의하여 사용할 경우 가상 메서드에 의해 C
클래스에서 재정의 된 메소드만 호출된다.
어차피 인터페이스 내부에서만 사용하는 메서드이고, 외부에서는 호출할 수도 없으므로 동일한 메서드가 존재해도 상관없다.
클래스 간의 상속이 가능하듯 인터페이스 간에도 상속이 가능하다. 다만 구현한 코드를 상속하는 개념과는 다르기 때문에 기능 상속이 아닌 형 상속이라고 표현한다.
클래스 간의 상속과 다르게 다중 상속도 가능하며, 이 때 하위 인터페이스는 모든 상위 인터페이스의 추상 메서드를 갖게 된다.
물론 하위 인터페이스를 사용하고 싶다면 하위 인터페이스를 구현한 클래스도 있어야하며, 구현한 클래스가 추상 클래스가 아닌 이상 모든 상위 인터페이스의 추상메서드를 구현하는 것은 필수다.
이들의 관계를 간단하게 그림과 코드로 표현해보면 아래와 같다.
public interface X extends A, B {
}
public class C implements X {
}
한 클래스에서 인터페이스를 구현하고 클래스를 상속 받는 것 모두 가능하다. 이와 같은 상황을 다이어그램과 코드로 간단하게 표현하면 다음과 같다.
public class C extends Y implements X {
}
Shelf
클래스package bookshelf;
import java.util.ArrayList;
public class Shelf {
protected ArrayList<String> shelf;
public Shelf() {
shelf = new ArrayList<String>();
}
public ArrayList<String> getShelf(){
return shelf;
}
public int getShelfSize() {
return shelf.size();
}
}
Queue
인터페이스package bookshelf;
public interface Queue {
void addBack(String title);
String popFront();
int getSize();
}
BookShelf
클래스package bookshelf;
public class BookShelf extends Shelf implements Queue {
@Override
public void addBack(String title) {
shelf.add(title);
}
@Override
public String popFront() {
// 비어있는 예외 경우도 생각해줘야 실행 오류가 안남
if (shelf.isEmpty()) {
return null;
}
return shelf.remove(0);
}
@Override
public int getSize() {
return shelf.size();
}
}
우선 인터페이스는 클래스가 제공하는 기능을 명시하여 설계하는 역할을 한다.
인터페이스에는 일반적으로 메서드 선언부만 존재하고, 구현부는 클래스에서 이루어진다. 인터페이스의 선언부를 통해 각각의 클래스를 직접 확인하지 않아도 메서드를 어떻게 사용할지 알 수 있다. 이는 개발 설계에 중요한 특성이 될 것이다.
그리고 상속과 마찬가지로 오버라이딩과 가상메서드에 의해 다형성을 활용할 수 있다는 장점이 있다.
다형성 구현이라는 부분에서 인터페이스의 구현은 상속과 유사한 점도 있지만 다른 점도 존재한다. 상속을 통해 구현한 다형성은 부모로부터 원치 않는 기능까지 물려받기 때문에 클래스간의 결합도가 높아지기 때문에 상속은 IS-A
관계에 있는 클래스에서 사용하는 것을 권장한다. 그러나 인터페이스의 경우 원하는 기능만 구현하여 사용하면 되기 때문에 유연성이 높은 편이다.
Animal
추상 클래스package animal;
public abstract class Animal {
public abstract void description();
public void wakeUp() {
System.out.println("잠에서 깨어납니다.");
}
public void sleep() {
System.out.println("잠을 잡니다.");
}
}
LandAnimal
인터페이스package animal;
public interface LandAnimal {
void walk();
}
MarineAnimal
인터페이스package animal;
public interface MarineAnimal {
void swim();
}
Cat
클래스package animal;
public class Cat extends Animal implements LandAnimal {
@Override
public void description() {
System.out.println("이 동물은 고양이입니다.");
}
@Override
public void walk() {
System.out.println("고양이가 살금살금 걷습니다.");
}
}
Whale
클래스package animal;
public class Whale extends Animal implements MarineAnimal {
@Override
public void description() {
System.out.println("이 동물은 고래입니다.");
}
@Override
public void swim() {
System.out.println("고래가 바다를 헤엄칩니다.");
}
}
Duck
클래스package animal;
public class Duck extends Animal implements LandAnimal, MarineAnimal {
@Override
public void description() {
System.out.println("이 동물은 오리입니다.");
}
@Override
public void walk() {
System.out.println("오리가 뒤뚱뒤뚱 걷습니다.");
}
@Override
public void swim() {
System.out.println("오리가 연못에서 헤엄칩니다.");
}
}
package animal;
import java.util.ArrayList;
public class AnimalTest {
public static void main(String args[]) {
ArrayList<Animal> animalList = new ArrayList<Animal>();
animalList.add(new Cat());
animalList.add(new Duck());
animalList.add(new Whale());
for (Animal animal : animalList) {
System.out.println("====================");
animal.description();
animal.wakeUp();
if (animal instanceof LandAnimal) {
LandAnimal land = (LandAnimal)animal;
land.walk();
}
if (animal instanceof MarineAnimal) {
MarineAnimal marine = (MarineAnimal)animal;
marine.swim();
}
animal.sleep();
}
System.out.println("====================");
}
}
====================
이 동물은 고양이입니다.
잠에서 깨어납니다.
고양이가 살금살금 걷습니다.
잠을 잡니다.
====================
이 동물은 오리입니다.
잠에서 깨어납니다.
오리가 뒤뚱뒤뚱 걷습니다.
오리가 연못에서 헤엄칩니다.
잠을 잡니다.
====================
이 동물은 고래입니다.
잠에서 깨어납니다.
고래가 바다를 헤엄칩니다.
잠을 잡니다.
====================