【发布时间】:2013-08-15 20:19:24
【问题描述】:
目标
例如,我们有一个T 类型的Tree 类:即Tree<T>。
我们想让这个Tree<T> 类能够持有
-
Tree<T>(当然), -
SubTree<T>SubTree extends Tree, -
Tree<SubT>其中SubT extends T和 -
SubTree<SubT>其中SubTree extends Tree和SubT extends T。
“Hold”表示接受某个子类,和根据请求分别返回某个子类的对象。
比如原来的ArrayList有这个属性:
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
}
这是因为原始的ArrayList 只是保留了输入对象的引用,而不是重新创建它。这不是构建我的类时所期望的行为,尤其是一棵树。考虑以下示例:
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
}
为什么这是一场灾难?考虑对于树中的某个节点,我们想要找到下一个兄弟节点。
其实我们可以在插入元素的时候做个快速修复:
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 :-)
}
但是当涉及到构建我们自己的类(Tree)时,这将成为一个大问题。
所以,我们的目标是:
- 持有“所有”子类,并且
- 结构中的每个元素都是独一无二的。
(下面的解决方案)
原来的问题
我们有一个Tree 类,它使用ArrayList 来保存节点:
public class Tree<T> {
// some constructors & methods skipped
private final ArrayList<Tree<T>> mChildren = new ArrayList<Tree<T>>();
}
我们有这个addChild 方法,没有问题:
public void addChild(final Tree<T> subTree) {
getChildren().add(new Tree<T>(this, subTree)); // copy the tree & set parent attach to this
}
然后,我们想让addChild 方法更通用,它允许添加子类型的树。
private class RedTree<T> extends Tree<T> {}
private void showArrayListIsOkForSubType() {
RedTree<T> redTree = new RedTree();
getChildren().add(redTree);
getChildren().add(new RedTree());
}
在概念上,我们想将addChild方法修改成这样:
(但以下代码存在编译错误,以 cmets 显示。)
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
}
我们已经通过 stackoverflow 进行了搜索,但仍然没有任何帮助。你能帮我们正确的语法吗?
我的代码
在@Jason C 的指导和解释下,这是我的代码。希望它可以帮助其他人:)
另外,请随时纠正我:)
注意:代码并非 100% 完整。但所有主要部分都包括在内。
首先,在默认的零参数构造函数中,确保所有子类都定义了复制构造函数。
/** 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;
}
}
}
那么这是基类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
}
}
}
只有通用参数<T> 需要子类<? extends T> 的通配符。
参数Tree 通过自动转换固有地接受Tree 的所有子类。
所以,这个复制构造函数已经能够接受Tree<T>、SubTree<T>、Tree<SubT> 和SubTree<SubT>。
对于扩展类的拷贝构造函数,可以很简单:
private static class BlueTree<T> extends Tree<T> {
private BlueTree(final BlueTree<T> blueTree) { super(blueTree); }
}
回到基类Tree。以下是addChild 存储对象的方式。
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;
}
因为我们已经检查了每个子类都必须包含默认构造函数,所以我们可以在这里通过反射安全地调用它,以获取子类的新实例,并将其存储到childrenArrayList 中。
附:我们需要使用反射调用,因为普通的new 不适用于泛型参数。
【问题讨论】:
-
你能告诉我们更多代码吗?构造函数等是什么?
-
public void add(final Tree<? extends T> subTree)就足够了。无需参数化add()。请参阅下面的答案。 -
“编译器错误”部分看起来像 Scala =D 在 Scala 中,我们可以编写采用泛型类型参数的函数。
标签: java generics methods arraylist generic-programming