인터페이스를 바꾸면 인터페이스를 구현한 모든 클래스도 모두 고쳐야 하지만 java 8에서는 기본 구현을 포함하는 인터페이스를 정의하는 2가지 방법이 있다.
- 인터페이스 내부에 정적 메서드 사용하기
- 기본 구현을 제공할 수 있도록 디폴트 메서드 기능을 사용하기
기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다.
각각의 호환성은 low level 공부를 좀 하고 다시 알아보자
![](https://blog.kakaocdn.net/dn/AoaUo/btsrr068JXo/dDmKYk2Z7Yat5pZSgRmNJK/img.png)
디폴트 메서드란 무엇인가?
공개된 API에 새로운 메서드를 추가하면(구현하고 있는 클래스들 override해야되는) 문제로 java 8 에서는 호환성을 유지하면서 API를 바꿀 수 있도록 default method를 제공한다.
- default method는 인터페이스 구현체에서 구현하지 않은 메서드는 인터페이스 자체에서 기본으로 제공한다.
Predicate, Function, Comparator 등 많은 함수형 인터페이스도 다양한 default method를 가지고 있다.
- 함수형 인터페이스는 오직 하나의 추상 메서드를 포함해야 되지만, 디폴트 메서드는 추상 메서드에 해당하지 않는다.
디폴트 메서드 활용 패턴
- 선택형 메서드
- 동작 다중 상속
선택형 메서드
Iterator 인터페이스에서 hasNext와 next메서드도 있지만 remove 메서드도 있다.
- java 8이전에는 사용자들이 remove 기능을 잘 사용하지 않아 Iterator를 구현하는 많은 클래스에서 remove에 빈 구현을 할 수 밖에 없었다.
default method를 이용하면 remove 같은 메서드에 기본 구현을 제공할 수 있어 빈 구현을 제공할 필요가 없어졌다.
- 불필요한 코드 제거
default void remove() {
throw new UnsupportedOperationException("remove");
}
동작 다중 상속
인터페이스는 여러 개 구현이 가능하기 때문에 default method를 이용하면 기존에 불가능했던 다중 상속 기능도 구현할 수 있다.
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- ArrayList는 한개의 클래스를 상속받고 여러 개의 인터페이스를 구현하여 default method를 사용하지 않아도 다중 상속을 활용하고 있다.
기능이 중복되지 않는 최소의 인터페이스
- java 8에서는 인터페이스가 구현을 포함할 수 있어 중복되지 않으면 쉽게 재사용하고 조합할 수 있다.
추상 메서드를 이용해 default 메서드를 구현할 수 있다.
public interface Rotatable {
void setRotationAngle(int angleInDegrees);
int getRotationAngle();
default void rotateBy(int angleInDegrees){ // rotateBy 메서드의 기본 구현
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);
}
}
- 추상메서드를 어떻게 구현하느냐에 따라서 뼈대 알고리즘이 결정되는 템플릿 디자인 패턴과 비슷하다.
인터페이스 조합
위에 인터페이스를 조합해서 움직이며, 회전할 수 있고 크기를 조절할 수 있는 다양한 클래스를 구현할 수 있다.
- 모든 추상 메서드의 구현은 제공해야 하지만 default method는 자동으로 상속받는다.
public class Monster implements Moveable, Resizable, Rotatable{
... //추상 클래스 구현
}
public class Sun implements Moveable, Rotatable{
... //추상 클래스 구현
}
- 코드를 복사 붙여넣기 할 필요없이 필요한 인터페이스만 구현하므로 default method를 재사용하여 클래스를 재정의할 수 있다.
UML 다이어그램 - 다중 동작 조합
한 개의 메서드를 재사용하려고 할 때 100개의 메서드와 필드가 정의되어 있는 클래스를 상속받는 것은 좋지 않다.
델리게이션(delegation): 멤버 변수를 이용해서 클래스에 필요한 메서드를 직접 호출하는 메서드를 작성하는 것이 좋다.
해석 규칙
여러 인터페이스를 구현했을 때 default method가 같은 시그니처를 갖게될 수 있다.
public class Main implements A,B{
public static void main(String[] args) {
new Main().hello(); // ? -> B
}
}
interface A {
default void hello(){
System.out.println("A");
}
}
interface B extends A {
default void hello(){
System.out.println("B");
}
}
interface C {
default void hello(){
System.out.println("C");
}
}
- C++의 다이아몬드 문제처럼 같은 시그니처를 갖는 두 메서드를 상속받는 클래스
충돌 해결하기
C까지 구현하면 오버라이드 해서 명시적으로 선택해야 한다. (java 8에서 X.super.m(...) 형태의 문법 제공)
- 컴파일 에러를 막기 위해서 오버라이드 했지만 A를 호출하려고 하면 B에서 A를 구현했기 때문에 컴파일 에러 발생 B,C 호출 해야한다.
public class Main implements A,B,C{
@Override
public void hello() {
A.super.hello(); //Bad type qualifier in default super call: redundant interface A is extended by B
}
public static void main(String[] args) {
new Main().hello(); // ? -> B
}
}
알아야 할 세가지 해결 규칙
다른 클래스나 인터페이스로부터 같은 시그니처를 같는 메서드를 상속받을 때는 세 가지 규칙이 있다.
- 클래스가 항상 이긴다
: 클래스나 슈퍼클래스에서 정의한 메서드가 default 메서드보다 우선권을 갖는다. - 1번 규칙 이외의 상황에서는 서브 인터페이스가 이긴다.
: 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브 인터페이스가 이긴다. (B가 출력되는 이유) - 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 default 메서드를 오버라이드 하고 호출해야 한다.
다이아몬드 문제
Main은 B와 C중에서 default method를 재정의 한 것이 있었다면 2번 규칙에 의해서 서브 인터페이스가 이기겠지만 여기서는 A만 default method를 정의하고 있어서 A가 나오게 된다.
- B, C 둘다 default method를 재정의 했다면 명시적으로 override 해줘야 한다.
public class Main implements B,C{
public static void main(String[] args) {
new Main().hello();
}
}
interface A {
default void hello(){
System.out.println("A");
}
}
interface B extends A{}
interface C extends A{}
다이어그램의 모양이 다이아몬드를 닮아 다이아몬드 문제라고 부른다.
C에 default method가 아니라 추상 메서드를 추가한다면 D에서 Override를 안해주면 컴파일 에러가 발생한다.
interface C extends A{
void hello();
}
'책 > 모던 자바 인 액션' 카테고리의 다른 글
12. 새로운 날짜와 시간 API (0) | 2023.08.17 |
---|---|
11. null 대신 optional 클래스 (0) | 2023.08.11 |
10. 람다를 이용한 도메인 전용 언어 (1) | 2023.08.04 |
9. 리팩터링, 테스팅, 디버깅 (0) | 2023.08.02 |
8. 컬렉션 API 개선 (0) | 2023.07.28 |