클래스를 자체 목록의 하위 클래스로 정의하면 어떤 단점이 있습니까? { … } 그러나 내 CS

최근 프로젝트에서 다음 헤더를 사용하여 클래스를 정의했습니다.

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

그러나 내 CS 교수와상의 한 후, 그는 수업이 “기억에 나쁘다”고 “나쁜 연습”이라고 말했다. 나는 첫 번째가 특히 사실이고 두 번째가 주관적인 것을 발견하지 못했습니다.

이 사용법에 대한 나의 추론은 임의의 깊이를 가질 수있는 것으로 정의 해야하는 객체에 대한 아이디어를 가지고 있었기 때문에 인스턴스의 동작은 사용자 정의 구현 또는 상호 작용하는 여러 유사한 객체의 동작에 의해 정의 될 수 있습니다 . 이를 통해 물리적 구현이 상호 작용하는 많은 하위 구성 요소로 구성된 객체를 추상화 할 수 있습니다 .¹

반면에, 이것이 어떻게 나쁜 습관이 될 수 있는지 봅니다. 무언가를 자체 목록으로 정의한다는 아이디어는 단순하거나 물리적으로 구현할 수 없습니다.

내 코드에서 이것을 사용 하지 말아야 하는 유효한 이유 가 있습니까?


¹ 더 자세히 설명해야한다면 기쁠 것입니다. 나는이 질문을 간결하게 유지하려고합니다.



답변

솔직히, 나는 여기서 상속의 필요성을 보지 못한다. 말이되지 않습니다. Node ArrayListNode?

이것이 재귀 적 인 데이터 구조 인 경우 간단히 다음과 같이 작성하십시오.

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

어떤 의미 있습니까? 노드 에는 하위 또는 하위 노드 목록이 있습니다.


답변

“메모리에 대한 끔찍한”주장은 전적으로 잘못되었지만 객관적으로 “나쁜 습관”입니다. 클래스에서 상속받을 때는 관심있는 필드와 메소드 만 상속하는 것이 아니라 모든 것을 상속 합니다. 유용하지 않더라도 선언하는 모든 메소드. 그리고 가장 중요한 것은 모든 계약을 상속받으며 클래스가 제공한다는 보장입니다.

약어 SOLID는 우수한 객체 지향 디자인을위한 몇 가지 휴리스틱을 제공합니다. 여기서 I nterface 독방 원리 (ISP)와 L의 iskov 대체 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는 훨씬 더 깊이 실행됩니다. 모든 부모 클래스와 인터페이스의 문서에서 약속 한 모든 것과의 호환성을 유지해야합니다. 에서 자신의 대답 , 린은 하나의 경우 발견했다 List(당신을 통해 상속 인터페이스 ArrayList) 보장 방법 equals()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);
  }

  ...

}

이를 위해서는에 value기여할 hashCode()수없고 영향을 줄 수 없습니다 equals(). List인터페이스 – 당신이 상속에 의해 명예를 약속 – 요구 new Node(0).equals(new Node(123))사실로.


클래스에서 상속 받으면 실수로 부모 클래스가 만든 약속을 깨뜨리기가 너무 쉬워지고 일반적으로 의도 한 것보다 많은 방법을 노출하기 때문에 상속보다 구성선호하는 것이 좋습니다 . 무언가를 상속해야한다면 인터페이스 만 상속하는 것이 좋습니다. 특정 클래스의 동작을 재사용하려면 인스턴스 변수에서 별도의 객체로 유지하면 모든 약속과 요구 사항이 적용되지 않습니다.

때때로, 우리의 자연어는 상속 관계를 암시합니다 : 자동차는 차량입니다. 오토바이는 차량입니다. 나는 클래스를 정의해야 Car하고 MotorcycleA로부터 그 상속을 Vehicle클래스? 객체 지향 디자인은 코드에서 실제 세계를 정확하게 반영하는 것이 아닙니다. 우리는 소스 코드에서 실제 세계의 풍부한 분류 체계를 쉽게 인코딩 할 수 없습니다.

그러한 예로는 직원-보스 모델링 문제가 있습니다. Person각각 이름과 주소를 가진 여러 개가 있습니다 . 은 Employee이다 Person하고있다 Boss. A는 Boss또한이다 Person. 따라서 및에 Person의해 상속되는 클래스를 만들어야 합니까? 이제 문제가 있습니다. 상사는 또한 직원이며 또 다른 상사입니다. 따라서 확장해야 할 것 같습니다 . 그러나이 A는 하지만은이되지 않는다 ? 어떤 종류의 상속 그래프라도 어떻게 든 분해됩니다.BossEmployeeBossEmployeeCompanyOwnerBossEmployee

OOP는 계층, 상속 및 기존 클래스의 재사용에 관한 것이 아니라 행동의 일반화 에 관한 입니다. OOP는“많은 객체를 가지고 있으며 특정 작업을하기를 원합니다. 그리고 어떻게 신경 쓰지 않습니까?”그것이 인터페이스 의 목적입니다. 반복 가능하도록 Iterable인터페이스를 구현하면 Node완벽하게 작동합니다. Collection자식 노드 등을 추가 / 제거하기 위해 인터페이스 를 구현하면 좋습니다 . 그러나 다른 클래스에서 상속받은 것은 위의 개요와 같이 신중하게 생각하지 않는 한 그렇지 않은 모든 것을 제공하기 때문입니다.


답변

컨테이너 자체를 확장하는 것은 일반적으로 나쁜 습관으로 받아 들여집니다. 컨테이너가 아닌 컨테이너를 확장해야 할 이유가 거의 없습니다. 자신의 컨테이너를 확장하면 추가로 이상해집니다.


답변

말한 것에 덧붙여, 이런 종류의 구조를 피해야하는 Java 고유의 ​​이유가 있습니다.

equals리스트를위한 방법 의 계약은 리스트가 다른 객체와 같은 것으로 간주되도록 요구합니다

지정된 객체도리스트 인 경우에만 두리스트의 크기가 동일 하고 두리스트의 모든 해당 요소 쌍이 동일 합니다.

출처 : https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)

특히, 클래스 자체 목록으로 설계된 클래스를 사용하면 동등 비교가 비싸고 (목록이 변경 가능한 경우 해시 계산도 가능) 클래스에 일부 인스턴스 필드가 있는 경우 동등 비교에서 무시 해야합니다. .


답변

메모리에 관하여 :

나는 이것이 완벽주의의 문제라고 말하고 싶습니다. 기본 생성자는 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또는 다른 클래스를 사용하면 이 문제를 해결할 수 있습니다.

솔직히 말해서, 대부분의 경우 사용 가능한 메모리의 양을 무시하더라도 이는 단지 완벽주의 일뿐입니다. A LinkedList는 대안으로 코드를 약간 느리게 할 수 있으므로 성능과 메모리 소비 사이의 절충점입니다. 여전히 이것에 대한 개인적인 견해는 이것만큼 쉽게 우회 할 수있는 방식으로 많은 메모리를 낭비하지 않는 것입니다.

편집 : 의견에 대한 설명 (@amon이 정확해야 함). 대답의이 부분은 상속 문제를 다루지 않습니다 . 메모리 사용량의 비교는 단일 링크 목록과 최상의 메모리 사용량으로 이루어집니다 (실제 구현에서는 요소가 약간 변경 될 수 있지만 여전히 약간의 낭비 된 메모리로 요약 할 수있을만큼 충분히 큽니다).

“나쁜 연습”에 관하여 :

명확히. 이것은 간단한 이유로 그래프를 구현하는 표준 방법이 아닙니다. 그래프 노드 에는 자식 노드 목록 아니라 자식 노드가 있습니다. 코드에서 의미하는 바를 정확하게 표현하는 것이 주요 기술입니다. 변수 이름이나 이와 같은 구조로 표현하십시오. 다음 포인트 : 인터페이스를 최소한으로 유지 : ArrayList상속을 통해 클래스 사용자가 사용할 수있는 모든 방법을 만들었습니다 . 코드의 전체 구조를 손상시키지 않고이를 변경할 수있는 방법이 없습니다. 대신 List내부 변수로 저장하고 어댑터 메소드를 통해 필요한 메소드를 사용 가능하게하십시오. 이렇게하면 모든 것을 망칠 필요없이 클래스에서 기능을 쉽게 추가하고 제거 할 수 있습니다.


답변

이것은 복합 패턴 의 의미론처럼 보입니다 . 재귀 데이터 구조를 모델링하는 데 사용됩니다.