Почему Java не разрешает множественное наследование, но позволяет соответствовать нескольким интерфейсам с реализацией по умолчанию

Я не спрашиваю об этом → Почему в Java нет множественного наследования, но допускается реализация нескольких интерфейсов?

В Java множественное наследование недопустимо, но после Java 8 интерфейсы могут иметь методы по умолчанию (могут сами реализовывать методы), так же как абстрактные классы. В этом контексте также следует допускать множественное наследование.

interface TestInterface 
{ 
    // abstract method 
    public void square(int a); 

    // default method 
    default void show() 
    { 
      System.out.println("Default Method Executed"); 
    } 
} 

Ответ 1

Все не так просто.
Если класс реализует несколько интерфейсов, которые определяют методы по умолчанию с одной и той же сигнатурой, компилятор заставит вас переопределить этот метод для класса.

Например, с этими двумя интерфейсами:

public interface Foo {
    default void doThat() {
        // ...
    }
}

public interface Bar {    
    default void doThat() {
        // ...
    }       
}

Это не скомпилирует:

public class FooBar implements Foo, Bar{
}

Вы должны определить/переопределить метод, чтобы устранить неоднозначность.
Например, вы можете делегировать реализацию Bar например:

public class FooBar implements Foo, Bar{    
    @Override
    public void doThat() {
        Bar.super.doThat();
    }    
}

или делегировать реализацию Foo такую как:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        Foo.super.doThat();
    }
}

или еще определим другое поведение:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        // ... 
    }
}

Это ограничение показывает, что Java не допускает множественного наследования даже для методов интерфейса по умолчанию.


Я думаю, что мы не можем применять одну и ту же логику для множественного наследования, потому что могут возникнуть множественные проблемы, основными из которых являются:

  • переопределение/удаление неоднозначности для метода в обоих унаследованных классах может привести к побочным эффектам и изменить общее поведение унаследованных классов, если они будут полагаться на этот метод внутренне. В случае интерфейсов по умолчанию этот риск также присутствует, но он должен быть гораздо менее редким, поскольку методы по умолчанию не предназначены для введения сложных обработок, таких как множественные вложенные вызовы внутри класса, или для полноты состояния (в действительности интерфейсы не могут размещать поле экземпляра).
  • как наследовать несколько полей? И даже если бы язык позволил это, у вас возникла бы точно такая же проблема, как и в приведенном ранее: побочный эффект в поведении унаследованного класса: поле int foo определенное в классах A и B которое вы хотите поместить в подкласс, не имеет тот же смысл и намерение.

Ответ 2

Разработчики языка уже думали об этом, поэтому эти вещи исполняются компилятором. Поэтому, если вы определяете:

interface First {
    default void go() {
    }
}

interface Second {
    default void go() {
    }
}

И вы реализуете класс для обоих интерфейсов:

static class Impl implements First, Second {

}

вы получите ошибку компиляции; и вам нужно будет переопределить go чтобы не создавать двусмысленность вокруг него.

Но вы могли подумать, что вы можете обмануть компилятор здесь, выполнив:

interface First {
    public default void go() {
    }
}

static abstract class Second {
    abstract void go();
}

static class Impl extends Second implements First {
}

Вы могли бы подумать, что First::go уже обеспечивает реализацию для Second::go и все должно быть хорошо. Это слишком позаботится, поэтому это тоже не скомпилируется.

JLS 9.4.1.3: Аналогично, когда абстрактный метод и метод по умолчанию с соответствующими сигнатурами наследуются, мы создаем ошибку. В этом случае можно было бы отдать приоритет одному или другому - возможно, мы предположили бы, что метод по умолчанию обеспечивает разумную реализацию абстрактного метода. Но это рискованно, потому что, помимо имени совпадения и подписи, у нас нет оснований полагать, что метод по умолчанию ведет себя последовательно с контрактом абстрактного метода - метод по умолчанию, возможно, даже не существовал, когда исходный интерфейс был первоначально разработан. В этой ситуации безопаснее просить пользователя активно утверждать, что реализация по умолчанию является подходящей (через переопределяющую декларацию).

Последнее, что я хотел бы внести, чтобы укрепить, что множественное наследование не допускается даже с новыми дополнениями в java, заключается в том, что статические методы из интерфейсов не наследуются. По умолчанию статические методы унаследованы:

static class Bug {
    static void printIt() {
        System.out.println("Bug...");
    }
}

static class Spectre extends Bug {
    static void test() {
        printIt(); // this will work just fine
    }
}

Но если мы изменим это для интерфейса (и вы можете реализовать несколько интерфейсов, в отличие от классов):

interface Bug {
    static void printIt() {
        System.out.println("Bug...");
    }
}

static class Spectre implements Bug {
    static void test() {
        printIt(); // this will not compile
    }
}

Теперь это также запрещено компилятором и JLS:

JLS 8.4.8: класс не наследует статические методы от своих суперинтерфейсов.

Ответ 3

Java не допускает множественного наследования для полей. Это будет трудно поддерживать в JVM, поскольку вы можете иметь только ссылки на начало объекта, где заголовок, а не произвольные ячейки памяти.

В Oracle/Openjdk объекты имеют заголовок, за которым следуют поля самого суперкласса, затем следующий суперкласс и т.д. Было бы существенным изменением, чтобы поля класса отображались в разных смещениях относительно заголовка объекта для разных подклассов. Скорее всего, ссылки на объекты должны стать ссылкой на заголовок объекта и ссылкой на поля для поддержки этого.

Ответ 4

Это в основном связано с "проблемой алмазов", я думаю. Прямо сейчас, если вы реализуете несколько интерфейсов с одним и тем же методом, компилятор заставляет вас переопределить метод тот, который вы хотите реализовать, потому что он не знает, что использовать. Я думаю, разработчики Java хотели удалить эту проблему, когда интерфейсы не могли использовать методы по умолчанию. Теперь им пришла идея, что хорошо иметь методы с реализацией в интерфейсах, поскольку вы все равно можете использовать их как функциональные интерфейсы в выражениях stream/lambda и использовать свои методы по умолчанию при обработке. Вы не можете делать это с помощью классов, но проблема с алмазами все еще существует. Это моя догадка :)

Ответ 5

Основные проблемы с множественным наследованием - упорядочение (для переопределения и вызовов супер), полей и конструкторов; интерфейсы не имеют полей или конструкторов, поэтому они не создают проблем.

Если вы посмотрите на другие языки, они обычно попадают в две широкие категории:

  1. Языки с множественным наследованием плюс несколько функций для устранения неоднозначности особых случаев: виртуальное наследование [C++], прямые вызовы для всех суперконструкторов в самом производном классе [C++], линеаризация суперклассов [Python], сложные правила для супер [ Python] и т.д.

  2. Языки с концепцией differente, обычно называемые интерфейсами, чертами, миксинами, модулями и т.д., Которые налагают некоторые ограничения, такие как: нет конструкторов [Java] или нет конструкторов с параметрами [Scala до недавнего времени], никаких изменяемых полей [Java], конкретных правила для переопределения (например, mixins имеют приоритет над базовыми классами [Ruby], поэтому вы можете включать их, когда вам нужна куча полезных методов) и т.д. Java стал таким языком.

Почему просто запрещая поля и конструкторы вы решаете многие проблемы, связанные с множественным наследованием?

  • Вы не можете дублировать поля в дублированных базовых классах.
    • Иерархия основного класса по-прежнему линейна.
  • Вы не можете построить свои базовые объекты неправильно.
    • Представьте, если у объекта были общедоступные/защищенные поля, и все подклассы имели конструкторы, устанавливающие эти поля. Когда вы наследуете более чем один класс (все из них производны от Object), на который нужно установить поля? Последний класс? Они становятся братьями и сестрами в иерархии, поэтому они ничего не знают друг о друге. Если у вас есть несколько экземпляров объекта, чтобы избежать этого? Все классы будут взаимодействовать правильно?
  • Помните, что поля в Java не являются виртуальными (переопределяемыми), это просто хранение данных.
    • Вы могли бы создать язык, на котором поля ведут себя как методы и могут быть переопределены (фактическое хранилище всегда было бы частным), но это было бы гораздо большим изменением и, вероятно, больше не будет называться Java.
  • Интерфейсы не могут быть созданы сами по себе.
    • Вы всегда должны сочетать их с конкретным классом. Это устраняет необходимость в конструкторах и делает программиста более четким (то есть, что означает конкретный класс и какой дополнительный интерфейс /mixin). Это также обеспечивает четко определенное место для решения всех двусмысленностей: конкретного класса.

Ответ 6

методы по default в интерфейсах создают проблему, которая:

Если оба реализованных интерфейса определяют метод по умолчанию с той же сигнатурой метода, то класс реализации не знает, какой метод по умолчанию использовать.

Класс реализации должен четко указывать, какой метод по умолчанию использовать или определять его собственный.

Таким образом, методы по default в Java-8 не облегчают множественное наследование. Основная мотивация методов по умолчанию заключается в том, что если в какой-то момент нам нужно добавить метод к существующему интерфейсу, мы можем добавить метод без изменения существующих классов реализации. Таким образом, интерфейс по-прежнему совместим со старыми версиями. Однако мы должны помнить о мотивации использования методов по умолчанию и должны поддерживать разделение интерфейса и реализации.