자바의 인터페이스에 대해 정리합니다.
학습할 내용은 다음과 같습니다.
- 인터페이스 정의하는 방법
- 인터페이스 구현하는 방법
- 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
- 인터페이스 상속
- 인터페이스의 기본 메소드 (Default Method), 자바 8
- 인터페이스의 static 메소드, 자바 8
- 인터페이스의 private 메소드, 자바 9
Reference: Oracle Java Document
인터페이스 정의는 interface라는 keyword와 interface name, 쉼표로 구별된 parent interfaces 그리고 inteface body로 구성됩니다.
예시는 다음과 같습니다.
public interface GroupedInterface extends Interface1, Interface2, Interface3 {
// constant declarations
// base of natural logarithms
double E = 2.718282;
// method signatures
void doSomething (int i, double x);
int doSomethingElse(String s);
}
public access는 이 인터페이스가 어느 패키지에서라도 사용될 수 있음을 말해줍니다. 만약 명시적으로 public을 지정하지 않았다면 이 인터페이스는 같은 패키지에서만 접근이 가능합니다.
인터페이스는 다른 인터페이스를 상속받아 확장할 수 있습니다. 그리고 클래스는 하나의 상위 클래스만 상속받을 수 있다면 인터페이스는 여러개의 상위 인터페이스로부터 상속받을 수 있습니다. 이때 ,로 구분해야합니다.
인터페이스 본문은 abstract methods, default methods, 그리고 static methods를 포함할 수 있습니다. 이 인터페이스의 모든 본문은 내재적으로 public keyword가 포함되어있습니다 그러므로 생략이 가능합니다.
그리고 인터페이스에서 상수를 선언할 수 있습니다. 이런 상수는 내재적으로 public, static, 그리고 final keyword가 포함되어있습니다.
Java 9부터는 Interface Private Methods를 사용할 수 있습니다.
예시는 다음과 깉습니다.
public interface TestInterface {
int constant = 100;
void abstractMethods();
default void defaultMethods(){
System.out.println("default methods");
}
private void privateMethods(){
System.out.println("private methods");
}
static void staticMethods(){
System.out.println("static methods");
}
}
인터페이스를 구현하는 클래스를 선언하기 위해서는 클래스 선언에 implements clause을 포함해야합니다. 클래스는 여러개의 인터페이스를 구현할 수 있으며 구별할 때 ,로 구별해야합니다.
보편적으로 클래스 상속에서 implements keyword는 extends keyword는 뒤에 쓰는게 관례입니다.
객체의 사이즈를 비교하는 method를 정의하는 인터페이스가 있다고 생각해봅시다.
public interface Relatable {
// this (object calling isLargerThan)
// and other must be instances of
// the same class returns 1, 0, -1
// if this is greater than,
// equal to, or less than other
public int isLargerThan(Relatable other);
}
이 Relatable 인터페이스를 구현하는 클래스들은 자신들만의 객체의 사이즈를 비교하는 방법이 있을겁니다. Book Class의 경우에는 Page를 통해서 사이즈를 비교할거고 Student Class의 경우에는 Weight를 통해서 사이즈를 비교할 것입니다.
즉 Relatable를 구현한 클래스는 자신들만의 객체의 사이즈를 비교하는 방법을 알 것입니다.
실제 Relatable를 구현한 RectanglePlus 클래스 예제를 살펴보겠습니다.
이 클래스는 직사각형 넓이 비교를 통해 객체의 사이즈를 비교하는 클래스입니다.
public class RectanglePlus
implements Relatable {
public int width = 0;
public int height = 0;
public Point origin;
// four constructors
public RectanglePlus() {
origin = new Point(0, 0);
}
public RectanglePlus(Point p) {
origin = p;
}
public RectanglePlus(int w, int h) {
origin = new Point(0, 0);
width = w;
height = h;
}
public RectanglePlus(Point p, int w, int h) {
origin = p;
width = w;
height = h;
}
// a method for moving the rectangle
public void move(int x, int y) {
origin.x = x;
origin.y = y;
}
// a method for computing
// the area of the rectangle
public int getArea() {
return width * height;
}
// a method required to implement
// the Relatable interface
public int isLargerThan(Relatable other) {
RectanglePlus otherRect
= (RectanglePlus)other;
if (this.getArea() < otherRect.getArea())
return -1;
else if (this.getArea() > otherRect.getArea())
return 1;
else
return 0;
}
새로운 인터페이스를 정의했다면 새로운 reference data type을 정의한 것입니다. 즉 data type으로 interface name을 이용할 수 있습니다.
interface type을 가진 reference variable을 정의한다면 이 객체는 인터페이를 구현하는 클래스의 인스턴스여야합니다.
예시는 다음과 깉습니다. 객체의 사이즈를 비교하는 방법을 정의하는 인터페이스인 Relativeable를 구현하는 클래스를 인스턴스한 객체들입니다.
public Object findLargest(Object object1, Object object2) {
Relatable obj1 = (Relatable)object1;
Relatable obj2 = (Relatable)object2;
if ((obj1).isLargerThan(obj2) > 0)
return object1;
else
return object2;
}
object1과 object2는 인터페이스 타입인 Relatable type으로 캐스팅될 수 있고 그래서 isLargerThan method를 사용할 수 있습니다.
이러한 isLargerThan 메소드는 Relatable 인터페이스를 구현한 객체라면 어떠한 객체든지 사용할 수 있습니다. 그리고 이런 객체는 자기 자신의 타입과 Relatable 타입 두개를 가질 수 있습니다. 즉 이를 통해 구현 객체의 타입에 의존없이 interface 타입에서 동작하도록 할 수 있으므로 multiple inheritance의 장점을 이용할 수 있습니다.
인터페이스는 클래스와 같이 다른 인터페이스를 상속할 수 있습니다. 이때 extends라는 clause를 통해 정의할 수 있습니다.
하나의 인터페이스가 다른 인터페이스를 상속한다면 상위 인터페이스의 모든 methods와 constantse들을 상속받게 되고 새롭게 정의할 수 있고 추가할 수 있습니다.
클래스의 상속과의 다른 점은 인터페이스는 여러개의 인터페이스를 상속받을 수 있는 점입니다.
예시는 다음과 같습니다.
interface Positionable extends Centered {
void setUpperRightCorner(double x, double y); double getUpperRightX();
double getUpperRightY();
}
interface Transformable extends Scalable, Translatable, Rotatable {}
interface SuperShape extends Positionable, Transformable {}
인터페이스를 구현하는 클래스는 인터페이스의 abstract methods들을 모두 구현해야합니다. 만약 상속받고 있는 인터페이스를 클래스가 구현한다면
해당 클래스는 상위 인터페이스까지 있는 모든 abstract methods들을 구현해야합니다.
default method가 등장하게 된 배경을 알기 위해서 하나의 상황을 가정해보겠습니다.
아래에 있는 DoIt 이라는 인터페이스를 개발하고 있는 상황입니다.
public interface DoIt {
void doSomething(int i, double x);
int doSomethingElse(String s);
}
시간이 지나서 DoIt 인터페이스에 새로운 메소드가 필요함을 알았습니다.
public interface DoIt {
void doSomething(int i, double x);
int doSomethingElse(String s);
boolean didItWork(int i, double x, String s);
}
그래서 인터페이스에 3번쨰 메소드인 didItWork()를 추가했습니다. 이렇게 변화를 주면 DoIt 인터페이스를 구현하고 있는 모든 클래스에서 didItWork 메소드를 구현해야 하는 문제가 생깁니다.
즉 인터페이스를 만들때 모든 상황을 처음에 예측하지 않는다면 중간에 추가할 때 문제가 생깁니다. 이런 문제를 해결하기 위해서 다음과 같이 DoIt 인터페이스를 상속하는 인터페이스를 만들어서 처리할수도 있습니다.
public interface DoItPlus extends DoIt {
boolean didItWork(int i, double x, String s);
}
이렇게 처리할 수 있지만 Java 8의 default method를 통해서 처리할 수 있습니다.
public interface DoIt {
void doSomething(int i, double x);
int doSomethingElse(String s);
default boolean didItWork(int i, double x, String s) {
// Method body
}
}
인터페이스에서 default 메소드에서는 구현을 할 수 있습니다. 물론 이 인터페이스를 구현하는 클래스에서도 새롭게 정의할 수 있습니다.
이렇게 default 메소드를 줌으로써 이 인터페이스를 사용하는 유저에게 컴파일 에러를 주지 않고 유저가 필요하다면 이 default method를 정의해서 사용할 수 있습니다.
이 질문에 대한 가장 간단한 답은 default method를 통해 Java에서 lambda expression이 가능해졌습니다.
lambda expression은 functional interface에서 가장 핵심적인 기능입니다. 원래 이런 lambda expression을 추가하기 위해서는 모든 core Class들 즉 java.util.List, JDK Classese들이 수정돼야 합니다.
default method들을 통해 이 수천개의 클래스를 수정하지 않고 새로운 기능을 추가할 수 있습니다.
다음 예시를 보겠습니다.
public interface Iterable<T>{
...
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
Java 8 이전에는 Collection을 순회할 때 Itreator Instance를 만들어서 hasNext()를 통해 순회를 했었습니다.
하지만 Java 8 이후부터는 default method로 새롭게 추가된 forEach()를 통해 Collection 순회가 가능합니다.
default methods외에도 Java 8에서는 인터페이스에서 static methods를 정의할 수 있습니다.
원래 static methods는 class에서만 정의할 수 있었고 인스턴스와 관련 있기보다는 클래스와 관련이 있었고 모든 인스턴스가 공유할 수 있는 기능으로 만들었습니다.
Java 8에서는 이런 static methods를 인터페이스에 관리할 수 있습니다.
static methods 정의는 클래스에서 정의하던 것과 유사하게 인터페이스 메소드 시그니처 앞에 static keyword를 붙여야 합니다. 인터페이스에 있는 모든 메소드 접근 제어자는 내재적으로 public이 포함되어있습니다.
예시는 다음과 같습니다.
public interface TimeClient {
static public ZoneId getZoneId (String zoneString) {
try {
return ZoneId.of(zoneString);
} catch (DateTimeException e) {
System.err.println("Invalid time zone: " + zoneString +
"; using default time zone instead.");
return ZoneId.systemDefault();
}
}
default public ZonedDateTime getZonedDateTime(String zoneString) {
return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
}
}
Java 9 이후부터는 인터페이스에서 private methods와 private static method를 사용할 수 있습니다.
이런 기능을 통해 인터페이스 내에서 코드 re-usability가 개선될 수 있습니다. 예를들면 인터페이스 default methods가 동일한 code를 가지고 있다면 이 부분을 private method로 빼서 처리하는게 가능합니다.
인터페이스 내 private method는 다음과 같은 규칙이 있습니다.
Private interface method cannot be abstract.
Private method can be used only inside interface.
Private static method can be used inside other static and non-static interface methods.
Private non-static methods cannot be used inside private static methods.
예시는 다음과 같습니다.
public interface CustomInterface {
public abstract void method1();
public default void method2() {
method4(); //private method inside default method
method5(); //static method inside other non-static method
System.out.println("default method");
}
public static void method3() {
method5(); //static method inside other static method
System.out.println("static method");
}
private void method4(){
System.out.println("private method");
}
private static void method5(){
System.out.println("private static method");
}
}
public class CustomClass implements CustomInterface {
@Override
public void method1() {
System.out.println("abstract method");
}
public static void main(String[] args){
CustomInterface instance = new CustomClass();
instance.method1(); // OK
instance.method2(); // OK
instance.method4(); // Compile Error
CustomInterface.method3(); // OK
CustomInterface.method5(); // Compile Error
}
}