Родовое дерево, самоограниченные дженерики - PullRequest
4 голосов
/ 26 июля 2010

Я хочу добавить щедрость к одному из моих проектов.Я люблю дженерики, так как это делает мой код более надежным, самодокументированным и стирает все эти уродливые приведения.

Однако я столкнулся с трудным случаем, и у меня возникли некоторые проблемы при попытке выразить «рекурсивное» ограничение для одногомои структуры.

Это в основном некое «родовое» дерево с двойными ссылками (на дочерние и родительские).Я максимально упростил класс, чтобы показать проблему:

public class GenericTree<
    ParentClass extends GenericTree<?, ?>, 
    ChildClass extends GenericTree<?, ?>> 
{
    // Attributes
    private ArrayList<ChildClass> children = new ArrayList<ChildClass>();
    private ParentClass parent = null;

    // Methods 
    public void setParent(ParentClass parent) {
        this.parent = parent;
    }

    public void addChild(ChildClass child) {
        child.setParent(this);
        this.children.add(child);
    }
}

Проблема для инструкции: child.setParent (this) .

Java даетследующая ошибка:

Несоответствие границ: Метод setParent (?) типа ChildClass не применим для аргументов
(GenericTree).Параметр шаблона?не имеет нижней границы и может на самом деле быть более ограничительным, чем аргумент GenericTree

Я хотел бы иметь возможность выразить что-то вроде:

public class GenericTree<
    ParentClass extends GenericTree<?, ?>, 
    ChildClass extends GenericTree<[THIS_VERY_CLASS], ?>> 

Сказать, что родителькласс дочернего класса должен быть сам по себе ...

Я просмотрел несколько статей о самоограниченных дженериках, но не знаю, как его применять в этом случае.

Любойбыла бы признательна за помощь.

Ответы [ 2 ]

2 голосов
/ 26 июля 2010

Не используйте дженерики для неоднородных деревьев, используйте интерфейсы и приведение.

Несмотря на то, что вы можете решить некоторые проблемы с помощью обобщений, полученный код будет хрупким, покажет вам сообщения об ошибках, которых вы никогда не видели, исправление ошибок обычно приводит к try & error, и даже если вы его компилируете, не знаю почему; -)

[EDIT2] Добавлено addChild() и пример использования ниже.

[ПРАВИТЬ] Все еще со мной? Если вам действительно нужно, используйте этот API:

interface ParentNode<Child> {
    List<Child> getChildren();
    void addChild(Child child);
}
interface ChildNode<Parent> {
    void setParent(Parent parent);
    Parent getParent();
}
// There is no way to avoid this because we would need to define
// "Node" recursively.
@SuppressWarnings( "rawtypes" )
class Node<
    Parent extends ParentNode<? extends Node>,
    Child extends ChildNode<? extends Node>
>
implements
    ParentNode<Child>,
    ChildNode<Parent>
{
    private Parent parent;
    public Parent getParent() { return parent; }
    public void setParent(Parent parent) { 
        this.parent = parent;
        // Here, we must case the child to a type that will accept Node
        @SuppressWarnings( "unchecked" )
        ParentNode<Node> cast = (ParentNode)parent;
        cast.addChild(this); // Note: Either add the child here ...
    }

    private List<Child> children;
    public List<Child> getChildren() { return children; }
    public void addChild( Child child ) { 
        children.add(child);
        // Here, we must case the child to a type that will accept Node
        @SuppressWarnings( "unchecked" )
        ChildNode<Node> cast = (ChildNode)child;
        cast.setParent(this); // ... or here but not twice :-)
    }
}

т.е.. разделите две функции (посмотрите вверх и посмотрите вниз) на два интерфейса, а затем создайте тип узла, который реализует оба. Это позволяет вам определять листья и корневые узлы как специальные узлы (без одного из двух API), или вы можете определить их как любой другой узел и вернуть null в «неподдерживаемом» методе.

Использование:

public class DirFileNode extends Node<DirFileNode, DirFileNode> {
}
public class TreeUsage {
    public static void main( String[] args ) {
        DirFileNode node = new DirFileNode();
        DirFileNode node2 = new DirFileNode();
        node.addChild( node2 );
        // Why yes, I do love infinite loops. How can you tell?
        node2.addChild( node );
    }
}

То, что вы можете видеть, это то, что API гарантирует, что все безопасно для типов, но внутри вы должны выполнить приведение. И это просто, если вы не используете универсальные типы в узлах. Если вы это сделаете, объявления, которые превращаются в огромный беспорядок.

1 голос
/ 26 июля 2010

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

public class GenericTree<T extends GenericTree<T, P, C>, P extends GenericTree<P, ?, T>, C extends GenericTree<C, T, ?>> {

    // Attributes
    private ArrayList<C> children = new ArrayList<C>();
    private P parent = null;

    // Methods
    public void setParent(P parent) {
        this.parent = parent;
    }

    public void addChild(C child) {
        @SuppressWarnings("unchecked")
        final T thisAsType = (T) this;
        child.setParent(thisAsType);
        this.children.add(child);
    }
}

Edit: Пример реализации

public static class SingleTypeTree<T> extends
    GenericTree<SingleTypeTree<T>, SingleTypeTree<T>, SingleTypeTree<T>> {

}
...