- 디폴트 메서드란?
- 진화하는 API가 호환성을 유지하는 방법
- 디폴트 메서드의 활용 패턴
- 해결 규칙
정적 메서드와 인터페이스
- 자바는 인터페이스, 인터페이스의 인스턴스를 활용할 수 있는 정적 메서드를 제공하는 유틸리티 클래스를 제공한다. ex) Collections는 Collection객체를 활용할 수 있는 유틸리티 클래스이다.
- 자바 8에서 인터페이스에 직접 정적 메서드를 작성할 수 있으므로 유틸리티 클래스를 없애도 되지만, 과거 버전과의 호환성때문에 유틸리티 클래스를 남겨두었다.
🍎 변화하는 API
- 인터페이스에 새로운 메서드를 추가하면 바이너리 호환성은 유지된다.
- 바이너리 호환성? 인터페이스에 새로 추가된 메서드를 호출하지만 않으면 새로운 메서드의 구현없이도 기존 클래스는 잘 동작한다.
- 하지만 하나의 구현체에서 새로운 메서드를 구현하여 사용한다면, 다른 구현체에도 구현해야 한다.
호환성
- 바이너리 호환성
- 소스 호환성 : 코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있음. 인터페이스에 메서드를 추가하면 추가한 메서드를 구현하도록 클래스를 수정해야 하기 때문에 소스 호환성이 아니다.
- 동작 호환성 : 코드를 바꿔도 같은 입력값이 주어지면 프로그램은 같은 동작을 실행함.
🍎 디폴트 메서드란
추상 클래스와 자바8의 인터페이스
- 클래스는 다중상속 X, 인터페이스는 다중상속 O
- 추상 클래스는 인스턴스 변수 O, 인터페이스는 인스턴스 변수를 가질 수 없다.
🍎 디폴트 메서드 활용하기
1. 선택형 메서드
- 디폴트 메서드가 없던 시절 인터페이스의 구현체에서 필요없는 메서드는 빈 구현을 제공했다.
- 디폴트 메서드의 등장으로 빈 구현 필요없이 선택적으로 메서드를 사용하면 된다.
interface Iterator<T> {
boolean hasNext();
T next();
// 구현체에서 빈 구현을 제공하지 않고, 디폴트 메서드를 정의한다.
default void remove() {
throw new UnsupportedOperationException();
}
}
2.동작 다중 상속
// 인터페이스의 다중 상속
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- 다중 상속은 여러 클래스에서 기능을 상속받는다. 디폴트 메서드를 사용하지 않아도 다중 상속을 활용할 순 있다. 하지만 디폴트 메서드를 다중 상속받는다면 어떤 이점이 있을까?
- 중복되지 않는 최소한의 인터페이스를 유지한다면 동작을 쉽게 재사용하고 조합할 수 있다.
기능이 중복되지 않는 최소의 인터페이스
- 어떤 모양은 회전X, 크기 조절 O
- 어떤 모양은 회전O, 움직임O, 크기 조절X
public interface Rotatable {
void setRotationAngle(int angleInDegrees);
int getRotationAngle();
default void rotateBy(int angleInDegrees) {
setRotationAngle((getRotationAngle() + angleInDegrees) % 360);
}
}
public interface Moveable {
int getX();
int getY();
void setX(int x);
void setY(int y);
default void moveHorizontally(int distance) {
setX(getX() + distance);
}
default void moveVertically(int distance) {
setY(getY() + distance);
}
}
public interface Resizable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height) ;
default void setRelativeSize(int wFactor, int hFactor) {
setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
}
}
인터페이스 조합 (다중 상속)
// 회전 O, 움직임 O, 크기조절 O
public class Monster implements Rotatable, Moveable, Resizable {
// 모든 추상 메서드의 구현
}
...
Monster m = new Monster();
m.rotateBy(180); //180도 회전
m.moveVertically(10); //10만큼 이동
m.setAbsoluteSize(10, 10) // 사이즈 증가
// 회전 O, 움직임 O, 크기조절 X
public class Sun implements Rotatable, Moveable {
// 모든 추상 메서드의 구현
}
...
Sun m = new Sun();
m.rotateBy(180);
m.moveVertically(10);
옳지 못한 상속
- 한 개의 메서드를 위해 100개의 메서드가 정의되어 있는 클래스를 상속하는건 옳지 않다. 이럴 땐 델리게이션이 좋다? 멤버 변수에 클래스 선언해서 메서드 호출하기
🍎 디폴트 메서드의 충돌
해결 규칙 세 가지
- 클래스가 항상 이긴다. 인터페이스의 디폴트 메서드보다 클래스의 메서드가 이긴다.
- 클래스에 메서드 정의가 없으면 서브 인터페이스의 디폴트 메서드가 이긴다.
- 디폴트 메서드의 우선순위기 결정되지 않으면 명시적으로 오버라이딩해줘야 한다.
public interface A {
default void hello(){ System.println.out("hello A~"); }
}
public class D implements A {}
public interface B extends A {
@Override
default void hello() { System.println.out("hello B~"); }
}
public class C extends D implements B, A {
public static void main(String... args) {
new C().hello(); // hello B~ 출력
}
}
// 1번 규칙에 의해 클래스가 먼저이니 D의 hello가 우선이다.
// 하지만 2번 규칙에 의해 D에 메서드가 없으니 인터페이스가 이긴다.
// 컴파일러는 A와 B 중 선택하는데 A를 상속한 B가 이긴다.
충돌 그리고 명시적인 문제 해결
- 인터페이스 A와 B에 동일한 디폴트 메서드가 있다. A,B를 구현한 C는 명시적으로 디폴트 메서드를 오버라이딩해야 한다.
- 만약 디폴트 메서드가 리턴 타입만 다르고 메서드명이 같으면 역시 충돌난다.
다이아몬드 문제
public interface A {
default void hello() {
System.println.out("Hello A~");
}
}
public interface B extends A {}
public interface C extends A {}
public class D implements B,C {
public static void main(String... args) {
new D().hello(); // B,C 모두 A의 디폴트 메서드이므로 A출력
}
}
댓글