- 인스턴스로 생성이 불가능하다.
- 선언부만 있는 추상 메서드를 갖는다.
- 인스턴스 메서드를 구현 형태로 제공할 수 있다.
- 추상 클래스
- 추상 클래스를 상속받아 기능을 이용하고 확장시켜야한다.
- 인터페이스
- 구현을 강제해서 구현 객체가 같은 동작을 하도록 보장한다.
-
추상 클래스
-
단일 상속만 지원하기 때문에, 다중 클래스 상속이 불가능하다.
// Amy에 Student을 추가로 상속 받고 싶으면? -> 불가능! public class Amy extends Person { // ... } abstract class Student { // ... }
-
-
인터페이스
-
구현해야하는 메소드만 구현하면 어떤 클래스를 상속하던 간에 같은 타입으로 취급한다.
-
여러 인터페이스를 상속이 가능하다.
// Amy에 Student을 추가로 상속 받고 싶으면? -> implements 구문만 추가하면 됨! public class Amy extends Person implements Student { // ... } interface Student { // ... }
-
-
추상 클래스
- 기존 클래스에 끼워넣기 어렵다.
// 만약 여기서 추상 클래스 Person을 확장해야한다면? public class Amy extends Student { // ... } public class Jason extends Student { // ... } abstract class Student { // ... } // 이렇게?? public abstract class Person { public abstract void walk(); } abstract class Student extends Person { // ... } // Student를 상속 받았던 객체들은 모두 walk()이라는 메소드의 구현을 해야한다. public class Amy extends Student { // ... }
-
인터페이스
- 손쉽게 인터페이스를 구현해 넣을 수 있다.
// 만약 여기서 추상 클래스 Person을 확장해야한다면? public class Amy extends Student { // ... } public class Jason extends Student { // ... } abstract class Student { // ... } // 원하는 객체만 구현을 할 수 있다. public class Amy extends Student implements Person { // ... }
특정 클래스의 주 기능에 **추가적인 기능을 혼합(mixed-in)**하는 것이다.
-
추상클래스
- 단일 상속만 지원하기 때문에 믹스인이 들어갈만한 자리가 없다.
-
인터페이스
- 손쉽게 구현이 가능하다.
- ex. Comparable, Clonable, Serializable
public class Amy extends Student implements Comparable { @Override public int compareTo(Object o) { return 0; } }
// 만약 Comparable이 추상 클래스라면? public abstract class Comparable<T> { public abstract int compareTo(T o); } // 들어갈 자리가 없다! 😱 public class Amy extends Student { // ... }
- 손쉽게 구현이 가능하다.
👧 Amy
라는 객체가 Student
인 동시에 Intern
인 경우에
-
추상클래스
- 복잡한 계층구조일 경우에 많은 조합이 필요하고, 고도 비만 계층으로 이어질 수 있다.
public abstract class Student { // ... } public abstract class Intern { // ... } // 단일 상속만 가능하기 때문에 인턴인 동시에 학생인 추상 클래스가 필요하다. public abstract class StudentIntern { // ... } public class Amy extends SutdentIntern { // ... }
-
인터페이스
- 계층구조가 없는 타입 프레임워크를 만들 수 있다.
public interface Student { // ... } public interface Intern { // ... } // 문제 없다. public class Amy implements Student, Intern { // ... }
- 추상 클래스
- 기능을 추가하는 방법이 상속밖에 없다. → 활용도가 떨어지고 깨지기도 쉽다 (래퍼 클래스보다)
- 인터페이스
- 래퍼 클래스와 함께 사용하면 상속보다 안전하고 강력하게 기능을 향상시킬 수 있다. (w. item18)
구현 내용이 있는 메소드로, 구현 방법이 명확하다면 인터페이스에서 사용이 가능하다.
public interface Student {
public void study();
public default void syaHello() {
System.out.println("안녕하세요");
}
}
- @impleSpec을 붙여 문서화하면 좋다. (w. item19)
- Object 메서드 (ex. equals, hashCode)를 디폴트 메서드로 제공하면 안된다.
- 인터페이스는 인스턴스 필드를 가질 수 없고, public이 아닌 정적 멤버도 가질 수 없다.
- Java 9 이후부터는 private static 메서드도 구현이 가능하게 변경되었다.
인터페이스 + 골격 구현 클래스
추상 클래스처럼 구현을 도와주는 동시에, 추상클래스로 타입을 정의할 때 따라오는 심각한 제약에서 자유롭다.
// 인터페이스
public interface Phone {
void booting();
void greeting();
void shutdown();
void process();
}
public class IPhone implements Phone {
@Override
public void booting() {
System.out.println("booting ...");
}
@Override
public void greeting() {
System.out.println("I am iPhone");
}
@Override
public void shutdown() {
System.out.println("shut down ...");
}
@Override
public void process() {
booting();
greeting();
shutdown();
}
}
public class GalaxyPhone implements Phone {
@Override
public void booting() {
System.out.println("booting ...");
}
@Override
public void greeting() {
System.out.println("I am galaxy phone");
}
@Override
public void shutdown() {
System.out.println("shut down ...");
}
@Override
public void process() {
booting();
greeting();
shutdown();
}
}
iPhone
과 GalaxyPhone
모두 같은 인터페이스(Phone
)를 구현하고 있고, booting()
과 shutdown()
은 같은 동작을 하고 있다.
여기서 추상골격 구현 클래스을 이용하면 중복 코드를 제거할 수 있다.
// 추상골격 구현 클래스 (보통 Abstract~의 네이밍을 사용한다)
public abstract class AbstractPhone implements Phone {
// 같은 동작을 하는 메소드를 여기에 정의한다.
@Override
public void booting() {
System.out.println("booting ...");
}
@Override
public void shutdown() {
System.out.println("shut down ...");
}
@Override
public void process() {
booting();
greeting();
shutdown();
}
}
public class IPhone extends AbstractPhone implements Phone {
@Override
public void greeting() {
System.out.println("I am iPhone");
}
}
실행결과
public class GalaxyPhone extends AbstractPhone implements Phone {
@Override
public void greeting() {
System.out.println("I am galaxy phone");
}
}
중복 제거 성공!
그냥
Phone
에default
를 사용해서 메소드를 구현하면 안되나?
외부에서의 호출을 막고 싶다면 추상클래스로 사용하는게 맞는 것 같다. 추상클래스를 사용하면 protected, private 제어자를 지정할 수 있기 때문이다. 인터페이스는 기본적으로 변수필드는 public static final 이며, 모든 메소드는 public abstract 이므로 인터페이스로 구현할 경우, 템플릿 메소드 내부에서만 호출되어야 할 메소드들이 public 제어자에 의해 의도치 않은 사용처에서 호출될 위험이 있다.
만약 iPhone
에서 PhoneManufacturer
라는 제조사 클래스를 상속받아야해서 추상골격 구현 클래스를 상속 받지 못한다면?
public class PhoneManufacturer {
public void printManuFacturer() {
System.out.println("Made by Apple");
}
}
// 골격 구현을 확장한 클래스
public class InnerAbstractPhone extends AbstractPhone {
@Override
public void greeting() {
System.out.println("I am iPhone");
}
}
public class IPhone extends PhoneManufacturer implements Phone {
InnerAbstractPhone innerAbstractPhone = new InnerAbstractPhone(); // 내부 클래스로 정의
@Override
public void booting() {
innerAbstractPhone.booting();
}
@Override
public void greeting() {
innerAbstractPhone.greeting();
}
@Override
public void shutdown() {
innerAbstractPhone.shutdown();
}
@Override
public void process() {
printManuFacturer();
innerAbstractPhone.process();
}
}
위와 우회적 골격 구현을 사용할 수 있다. 이와 같은 방식을 **시뮬레이트한 다중 상속(simulated multiple inheritance)**이라고 한다.
- 인터페이스를 보며 다른 메서드들의 구현에 사용되는 기반 메서드 선정
- 기반 메서드들을 사용해 구현할 수 있는 메서드들을 디폴트 메서드로 제공
- 기반 메서드나 디폴트 메서드로 만들지 못한 메서드는 해당 인터페이스를 구현하는 골격 구현 클래스에서 작성
- 설계, 문서화 필수!
상속을 위해 인터페이스를 구현했으나 추상클래스가 아닌 것
ex. AbstractMap.SimpleEntry
골격 구현으로 만든 추상 클래스
// 골격 구현 클래스
abstract class CustomAbstractMapEntry<K, V> implements Map.Entry<K, V> {
private K key;
private V value;
public CustomAbstractMapEntry(final K key, final V value) {
this.key = key;
this.value = value;
}
abstract void printKey();
abstract void printValue();
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(final V value) {
return this.value = value;
}
}
// 골격 구현 클래스를 상속받은 클래스
public class CustomMapEntry<K, V> extends CustomAbstractMapEntry<K, V> {
public CustomMapEntry(K key, V value) {
super(key, value);
}
@Override
void printKey() {
}
@Override
void printValue() {
}
}
@Test
public void 단순구현_골격구현_비교() {
String key = "key";
String value = "value";
CustomMapEntry<String, String> customMapEntry = new CustomMapEntry<>(key, value);
AbstractMap.SimpleEntry<String, String> simpleEntry = new SimpleEntry<>(key, value); // 단순구현 -> 추상 메소드를 구현할 필요가 없다.
Assert.assertEquals(customMapEntry.getKey(), simpleEntry.getKey());
Assert.assertEquals(customMapEntry.getValue(), simpleEntry.getValue());
}
일반적으로 다중 구현을 할 때에는 인터페이스가 가장 적합하다.
만약 복잡한 경우일 때에는 골격 구현을 고려해보자!
단, 골격 구현은 가능한 인터페이스의 default 메서드 제공해야하며, 그 인터페이스를 구현한 모든 곳에서 사용하는 것이 좋다.