디폴트 메서드
많은 프로그래밍 언어에서 함수 표현식을 컬렉션 라이브러리와 통합한다.
이는 종종 루프를 이용한 버전보다 짧고 이해하기 쉬운 코드로 이어진다.
예)
for(int i =0; i<list.size(); i++) { System.out.println(list.get(i)); } |
하지만 더 나은 방법이 있다.
라이브러리 설계자는 각 요소의 함수를 적용하는 forEach 메서드를 제공할 수 있다.
list.forEach(System.out::println); |
컬렉션 라이브러리를 바닥부터 설계해왔다면 문제가 없다.
하지만 자바 컬렉션이 라이브러리는 수년 전부터 설계되어왔기 때문에 문제가 된다.
Collection 인터페이스가 forEach 같은 새로운 메서드를 얻게 되면, Collection을 구현하는 자체 클래스를 정의하고 있는 모든 프로그램은 이 메서드를 구현하기 전에 동작하지 않는다.
쉽게 말해 자바에서는 이 상황을 받아들일 수 없다.
자바 설계자들은 구체적인 구현을 담은 인터페이스 메서드(디폴트 메서드 default method)를 허용함으로써 이 문제를 한 번에 해결하기로 결정했다.
디폴트 메서드는 기존 인터페이스에 안전하게 추가할 수 있다.
자바 8에서는 Collection의 슈퍼인터페이스인 Iterable 인터페이스에 forEach 메서드를 추가했다.
interface Person { long getId(); default String getName() { return "John Q. Public"; } } |
이 인터페이스를 추상 메서드 getId와 디폴트 메서드 getName을 포함하고 있다.
당연히 Person 인터페이스를 구현하는 구체 클래스 concrete class는 getId의 구현을 제공해야 하지만, getName의 구현은 그대로 두거나 오버라이드 하는 방법 중 선택할 수 있다.
디폴트 메서드는 인터페이스와 해당 인터페이스의 대부분 혹은 모든 메서드를 구현한 추상 클래스를 제공하는 고전적인 패턴(예 Collection/AbstractCollection 또는 WindowListener/WindowAdaptor)의 종말을 선고했다.
이제는 인터페이스에서 바로 메서드를 구현할 수 있다.
만일 똑같은 메서드가 한 인터페이스의 디폴트 메서드로 정의되어있고, 슈퍼 클래스나 다른 인터페이스의 메서드로도 정의되어 있으면 무슨일이 일어날까?
스칼라나, C++같은 언어는 이와 같은 모호함을 해결하는 복잡한규칙을 갖추고 있다.
다행이도 자바에서는 이 규칙이 훨씬 단순하다.
자바의 규칙
1. 슈퍼클래스가 우선한다. 슈퍼클래스에서 구체적인 메서드를 제공하는 경우, 이와 이름의 파라미터 타입이 같은 디폴트 메서드는 단순히 무시된다.
2. 인터페이스들이 충돌한다. 어떤 슈퍼인터페이스에서 디폴트 메서드를 제공하고, 또 다른 인터페이스에서 (디폴트 메서드든 아니든) 이름 및 파라미터 타입이 같은 메서드를제공하는 경우에는 해당 메서드를 오버라이드해서 충돌을 해결해야 한다.
두번째 규칙에서
getName 메서드를 포함하는 또 다른 인터페이스가 있다고 하자.
interface Named { default String getName() { return getClass().getName() + "_" + hashCode(); } } |
다음과 같이 두 인터페이스를 모두를 구현하는 클래스를 정의하면?
class Student implements Person, Named {
...
}
Student 클래스는 Person과 Named 인터페이스가 제공하는 두 가지 모순되는 getName 메서드를 상속한다.
자바는 이 중 하나를 우선해서 선택하기보다는 오류를 보고하고 프로그래머에게 모호함을 해결하도록 맡긴다.
단순히 Student 클래스에 getName 메서드를 제공하자.
이 메서드 안에서 다음 처럼 두 충돌 메서드 중 하나를 선택할 수 있다.
class Student implements Person, Named {
public String getName() { return Person.super.getName(); }
}
이번에는 Named 인터페이스가 getName의 디폴트 구현을 제공하지 않는다고 가정하자.
interface Named {
String getName();
}
Student 클래스가 Person 인터페이스로부터 디폴트 메서드를 상속할 수 있을까?
타당한 이야기일 수 있지만, 자바 설계자들은 일관성을 지지하기로 결정했다.
두 인터페이스가 어떻게 충돌하는지는 문제가 되지 않는다.
적어도 한 인터페이스에서 구현을 제공하면 컴파일러는 오류를 보고하며, 프로그래머는 반드시 모호함을 해결해야 한다.
방금 두 인터페이스 사이의 이름 충돌을 설명했다.
이제 슈퍼클래스를 확장하면서 인터페이스 하나를 구현하여, 이 둘에서 같은 메서드를 상속하는 클래스를 고려해보자
class Student extends Person implements Named { ... }
이 경우에는 오직 슈퍼클래스의 메서드만 중요하며, 인터페이스의 모든 디폴트 메서드는 단순히 무시된다.
Student는 Person에서 getName 메서드를 상속하며, Named 인터페이스에서 getName의 디폴트 구현 제공 여부는 상관이 없다.
이것이 바로 클래스 우선 규칙이다.
클래스 우선 규칙은 자바 7과 호완성을 보장한다.
인터페이스에 디폴트 메서드를 추가해도 해당 메서드가 존재하기 전부터 동작하던 코드에는 아무런 영향을 주지 않는다.
Object 클래스에 있는 메서드중 하나를 재정의하는 디폴트 메서드는 만들수 없다.
예) List같은 인터페이스에서 toString이나 equals에 해당하는 디폴트 메서드가 매력적인 방법일 수 있지만 정의할 수 없다.
클래스 우선 규칙의 결과로 이러한 메서드는 결코 Object.toString이나 Object.equals보다 우선할 수 없다.
인터페이스의 정적 메서드
자바 8부터는 인터페이스에 정적 메서드를 추가할 수 있다.
일반적으로 인터페이스와 동반하는 클래스들에 정적 메서드를 두었다.
자바 표준 라이브러리에서 Collection/Collections 또는 Path/Paths 같은 인터페이스와 유틸리티 클래스 쌍을 찾아볼수 있다.
Paths 클래스는 몇가지 팩토리 메서드factory method만 포함하고 있다.
Paths.get("jdk1.8.0", "jre", "bin")처럼 일련의 문자열로부터 경로(Path)를 만들수 있다.
자바8에서는 Path 인터페이스에 이 메서드를 추가할 수도 있었다.
public interface Path { public static Path get(String first, String... more) { return FileSystems.getDefault().getPath(first, more); } ... } |
이렇게 하면 Paths 클래스가 더는 필요 없다.
Collections 클래스 두종류의 메서드
public static void shuffle(List<?> list)
이러한 메서드는 List 인터페이스의 디폴트 메서드로 잘 동작할 것이다.
public default void suffle()
이 경우 모든 리스트를 대상으로 List.suffle()을 호출할 수 있다.
팩토리 메서드인 경우 메서드를 호출할 대상 객체가 없으므로 동작하지 않는다.
바로 이 부분이 정적 인터페이스 메서드가 등장할 곳이다.
예) List 인터페이스의 정적 메서드가 될수 있다.
public static <T> List<T> nCopies(int n, T o) { //o 인스턴스 n개로 구성된 리스트를 생성한다. |
이 경우 Collections.nCopies(10, "Fred") 대신 List.nCopies(10, "Fred")를 호출할 수 있고, 코드를 읽는 사람은 결과가 List임을 분명히 알 수 있다.
자바 컬렉션 라이브러리가 이와 같은 방식으로 리팩토리될 것 같지는 않지만, 자신만의 인터페이스를 구현한다면 더는 유틸리티 메서드용 별도의 동반 클래스를 제공할 이유가 없다.
자바 8에서는 상당히 많은 인터페이스에 정적 메서드를 추가했다.
예) Comparator 인터페이스는 '키 추출'함수를 받아서 추출된 키를 비교하는 비교자를 돌려주는 유용한 정적 comparing 메서드를 제공한다.
Person 객체를 이름으로 비교하려면 Comparator.comparing(Person::name)을 사용하면된다.
람다 표현식 (first, second) -> Integer.compare(firtst.length(), second.length())를 이용해서 문자열 길이로 비교했다.
하지만 정적 comparing 메서드를 이용하면 간단하게 Comparator.comparing(String::length)를 사용하면된다.
comparing 메서드는 함수(키 추출기)를 더 복잡한 함수(키 기반 비교자)로 변환한다.
'JAVA > JAVA 8' 카테고리의 다른 글
자바 8 #스트림(Stream) 관련 메서드 (0) | 2018.02.26 |
---|---|
자바 8 #스트림(Stream) (0) | 2018.02.23 |
자바8 람다 #레퍼런스 (0) | 2018.02.22 |
자바8 람다 #함수형 인터페이스 (0) | 2018.02.21 |
자바8 람다 (0) | 2018.02.18 |