类型安全、Java泛型和查询

10
我有一个有趣的情况,想知道是否有更好的方法来解决。情况是这样的,我有一个树形结构(具体来说是抽象语法树),某些节点可以包含各种类型的子节点,但都继承自给定的基类。
我经常需要在这个树上进行查询,并且希望得到我感兴趣的特定子类型的结果。因此,我创建了一个谓词类,然后将其传递给一个通用的查询方法。起初,我的查询方法看起来像这样:
public <T extends Element> List<T> findAll(IElementPredicate pred, Class<T> c);

在这里,Class参数只是用来指示返回类型。我对这种方法感到不满的原因是我的所有谓词已经针对特定类型了,所以这里存在冗余信息。一个典型的调用可能如下所示:

List<Declaration> decls = 
    scope.findAll(new DeclarationPredicate(), Declaration.class);

所以我像这样重构了它:

public <T extends Element> List<T> findAll(IElementPredicate<T> pred);

IElementPredicate接口的样式如下:

public interface IElementPredicate<T extends Element> {
    public boolean match(T e);
    public String getDescription();
    public Class<T> getGenericClass();
}

这里的重点是,谓词接口扩展为提供Class对象。这使得编写实际的findAll方法需要更多的工作,并且在编写谓词时增加了一些工作,但这两个都基本上是微小的“一次性”任务,因为它使查询调用变得更加友好,因为您不必添加额外的(可能是冗余的)参数,例如。
List<Declaration> decls = scope.findAll(new DeclarationPredicate());

我以前没有注意到这种模式。这是处理Java泛型语义的典型方式吗?只是好奇我是否错过了更好的模式。

有评论吗?

更新:

一个问题是你需要类(Class)做什么?下面是findAll的实现:

public <T extends Element> List<T> findAll(IElementPredicate<T> pred) {
    List<T> ret = new LinkedList<T>();
    Class<T> c = pred.getGenericClass();
    for(Element e: elements) {
        if (!c.isInstance(e)) continue;
        T obj = c.cast(e);
        if (pred.match(obj)) {
            ret.add(c.cast(e));
        }
    }
    return ret;
}

虽然匹配只需要一个T,但我需要确保对象是T类型才能调用它。为了做到这一点,我需要Class的“isInstance”和“cast”方法(据我所知)。

4个回答

1

如果你愿意,你可以使用访问者模式或其变体来避免显式构造和转换。请参见hibernate wiki页面。考虑到类型擦除的特殊性,我一直在思考是否能完全解决你的问题,但我并不完全确定它会一直有效。

编辑:我看到了你的补充。如果你使谓词层次结构遵循访问者层次结构,你就不需要在匹配调用之前进行强制转换。像这样(未经测试):

interface Element {
    public boolean accept(ElementPredicateVisitor v);
}

class Declaration implements Element {
    public boolean accept(ElementPredicateVisitor v) {
        return v.visit(this);
    }    
}

class TaxReturn implements Element {
    public boolean accept(ElementPredicateVisitor v) {
        return v.visit(this);
    }    
}


interface IElementPredicate {
    public void match(Element e);
}

class ElementPredicateVisitor implements IElementPredicate {
    public boolean match(Element e) { 
        return e.accept(this); 
    }
    /**
     * default values
     */
    boolean visit(Declaration d) { return false; }
    boolean visit(TaxReturn tr) { return false; }
}

class DeclarationNamePredicate extends ElementPredicateVisitor {
    boolean visit(Declaration d) {
        return d.dSpecificExtraName() == "something"
    }
}

class TaxReturnSumPredicate extends ElementPredicateVisitor {
    boolean visit(TaxReturn tr) {
        return tr.sum() > 1000;
    }
}


public <T extends Element> List<T> findAll(IElementPredicate pred) {
    List<T> ret = new LinkedList<T>();
    for(Element e: elements) {            
        if (pred.match(obj)) {
                ret.add((T) obj);
        }
    }
    return ret;
}

我经常使用访问者模式(实际上在这个项目中也是如此),但在这种特定情况下,使用访问者模式会增加很多代码和复杂性,而我认为这并不是必需的。 - Michael Tiller
我同意你的观点,这种开关有很大的副作用,使问题变得更加复杂。像你所期望的那样,要撤销这种泛化是很困难的。我认为你的解决方案是最好的,因为如果没有访问者,你需要使用后期绑定或显式类型检查(就像equals()函数一样)。你的解决方案更好,因为它将类型检查集中在一起,而不是让实现者承担这个负担。你也可以在通用的Comparable实现中看到这个问题。 - Alexander Torstling
我所说的“晚绑定”是指Java应该根据对象类型而不是引用类型实现重载方法解析。这基本上就是您在代码中实现的内容。 - Alexander Torstling

1
你的树形结构听起来很像一个XML DOM对象。你考虑过将你的树转换成DOM结构并使用XPath来查询吗?这样可能需要更少的自定义代码。

1

我认为最接近的“模式”是类型标记,并且泛型教程也推荐使用它们。您还可以将基本谓词转换为超类型标记(又名Gafter小工具),在定义新谓词时节省额外的几行代码。


0

我认为这是一种不错且干净的做法,我可能会以同样的方式去做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接