【问题标题】:Java: Type aliases support annotation processing tool (APT)?Java:类型别名支持注释处理工具(APT)?
【发布时间】:2014-11-27 07:41:37
【问题描述】:

我从未使用过类型别名,但这个概念似乎是一个非常有用的功能,可以在相同类型的对象上添加语义并防止常见的拼写错误。

假设有void foo(float volume, float weight)。可以这样调用:foo(v, m),但foo(m, v) 不是一个明显的错字。 void process(Iterable<File> javaPath, Iterable<File> classPath) 可以是另一个用例。不幸的是,Java 中没有类型别名,一些变通方法确实是矫枉过正:

  • 将单个字段聚合到一个类中(将原语装箱到对象中;对象中的对象具有额外的引用;更复杂的序列化/反序列化规则)
  • 从另一个类扩展一个类(如how do i create some variable type alias in Java - 基元不可能;类可能是最终类或具有不可访问的构造函数)。

所以它们都有缺点和运行时/性能成本。

据我所知,在 Java 中,基于运行时的“类型别名”可能会替换为编译时检查,例如处理 @NotNull 和 @Nullable。是否有任何静态类型别名检查器/APT 工具支持 void foo(@Volume float volume, @Weight float weight) 之类的结构,以便检查器可以在编译时验证这种“类型安全”,并要求对传递的变量/常量和文字进行注释(在声明和调用站点分别)?


之所以提出这个问题,是因为我目前正在开发一个可以使用多个后端的 Java 源代码处理工具(目前,仅限 JDT)。只要我希望它与 JDT 无关,我就希望拥有类型、方法和字段的业务对象。尽管如此,我丢失了很多来自 JDT AST 的信息,我的工具目前最好的方法是使用字符串(它允许执行一些足以满足工具范围的分析)。但是像void process(String type, String field, String method) 这样的方法令人困惑,所以我创建了TypeFieldMethod,它们代表域对象并且与JDT 无关。它们很好,但我不希望它们在未来得到扩展(所有这三种类型都有单个 private final String name; 字段 + 我必须覆盖 hashCode/equalstoString)。这让我想到了再次对 APT 进行类型检查的想法。只要可以处理原始数据,有void process(@Type String type, @Field String field, @Method String method)之类的就足够了(我仍然可以使用单个对象)。

【问题讨论】:

  • 我不明白。如果我打电话给foo(a,b),其中a 和b 都是int,那么工具应该如何验证我的a 是否是Volume?如果您需要Volume 具有语义含义,请将其设为类。
  • @AdrianShum 我只需要在浮点数上提供更多语义。将单个原语包装到一个类中会导致另一个类实例化(更多堆),在序列化器/反序列化器中需要更多适配器(例如,GSON 不适用于新类——需要更多适配器)。这个想法就像 NotNull 和 Nullable 的想法一样——你创建类不只是为了添加语义,你注释,对吧? .NET 支持可以轻松保存原语而没有性能成本的值类型(它本身没有原语,一切都是真正的对象)。但是Java中没有值类型。
  • 除非你能告诉我它如何检查输入int 是否是Volume,否则我认为在这种情况下使用注释不是一个可行的想法。如果您正在寻找减少意外拼写错误的方法,则可能是您想要的类似 fluent-builder 的模式:foo().withVolume(a).withWeight(b).run() 之类的东西。当然还有额外的开发工作,但如果方法参数真的那么模棱两可且容易混淆,那么额外的努力是值得的。而且,C# 中的值对象并不能解决问题。是的,从堆栈实例化的开销较小,但是
  • 如果我的理解是正确的,仍然会有额外的开销。 Java 中没有针对此问题的直接解决方案。如果情况真的那么糟糕,那么该包装类的额外性能损失应该不是什么大问题。有很多方法可以减少额外开销:享元、工厂方法、不可变 obj 等。
  • 嗯....我有点困惑。使用 fluent-builder-like 模式不会使您的模块相互依赖。这只是编写方法的另一种方式

标签: java type-safety annotation-processing type-alias


【解决方案1】:

如果您的问题是当您调用 foo(v,m) 时看起来模棱两可,人们可能会误写为 foo(m,v) 而没有任何警告或明显的错误线索,您可以使用类似 fluent-builder 的模式:

(代码未测试,只是写来演示这个想法)

public class MyClass {
    public class FooStub {
        int volume;
        int weight;
        public FooStub withVolume(int v) {
            this.volume = v;
            return this;
        }
        public FooStub withWeight(int w) {
            this.weight = w;
            return this;
        }
        public int run() {   // or call it getResult() or whatever you want
            Assert.notNull(volume);
            Assert.notNull(weight);
            return foo(volume,weight);  // calling method of outer class
        }
    }

    public int foo(int volume, int weight) {   // you even hide this if you want
        return volume * weight;
    }
    public FooStub foo() {
        return new FooStub();
    }

}

通过这样做,调用者可以这样做

MyClass myObj = ....;
int result = myObj.foo().withVolume(a).withWeight(b).run();
// which is the same as result = myObject.foo(a,b);

【讨论】:

  • 现在我明白你的意思了,谢谢。我想这里还有一件事:myObj.foo().withVolume(b).withWeight(a).run() -- 这里ab 被意外交换了。
  • 使用适当的命名约定,您可以通过肉眼发现它。使用普通的旧方法,如果您使用myObj.foo(w,v);,它仍然“看起来”不错,而myObj.foo().withVolume(w).withWeight(v).run() 则不是
  • 是的,所以这就是为什么我想要一个工具来捕捉这种情况并添加更多语义。例如,IntelliJ IDEA 有一些内部检查尝试(因为它们可能不那么可靠)覆盖显示警告的可疑调用。
  • 正如我所说,如果您需要赋予更多语义含义,创建另一个类几乎是您在 Java 中可以做到的唯一方法。在这种情况下,注释几乎无济于事。告诉我,你将如何确定传入的整数是否为@Volume
  • 如果您正在考虑使用相同的注释来注释局部变量,afaik,您无法在 apt 或运行时处理局部变量注释。您需要自己解析源代码,或者使用 ASM 挖掘字节码并在运行时检查它。不过有趣的想法:)如果你能做到并分享给社区会很棒:)
猜你喜欢
  • 1970-01-01
  • 2017-01-05
  • 2012-01-15
  • 1970-01-01
  • 2011-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多