클래스를 자체 목록의 하위 클래스로 정의하면 어떤 단점이 있습니까?


48

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

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

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

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

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

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


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



1
@gnat 이것은리스트를 포함하는리스트를 포함하는 것이 아니라 클래스를 자체리스트로 엄격하게 정의하는 것과 더 관련이 있습니다. 나는 그것이 비슷한 것의 변형이지만 완전히 동일하지는 않다고 생각합니다. 이 질문은 로버트 하비 (Robert Harvey)의 답변에 따른 내용을 말합니다 .
애디슨 크럼 21

16
C ++에서 일반적으로 사용되는 CRTP 관용구 를 보여 주듯이 자체 매개 변수화 된 것에서 상속하는 것은 드문 일이 아닙니다 . 그러나 컨테이너의 경우 핵심 질문은 컴포지션 대신 상속을 사용해야하는 이유를 정당화하는 것입니다.
Christophe

1
나는 그 아이디어를 좋아한다. 결국, 트리의 각 노드 (하위) 트리입니다. 일반적으로 노드의 트리는 유형 뷰 (클래식 노드는 트리가 아니라 단일 객체)에서 숨겨지고 대신 데이터 뷰로 표시됩니다 (단일 노드는 분기에 액세스 할 수 있음). 여기 다른 방법이 있습니다. 우리는 분기 데이터를 볼 수 없으며 유형입니다. 아이러니하게도 C ++의 실제 객체 레이아웃은 데이터를 포함하는 것과 매우 유사하게 상속 될 가능성이 높으므로 동일한 것을 표현하는 두 가지 방법을 다루고 있다고 생각합니다.
피터-모니카 복원 복원

1
어쩌면 나는 조금 누락되었지만 구현에 비해 실제로 확장 해야 합니까? ArrayList<Node> List<Node>
Matthias

답변:


108

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

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

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

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


34
같은 것이 아닙니다. 목록 목록 목록 광고 인해서은 당신이 저장되지 않는 한 의미가 무엇인가 목록에서 새 목록 외에합니다. 그것이 Item 거기 Item에 있고 목록 과 독립적으로 작동합니다.
Robert Harvey

6
아, 지금 봅니다. 나는 접근했다 Node ArrayListNode등은 Node 0 많은에 포함 Node 객체. 이들의 기능은 기본적으로, 대신에, 동일 되고 같은 개체의 목록, 그것은 같은 개체의 목록을. 작성된 클래스의 원래 디자인은 어떤 것이라도 Node서브 세트로 표현 될 수 있다는 믿음으로 Node, 두 가지 구현이 모두 수행하지만 혼란 스럽지는 않습니다. +1
Addison Crump

11
C 또는 Lisp로 단독으로 연결된 목록을 작성할 때 노드와 목록 간의 ID가 "자연적으로"발생하는 경향이 있다고 말합니다. 특정 디자인에서는 헤드 노드가 유일한 의미라는 의미에서 목록입니다. 목록을 나타내는 데 사용되는 것. 따라서 일부 컨텍스트에서 노드가 "모음"이 아니라 노드 모음이라는 느낌을 완전히 없애지 못합니다. 비록 Java에서는 EvilBadWrong이라고 느낀다는 데 동의합니다.
Steve Jessop

12
@ nl-x 하나의 구문이 짧다고해서 더 좋다는 것은 아닙니다. 또한 우연히 그런 트리의 항목에 액세스하는 것은 매우 드 rare니다. 일반적으로 컴파일 타임에 구조를 알 수 없으므로 상수 값으로 이러한 트리를 통과하지 않는 경향이 있습니다. 깊이도 고정되어 있지 않으므로 해당 구문을 사용하여 모든 값을 반복 할 수도 없습니다. 전통적인 트리 탐색 문제를 사용하도록 코드를 조정할 때 Children한두 번만 작성 하게 되므로 일반적으로 이러한 경우 코드를 명확 하게 작성하는 것이 좋습니다 .
Servy


57

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

약어 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자식 노드 등을 추가 / 제거하기 위해 인터페이스 를 구현하면 좋습니다 . 그러나 다른 클래스에서 상속받은 것은 위의 개요와 같이 신중하게 생각하지 않는 한 그렇지 않은 모든 것을 제공하기 때문입니다.


10
마지막 네 단락을 인쇄하여 OOP 및 상속 정보 로 제목을 지정하고 직장에서 벽에 고정했습니다. 내 동료 중 일부는 내가 알고 있듯이 당신이 그것을 넣는 방식에 감사 할 것임을 알 필요가 있습니다 :) 감사합니다.
YSC

17

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


14

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

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

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

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

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


1
이것은 나쁜 습관을 제외하고 내 코드에서 이것을 제거하는 훌륭한 이유입니다. : P
애디슨 크럼프

3

메모리에 관하여 :

나는 이것이 완벽주의의 문제라고 말하고 싶습니다. 기본 생성자는 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내부 변수로 저장하고 어댑터 메소드를 통해 필요한 메소드를 사용 가능하게하십시오. 이렇게하면 모든 것을 망칠 필요없이 클래스에서 기능을 쉽게 추가하고 제거 할 수 있습니다.


1
무엇 보다 10 배 더 높 습니까? 기본 구성 ArrayList가 인스턴스 필드로 상속되지 않고 인스턴스 필드로 있으면 실제로 객체에 ArrayPoint에 대한 추가 포인터를 저장해야하기 때문에 실제로 약간 더 많은 메모리를 사용하고 있습니다. 사과를 오렌지와 비교하고 ArrayList의 상속을 ArrayList를 사용하지 않는 것과 비교하더라도 Java에서는 항상 객체에 대한 포인터를 처리하지만 C ++처럼 값으로 객체를 처리하지 않습니다. new Object[0]총 4 단어를 사용 한다고 가정하면 a new Object[10]는 14 단어의 메모리 만 사용합니다.
amon

@ amon 나는 그것을 명시 적으로 언급하지 않았다. 컨텍스트는 내가 LinkedList와 비교하고 있음을 알기에 충분해야하지만. 그리고 포인터 btw가 아닌 ​​참조라고합니다. 그리고 메모리가있는 요점은 클래스가 상속을 통해 사용되는지 여부에 관한 것이 아니라 사용되지 않은 (거의) 사용되지 않는 ArrayList메모리가 상당히 많은 것입니다.
Paul

-2

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


13
나는 이것을 내려 놓지 않을 것이지만, 이것이 내가 GoF 책을 정말로 싫어하는 이유 입니다. 피가 나는 나무입니다! 나무를 설명하기 위해 왜 "복합 패턴"이라는 용어를 발명해야합니까?
David Arno

3
@DavidArno "복합 패턴"으로 정의하는 것은 다형성 노드 측면이라고 생각합니다. 트리 데이터 구조의 사용법은 그 일부입니다.
JAB

2
@RobertHarvey, 나는 당신의 답변에 대한 귀하의 의견과 여기에 동의하지 않습니다. public class Node extends ArrayList<Node> { public string Item; }귀하의 답변의 상속 버전입니다. 당신은 추론하기가 더 간단하지만 둘 다 어떤 것을 달성합니다 : 그들은 이진이 아닌 나무를 정의합니다.
David Arno

2
@DavidArno : 나는 그것이 작동하지 않을 것이라고 결코 말하지 않았고, 나는 그것이 이상적이지 않다고 말했다.
Robert Harvey

1
@DavidArno 느낌과 맛에 관한 것이 아니라 사실에 관한 것입니다. 트리는 노드로 구성되며 모든 노드에는 잠재적 인 자식이 있습니다. 주어진 노드는 주어진 순간에만 리프 노드입니다. 컴포지트에서 리프 노드는 구성에 따라 리프이며 특성은 변경되지 않습니다.
Christophe
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.