I am building a generic Tree<T>
class, which supports inheritance of sub-trees. But I've encountered some problems. Would you please kindly help me?
Let's define the Tree
class and the BlueTree
class, where BlueTree extends Tree
.
Let's define the Leaf
class and the RedLeaf
class, where RedLeaf extends Leaf
. They are used as the "data" the Trees contain.
A Tree<Leaf>
means a Tree of type Tree
, and its "data" is of type Leaf
.
For inheritance (this is not proper Java inheritance):
Tree<Leaf>
can have child of type
Tree<Leaf>
, Tree<RedLeaf>
, BlueTree<Leaf>
, and BlueTree<RedLeaf>
..
Tree<RedLeaf>
can have child of type
Tree<RedLeaf>
, and BlueTree<RedLeaf>
,Tree<Leaf>
, or BlueTree<Leaf>
..
BlueTree<Leaf>
can have child of type
BlueTree<Leaf>
, and BlueTree<RedLeaf>
,Tree<Leaf>
, or Tree<RedLeaf>
..
BlueTree<RedLeaf>
can have child of type
BlueTree<RedLeaf>
,Tree<Leaf>
, Tree<RedLeaf>
, or BlueTree<Leaf>
.*Here, "child" means the branches / leaves of the Tree.
(a bit complicated, that's why I separate the lines.)
(If you have a solution, you may not need to read the verbose illustration of my attempts below. If you wish to find out the solution together, my code may give you some ideas - or, it may confuse them.)
First Trial: (the simple one)
// This is the focus of this question, the class signature
public class Tree<T> {
// some fields, but they are not important in this question
private Tree<? super T> mParent;
private T mData;
private ArrayList<Tree<? extends T>> mChildren;
// This is the focus of this question, the addChild() method signature
public void addChild(final Tree<? extends T> subTree) {
// add the subTree to mChildren
}
}
This class structure meets most of the requirements in the description. Except, it allows
class BlueTree<T> extends Tree<T> { }
class Leaf { }
class RedLeaf extends Leaf { }
Tree<Leaf> tree_leaf = new Tree<Leaf>();
BlueTree<Leaf> blueTree_leaf = new BlueTree<Leaf>();
blueTree_leaf.addChild(tree_leaf); // should be forbidden
which violates
BlueTree<Leaf>
cannot have child of type Tree<Leaf>
.The problem is because, in BlueTree<Leaf>
, its addChild()
method signature is still
public void addChild(final Tree<? extends Leaf> subTree) {
// add the subTree to mChildren
}
The ideal case is, the BlueTree<Leaf>.addChild()
method signature is changed (automatically, upon inheritance) to
public void addChild(final BlueTree<? extends Leaf> subTree) {
// add the subTree to mChildren
}
(Note that this method cannot override the above method by inheritance, as the parameter types differ.)
There is a workaround. We may add a class inheritance check, and throw RuntimeException
for this case:
public void addChild(final Tree<? extends Leaf> subTree) {
if (this.getClass().isAssignableFrom(subTree.getClass()))
throw new RuntimeException("The parameter is of invalid class.");
// add the subTree to mChildren
}
But making it a compile-time error is far better than a run-time error. I would like to enforce this behaviour at compile-time.
Second Trial
The problem in the first trial structure is, the parameter type Tree
in the method addChild()
is not a generic type parameter. Thus it will not be updated upon inheritance. This time, let's try to make it a generic type parameter also.
Firstly, define the general Tree
class.
public class Tree<T> {
private Tree<? super T> mParent;
private T mData;
private ArrayList<Tree<? extends T>> mChildren;
/*package*/ void addChild(final Tree<? extends T> subTree) {
// add the subTree to mChildren
}
}
Then the TreeManager
which manages a Tree
object.
public final class TreeManager<NodeType extends Tree<? super DataType>, DataType> {
private NodeType mTree;
public TreeManager(Class<NodeType> ClassNodeType) {
try {
mTree = ClassNodeType.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
public void managerAddChild(final NodeType subTree) {
mTree.addChild(subTree);
// compile error: The method addChild(Tree<? extends capture#1-of ? super DataType>)
// in the type Tree<capture#1-of ? super DataType>
// is not applicable for the arguments (NodeType)
}
// for testing
public static void main(String[] args) {
@SuppressWarnings("unchecked")
TreeManager<Tree <Leaf> , Leaf> tm_TreeLeaf_Leaf = new TreeManager<Tree <Leaf>, Leaf> ((Class<Tree <Leaf>>) new Tree <Leaf> ().getClass());
TreeManager<Tree <RedLeaf>, RedLeaf> tm_TreeRedLeaf_RedLeaf = new TreeManager<Tree <RedLeaf>, RedLeaf>((Class<Tree <RedLeaf>>) new Tree <RedLeaf>().getClass());
TreeManager<BlueTree<Leaf> , Leaf> tm_BlueTreeLeaf_Leaf = new TreeManager<BlueTree<Leaf>, Leaf> ((Class<BlueTree<Leaf>>) new BlueTree<Leaf> ().getClass());
TreeManager<BlueTree<RedLeaf>, RedLeaf> tm_BlueTreeRedLeaf_RedLeaf = new TreeManager<BlueTree<RedLeaf>, RedLeaf>((Class<BlueTree<RedLeaf>>) new BlueTree<RedLeaf>().getClass());
System.out.println(tm_TreeLeaf_Leaf .mTree.getClass()); // class Tree
System.out.println(tm_TreeRedLeaf_RedLeaf .mTree.getClass()); // class Tree
System.out.println(tm_BlueTreeLeaf_Leaf .mTree.getClass()); // class BlueTree
System.out.println(tm_BlueTreeRedLeaf_RedLeaf.mTree.getClass()); // class BlueTree
@SuppressWarnings("unchecked")
TreeManager<Tree <Leaf> , RedLeaf> tm_TreeLeaf_RedLeaf = new TreeManager<Tree <Leaf>, RedLeaf>((Class<Tree <Leaf>>) new Tree <Leaf> ().getClass());
TreeManager<BlueTree<Leaf> , RedLeaf> tm_BlueTreeLeaf_RedLeaf = new TreeManager<BlueTree<Leaf>, RedLeaf>((Class<BlueTree<Leaf>>) new BlueTree<Leaf> ().getClass());
System.out.println(tm_TreeLeaf_RedLeaf .mTree.getClass()); // class Tree
System.out.println(tm_BlueTreeLeaf_RedLeaf .mTree.getClass()); // class BlueTree
// the following two have compile errors, which is good and expected.
TreeManager<Tree <RedLeaf>, Leaf> tm_TreeRedLeaf_Leaf = new TreeManager<Tree <RedLeaf>, Leaf> ((Class<Tree <RedLeaf>>) new Tree <RedLeaf>().getClass());
TreeManager<BlueTree<RedLeaf>, Leaf> tm_BlueTreeRedLeaf_Leaf = new TreeManager<BlueTree<RedLeaf>, Leaf> ((Class<BlueTree<RedLeaf>>) new BlueTree<RedLeaf>().getClass());
}
}
The TreeManager
initialises with no problems; the lines are a bit long though. It conforms to the rules in the description also.
However, there is a compile-error when calling Tree.addChild()
inside TreeManager
, as illustrated above.
Third Trial
To fix the compile-error in the second trial, I tried changing the class signature (to even longer). Now mTree.addChild(subTree);
compiles with no problems.
// T is not used in the class. T is act as a reference in the signature only
public class TreeManager3<T, NodeType extends Tree<T>, DataType extends T> {
private NodeType mTree;
public TreeManager3(Class<NodeType> ClassNodeType) {
try {
mTree = ClassNodeType.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
public void managerAddChild(final NodeType subTree) {
mTree.addChild(subTree); // compile-error is gone
}
}
And I have tested it with very similar code as to the second trial. It creates without any problems, as the second trial does. (Just even longer.)
(You may skip the code block below, as it is just logically repeating.)
public static void main(String[] args) {
@SuppressWarnings("unchecked")
TreeManager3<Leaf , Tree <Leaf> , Leaf> tm_TreeLeaf_Leaf = new TreeManager3<Leaf , Tree <Leaf>, Leaf> ((Class<Tree <Leaf>>) new Tree <Leaf> ().getClass());
TreeManager3<RedLeaf, Tree <RedLeaf>, RedLeaf> tm_TreeRedLeaf_RedLeaf = new TreeManager3<RedLeaf, Tree <RedLeaf>, RedLeaf>((Class<Tree <RedLeaf>>) new Tree <RedLeaf>().getClass());
TreeManager3<Leaf , BlueTree<Leaf> , Leaf> tm_BlueTreeLeaf_Leaf = new TreeManager3<Leaf , BlueTree<Leaf>, Leaf> ((Class<BlueTree<Leaf>>) new BlueTree<Leaf> ().getClass());
TreeManager3<RedLeaf, BlueTree<RedLeaf>, RedLeaf> tm_BlueTreeRedLeaf_RedLeaf = new TreeManager3<RedLeaf, BlueTree<RedLeaf>, RedLeaf>((Class<BlueTree<RedLeaf>>) new BlueTree<RedLeaf>().getClass());
System.out.println(tm_TreeLeaf_Leaf .mTree.getClass()); // class Tree
System.out.println(tm_TreeRedLeaf_RedLeaf .mTree.getClass()); // class Tree
System.out.println(tm_BlueTreeLeaf_Leaf .mTree.getClass()); // class BlueTree
System.out.println(tm_BlueTreeRedLeaf_RedLeaf.mTree.getClass()); // class BlueTree
@SuppressWarnings("unchecked")
TreeManager3<Leaf , Tree <Leaf> , RedLeaf> tm_TreeLeaf_RedLeaf = new TreeManager3<Leaf , Tree <Leaf>, RedLeaf>((Class<Tree <Leaf>>) new Tree <Leaf> ().getClass());
TreeManager3<Leaf , BlueTree<Leaf> , RedLeaf> tm_BlueTreeLeaf_RedLeaf = new TreeManager3<Leaf , BlueTree<Leaf>, RedLeaf>((Class<BlueTree<Leaf>>) new BlueTree<Leaf> ().getClass());
System.out.println(tm_TreeLeaf_RedLeaf .mTree.getClass()); // class Tree
System.out.println(tm_BlueTreeLeaf_RedLeaf .mTree.getClass()); // class BlueTree
// the following two have compile errors, which is good and expected.
TreeManager3<RedLeaf, Tree <RedLeaf>, Leaf> tm_TreeRedLeaf_Leaf = new TreeManager3<RedLeaf, Tree <RedLeaf>, Leaf> ((Class<Tree <RedLeaf>>) new Tree <RedLeaf>().getClass());
TreeManager3<RedLeaf, BlueTree<RedLeaf>, Leaf> tm_BlueTreeRedLeaf_Leaf = new TreeManager3<RedLeaf, BlueTree<RedLeaf>, Leaf> ((Class<BlueTree<RedLeaf>>) new BlueTree<RedLeaf>().getClass());
}
However, a problem arises when I try to call TreeManager3.managerAddChild()
.
tm_TreeLeaf_Leaf.managerAddChild(new Tree<Leaf>());
tm_TreeLeaf_Leaf.managerAddChild(new Tree<RedLeaf>()); // compile error: managerAddChild(Tree<RedLeaf>) cannot cast to managerAddChild(Tree<Leaf>)
tm_TreeLeaf_Leaf.managerAddChild(new BlueTree<Leaf>());
tm_TreeLeaf_Leaf.managerAddChild(new BlueTree<RedLeaf>()); // compile error: managerAddChild(BlueTree<RedLeaf>) cannot cast to managerAddChild(BlueTree<Leaf>)
This is understandable. TreeManager3.managerAddChild(NodeType)
means TreeManager3.managerAddChild(Tree<T>)
and there is no wildcard Tree<? extends T>
in the parameter type, like Tree.addChild(final Tree<? extends T> subTree)
in the first trial.
I have already run out of ideas. Was I going in the wrong direction to solve this problem? I have spent a lot of time typing up this question and tried my greatest effort to make it more readable, easier to understand, and to follow. I have to say sorry that it is still very long and verbose. But could you please help if you know the way, or please give me any ideas you have? Your every input is highly appreciated. Thanks a lot!
Based in the First Trial, only allow mChildren
to be modified by addChild()
(and other methods with the isAssignableFrom()
check), so even allowing user inheritance of Tree
and overriding addChild()
will not break the Tree integrity.
/developer/util/Tree.java
package developer.util;
import java.util.ArrayList;
public class Tree<T> {
private Tree<? super T> mParent;
private final ArrayList<Tree<? extends T>> mChildren = new ArrayList<Tree<? extends T>>();
public int getChildCount() { return mChildren.size(); }
public Tree<? extends T> getLastChild() { return mChildren.get(getChildCount()-1); }
public void addChild(final Tree<? extends T> subTree) {
if (this.getClass().isAssignableFrom(subTree.getClass()) == false)
throw new RuntimeException("The child (subTree) must be a sub-class of this Tree.");
subTree.mParent = this;
mChildren.add(subTree);
}
}
/user/pkg/BinaryTree.java
package user.pkg;
import developer.util.Tree;
public class BinaryTree<T> extends Tree<T> {
@Override
public void addChild(final Tree<? extends T> subTree) {
if (getChildCount() < 2) {
super.addChild(subTree);
}
}
}
/Main.java
import user.pkg.BinaryTree;
import developer.util.Tree;
public class Main {
public static void main(String[] args) {
Tree<Integer> treeOfInt = new Tree<Integer>();
BinaryTree<Integer> btreeOfInt = new BinaryTree<Integer>();
treeOfInt.addChild(btreeOfInt);
System.out.println(treeOfInt.getLastChild().getClass());
// class user.pkg.BinaryTree
try {
btreeOfInt.addChild(treeOfInt);
} catch (Exception e) {
System.out.println(e);
// java.lang.RuntimeException: The child (subTree) must be a sub-class of this Tree.
}
System.out.println("done.");
}
}
What do you think?
As I see it, there is no perfect solution to this problem. This is basically due to type erasure. The Erasure of Generic Methods article explains that your addChild(final Tree<? extends Leaf> subTree)
function will become a addChild(final Tree subTree)
function. So, even if you could somehow have a generic parameter <TreeType extends Tree<? extends Leaf>> addChild(final TreeType subTree)
(not valid syntax!) it would be erased to addChild(final Tree subTree)
at compile time. Adding your runtime test will work though, so the edit you made will do the job.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With