web-dev-qa-db-ja.com

クラスをそれ自体のリストのサブクラスとして定義することの欠点は何ですか?

私の最近のプロジェクトでは、次のヘッダーを持つクラスを定義しました:

public class Node extends ArrayList<Node> {
    ...
}

しかし、私のCS教授と話し合った後、彼はクラスが「記憶にとって恐ろしい」と「悪い習慣」の両方になるだろうと述べました。最初のものは特に真実であり、2番目のものは主観的であるとは思いませんでした。

この使用法の私の推論は、インスタンスの動作がカスタム実装または相互作用するいくつかの同様のオブジェクトの動作のいずれかによって定義できる、任意の深さを持つことができるものとして定義する必要があるオブジェクトについてのアイデアを持っていたということです。これにより、物理的な実装が相互作用する多くのサブコンポーネントで構成されるオブジェクトの抽象化が可能になります。¹

一方、私はこれがどのように悪い習慣になるのかを理解しています。何かをそれ自体のリストとして定義するという考えは単純ではなく、物理的に実装することもできません。

私がすべきではない私のコードでこれを使用する正当な理由はありますか?


thisこれについてさらに説明する必要がある場合は、喜んでお知らせします。私はこの質問を簡潔にしようとしています。

48
Addison Crump

率直に言って、継承の必要性はここではわかりません。意味がありません。 Nodeis anArrayList of Node

これが再帰的なデータ構造である場合は、次のように記述します。

public class Node {
    public String item;
    public List<Node> children;
}

どちらが何をする意味がありますか。 node has a子ノードまたは子孫ノードのリスト。

106
Robert Harvey

「メモリにとって恐ろしい」という議論は完全に間違っていますが、それは客観的に「悪い習慣」です。クラスから継承する場合、関心のあるフィールドとメソッドを単に継承するのではなく、代わりにeverythingを継承します。たとえそれがあなたにとって役に立たなくても、それが宣言するすべてのメソッド。そして最も重要なのは、クラスが提供するすべてのコントラクトと保証も継承することです。

頭字語SOLIDは、優れたオブジェクト指向設計のヒューリスティックを提供します。ここでは、[〜#〜] i [〜#〜]インターフェイス分離原理(ISP)および[〜#〜] l [〜 #〜]iskov Substitution Pricinple(LSP)は言いたいことがある。

ISPは、インターフェイスをできるだけ小さくするように指示しています。しかし、ArrayListから継承することで、非常に多くのメソッドを取得できます。特定のインデックスにある子ノードをget()remove()set()(置換)、またはadd()(挿入)することは意味がありますか?基礎となるリストのensureCapacity()が賢明ですか?ノードのsort()とはどういう意味ですか?クラスのユーザーは本当にsubList()を取得することになっていますか?不要なメソッドは非表示にできないため、唯一の解決策は、ArrayListをメンバー変数として持ち、実際に必要なすべてのメソッドを転送することです。

_private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }
_

ドキュメントに表示されているすべてのメソッドが本当に必要な場合は、LSPに進むことができます。 LSPは、親クラスが期待されるところならどこでもサブクラスを使用できなければならないことを教えてくれます。関数がArrayListをパラメーターとして取り、代わりにNodeを渡した場合、何も変更されません。

サブクラスの互換性は、型シグネチャのような単純なものから始まります。メソッドをオーバーライドする場合、親クラスで正当であった使用が除外される可能性があるため、パラメータータイプをより厳密にすることはできません。しかし、それはコンパイラがJavaでチェックするものです。

しかし、LSPはより深く実行されます。すべての親クラスとインターフェイスのドキュメントで約束されているすべてとの互換性を維持する必要があります。 その答え で、LynnはListインターフェイス(ArrayListを介して継承したもの)がequals()を保証するようなケースを1つ見つけました。 hashCode()メソッドが機能するはずです。 hashCode()の場合、正確に実装する必要がある特定のアルゴリズムさえ与えられます。あなたがこれを書いたとしましょうNode

_public class Node extends ArrayList<Node> {
  public final int value;

  public Node(int value, Node... children) {
    this.value = Value;
    for (Node child : children)
      add(child);
  }

  ...

}
_

これには、valuehashCode()に貢献できず、equals()に影響を与えられないことが必要です。 Listインターフェース-継承することで尊重することを約束します-は、new Node(0).equals(new Node(123))がtrueである必要があります。


クラスから継承すると、親クラスが行った約束を誤って破るのが非常に簡単になり、通常は意図したよりも多くのメソッドが公開されるため、一般に継承よりも構成を優先することをお勧めします。何かを継承する必要がある場合は、インターフェイスのみを継承することをお勧めします。特定のクラスの動作を再利用する場合は、インスタンス変数内の別のオブジェクトとしてそれを保持できます。これにより、すべての約束と要件が強制されなくなります。

時々、私たちの自然言語は継承関係を示唆します:車は乗り物です。オートバイは乗り物です。 Carクラスから継承するクラスMotorcycleおよびVehicleを定義する必要がありますか?オブジェクト指向設計は、実際の世界をコードに正確に反映することではありません。現実世界の豊富な分類法をソースコードに簡単にエンコードすることはできません。

そのような例の1つは、従業員とボスのモデリング問題です。複数のPersonsがあり、それぞれに名前とアドレスがあります。 EmployeePersonであり、Bossがあります。 BossPersonでもあります。 PersonBossによって継承されるEmployeeクラスを作成する必要がありますか?今、私は問題を抱えています。上司も従業員であり、別の上司がいます。したがって、BossEmployeeを拡張する必要があるようです。しかし、CompanyOwnerBossですが、Employeeではありませんか?どんな種類の継承グラフもどういうわけかここで壊れます。

OOPは、階層、継承、および既存のクラスの再利用についてではなく、一般化動作についてです。 OOPは、「たくさんのオブジェクトがあり、それらに特定のことをしてもらいたい–そして私はどうでもいい」についてです。それがインターフェースfor。反復可能にしたいのでIterableNodeインターフェースを実装する場合、それは完全に問題ありません。必要に応じてCollectionインターフェースを実装する場合子ノードなどを追加/削除する場合は問題ありませんが、別のクラスから継承すると、そうでない場合はすべて、少なくとも上記のように慎重に検討しない限り、そうでない場合があります。

57
amon

それ自体でコンテナを拡張することは、通常、悪い習慣として受け入れられています。コンテナを1つだけ持つのではなく、拡張する理由はほとんどありません。自分のコンテナを拡張すると、奇妙になります。

17
DeadMG

これまでに述べたことに加えて、この種の構造を回避するのには、Java固有の理由があります。

リストのequalsメソッドの規約では、リストを別のオブジェクトと等しいと見なす必要があります

指定されたオブジェクトもリストである場合に限り、両方のリストは同じサイズであり、2つのリスト内の対応する要素のペアはすべてequalです。

ソース: https://docs.Oracle.com/javase/7/docs/api/Java/util/List.html#equals(Java.lang.Object)

具体的には、これは、それ自体のリストとして設計されたクラスがあると、等値比較が高価になる可能性があり(リストが変更可能な場合はハッシュ計算も同様)、クラスにいくつかのインスタンスフィールドがある場合は、これら等しい比較では無視する必要があります

14
Lynn

メモリについて:

これは完璧主義の問題だと思います。 ArrayListのデフォルトコンストラクターは次のようになります。

public ArrayList(int initialCapacity) {
     super();

     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);

     this.elementData = new Object[initialCapacity];
 }

public ArrayList() {
     this(10);
}

ソース 。このコンストラクタは、Oracle-JDKでも使用されます。

次に、コードを使用して単一リンクリストを作成することを検討してください。これで、メモリ消費量が10倍に膨らみました(正確には、わずかに高くなります)。ツリーの場合、これはツリーの構造に特別な要件がないと、簡単にかなり悪化する可能性もあります。 LinkedListまたは他のクラスを代わりに使用すると、この問題が解決するはずです。

正直に言うと、たとえ利用可能なメモリの量を無視しても、ほとんどの場合、これは単なる完全主義にすぎません。 LinkedListを使用すると、代わりにコードが少し遅くなるため、どちらにしてもパフォーマンスとメモリ消費量のトレードオフになります。それでも、これについての私の個人的な意見は、これと同じくらい簡単に回避できる方法でそれほど多くのメモリを無駄にしないことだろう。

編集:コメントに関する説明(正確には@amon)。回答のこのセクションでは、継承の問題を扱いません。メモリ使用量の比較は、シングルリンクリストに対して行われ、メモリ使用量が最適です(実際の実装では、係数が少し変わる可能性がありますが、それでもかなりのメモリを浪費します)。

「悪い習慣」について:

絶対に。これは、単純な理由でグラフを実装する標準的な方法ではありません:グラフノード子ノードであり、はありません子ノードのリストとして。コードであなたの意味を正確に表現することは、主要なスキルです。変数名であっても、このような構造を表現していてもかまいません。次のポイント:インターフェースを最小限に抑える:ArrayListのすべてのメソッドを、継承を介してクラスのユーザーが利用できるようにしました。コードの構造全体を壊さずにこれを変更する方法はありません。代わりに、Listを内部変数として保存し、必要なメソッドをアダプターメソッドを介して使用できるようにします。このようにして、すべてを台無しにすることなく、クラスの機能を簡単に追加および削除できます。

3
Paul