Search code examples
javagenericsmethodsarraylistgeneric-programming

Generic Type x Generic Parameter: Building a "very generic" structure


AIM

For instance, we have a Tree class of type T: i.e. Tree<T>.

We would like to make this Tree<T> class to be able to hold

  1. Tree<T> (of course),
  2. SubTree<T> where SubTree extends Tree,
  3. Tree<SubT> where SubT extends T, and
  4. SubTree<SubT> where SubTree extends Tree and SubT extends T.

"Hold" means it accept the certain sub-class, and it returns the object of certain sub-class respectively, upon request.

For example, the original ArrayList has this property:

private static class Leaf {
}
private static class RedLeaf extends Leaf {
}
@Test
public final void test() {
    ArrayList<Leaf> al = new ArrayList<Leaf>();
    al.add(new Leaf());
    System.out.println(al.get(al.size()-1).getClass());     // class Leaf
    al.add(new RedLeaf());
    System.out.println(al.get(al.size()-1).getClass());     // class RedLeaf
}

It is because the original ArrayList just keep a reference of the input object, but not re-creating it. This is not a desired behaviour while building my class, especially a tree. Consider the following example:

public final void test() {
    ArrayList<Leaf> al = new ArrayList<Leaf>();
    Leaf leaf = new Leaf();
    RedLeaf redLeaf = new RedLeaf();
    al.add(leaf);
    al.add(redLeaf);
    al.add(leaf);
    System.out.println(al.indexOf(al.get( 0 )));    // 0
    System.out.println(al.indexOf(al.get( 1 )));    // 1
    System.out.println(al.indexOf(al.get( 2 )));    // 0 <-- disaster
}

Why this is a disaster? Consider for a certain node in a tree, we want to find the next sibling.

In fact, we can do a quick fix while inserting the elements:

private static class Leaf {
    Leaf() { super(); }
    Leaf(Leaf leaf) { super(); }
}
private static class RedLeaf extends Leaf {
    RedLeaf() { super(); }
    RedLeaf(RedLeaf redLeaf) { super(redLeaf); }
}
@Test
public final void test() {
    ArrayList<Leaf> al = new ArrayList<Leaf>();
    Leaf leaf = new Leaf();
    RedLeaf redLeaf = new RedLeaf();
    al.add(new Leaf(leaf));
    al.add(new RedLeaf(redLeaf));
    al.add(new Leaf(leaf));
    System.out.println(al.indexOf(al.get( 0 )));    // 0
    System.out.println(al.indexOf(al.get( 1 )));    // 1
    System.out.println(al.indexOf(al.get( 2 )));    // 2 <-- nice :-)
}

But when it comes to building our own class (of the Tree), this becomes a big problem.

So, our aims are:

  1. holding "all" the sub-classes, and
  2. having every element in the structure unique.

(solution below)


The original question

We have a Tree class, which uses ArrayList to hold the nodes:

public class Tree<T> {
    // some constructors & methods skipped
    private final ArrayList<Tree<T>> mChildren = new ArrayList<Tree<T>>();
}

We have this addChild method, with no problems:

public void addChild(final Tree<T> subTree) {
    getChildren().add(new Tree<T>(this, subTree));  // copy the tree & set parent attach to this
}

Then, we want to make the addChild method more general, which allows adding a tree of sub-type.

private class RedTree<T> extends Tree<T> {}
private void showArrayListIsOkForSubType() {
    RedTree<T> redTree = new RedTree();
    getChildren().add(redTree);
    getChildren().add(new RedTree());
}

In concept, we want to modify the addChild method to this:

(But the following code is having compile errors, shown in comments.)

public <Leaf extends T, SubTree extends Tree<T>> void add(final SubTree<Leaf> subTree) {
    // The type SubTree is not generic; it cannot be parameterized with arguments <Leaf>

    SubTree<Leaf> tr = new SubTree<Leaf>();
    getChildren().add(new SubTree<Leaf>());
    // SubTree cannot be resolved to a type
    // Leaf cannot be resolved to a type
}

We have searched through stackoverflow, but still with no helps. Could you please help us with the correct syntax?


My codes

With the guide and explanation from @Jason C, here is my codes. Hope it helps others :)

Also, please feel free to correct me :)

Note: the codes are not 100% complete. But all the major pieces are included.

First, in the default zero-argument constructor, ensure all the sub-classes define the copy-constructor.

/** Default constructor. **/
public Tree() {     // All sub-classes instantiation must invoke this default constructor
    super();                // here is a good place to ensure every sub-class has a copy constructor
    if (Reflection.hasCopyConstructor(this) == false)
        throw new CopyConstructorRequiredException(this.getClass());
}
class Reflection {
    public static boolean hasCopyConstructor(final Object object) {
        return hasCopyConstructor(object.getClass());
    }
    public static boolean hasCopyConstructor(final Class<?> clazz) {
        try {
            clazz.getDeclaredConstructor(clazz);
            return true;
        } catch (SecurityException e) {
            e.printStackTrace();
            return false;
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            return false;
        }
    }
}

Then this is the copy constructor of the base Tree<T>:

private Tree(final Tree<? extends T> copyFrom) {
    super();
    if (copyFrom != null) {
        this.setData(copyFrom.getData());
        for (final Tree<? extends T> child : copyFrom.getChildren()) {
            this.addChildren(child);    // addChildren() handles null well
        }
    }
}

Only the Generic Parameter <T> needs a wildcard of sub-class <? extends T>.

The parameter Tree inherently accepts all the sub-classes of Tree, by auto casting.

So, this copy constructor already able to accept Tree<T>, SubTree<T>, Tree<SubT>, and SubTree<SubT>.

For the copy constructor of the extended classes, it can be just simply:

private static class BlueTree<T> extends Tree<T> {
    private BlueTree(final BlueTree<T> blueTree) { super(blueTree); }
}

Back to the base class, Tree. Here is how addChild stores the objects.

public Tree<T> addChildren(final Tree<? extends T>... subTrees) {
    if (subTrees == null)               // called addChildren((Tree<T>) null)
        addChild((Tree<T>) null);           // add null to children
    else
        for (final Tree<? extends T> subTree : subTrees)      // empty parameter goes here != null array
            addChild(subTree);
    return this;
}
public Tree<T> addChild(final Tree<? extends T> subTree) {
    if (subTree == null)            // for addChild((Tree<T>) null)
        getChildren().add(null);        // add null to children
    else {                          // else
        getChildren().add(              // copy (constructor) the tree & set parent attach to this
                Reflection.<Tree<T>>invokeConstructor(subTree, new ParameterTypeAndArg(subTree.getClass(), subTree))
                .setParent(this));
    }
    return this;
}

Because we have checked every sub-class must contain the default constructor, we can safely invoke it here by reflection, to get a new instance of the sub-class, and store it into the children ArrayList.

P.S. we need to use reflection invoke because the ordinary new doesn't work for generic parameters.


Solution

  • Well, first of all, you're overcomplicating this. All you really need to do is:

    public void add(final Tree<? extends T> subTree) {
    

    Theres no need to parameterize add().

    But anyways, I'll address your original attempt: You want SubTree extends Tree<Leaf>, because even if Leaf extends T you can't guarantee that SubTree extends Tree<T> with SubTree<Leaf> matches. E.g. if your class hierarchy is:

    public class Base { }
    public class A extends Base { }
    public class B extends Base { }
    

    If Leaf is A and SubTree is Tree<B> then add (final SubTree<Leaf>) does not match Tree<B>.

    So conceptually you actually want this:

    public <Leaf extends T, SubTree extends Tree<Leaf>> void add(final SubTree<Leaf> subTree) {
    

    Of course that is not valid syntax. Really all you need to do is this:

    public <Leaf extends T, SubTree extends Tree<Leaf>> void add(final SubTree subTree) {
    

    That will be sufficient to match all necessary types. Try it:

    {
        Tree<Object> x = new Tree<Object>();
        MyTree<Integer> y = new MyTree<Integer>();
        Tree<Integer> z = new Tree<Integer>();
    
        x.add(y);
        y.add(x); // not valid, as Tree<Object> does not extend Tree<Integer>  
        y.add(z); // fine, as Tree<Integer> matches
    }
    
    public static class MyTree<T> extends Tree<T> {     
    }
    

    Within add() you also do not parameterize SubTree, since its type is already specified elsewhere:

    SubTree tr = ...;
    

    However, now you have a bigger problem, which is a classic one and is answered in many other places here:

    tr = new SubTree();
    

    You cannot instantiate an object of a generic type because of type erasure. You will have to specify the SubTree's Class somewhere and instantiate it with .newInstance().