Interface와 abstract class의 차이를 알아보자.

Java 8부터 interface에는 default method가 생겼다. 이로 인해 interface와 abstract class의 차이가 하나 줄었다.

Interface와 abstract class의 차이는 크게 두 가지다.

  • Abstract class는 interface와 달리 다중 상속의 대상이 될 수 없다.
  • Abstract class는 state를 가질 수 있지만 interface는 state를 가질 수 없다.

이외에도 lambda expression과 관련한 차이도 있다. 하지만 여기서는 다루지 않는다. 위에 명시한 두 가지 차이에 대해서 알아보자.

다중 상속

Effective Java를 보면 abstract class보다는 interface를 사용하라고 한다. 여러 이유가 있는데 그 중 위에서 언급한 첫 번째 이유가 있다. Abstract class 다중 상속의 대상이 될 수 없다. 결국, abstract class는 hierarchy와 관련된다. 하지만 hierarchy가 부적절한 경우가 많다. 예를 들어 interface Husbandinterface Son을 조합해 class MarriedMan을 정의할 수 있다. MarriedMan은 Husband이면서 Son이다. Hierarchy로 이 셋의 관계를 정의하는 건 부적절하다.

class MarriedMan() : Husband, Son {
  override fun actAsHusband() {}
  override fun actAsSon() {}
}

interface Husband {
  fun actAsHusband()
}

interface Son {
  fun actAsSon()
}

만약 HusbandSon이 interface가 아닌 abstract class였다면 위처럼 MarriedMan을 정의할 수 없다.

State

Interface: stateless

Interface는 state를 가질 수 없기 때문에 mutable instance variables를 사용할 수 없다. 즉, instance fields가 없거나 final variables만 가질 수 있다. 아래 예시는 final variables만 가진 경우다.

public interface Son {
  String nameOfFather = "John";
  String nameOfMother = "Joan";
}

실제로 아래 코드는 컴파일 에러를 낸다.

public class Demo {
    public static void main(String[] args) {
        Son.nameOfMother = "Anne";
    }
}
java: cannot assign a value to final variable nameOfMother

Abstract class: stateful

Abstract class는 여느 클래스와 마찬가지로 state를 가질 수 있다. 즉, mutable instance variables를 사용할 수 있다.

abstract class AbstractSon {
    String nameOfMother = "Joan";
}

public class ConcreteSon extends AbstractSon {
}

그렇기 때문에 아래 코드는 컴파일이 된다.

public class Demo {

    public static void main(String[] args) {
        AbstractSon son = new ConcreteSon();
        son.nameOfMother = "Anne";
    }
}

Skeletal implementation = abstract class + interface

위에서 interface의 특징에 대해 언급했다. Interface에는 또 다른 제약 사항이 있다. Interface는 Object class의 메서드인 hashCode(), toString() equals() 등에 대해 default method를 정의할 수 없다. 또한 interface는 public이 아닌 static members를 가질 수 없다 (private static 메서드 제외).

Skeletal implementation은 interface의 특징과 abstract class의 특징을 조합하기 위한 방법으로, interface를 상속하는 abstract class를 정의한다. 예를 들어, java.util.AbstractList, java.util.AbstractMap 등이 있다.

AbstractList 코드 일부를 보자.

package java.util;

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    // 생략

    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof List))
            return false;
        // 생략
    }
}

위 코드에서 equals()가 정의돼 있다. 위에서 언급했듯이, interface인 Listequals() 이름의 default method를 만들 수 없다. Skeletal implementation은 이를 극복했다.

이 Skeletal implementation은 어떻게 사용될까? 예시로 AbstractList를 상속하는 ArrayList를 살펴보자.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
  // 생략
}

ArrayListequals() 메서드는 AbstractList의 것이다. ArrayList class는 AbstractList를 상속하기 때문이다. 추가로 위 코드에서 알 수 있듯이 ArrayList는 다른 여러 interfaces를 다중 상속한다.

덕분에 interface에 default method equals()가 없지만 아래와 같이 코드가 잘 작동한다.

public class Demo {

    public static void main(String[] args) {
        List a = new ArrayList<Integer>(1);
        List b = new ArrayList<Integer>(1);

        System.out.println(a.equals(b));
    }
}
true

의문점: Kotlin interface

서두에서 정의한 son interface를 보자.

public interface Son {
  String nameOfFather = "John";
  String nameOfMother = "Joan";
}

Kotlin으로 표현하면 아래와 같이 표현할 수 있다 (IntelliJ의 java -> kotlin 자동 변환 사용).

interface Son {
    companion object {
        const val NAME_OF_FATHER = "John"
        const val NAME_OF_MOTHER = "Joan"
    }
}

참고로 kotlin의 성질상 아래와 같이 interface를 정의하면 아래와 같은 컴파일 오류가 발생한다.

interface Son {
  val NAME_OF_FATHER = "John"
  val NAME_OF_MOTHER = "Joan"
}
Property initializers are not allowed in interfaces

Kotlin에서 const val 대신에 var를 사용하면 interface가 state를 가질 수 있는 것으로 보인다. 아래와 같이 말이다.

interface Son {
    companion object {
        var NAME_OF_FATHER = "John"
        var NAME_OF_MOTHER = "Joan"
    }
}

진짜 그런가? 그렇다면 이렇게 코딩하는 건 바람직할까?

답은 No다. IntelliJ에서 위 코드를 java로 decompile해보면 아래와 같이 나온다.

public interface Son {
    // 생략

    public static final class Companion {

    // 생략

    static {
         $$INSTANCE = new Companion();
         NAME_OF_FATHER = "John";
         NAME_OF_MOTHER = "Joan";
     }
    }
}

즉, 애초에 java interface fields에 속하지 않는다.

Reference