추상 클래스가 이미 있는데 왜 기본 및 정적 메소드가 Java 8의 인터페이스에 추가 되었습니까?


99

Java 8에서 인터페이스에는 구현 된 메소드, 정적 메소드 및 소위 "기본"메소드 (구현 클래스를 대체 할 필요가 없음)가 포함될 수 있습니다.

내 (아마도 순진한) 견해로는 이와 같은 인터페이스를 위반 할 필요가 없었습니다. 인터페이스는 항상 이행해야 할 계약이었으며 매우 단순하고 순수한 개념입니다. 이제 여러 가지가 혼합되어 있습니다. 내 의견으로는 :

  1. 정적 메소드는 인터페이스에 속하지 않습니다. 유틸리티 클래스에 속합니다.
  2. "default"메소드는 인터페이스에서 전혀 허용되지 않아야합니다. 이 목적을 위해 항상 추상 클래스를 사용할 수 있습니다.

한마디로 :

자바 8 이전 :

  • 추상 및 일반 클래스를 사용하여 정적 및 기본 메소드를 제공 할 수 있습니다. 인터페이스의 역할은 분명합니다.
  • 인터페이스의 모든 메소드는 클래스를 구현하여 대체해야합니다.
  • 모든 구현을 수정하지 않고 인터페이스에 새 메소드를 추가 할 수는 없지만 실제로는 좋은 방법입니다.

자바 8 :

  • 인터페이스와 추상 클래스 (복수 상속 제외) 사이에는 거의 차이가 없습니다. 실제로 인터페이스를 사용하여 일반 클래스를 에뮬레이션 할 수 있습니다.
  • 구현을 프로그래밍 할 때 프로그래머는 기본 메소드를 무시하는 것을 잊을 수 있습니다.
  • 클래스가 동일한 서명을 가진 기본 메소드를 가진 둘 이상의 인터페이스를 구현하려고하면 컴파일 오류가 발생합니다.
  • 인터페이스에 기본 메소드를 추가하면 모든 구현 클래스가이 동작을 자동으로 상속합니다. 이러한 클래스 중 일부는 새로운 기능을 염두에두고 설계되지 않았을 수 있으며 이로 인해 문제가 발생할 수 있습니다. 예를 들어 누군가 누군가 default void foo()인터페이스에 새로운 기본 메소드 를 추가 하면 동일한 서명을 가진 개인 메소드를 구현 하고 갖는 Ix클래스 는 컴파일되지 않습니다.CxIxfoo

그러한 중대한 변화의 주된 이유는 무엇이며, 어떤 새로운 이점이 있습니까?


30
보너스 질문 : 왜 클래스 대신 다중 상속을 도입하지 않았습니까?

2
정적 메소드는 인터페이스에 속하지 않습니다. 유틸리티 클래스에 속합니다. 그들은 @Deprecated카테고리 에 속하지 않습니다 ! 정적 메소드는 무지와 게으름 때문에 Java에서 가장 악용되는 구문 중 하나입니다. 많은 정적 메소드는 일반적으로 무능한 프로그래머를 의미하고, 수십 배 정도의 커플 링을 증가 시키며, 왜 나쁜 아이디어인지 깨달았을 때 단위 테스트 및 리팩터링에 악몽입니다!

11
@JarrodRoberson "부적절한 아이디어가 무엇인지 깨달았을 때 단위 테스트와 리팩토링에 악몽이된다"는 힌트를 줄 수 있습니까? 나는 그것을 생각하지 않았고 그것에 대해 더 많이 알고 싶습니다.
watery

11
@Chris 상태의 다중 상속은 특히 메모리 할당 ( 클래식 다이아몬드 문제 ) 에서 많은 문제를 일으 킵니다 . 그러나 동작의 다중 상속은 인터페이스에 의해 이미 설정된 계약을 충족하는 구현에만 의존합니다 (인터페이스는 선언하고 요구하는 다른 메소드를 호출 할 수 있음). 미묘하지만 매우 흥미로운 구별.
ssube

1
기존 인터페이스에 메소드를 추가하고 싶거나 java8 이전에는 번거 롭거나 거의 불가능했습니다. 이제 기본 메소드로 추가 할 수 있습니다.
VdeX

답변:


59

기본 메소드에 대한 좋은 동기 부여 예제는 Java 표준 라이브러리에 있습니다.

list.sort(ordering);

대신에

Collections.sort(list, ordering);

나는 그들이 하나 이상의 동일한 구현 없이는 그렇게 할 수 있다고 생각하지 않습니다 List.sort.


19
C #은 확장 방법으로이 문제를 극복합니다.
Robert Harvey

5
그리고 연결리스트 자바 7 자리에 통합 될 수 있기 때문에 링크 된 목록이 외부 배열 덤프하고 정렬하는, (1) 넓은 공간과 O 시간 머지 소트 (N, N 로그)는 O를 사용할 수
래칫 괴물

5
Goetz가 문제를 설명하는 이 문서를 찾았습니다 . 그래서 지금은이 답변을 해결책으로 표시 할 것입니다.
Mister Smith

1
@RobertHarvey : 200 억 개의 항목 인 List <Byte>를 만들어서 합류 한 다음에을 IEnumerable<Byte>.Append호출 Count한 다음 확장 방법으로 문제를 해결하는 방법을 알려주십시오. 경우 CountIsKnownCount의 멤버였다 IEnumerable<T>,의 반환은 Append광고 할 수있는 CountIsKnown구성 컬렉션했다면,하지만 불가능 그러한 방법이없는.
supercat

6
@ supercat : 나는 당신이 무엇을 이야기하고 있는지 약간의 생각이 없습니다.
Robert Harvey

48

정답은 실제로 Java Documentation 에서 찾을 수 있습니다.

[d] efault 메소드를 사용하면 라이브러리 인터페이스에 새로운 기능을 추가하고 해당 인터페이스의 이전 버전 용으로 작성된 코드와 바이너리 호환성을 보장 할 수 있습니다.

인터페이스는 공개 된 후에는 진화가 불가능한 경향이 있었기 때문에 Java의 오랜 고통의 원천이되었습니다. (문서의 내용은 주석에서 링크 한 논문과 관련이 있습니다 : 가상 확장 방법을 통한 인터페이스 진화 .) 또한 새로운 기능 (예 : 람다 및 새로운 스트림 API)의 빠른 채택은 기존 컬렉션 인터페이스 및 기본 구현을 제공합니다. 바이너리 호환성을 깨거나 새로운 API를 도입한다는 것은 Java 8의 가장 중요한 기능이 일반적으로 사용되기까지 몇 년이 지나야한다는 것을 의미합니다.

인터페이스에서 정적 메소드를 허용하는 이유는 다음 문서에 다시 설명 되어 있습니다. 라이브러리에서 헬퍼 메소드를 쉽게 구성 할 수 있습니다. 별도의 클래스가 아닌 동일한 인터페이스에서 인터페이스에 고유 한 정적 메소드를 유지할 수 있습니다. 다시 말해, 같은 정적 유틸리티 클래스 java.util.Collections는 이제 (마지막으로) 반 패턴, 일반 (물론 항상 은 아님)으로 간주 될 수 있습니다 . 내 생각에 가상 확장 방법이 구현되면이 동작에 대한 지원을 추가하는 것이 쉽지 않은 것입니다. 그렇지 않으면 아마도 완료되지 않았을 것입니다.

이와 비슷하게, 이러한 새로운 기능이 어떻게 혜택을 얻을 수 있는지에 대한 예는 최근에 나를 괴롭힌 한 클래스를 고려하는 것 java.util.UUID입니다. 실제로 UUID 유형 1, 2 또는 5를 지원하지 않으므로 쉽게 수정할 수 없습니다. 또한 재정의 할 수없는 사전 정의 된 임의 생성기가 붙어 있습니다. 지원되지 않는 UUID 유형의 코드를 구현하려면 인터페이스가 아닌 타사 API에 직접 의존하거나 변환 코드를 유지 관리하고 추가 가비지 수집 비용을 지불해야합니다. 정적 메서드를 사용하면 UUID대신 인터페이스로 정의되어 누락 된 부분을 실제 타사에서 구현할 수 있습니다. ( UUID원래 인터페이스로 정의 된 경우 , 어쩌면 약간의 혼란이있을 것입니다.UuidUtil 인터페이스에 기반을 두지 않으면 많은 Java의 핵심 API가 저하되지만 Java 8부터는 이러한 잘못된 동작에 대한 변명이 많이 줄어 들었습니다.

추상 클래스는 상태 (즉, 필드 선언)를 가질 수 있지만 인터페이스는 할 수 없기 때문에 인터페이스와 추상 클래스 사이에는 거의 차이가 없다고 말하는 것은 옳지 않습니다. 따라서 다중 상속 또는 믹스 인 스타일 상속과 동일하지 않습니다. 적절한 믹스 인 (예 : Groovy 2.3의 특성 )은 상태에 액세스 할 수 있습니다. (Groovy는 정적 확장 방법도 지원합니다.)

제 생각에는 Doval 의 모범 을 따르는 것도 좋지 않습니다 . 인터페이스는 계약을 정의해야하지만 계약을 시행하지는 않습니다. (어쨌든 Java에는 없습니다.) 구현의 적절한 검증은 테스트 스위트 또는 기타 도구의 책임입니다. 주석을 사용하여 계약을 정의 할 수 있으며 OVal 이 좋은 예이지만 인터페이스에 정의 된 제약 조건을 지원하는지 여부는 알 수 없습니다. 이러한 시스템은 현재 존재하지 않더라도 가능합니다. ( 애노테이션 프로세서javac통한 컴파일 타임 커스터마이징 전략 포함API 및 런타임 바이트 코드 생성) 이상적으로는 계약을 컴파일 타임에 시행하고 최악의 경우 테스트 스위트를 사용하여 계약을 시행하지만 런타임 시행이 어려워진다는 것을 이해합니다. Java에서 계약 프로그래밍을 지원할 수있는 또 다른 흥미로운 도구는 Checker Framework 입니다.


1
추가 (즉, 내 마지막 단락에 대한 후속 조치를 위해 인터페이스의 계약을 적용하지 않는다 ), 그것은 지적 가치 default방법을 재정의 할 수 없습니다 equals, hashCode하고 toString. 이 허용되지 않는 이유의 매우 유익한 비용 / 편익 분석은 여기에서 찾을 수 있습니다 : mail.openjdk.java.net/pipermail/lambda-dev/2013-March/...을
ngreen

콜렉션이 테스트해야 할 두 가지 다른 유형의 동등성 이 있고 Java가 단일 가상 equals및 단일 hashCode메소드 만 갖는 것은 너무 나쁘며 여러 인터페이스를 구현하는 항목은 계약 요구 사항이 상충 할 수 있습니다. hashMap키로 변경되지 않는 목록을 사용할 수 있으면 도움이되지만 hashMap현재 상태가 아닌 동등성을 기반으로 일치하는 항목에 컬렉션을 저장하는 것이 도움이되는 경우도 있습니다 [동등성은 상태와 불변성을 의미합니다 ] .
supercat

Java는 일종의 Comparator 및 Comparable 인터페이스에 대한 해결 방법이 있습니다. 하지만 그런 것들은 추악합니다.
ngreen

이러한 인터페이스는 특정 컬렉션 유형에 대해서만 지원되며 고유 한 문제가 있습니다. 비교기 자체가 잠재적으로 상태를 캡슐화 할 수 있습니다 (예 : 특수화 된 문자열 비교기는 각 문자열의 시작 부분에서 구성 가능한 문자 수를 무시할 수 있음) 무시할 문자는 비교기 상태의 일부가되며,이 문자는 정렬 된 컬렉션의 상태의 일부가되지만 두 비교기가 동등한 지 묻는 메커니즘은 정의되어 있지 않습니다.
supercat

그래, 나는 비교기의 고통을 얻는다. 나는 단순해야하지만 비교기를 올바르게 얻는 것이 어렵 기 때문에 나무 구조를 연구하고 있습니다. 아마도 문제를 해결하기 위해 사용자 정의 트리 클래스를 작성하려고합니다.
ngreen

44

하나의 클래스 만 상속 할 수 있기 때문입니다. 구현이 추상적 인 기본 클래스를 필요로하기에 충분히 복잡한 두 개의 인터페이스가있는 경우이 두 인터페이스는 실제로 상호 배타적입니다.

대안은 이러한 추상 기본 클래스를 정적 ​​메소드 콜렉션으로 변환하고 모든 필드를 인수로 바꾸는 것입니다. 인터페이스의 모든 구현자가 정적 메소드를 호출하고 기능을 얻을 수는 있지만 이미 너무 장황한 언어로 많은 보일러 플레이트입니다.


인터페이스에서 구현을 제공 할 수있는 이유에 대한 동기 부여 예제로서이 스택 인터페이스를 고려하십시오.

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

누군가가 인터페이스를 구현할 때 pop스택이 비어있는 경우 예외를 발생시킬 것이라고 보장 할 방법이 없습니다 . 우리는이 규칙 pop을 두 가지 방법 public final, 즉 계약을 시행하는 protected abstract방법 과 실제 팝핑을 수행 하는 방법 으로 분리 하여 시행 할 수 있습니다.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

모든 구현이 계약을 준수하도록 보장 할뿐만 아니라 스택이 비어 있는지 확인하고 예외를 발생시키지 않도록했습니다. 인터페이스를 추상 클래스로 변경해야한다는 사실을 제외하고는 큰 승리입니다! 단일 상속 언어에서는 유연성이 크게 상실됩니다. 인터페이스가 상호 배타적입니다. 인터페이스 메소드에만 의존하는 구현을 제공 할 수 있으면 문제가 해결됩니다.

인터페이스에 메소드를 추가하는 Java 8의 접근 방식이 최종 메소드 또는 보호 된 추상 메소드를 추가 할 수 있는지 확실하지 않지만 D 언어가 이를 허용하고 Design by Contract 를 기본적으로 지원한다는 것을 알고 있습니다 . 이 기술 pop에는 최종적인 것이기 때문에이 기술에는 위험이 없으므로 구현 클래스는이를 무시할 수 없습니다.

재정의 가능한 메소드의 기본 구현과 관련하여 Java API에 추가 된 기본 구현은 추가 된 인터페이스의 계약에만 의존한다고 가정하므로 인터페이스를 올바르게 구현하는 모든 클래스는 기본 구현에서도 올바르게 작동합니다.

그 위에,

인터페이스와 추상 클래스 (복수 상속 제외) 사이에는 거의 차이가 없습니다. 실제로 인터페이스를 사용하여 일반 클래스를 에뮬레이션 할 수 있습니다.

인터페이스에서 필드를 선언 할 수 없기 때문에 이것은 사실이 아닙니다. 인터페이스에서 작성하는 메소드는 구현 세부 사항에 의존 할 수 없습니다.


인터페이스의 정적 메소드를 선호하는 예로서 Java API의 콜렉션 과 같은 유틸리티 클래스를 고려 하십시오. 해당 클래스는 해당 정적 메소드를 해당 인터페이스에서 선언 할 수 없기 때문에 존재합니다. 인터페이스 Collections.unmodifiableList에서도 선언되었을 수 있었고 List찾기가 더 쉬웠을 것입니다.


4
반대 주장 : 정적 메소드는 올바르게 작성되면 독립적 인 클래스이므로 클래스 이름으로 수집하고 분류 할 수있는 별도의 정적 클래스에서 더 의미가 있으며 본질적으로 편리합니다. 객체에 정적 상태를 유지하거나 부작용을 일으키는 것과 같은 악용을 유발하여 정적 메소드를 테스트 할 수 없습니다.
Robert Harvey

3
정적 메소드가 클래스에있는 경우 똑같이 바보 같은 일을하지 못하게하는 것은 무엇입니까? 또한 인터페이스의 메소드는 상태를 전혀 요구하지 않을 수 있습니다. 단순히 계약을 시행하려고 할 수도 있습니다. Stack인터페이스 pop가 있고 빈 스택으로 호출 될 때 예외가 발생 하는지 확인하려고 한다고 가정하십시오 . 주어진 추상적 인 방법 boolean isEmpty()protected T pop_impl()구현할 수 있습니다. final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }이것은 모든 구현 자에 대한 계약을 시행합니다.
Doval

무엇을 기다립니다? 스택에 푸시 팝 방법은 없습니다 될 것이다 static.
Robert Harvey

@RobertHarvey 주석의 문자 제한이 아니라면 더 명확했지만 정적 메소드가 아닌 인터페이스의 기본 구현 사례를 작성했습니다.
Doval

8
기본 인터페이스 메소드는 표준 코드를 기반으로 기존 코드를 조정할 필요없이 표준 라이브러리를 확장 할 수 있도록 도입 된 해킹이라고 생각합니다.
Giorgio

2

아마도 의도는 의존성을 통해 정적 정보 나 기능을 주입 할 필요성을 대체함으로써 믹스 인 클래스 를 생성하는 능력을 제공하는 것이 었습니다 .

이 아이디어는 C #에서 확장 메서드를 사용하여 구현 된 기능을 인터페이스에 추가하는 방법과 관련이있는 것 같습니다.


1
확장 메소드는 인터페이스에 기능을 추가하지 않습니다. 확장 메소드는 편리한 list.sort(ordering);형식을 사용하여 클래스에서 정적 메소드를 호출하기위한 구문 설탕입니다 .
Robert Harvey

IEnumerableC # 에서 인터페이스 를 살펴보면 해당 인터페이스에 확장 메소드를 구현하는 방식 (예 : LINQ to Objects수행)이 구현하는 모든 클래스에 기능을 추가 하는 방법을 알 수 있습니다 IEnumerable. 그것이 기능을 추가한다는 의미입니다.
rae1

2
이것이 확장 방법에 대한 좋은 점입니다. 그것들은 당신이 클래스 나 인터페이스에 기능성을 부여한다는 착각 을줍니다. 실제 메소드를 클래스에 추가하는 것과 혼동하지 마십시오. 클래스 메소드는 객체의 개인 멤버에 액세스 할 수 있지만 확장 메소드는 그렇지 않습니다 (정적 메소드를 호출하는 또 다른 방법이기 때문에).
Robert Harvey

2
정확히, 그래서 Java 인터페이스에서 정적 또는 기본 메소드를 갖는 것과 관련이있는 이유가 있습니다. 구현은 클래스 자체가 아닌 인터페이스에서 사용할 수있는 것을 기반으로합니다.
rae1

1

default메소드 에서 볼 수있는 두 가지 주요 목적 (일부 사용 사례는 두 가지 목적을 모두 제공함) :

  1. 구문 설탕. 유틸리티 클래스가 그 목적에 부합 할 수 있지만 인스턴스 메소드가 더 좋습니다.
  2. 기존 인터페이스의 확장 구현은 일반적이지만 비효율적입니다.

그것이 두 번째 목적에 관한 것이라면, 당신은 같은 새로운 인터페이스에서 그것을 보지 못할 것 Predicate입니다. 모든 @FunctionalInterface주석이 달린 인터페이스에는 람다가 구현할 수 있도록 정확히 하나의 추상 메소드가 있어야합니다. 추가 default와 같은 방법은 and, or, negate단지 유틸리티, 그리고 당신이 그들을 무시 안된다. 그러나 때로는 정적 메소드가 더 좋습니다 .

기존 인터페이스의 확장에 관해서는, 심지어 거기에도 불구하고 일부 새로운 방법은 단지 구문 설탕입니다. 방법 Collectionstream, forEach, removeIf- 기본적으로, 당신이 오버라이드 (override) 할 필요가 없습니다 단지 유틸리티입니다. 그리고 다음과 같은 방법이 spliterator있습니다. 기본 구현은 차선책이지만 적어도 코드는 컴파일됩니다. 인터페이스가 이미 게시되어 널리 사용되는 경우에만이 방법을 사용하십시오.


static메소드에 관해서는, 다른 것들도 꽤 잘 다루고 있다고 생각합니다. 인터페이스가 자체 유틸리티 클래스가 될 수 있습니다. 아마도 우리는 Collections자바의 미래를 제거 할 수 있을까요? Set.empty()흔들 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.