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

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

В эти дни я проясняю некоторые из самых основных концепций Java. Итак, я перехожу к теме Наследование и интерфейс Java.

Читая это, я обнаружил, что Java не поддерживает множественное наследование, а также понял, что я не могу понять, почему везде обсуждается проблема с фигурой бриллианта (по крайней мере, 4 класса для создания бриллианта), чтобы объяснить это поведение, не можем ли мы понять эту проблему, используя только 3 класса.

Скажем, у меня есть класс A и класс B, эти два класса разные (они не являются дочерними классами общего класса), но у них есть один общий метод, и они выглядят так:

class A {
    void add(int a, int b) {

    }
}

class B {
    void add(int a, int b) {

    }
}

Хорошо, теперь скажите, поддерживает ли Java множественное наследование и есть ли один класс, который является подклассом A и B, например: -

class C extends A,B{ //If this was possible
    @Override
    void add(int a, int b) { 
        // TODO Auto-generated method stub
        super.add(a, b); //Which version of this, from A or B ?
    }
 }

тогда компилятор не сможет найти, какой метод вызывать из A или B, и поэтому Java не поддерживает множественное наследование. Так есть ли что-то неправильное в этой концепции?

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

Дайте мне знать, не подходит ли этот пример для объяснения проблемы или его также можно использовать для понимания проблемы.

Редактировать: я получил здесь один близкий голос, заявив, что вопрос не ясен. Вот главный вопрос: -

Могу ли я понять, почему «Java не поддерживает множественное наследование» только с 3 классами, как описано выше, или мне нужно иметь 4 класса (структура Diamond), чтобы понять проблему.


person Roshan Jha    schedule 13.10.2014    source источник
comment
Пожалуйста, укажите место, где четыре класса используются в поддержку аргументов против наследования алмазов. На данный момент ваш вопрос основан на неподтвержденном утверждении.   -  person user207421    schedule 13.10.2014
comment
@EJP Алмазная фигура имеет четыре вершины, соответствующие четырем классам. Зачем нужна цитата, чтобы поддержать эту основную истину?   -  person Marko Topolnik    schedule 13.10.2014
comment
@MarkoTopolink Потому что он утверждает, что это указано «везде без единого цитирования». У тебя есть?   -  person user207421    schedule 13.10.2014
comment
@EJP Готов поспорить. Их очень легко найти.   -  person Marko Topolnik    schedule 13.10.2014
comment
@EJP Говоря везде, я имею в виду, где и когда я ищу причину, почему Java не поддерживает множественное наследование. Я привожу пример, основанный на четырех классах (Diamond). Пожалуйста, не улавливайте только это слово. Что ж, очень легко найти несколько руководств, в которых говорится об этом, но вот один только для справки: - javapapers.com/core-java/   -  person Roshan Jha    schedule 13.10.2014
comment
Я бы сделал этот вопрос независимым от языка, поскольку это не строго вопрос Java, а применимый к нескольким языкам.   -  person lornova    schedule 13.10.2014
comment
Я всегда задавался этим вопросом. Это кажется очень сложным примером, когда вашей версии 3 класса достаточно, чтобы продемонстрировать проблему.   -  person Brandon    schedule 13.10.2014
comment
Связано: stackoverflow.com/questions/22591499/   -  person David says Reinstate Monica    schedule 13.10.2014


Ответы (6)


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

Имея только три класса, проблема решается относительно легко путем введения простой конструкции, такой как super.A или super.B. И пока вы смотрите только на переопределенные методы, на самом деле не имеет значения, есть ли у вас общий предок или только три основных класса.

Однако, если A и B имеют общего предка, состояние которого они оба наследуют, то у вас серьезные проблемы. Вы храните две отдельные копии состояния этого общего предка? Это было бы больше похоже на композицию, чем на наследование. Или вы храните только тот, который используется как A, так и B, вызывая странные взаимодействия, когда они манипулируют своим унаследованным общим состоянием?

class A {
  protected int foo;
}

class B extends A {
  public B() {
    this.foo = 42;
  }
}

class C extends A {
  public C() {
    this.foo = 0xf00;
  }
}

class D extends B,C {
  public D() {
    System.out.println( "Foo is: "+foo ); //Now what?
  }
}

Обратите внимание, что приведенное выше не было бы такой большой проблемой, если бы класс A не существовал, а B и C объявляли свои собственные поля foo. По-прежнему будет возникать проблема с конфликтующими именами, но ее можно решить с помощью некоторой конструкции пространства имен (может быть, B.this.foo и C.this.foo, как мы делаем с внутренними классами?). С другой стороны, истинная проблема бриллианта — это больше, чем конфликт имен, это вопрос того, как поддерживать инварианты классов, когда два несвязанных суперкласса D (B и C) имеют одно и то же состояние, которое они оба наследуют от A. Вот почему все четыре класса необходимы, чтобы продемонстрировать всю полноту проблемы.

Совместное поведение при множественном наследовании не вызывает такой же проблемы. Настолько, что недавно представленные методы по умолчанию делают именно это. Это означает, что теперь разрешено и множественное наследование реализаций. Все еще есть некоторые сложности, связанные с разрешением того, какую реализацию вызывать, но, поскольку интерфейсы не имеют состояния, можно избежать самой большой проблемы.

person biziclop    schedule 13.10.2014
comment
Итак, Java был спроектирован так, что множественное наследование было невозможным? - person Svante; 13.10.2014
comment
@Svante Вы можете сказать это так, но вы, вероятно, посмотрите на это с другой стороны. Множественное наследование (состояния) не является целью. Это инструмент, который вы либо хотите использовать, либо нет. Они сказали, что от множественного наследования состояния очень мало пользы, но это вызовет много проблем. С другой стороны, множественное наследование типов — это хорошо, мы этого хотим. Таким образом, был достигнут компромисс, при котором вы получаете большинство преимуществ множественного наследования без всякого багажа, связанного с множественным наследованием состояния. - person biziclop; 13.10.2014
comment
Этот пример имеет больше смысла, поскольку он хорошо поддерживается и лучше управляется, однако у меня до сих пор нет ответа, дело в том, что Java не сможет решить, какой метод вызывать, если используется более одного и того же метода с одинаковой сигнатурой (переопределением) другого суперкласса присутствует одновременно, и это то, что я также делаю с тремя вышеуказанными классами, так что это правильный пример? или у меня должен быть один общий суперкласс, который является базовым классом для всех классов, используемых для демонстрации проблемы. - person Roshan Jha; 13.10.2014
comment
@RoshanJha Ваш сценарий с тремя классами является допустимым примером, демонстрирующим одну из проблем, возникающих при множественном наследовании. Но это не покрывает всех проблем, и это не бриллиант. - person biziclop; 13.10.2014
comment
Ваш пример и объяснение @kapep говорят о том, что ромбовидная фигура очень хорошо подходит для объяснения проблемы. Когда мы говорим о наследовании, классы должны каким-то образом относиться друг к другу (родитель-потомок), и ромбовидная фигура показывает это, в то время как треугольная фигура не вписывается в нее. очень хорошо (из-за двух разных родительских классов, которые никогда не взаимодействуют друг с другом). Но дело в том, что Java сталкивается с проблемой вызова правильной версии метода в таких случаях, и треугольник также показывает этот случай, поэтому мы также можем использовать пример треугольника. Это Некоторым людям (таким как я :) кажется, что это более простая и быстрая фигура для понимания. - person Roshan Jha; 14.10.2014
comment
Спасибо всем за ваши усилия и время. - person Roshan Jha; 14.10.2014
comment
Теперь самое простое: определите порядок вызова конструктора. Это просто обход графа. - person Svante; 14.10.2014
comment
@Svante Это непросто, потому что какой бы порядок вы ни выбрали, вы нарушили инварианты либо B, либо C. Или вы продублировали состояние A, чтобы сохранить инварианты, но нарушили принцип замещения Лискова, поскольку D находится не в отношении is a с A, а скорее в отношении is two. В любом случае, дело не в том, что проблема не может быть решена, конечно, может быть. Дело в том, чтобы подчеркнуть, почему эта проблема возникает только тогда, когда у вас есть 4 класса, а не когда у вас есть 3. - person biziclop; 14.10.2014
comment
@Roshan Jha: пример с тремя классами не убеждает, потому что сегодня с Java 8 мы можем создать такую ​​проблему, поскольку интерфейсы могут иметь методы реализации (также известные как default). Ответ прост: компилятор выдаст вам ошибку, если вы не переопределите неоднозначные унаследованные методы в классе реализации. Однако в случае унаследованного состояния невозможно переопределить и исправить проблему. - person Holger; 14.10.2014
comment
@biziclop: если инварианты B и C несовместимы, то наследование от обоих - просто ошибка. Дублировать состояние, определенное в A, не имеет смысла. Что действительно действует мне на нервы, так это останавливаться на чем-то вроде непреодолимой проблемы, но я понимаю, что вопрос на самом деле гораздо уже. - person Svante; 14.10.2014
comment
@Svante Нет никакого намека на то, что проблема непреодолима. Это было решено различными способами на многих других языках. Но повторюсь: вопрос был в том, зачем нам нужны 4 класса для задачи о алмазе? и этот пример является просто демонстрацией того, как создать проблему, которая возникает только с 4 классами, а не с 3. - person biziclop; 14.10.2014

Java не поддерживает множественное наследование, потому что разработчики языка разработали Java именно таким образом. Другие языки, такие как C++, прекрасно поддерживают множественное наследование, так что это не техническая проблема, а просто критерии проектирования.

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

C++ решает проблему класса в форме ромба с помощью виртуального наследования:

Виртуальное наследование — это метод, используемый в объектно-ориентированном программировании, где конкретный базовый класс в иерархии наследования объявляется для совместного использования своих экземпляров данных-членов с любыми другими включениями той же базы в дальнейших производных классах. Например, если класс A обычно (не виртуально) является производным от класса X (предполагается, что он содержит элементы данных), а класс B аналогичным образом, а класс C наследуется от обоих классов A и B, он будет содержать два набора элементов данных. связанный с классом X (доступный независимо, часто с подходящими квалификаторами устранения неоднозначности). Но если вместо этого класс A фактически является производным от класса X, то объекты класса C будут содержать только один набор элементов данных из класса X. Наиболее известным языком, реализующим эту функцию, является C++.

В отличие от Java, в C++ вы можете устранить неоднозначность, какой метод экземпляра вызывать, указав перед вызовом имя класса:

class X {
  public: virtual void f() { 

  } 
};

class Y : public X {
  public: virtual void f() { 

  } 
};

class Z : public Y {
  public: virtual void f() { 
    X::f();
  } 
};
person vz0    schedule 13.10.2014
comment
На самом деле Java поддерживает множественное наследование. По крайней мере вроде. До Java 8 можно было определять константы на интерфейсах. Начиная с Java 8 можно определять методы на интерфейсах (методы по умолчанию). В случае неоднозначности вы должны явно использовать имя интерфейса. - person dusky; 14.10.2014
comment
Я не знаю, что вы имели в виду, но реализация двух интерфейсов не является множественным наследованием. - person Arthur Rizzo; 14.10.2014
comment
@ArthurRizzo Это просто другой. Существует множественное наследование типа, поведения и состояния. Множественное наследование типов поддерживается Java через интерфейсы. Множественное наследование поведения поддерживается методами по умолчанию (что в основном делает их трейтами), а множественное наследование состояния не поддерживается и вряд ли будет в обозримом будущем. - person biziclop; 14.10.2014
comment
По крайней мере, это терминология, используемая в документации по Java, на что ссылается @dusky. - person biziclop; 14.10.2014

Это только одна трудность, которую вам нужно решить для множественного наследования в языке. Поскольку существуют языки с множественным наследованием (например, Common Lisp, C++, Eiffel), это, очевидно, не является непреодолимым.

Common Lisp определяет точную стратегию приоритезации (упорядочивания) графа наследования, поэтому в тех редких случаях, когда это имеет значение на практике, двусмысленности не возникает.

С++ использует виртуальное наследование (я еще не пытался понять, что это значит).

Eiffel позволяет указать, как именно вы хотите наследовать, возможно, переименовав методы в подклассе.

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

person Svante    schedule 13.10.2014

Проблема алмаза с четырьмя классами проще, чем проблема трех классов, описанная в вопросе.

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

Добавляя четвертый класс, который определяет add, становится ясно, что и A, и B реализуют один и тот же метод add.

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

person kapex    schedule 13.10.2014
comment
Я не могу понять, что вы подразумеваете под дополнительной проблемой конфликта имен. Однако я понял вашу мысль о том, что вместо использования двух разных классов, которые никак не взаимодействуют друг с другом, полезно создавать своего рода семейные классы. Но дело в том, что Java не сможет решить, какой метод вызывать, если одновременно присутствует более одного и того же метода с одинаковой сигнатурой (переопределением) другого суперкласса, поэтому является ли пример класса 3 допустимым примером или нет? - person Roshan Jha; 13.10.2014
comment
Конфликт заключается в том, что эти два метода не связаны, но по совпадению имеют одну и ту же сигнатуру, и существует несколько способов, которыми язык мог бы справиться с этим. Это легко решить с помощью спецификации, которая либо разрешает, либо запрещает существование в классе двух методов с одинаковой сигнатурой, но без общей базы. Но в java такой спецификации нет. Если мы предположим, что это разрешено (что вполне вероятно), то я думаю, что вместо ромба можно использовать пример 3-го класса. Тем не менее, ромб является более общим и не требует каких-либо предположений о том, как справиться с конфликтом. - person kapex; 13.10.2014

Множественное наследование не проблема, если вы найдете разумное решение для разрешения методов. Когда вы вызываете метод для объекта, разрешение метода — это действие по выбору версии метода класса, которую следует использовать. Для этого граф наследования линеаризуется. Затем выбирается первый класс в этой последовательности, обеспечивающий реализацию запрошенного метода.

Чтобы проиллюстрировать следующие примеры стратегий разрешения конфликтов, мы будем использовать этот алмазный график наследования:

    +-----+
    |  A  |
    |=====|
    |foo()|
    +-----+
       ^
       |
   +---+---+
   |       |
+-----+ +-----+
|  B  | |  C  |
|=====| |=====|
|foo()| |foo()|
+-----+ +-----+
   ^       ^
   |       |
   +---+---+
       |
    +-----+
    |  D  |
    |=====|
    +-----+
  • Наиболее гибкая стратегия требует от программиста явного выбора реализации при создании неоднозначного класса путем явного переопределения конфликтующего метода. Вариантом этого является запрет множественного наследования. Если программист хочет наследовать поведение от нескольких классов, придется использовать композицию и написать ряд прокси-методов. Однако наивно явно разрешенные конфликты наследования имеют те же недостатки, что и…

  • Поиск в глубину, который может привести к линеаризации D, B, A, C. Но таким образом A::foo() считается перед C::foo(), хотя C::foo() переопределяет A::foo()! Это не может быть тем, что мы хотели. Примером языка, использующего DFS, является Perl.

  • Используйте хитрый алгоритм, гарантирующий, что если X является подклассом Y, то при линеаризации он всегда будет стоять перед Y. Такой алгоритм не сможет разгадать все графы наследования, но в большинстве случаев он обеспечивает разумную семантику: если класс переопределяет метод, он всегда будет предпочтительнее переопределенного метода. Этот алгоритм существует и называется C3. Это создаст линеаризацию D, B, C, A. C3 был впервые представлен в 1996 году. К сожалению, Java был опубликован в 1995 году, поэтому C3 не был известен, когда Java изначально проектировался.

  • Используйте композицию, а не наследование — новый взгляд. Некоторые решения для множественного наследования предлагают избавиться от бита «наследования классов» и вместо этого предлагают другие единицы композиции. Одним из примеров являются mixins, которые «копируют и вставляют» определения методов в ваш класс. Это невероятно грубо.

    Идея примесей была переработана в черты (представлены в 2002 г., тоже слишком поздно для Java). ). Черты — это более общий случай как классов, так и интерфейсов. Когда вы «наследуете» трейт, определения встраиваются в ваш класс, так что это не усложняет разрешение методов. В отличие от примесей, трейты предоставляют более тонкие стратегии для разрешения конфликтов. Особенно важен порядок, в котором составляются признаки. Признаки играют заметную роль в объектной системе Perl «Moose» (называемой ролями) и в Scala.

Java предпочитает одиночное наследование по другой причине: если у каждого класса может быть только один суперкласс, нам не нужно применять сложные алгоритмы разрешения методов — цепочка наследования уже является порядком разрешения методов. Это делает методы немного более эффективными.

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

В большинстве схем разрешения методов множественного наследования порядок суперклассов имеет значение. То есть между class D extends B, C и class D extends C, B есть разница. Поскольку порядок можно использовать для простого устранения неоднозначности, пример с тремя классами недостаточно демонстрирует проблемы, связанные с множественным наследованием. Для этого вам понадобится полная задача о бриллиантах с четырьмя классами, так как она показывает, как наивный поиск в глубину приводит к неинтуитивному порядку разрешения методов.

person amon    schedule 14.10.2014
comment
Выбор правильных методов из набора применимых методов решается и в Common Lisp, который был стандартизирован в 1994 году (но черновики были доступны намного раньше). - person Svante; 17.10.2014
comment
@Svante Да, Common Lisp имеет потрясающую объектную систему, которая была и в некоторых отношениях остается «лидером» (некоторые функции, такие как протокол метаобъектов и комбинаторы методов, постепенно проникают в более распространенные языки). CL имеет очень интуитивную линеаризацию, и на нее ссылаются и сравнивают с ней в статье о линеаризации C3 («Монотонная линеаризация суперкласса для Дилана»). К сожалению, эта статья находится за платным доступом, поэтому я не уверен, какие конкретные проблемы с алгоритмом приоритета классов CL были исправлены C3. - person amon; 17.10.2014

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

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

person FrobberOfBits    schedule 13.10.2014
comment
Я рекомендую книгу «Шаблоны проектирования и контракты», где шаблоны проектирования GoF исследуются в Eiffel, что допускает множественное наследование. В ряде случаев авторы вполне естественно используют множественное наследование. Просто трудно думать о кубах, когда твой разум плоский. - person Svante; 13.10.2014
comment
Да, это то, что я хочу спросить здесь. Можем ли мы использовать этот пример, чтобы понять, почему Java не поддерживает множественное наследование или этот пример дает сбой где-то, чего я не вижу? - person Roshan Jha; 13.10.2014
comment
@RoshanJha Боюсь, что проблема с бриллиантами - это не решение. Это проблема, но для методов ее можно решить тривиально (принудительное устранение неоднозначности). Для состояния просто запретите наследование класса с состоянием дважды. С этими правилами больше проблем не вижу... - person maaartinus; 13.10.2014
comment
@RoshanJha Что ты имеешь в виду? Вы начали с треугольника, и у него есть своя проблема. Как показывает Марко Топольник, в ромбе дело обстоит немного хуже. Пока что все можно решить, применяя устранение неоднозначности. И нет большой разницы между треугольником и ромбом. Теперь давайте добавим состояние. Никаких новых проблем с треугольником, но ромбы приводят к большим головным болям. Должно ли двойное наследуемое состояние существовать один или два раза? Как методы находят его в объекте (до сих пор было фиксированное смещение, это больше не работает)? - person maaartinus; 13.10.2014