Javaの多重継承の問題を解決する方法を完全に理解するために、私は明確にしておく必要がある典型的な質問をしています。
Animal
にはサブクラスBird
とHorse
があり、Pegasus
とBird
から派生するクラスHorse
を作成する必要があります。Pegasus
は鳥と馬の両方です。
これは古典的なダイヤモンドの問題だと思います。私がこれを解決する古典的な方法を理解することができることからAnimal
、Bird
およびHorse
クラスをインターフェースし、それらからPegasus
を実装することです。
私はまだ鳥や馬のためにオブジェクトを作成することができるという問題を解決するための別の方法があるのだろうかと思いました。動物を作り出すことができる方法があったならば、それはまた素晴らしいであろうが必要ではないでしょう。
馬にはpublic interface Equidae
、鳥にはpublic interface Avialae
のような動物のクラス(生物学的に意味のあるクラス)用のインターフェースを作成することができます(私は生物学者ではないので、用語は間違っているかもしれません)。
それでもあなたはまだ作成することができます
public class Bird implements Avialae {
}
そして
public class Horse implements Equidae {}
そしてまた
public class Pegasus implements Avialae, Equidae {}
重複コードを減らすために、実装したい動物の共通コードのほとんどを含む抽象クラスを作成することができます。
public abstract class AbstractHorse implements Equidae {}
public class Horse extends AbstractHorse {}
public class Pegasus extends AbstractHorse implements Avialae {}
もう1つ詳細を追加したいのですが。 ブライアンの発言 、これはOPがすでに知っていたことです。
しかし、私は強調したいのですが、私はインターフェースに関する「多重継承」問題を回避することをお勧めします。すでに具体的な型(Birdなど)を表すインターフェースを使用することはお勧めしません。アヒルタイピング、それも良いですが、私はただ意味する:鳥の生物学的クラス、Avialae)。また、IBird
のように、大文字の「I」で始まるインターフェース名を使用することもお勧めしません。これは、インターフェースが必要な理由について何も述べていないためです。質問との違いは、インターフェースを使用して継承階層を構築し、有用な場合は抽象クラスを使用し、必要に応じて具象クラスを実装し、必要に応じて委任を使用することです。
オブジェクトを組み合わせるには、2つの基本的な方法があります。
これが機能する方法はあなたがAnimalオブジェクトを持っているということです。そのオブジェクト内に、必要なプロパティや動作を提供するオブジェクトをさらに追加します。
例えば:
今IFlier
はちょうどこのようになります:
interface IFlier {
Flier getFlier();
}
Bird
は次のようになります。
class Bird extends Animal implements IFlier {
Flier flier = new Flier();
public Flier getFlier() { return flier; }
}
これで、継承のすべての利点があります。あなたはコードを再利用することができます。あなたはIFliersのコレクションを持つことができ、多型などの他のすべての利点を使うことができます。
しかし、あなたはコンポジションからすべての柔軟性も持っています。各タイプのAnimal
には、各ビットの設定方法を自由に制御しながら、さまざまなインターフェイスや複合バッキングクラスを適用できます。
作文への戦略パターン代替アプローチ
何をどのようにして行っているかに応じて、別のアプローチとしてAnimal
基本クラスにさまざまな動作のリストを保持するための内部コレクションを含めることができます。その場合、あなたは戦略パターンにより近いものを使うことになります。これはコードを単純化するという点では利点があります(例えばHorse
はQuadruped
やHerbivore
について何も知る必要はありません)が、インターフェースのアプローチもしなければ多態性などの多くの利点を失います。
私は愚かな考えがあります。
public class Pegasus {
private Horse horseFeatures;
private Bird birdFeatures;
public Pegasus(Horse horse, Bird bird) {
this.horseFeatures = horse;
this.birdFeatures = bird;
}
public void jump() {
horseFeatures.jump();
}
public void fly() {
birdFeatures.fly();
}
}
Duck-typing の概念を提案できますか。
ほとんどの場合、PegasusでBirdとHorseのインターフェースを拡張する傾向がありますが、アヒルの型付けは実際には動作を継承することをお勧めします。コメントで既に述べたように、ペガサスは鳥ではありませんが飛ぶことができます。それであなたのペガサスはむしろFlyable
インターフェースを継承するべきで、Gallopable
インターフェースと言うことができます。
この種の概念は Strategy Pattern で利用されています。与えられた例は、実際にアヒルがどのようにFlyBehaviour
とQuackBehaviour
を継承しているかを示しています、そしてまだアヒルがあることができます、例えば。飛ぶことができないRubberDuck
。 Duck
をBird
クラスに拡張することもできましたが、すべてのDuck
は、たとえ貧弱なRubberDuck
であっても飛ぶことができるため、ある程度の柔軟性を放棄することになります。
技術的に言えば、一度に1つのクラスを拡張して複数のインターフェースを実装することしかできませんが、ソフトウェア工学に手を貸すときは、一般的には答えられない問題解決策を提案します。ちなみに、具象クラスを拡張するのは良いOO慣習ではなく、不必要な継承の振る舞いを防ぐために抽象クラスのみを拡張することは - - 「動物」のようなものや動物目的の使用はなく、具体的な動物だけがあります。
ウマがハーフドアを乗り越えることはできないので、ハーフドアで馬を安定した状態に保つことは安全です。そこで私は、どんなタイプの馬のアイテムも受け入れて、それを半ドアのある厩舎に入れる馬の住居サービスを立ち上げました。
それで、馬でさえ飛ぶことができる動物のような馬ですか?
私は多重継承について多くのことを考えていましたが、15年以上プログラミングを続けてきたので、多重継承を実装することを気にする必要はなくなりました。
多くの場合、多重継承を指す設計に対処しようとしたとき、問題領域を理解していなかったことを後になってリリースしました。
OR
Javaには多重継承がないため、多重継承の問題はありません。これは、本当の多重継承問題(ダイアモンド問題)を解決するためのものです。
問題を軽減するためのさまざまな戦略があります。最もすぐに達成可能なものは、Pavelが提案しているCompositeオブジェクトです(本質的にはC++がそれを処理する方法)。 C3線形化(またはそれに類似した方法)による多重継承が、Javaの将来に対応するものかどうかはわかりませんが、私はそれを疑います。
あなたの質問が学問的であるならば、正しい解決策は鳥と馬がより具体的であるということです、そしてそれはペガサスが単に鳥と馬を結合したと仮定するのは間違っています。ペガサスは鳥や馬と共通の特定の固有の性質を持っている(つまり、共通の祖先を持っている可能性がある)と言った方が正しいでしょう。 Moritzの答えが指摘するように、これは十分にモデル化することができます。
それはあなたのニーズ、そしてあなたの動物クラスがあなたのコードの中でどのように使われることになっているかに大きく依存すると思います。
Pegasusクラス内でHorseとBirdの実装のメソッドと機能を利用できるようにする場合は、PegasusをBirdとHorseの 構成 として実装できます。
public class Animals {
public interface Animal{
public int getNumberOfLegs();
public boolean canFly();
public boolean canBeRidden();
}
public interface Bird extends Animal{
public void doSomeBirdThing();
}
public interface Horse extends Animal{
public void doSomeHorseThing();
}
public interface Pegasus extends Bird,Horse{
}
public abstract class AnimalImpl implements Animal{
private final int numberOfLegs;
public AnimalImpl(int numberOfLegs) {
super();
this.numberOfLegs = numberOfLegs;
}
@Override
public int getNumberOfLegs() {
return numberOfLegs;
}
}
public class BirdImpl extends AnimalImpl implements Bird{
public BirdImpl() {
super(2);
}
@Override
public boolean canFly() {
return true;
}
@Override
public boolean canBeRidden() {
return false;
}
@Override
public void doSomeBirdThing() {
System.out.println("doing some bird thing...");
}
}
public class HorseImpl extends AnimalImpl implements Horse{
public HorseImpl() {
super(4);
}
@Override
public boolean canFly() {
return false;
}
@Override
public boolean canBeRidden() {
return true;
}
@Override
public void doSomeHorseThing() {
System.out.println("doing some horse thing...");
}
}
public class PegasusImpl implements Pegasus{
private final Horse horse = new HorseImpl();
private final Bird bird = new BirdImpl();
@Override
public void doSomeBirdThing() {
bird.doSomeBirdThing();
}
@Override
public int getNumberOfLegs() {
return horse.getNumberOfLegs();
}
@Override
public void doSomeHorseThing() {
horse.doSomeHorseThing();
}
@Override
public boolean canFly() {
return true;
}
@Override
public boolean canBeRidden() {
return true;
}
}
}
他の可能性はあなたの動物を定義するために継承の代わりに Entity-Component-System アプローチを使うことです。もちろんこれは、あなたが動物の個々のJavaクラスを持つことはないということを意味します、その代わりにそれらはそれらのコンポーネントによって定義されるだけです。
Entity-Component-Systemアプローチの擬似コードは、次のようになります。
public void createHorse(Entity entity){
entity.setComponent(NUMER_OF_LEGS, 4);
entity.setComponent(CAN_FLY, false);
entity.setComponent(CAN_BE_RIDDEN, true);
entity.setComponent(SOME_HORSE_FUNCTIONALITY, new HorseFunction());
}
public void createBird(Entity entity){
entity.setComponent(NUMER_OF_LEGS, 2);
entity.setComponent(CAN_FLY, true);
entity.setComponent(CAN_BE_RIDDEN, false);
entity.setComponent(SOME_BIRD_FUNCTIONALITY, new BirdFunction());
}
public void createPegasus(Entity entity){
createHorse(entity);
createBird(entity);
entity.setComponent(CAN_BE_RIDDEN, true);
}
ええと、あなたのクラスは他の1つだけのサブクラスになることができますが、それでも、あなたは望むだけの数のインターフェースを実装することができます。
ペガサスは実際には馬です(それは馬の特別な場合です)、それは飛ぶことができます(これはこの特別な馬の「スキル」です)。一方、ペガサスは歩くことができ、4足である鳥だと言えるでしょう。それはすべて、コードを書くのがいかに簡単かにかかっています。
あなたの場合のようにあなたは言うことができます:
abstract class Animal {
private Integer hp = 0;
public void eat() {
hp++;
}
}
interface AirCompatible {
public void fly();
}
class Bird extends Animal implements AirCompatible {
@Override
public void fly() {
//Do something useful
}
}
class Horse extends Animal {
@Override
public void eat() {
hp+=2;
}
}
class Pegasus extends Horse implements AirCompatible {
//now every time when your Pegasus eats, will receive +2 hp
@Override
public void fly() {
//Do something useful
}
}
あなたはインタフェース階層を持ち、それから選択したインタフェースからクラスを拡張することができます。
public interface IAnimal {
}
public interface IBird implements IAnimal {
}
public interface IHorse implements IAnimal {
}
public interface IPegasus implements IBird,IHorse{
}
次に、特定のインタフェースを拡張して、必要に応じてクラスを定義します。
public class Bird implements IBird {
}
public class Horse implements IHorse{
}
public class Pegasus implements IPegasus {
}
以下の例を見て理解を深めてください。
インタフェースは多重継承をシミュレートしません。 Javaの作成者は多重継承が間違っていると考えたため、Javaにはそのようなことはありません。
2つのクラスの機能を1つにまとめる場合は、オブジェクトコンポジションを使用します。すなわち.
public class Main {
private Component1 component1 = new Component1();
private Component2 component2 = new Component2();
}
また、特定のメソッドを公開したい場合は、それらを定義して、対応するコントローラに呼び出しを委任させます。
Component1
がInterface1
を実装し、Component2
がInterface2
を実装するなら、あなたは以下のように定義することができます
class Main implements Interface1, Interface2
コンテキストが許す限り、オブジェクトを互換的に使用することができます。
だから私の視点では、あなたはダイヤモンドの問題に入ることはできません。
すでにご存知のとおり、Javaでクラスを多重継承することはできませんが、インターフェースでは可能です。また、構図デザインパターンの使用を検討することもできます。
私は数年前に非常に包括的な作文に関する記事を書きました...
Javaで複数の継承の問題を解決するために→インターフェースが使用されます
日 - 27
- インタフェースは基本的にユーザー定義のデータ型を開発するために使用されます。
- インターフェースに関しては、多重継承の概念を達成することができます。
- インターフェイスを使用すると、ポリモーフィズム、動的バインディングの概念を実現できるため、メモリスペースと実行時間の点でJavaプログラムのパフォーマンスを向上させることができます。
インターフェースは純粋に未定義のメソッドの集まりを含む構成体、あるいはインターフェースは純粋に抽象的メソッドの集まりです。
[...]
28日目
class-1のインタフェースの機能を再利用するための構文-1:
[abstract] class <clsname> implements <intf 1>,<intf 2>.........<intf n> { variable declaration; method definition or declaration; };
上記の構文で、clsnameは 'n'個のインターフェースから機能を継承しているクラスの名前を表します。 「実装」は、派生クラスへのインタフェースの機能を継承するために使用されるキーワードです。
[...]
構文2では、n個のインタフェースを別のインタフェースに継承しています。
interface <intf 0 name> extends <intf 1>,<intf 2>.........<intf n> { variable declaration cum initialization; method declaration; };
[...]
構文3:
[abstract] class <derived class name> extends <base class name> implements <intf 1>,<intf 2>.........<intf n> { variable declaration; method definition or declaration; };
複雑さを軽減し、言語を単純化するために、多重継承はJavaではサポートされていません。
A、B、Cが3つのクラスであるシナリオを考えます。 CクラスはAクラスとBクラスを継承します。 AクラスとBクラスのメソッドが同じで、子クラスオブジェクトから呼び出すと、AクラスまたはBクラスのメソッドを呼び出すことがあいまいになります。
コンパイル時エラーは実行時エラーよりも優れているため、2つのクラスを継承した場合、Javaはコンパイル時エラーをレンダリングします。同じ方法でも違う方法でも、コンパイル時エラーが発生します。
class A {
void msg() {
System.out.println("From A");
}
}
class B {
void msg() {
System.out.println("From B");
}
}
class C extends A,B { // suppose if this was possible
public static void main(String[] args) {
C obj = new C();
obj.msg(); // which msg() method would be invoked?
}
}