是否可以检测不兼容类型与Java类型系统的比较?

时间:2014-10-24 15:37:36

标签: java generics type-erasure type-safety type-systems

我正在尝试编写一个辅助函数来以类型安全的方式比较两种类型:

typesafeEquals("abc", new Integer(42)); // should not compile

我的第一次直接尝试失败了:

<T> boolean typesafeEquals(T x, T y) { // does not work!
    return x.equals(y);
}

嗯,问题是T可以推断为Object。现在,我想知道在Java类型系统中实现typesafeEquals是不可能的。

我知道FindBugs找到的工具可以警告不兼容类型的比较。无论如何,要么在没有外部工具的情况下看到解决方案,要么解释为什么它是不可能的,这将是有趣的。


更新:我认为答案是不可能的。但是,我没有证据支持这种说法。只是似乎很难找到适用于所有情况的解决方案。

有些答案很接近,但我相信Java的类型系统并不支持解决这个问题。

7 个答案:

答案 0 :(得分:6)

在方法中添加第三个参数 - Class<T>类型 - 将有助于解决此类问题。如你所说,你可以从Object获得T;除非你为它添加界限,否则它永远无法识别Object以外的任何内容。

虽然添加类类型将强制您在每次要使用该方法时将其放入,但它会强制执行编译器和隐含的合同,您将它们相等,就好像它们是某个类一样。

<T> boolean typesafeEquals(T x, T y, Class<T> clazz) {
    return x.equals(y);
}

请注意,从开发人员的角度来看,这种检查有点过度反应。情况应该是两个等价的对象应该只返回false

鉴于要求它是Object.equals的直接替代品,考虑到方法本身已经建立的松散类型安全性,这根本不可能。虽然泛型是解决这个问题的理想方法,但Java 5中存在泛型,而equals从一开始就存在。

答案 1 :(得分:5)

询问Apple的实例是否等于Orange的特定实例不应该造成任何麻烦。它应该简单地观察到因为它不认为自己等于任何不属于Apple类型的对象,并且因为它被赋予了对不属于Apple类型的东西的引用,所以它不会认为自己等于传入的对象。

此外,某些接口具有合同,这些合同要求实现它们认为自己等于满足特定标准的任何其他实现。因此,两个不相关类型的对象都可以实现一个界面,该界面要求两个对象彼此相互报告。即使引用的类型没有共同的接口,除非至少有一个被密封,否则从它们派生的类型的实例可能共享一个会强制相互平等关系的接口。

因此,虽然有些情况下静态分析可以揭示由两个不同类的引用标识的对象不可能认为自己是相等的,但所需的静态分析级别远远超出泛型类型系统可能封装的任何级别[除其他外,它需要检查不同类型的“equals方法”中的代码。

我建议虽然理论上可能很好,如果编译器可以识别代码试图比较不可能相等的事情的情况,那么定义关于equals的规则的方式(包括事实)允许 - 在某些情况下需要 - 对于不相关的类的实例来比较它们彼此相等)意味着一般来说你所寻求的是不可能的。如果可以询问Apple的实例是否等于任何特定的Fruit,则必须可以询问它是否等于从Fruit派生的任何类型的任何特定实例。 。 Apple不应该关心这个问题是否愚蠢;它应该简单地回答它。

答案 2 :(得分:2)

  

是否可以检测不兼容类型与Java类型系统的比较?
  [...]
  我相信Java的类型系统不支持一般性地解决问题。

单独的类型系统不能做到这一点,至少不能以绝对通用的方式适用于所有类型的通用方式,因为它无法告诉你的类型在equals实现中做了什么(如已经在supercat的回答中说,Foo可以接受与Bar进行比较,该逻辑被刻在代码本身中,因此,编译器无法检查。

话虽如此,对于“类型安全”的“限制”定义,如果你能声明你对每次使用的期望,你自己的方法几乎就在那里,只是让它静止,使用类名称调用它,并显式指定期望的类型:

public class TypeSafe {

    public static <T> boolean areEqual(T x, T y) {
        return x.equals(y);
    }

    void test() {
        TypeSafe.areEqual("a", 1); // Compiles because no restriction is present.
                                   // Both are resolved to Serializable
                                   // [there is not only "Object" in common ;)]

        TypeSafe.<CharSequence>areEqual("a", 1); // Does not compile
        //                                   ^
        //          Found: int, required: java.lang.CharSequence
    }
}

类似于Makoto的回答,没有将类型作为参数传递给Jeremy,而没有创建新对象。 (upvoted both。)

虽然这会有点误导,因为TypeSafe.<Number>areEqual(1f, 1d)编译但返回false。它可以给出一种错误的意义“这两个数字是等价的吗?” (这不是它的作用)。所以你必须知道你在做什么......

现在,即使你比较两个Long值,一个可以是基于纪元的时间戳(以毫秒为单位),另一个可以是秒,2个相同的原始值不会“相等”。

使用Java 8,我们有type annotations,编译器可以使用注释处理器进行检测,以根据这些注释执行其他检查(参见checker framework)。

假设我们有这些方法以毫秒和秒为单位返回持续时间,如注释@m@sprovided by framework)所指定的那样:

@m long getFooInMillis() { /* ... */ }
@s long getBarInSeconds() { /* ... */ }

(当然在这种情况下,最好先使用正确的类型,例如InstantDuration ......但请忽略它一分钟)

通过这种方式,您可以使用检查器框架和注释更加具体地将您作为泛型类型参数传递给方法的约束:

long t1 = getFooInMillis();
long t2 = getBarInSeconds();

TypeSafe.<Long>areEqual(t1, t2);    // OK, just as we've seen earlier

TypeSafe.<@m Long>areEqual(t1, t2); // Error: incompatible types in argument
//                         ^^  ^^
// found   : @UnknownUnits long
// required: @m Long

@m long t1m = getFooInMillis();
@s long t2s = getBarInSeconds();
@m long t2m = getFooInMillis();

TypeSafe.<Long>areEqual(t1m, t2s);    // OK

TypeSafe.<@m Long>areEqual(t1m, t2s); // Error: incompatible types in argument
//                              ^^^
// found   : @s long
// required: @m Long

TypeSafe.<@m Long>areEqual(t1m, t2m); // OK

答案 3 :(得分:2)

您尝试过的工作因为T不明确而且编译器会尝试编译代码(通过将其解释为Object)而无法工作。您需要通过明确设置要检查的类型来强制它。

Makoto's answer非常有用。

而且,如果您可以应对使用其他对象,您还可以使用以下内容:

public class EqualsHelper<T> {
    public boolean areEquals(T one, T another){
        if(one == null){
            return another == null;
        }
        return one.equals(another);
    }
}

public static void main(String[] args) {
    EqualsHelper<String> stringHelper = new EqualsHelper<>();
    stringHelper.areEquals("hello","world");  // compile
    stringHelper.areEquals("hello",1);        // doesn't
}

答案 4 :(得分:0)

这个怎么样?

public static <T, E extends T> boolean typesafeEquals(T x, E y) {
    return x.equals(y);
}

当然它会为T的子类编译,但这有点意义,取而代之的是原则。唯一的问题是它对参数强制执行一个命令,所以它应该重命名为&#34; isAssignable&#34;。

编辑:或者如果你要求严格相等,在切换参数时调用它两次:

typesafeEquals("", 1); // doesn't compile
typesafeEquals(1, ""); // doesn't compile
typesafeEquals(base, child); // compile
typesafeEquals(child, base); // doesn't compile

答案 5 :(得分:0)

这里有几种选择。

使用类型安全接口/类

这类似于直接传递给方法,除了额外的作用域阻止编译器将泛型类型变为对象。

static interface ITypeSafe<T> {
    public default boolean typeSafeEquals(T o) {
        return equals(o);
    };
}

static class TypeSafeEquals<T> {

    public static <A, B extends A> boolean stypeSafeEquals(B o1, B o2) {
        return o1.equals(o2);// tse(o2, o1);
    }

    private boolean typeSafeEquals(T o1, T o2) {
        return o1.equals(o2);
    }
}

对编译器挂钩使用注释

您还可以使用注释添加compiler hook,然后使用注释标记类型安全。这可能不适用于大型团队,因为它确实需要额外的项目设置才能工作,但到目前为止是最通用的。 (我不能在这里包含例子,因为它很复杂,我不完全理解它。)

签入单元测试

您可以使用assert assert (o1.getClass() == o2.getClass());等开发测试行在单元测试期间检查是否始终不违反真正的假设。 (无论如何你应该这样做)只要确保这些行在开发期间而不是在生产期间执行。 (这应该是编译器钩子的一个更容易的替代方案)

答案 6 :(得分:-1)

首先是函数重载:)

typesafeEquals(Integer, Integer);
typesafeEquals(int, int);
typesafeEquals(String, String);
...

第二是

<N extends Number> typesafeEquals(N, N);
<C extends CharSequence> typesafeEquals(C, C);
...