EPISODE 13. 모던 자바 인 액션(CHAPTER 13 디폴트 메서드)
인터페이스를 정의하는 두 가지 방법을 제공한다.
첫 번째는 인터페이스 내부에 정적 메서드(static method)를 사용하는 것
두 번째는 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드(default method)기능을 사용 하는것
변화하는 API
API 버전1
public interface Resizeable extends Drawable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeigh(int height);
void setAbsoluteSize(int width, int height);
}
사용자 구현
public class Game {
public static void main(String...args) {
List<Resizable> resizableShapes =
Arrays.asList(new Square(), new Rectangle(), new Ellipse()); // 크기를 조절할 수 있는 모양 리스트
Utils.paint(resizableShapes);
}
}
public class Utils {
public static void paint(List<Resizable> l) {
l.forEach(r -> {
r.setAbsoluteSize(42, 42); // 각 모양에 setAbsoluteSize 호출
r.draw();
});
}
}
API 버전2
public interface Resizable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
void setRelativeSize(int wFactor, int hFactor); // API 버전2에 추가된 새로운 메서드
}
디폴트 메서드란 무엇인가?
자신을 구현하는 클래스에서 메서드를 구현하지 않도록 하는 세로운 메서드 시그니처
default라는 키워드 사용한다.
[퀴즈 13-1] removeIf
여러분이 자바 언어와 API의 달인이라고 가정하자. 어느 날 다수의 사용자로부터 ArrayList, TreeSet, LinkedList 및 다른 모든 컬렉션에서 사용할 수 있는 removeIf 메서드를 추가해달라는 요청을 받았다. removeIf 메서드는 주어진 프레디케이트와 일치하는 모든 요소를 컬렉션에서 제거하는 기능을 수행한다. 새로운 removeIf를 기존 컬렉션 API에 가장 적절하게 추가하는 방법은 무엇일까?
정답
우선 반대로 컬렉션 API를 고치는 가장 좋지 않은 방법은 무엇일까? 컬렉션 API의 모든 구현 클래스에 removeIf를 복사&붙여넣기하는 것이다. 이 방법은 얼마 못 가서 자바 커뮤니티의 거센 저항에 직면할 것이다. 다른 방법은 없을까? 모든 컬렉션 클래스는 java.util.Collection인터페이스를 구현한다. 그러면 Collection 인터페이스에 메서드를 추가할 수 있을까? 지금까지 확인한 것처럼 디폴트 메서드를 인터페이스에 추가함으로써 소스 호환성을 유지할 수 있다. 그러면 Collection을 구현하는 모든 클래스(물론 컬렉션 라이브러리의 클래스뿐 아니라 Collection 인터페이스를 직접 구현한 모든 사용자의 클래스도 포함)는 자동으로 removeIf를 사용할 수 잇게 된다.
default boolean removeIf(Predicate<? super E> filter) {
boolean removed = false;
Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
디폴트 메서드 활용 패턴
디폴트 메서드를 이용하는 두가지 방식 선택형 메서드(optional method)와 동작 다중 상속(multiple inheritance of behavior)이 있다.
선택형 메서드
Iterator는 hasNext와 next뿐 아니라 remove메서드도 정의한다. 사용자들이 remove 기능은 잘 사용하지 않으므로 자바 8 이전에는 remove 기능을 무시했다. 결과적으로 Iterator를 구현하는 많은 클래스에서는 remove에 빈 구현을 제공했다.
디폴트 메서드를 이용하면 remove 같은 메서드에 기본 구현을 제공할 수 있으므로 인터페이스를 구현하는 클래스에서 빈 구현을 제공할 필요가 없다. 예를 들어 아래와 같이 remove를 정의한다.
interface Iterator<T> {
boolean hasNext();
T next();
default void remove() {
throw new UnsupportedOperationException();
}
}
기본 구현이 제공되므로 Iterator 인터페이스를 구현하는 클래스는 빈 remove 메서드를 구현할 필요가없고, 불필요한 코드를 줄일 수 있게되었다.
동작 다중 상속
public class ArrayList<E> extends AbstractList<E> // 한개의 클래스를 상속받는다.
implements List<E>, RandomAccess, Clonable,
Serializable { // 네 개의 인터페이스를 구현한다.
다중 상속 형식
ArrayList는 한 개의 클래스를 상속받고, 여섯 개의 인터페이스를 구현한다. 결과적으로 ArrayList는 AbstractList, List, RandomAccess, Cloneable, Serializable, Iterable, Collection의 서브형식(subtype)이 된다. 따라서 디폴트 메서드를 사용하지 않아도 다중 상속을 활용할 수 있다.
기능이 중복되지 않는 최소의 인터페이스
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);
}
}
해석 규칙
public interface HappyA {
default void hello() {
System.out.println("Hello from A");
}
}
public interface HappyB extends HappyA {
default void hello() {
System.out.println("Hello from B");
}
}
public class HappyC implements HappyB, HappyA {
public static void main(String... args) {
new HappyC().hello(); // 무엇이 출력될까?
}
}
알아야 할 세 가지 해결 규칙
1. 클래스가 항상 이긴다. 클래스나 슈퍼클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 갖는다.
2. 1번 규칙 이외의 상황에서는 서브인터페이스가 이긴다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브인터페이스가 이긴다. 즉, B가 A를 상속받는다면 B가 A를 이긴다.
3. 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.
디폴트 메서드를 제공하는 서브인터페이스가 이긴다.
HappyB가 HappyA를 상속 받았으므로 컴파일러는 HappyB의 hello를 선택한다.
public class HappyD implements A {}
public class HappyC extends HappyD implements HappyB, HappyA {
public static void main(String... args) {
new HappyC().hello(); // 무엇이 출력될까?
}
}
HappyD는 hello를 오버라이드하지 않았고, 단순히 인터페이스 HappyA를 구현했다. 따라서 HappyD는 인터페이스의 HappyA의 디폴트 메서드 구현을 상속받는다. 컴파일러 인터페이스 HappyA의 hello나 인터페이스 HappyB의 hello 둘 중 하나를 선택해야 한다. 여기서 HappyB가 HappyA를 상속받는 관계이므로 이번에도 HappyB가 출력된다.
[퀴즈 13-2] 해석 규칙을 기억하라
HappyD가 명시적으로 HappyA의 hello 메서드를 오버라이드한다. 프로그램의 실행 결과는 무엇일까?
public class HappyD implements HappyA {
void hello() {
System.out.println("Hello from HappyD");
}
}
public class HappyC extends HappyD implements HappyB, HappyA {
public static void main(String... args) {
new HappyC().hello();
}
}
정답
결과는 ‘Hello from HappyD’다 규칙 1에 의해 슈퍼클래스의 메서드 정의가 우선권을 갖기 때문이다. 그러면 HappyA에서 디폴트 메서드를 제공함에도 불구하고 HappyC는 hello를 구현해야 한다.
충돌 그리고 명시적인 문제 해결
public interface HappyA {
default void hello() {
System.out.println("Hello from HappyA");
}
}
public interface HappyB {
default void hello() {
System.out.println("Hello from HappyB");
}
}
public class HappyC implements B, A {}
인터페이스 간에 상속관계가 없으므로 2번 규칙을 적용할 수 없다. 그러므로 HappA, HappyB의 hello 메서드를 구별할 기준이 없다. 자바 컴파일러는 어떤 메서드를 호출해야 할지 알수 없으므로 “Error: class HappyC inherits unrelated defaults for Hello() from types HappyB and HappyA.”같은 에러가 발생한다.
충돌 해결
X.super.m(…) 형태의 새로운 문법을 제공한다. 여기서 X는 호출하려는 메서드 m의 슈퍼인터페이스다.
public class HappyC implements HappyB, HappyA {
void hello() {
HappyB.super.hello(); // 명시적으로 인터페이스의 B의 메서드를 선택한다.
}
}
[퀴즈 13-3] 거의 비슷한 시그니처
public interface HappyA {
default Number getNumber() {
return 10;
}
}
public interface HappyB {
default Integer getNumber() {
return 42;
}
}
public class HappyC implements HappyB, HappyA {
public static void main(String... args) {
System.out.println(new HappyC().getNumber());
}
}
정답
HappyC는 HappyA, HappyB 메서드를 구분할 수 없다. 클래스 C에서 컴파일 에러가 발생한다.
다이아몬드 문제
public interface HappyA {
default void hello() {
System.out.println("Hello from HappyA");
}
}
public interface HappyB extends HappyA {}
public interface HappyC extends HappyA {}
public class HappyD implements HappyB, HappyC {
new HappyD().hello(); // 무엇이 출력될까?
}
다이어그램의 모양이 다이아몬드를 닮았으므로 이를 다이아몬드 문제(diamond problem)라고 부른다. A만 디폴트 메서드를 정의하고 있기 때문에 프로그램 출력 결과는 ‘Hello from HappyA’가 된다.
'Programming > Java' 카테고리의 다른 글
EPISODE 15. 모던 자바 인 액션 (CHAPTER 15 CompletableFuture와 리액티브 프로그래밍 컨셉의 기초) (0) | 2023.08.01 |
---|---|
EPISODE 14. 모던 자바 인 액션(CHAPTER 14 자바 모듈 시스템) (0) | 2023.07.25 |
EPISODE 12. 모던 자바 인 액션(CHAPER 12 새로운 날짜와 시간 API) (0) | 2023.07.21 |
EPISODE 11. 모던 자바 인 액션(CHAPTER 11 null 대신 Optional 클래스) (0) | 2023.07.20 |
EPISODE 10. 모던 자바 인 액션 (CHAPTER 10. 람다를 이용한 도메인 전용 언어) (0) | 2023.07.18 |