我们能区分金刚石算子和原始构造函数的结果吗?

2022-09-01 17:48:15

我有一些代码,我会写

GenericClass<Foo> foos = new GenericClass<>();

虽然同事会写它

GenericClass<Foo> foos = new GenericClass();

认为在这种情况下,钻石运营商没有添加任何东西。

我知道实际使用与泛型类型相关的参数的构造函数可能会导致编译时错误,而不是原始情况下的运行时错误。并且编译时错误要好得多。(如本问题所述<>)

我也非常清楚编译器(和IDE)可以生成将原始类型分配给泛型的警告。

相反,该问题是针对没有参数或没有与泛型类型相关的参数的情况。在这种情况下,构造的对象是否有任何方法可以根据使用的构造函数而有所不同,或者Javas类型擦除是否保证它们是相同的?GenericClass<Foo> foos


答案 1

对于两个 s 的实例化,一个在末端有菱形运算符,另一个没有...ArrayList

List<Integer> fooList = new ArrayList<>();
List<Integer> barList = new ArrayList();

...生成的字节码是相同的。

LOCALVARIABLE fooList Ljava/util/List; L1 L4 1
// signature Ljava/util/List<Ljava/lang/Integer;>;
// declaration: java.util.List<java.lang.Integer>
LOCALVARIABLE barList Ljava/util/List; L2 L4 2
// signature Ljava/util/List<Ljava/lang/Integer;>;
// declaration: java.util.List<java.lang.Integer>

因此,根据字节码,两者之间没有任何区别。

但是,如果您使用第二种方法,编译器将生成未选中的警告。因此,第二种方法实际上没有价值;您所要做的就是使用编译器生成一个未经检查的误报警告,这会增加项目的噪音。


我已经设法演示了一个场景,在这个场景中,这样做是非常有害的。它的正式名称是堆污染这不是您希望在代码库中发生的事情,并且每当看到这种调用时,都应该将其删除。

请考虑以下类,它扩展了 的某些功能。ArrayList

class Echo<T extends Number> extends ArrayList<T> {
    public Echo() {

    }

    public Echo(Class<T> clazz)  {
        try {
            this.add(clazz.newInstance());
        } catch (InstantiationException | IllegalAccessException e) {
            System.out.println("YOU WON'T SEE ME THROWN");
            System.exit(-127);
        }
    }
}

似乎无害;您可以添加绑定类型的任何实例。

但是,如果我们在玩原始类型...这样做可能会有一些不幸的副作用。

final Echo<? super Number> oops = new Echo(ArrayList.class);
oops.add(2);
oops.add(3);

System.out.println(oops);

这将打印而不是引发任何类型的异常。如果我们想对这个列表中的所有 s 执行一个操作,由于这个令人愉快的调用,我们会遇到一个 。[[], 2, 3]IntegerClassCastExceptionArrayList.class

当然,如果添加钻石运营商,所有这些都可以避免,这将保证我们不会遇到这种情况。

现在,由于我们已经将原始类型引入到组合中,因此Java无法根据JLS 4.12.2执行类型检查:

例如,代码:

List l = new ArrayList<Number>();
List<String> ls = l;  // Unchecked warning

导致编译时未选中警告,因为在编译时(在编译时类型检查规则的限制内)或在运行时,无法确定变量是否确实引用了 。lList<String>

上述情况非常相似;如果我们看一下我们使用的第一个例子,我们所做的就是不要在这件事上添加一个额外的变量。堆污染是一样的。

List rawFooList = new ArrayList();
List<Integer> fooList = rawFooList;

因此,虽然字节码是相同的(可能是由于擦除),但事实仍然是,像这样的声明可能会产生不同或异常的行为。

不要使用原始类型,嗯?


答案 2

JLS在这一点上实际上很清楚。http://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.1.2

首先,它说“泛型类声明定义了一组参数化类型(§4.5),每个类型参数对应类型参数部分的类型参数化。所有这些参数化类型在运行时共享同一个类。

然后它给了我们代码块

Vector<String>  x = new Vector<String>();
Vector<Integer> y = new Vector<Integer>();
boolean b = x.getClass() == y.getClass();

并说它“将导致变量b保持值为真。"

例如,相等性 () 的测试表示两者共享完全相同的 Class 对象。==xy

现在,使用钻石操作员和不带钻石操作员即可。

Vector<Integer> z = new Vector<>();
Vector<Integer> w = new Vector();
boolean c = z.getClass() == w.getClass();
boolean d = y.getClass() == z.getClass();

同样,是 ,也是 。ctrued

因此,据我所知,如果你问在运行时或字节码中使用菱形与不使用菱形之间是否存在一些差异,答案很简单。没有区别。

在这种情况下,使用钻石运算符是否更好是一个风格和意见问题。

附言:不要射杀信使。在这种情况下,我总是使用钻石运算符。但这只是因为我喜欢编译器在一般的w / r / t泛型中为我所做的一切,我不想陷入任何坏习惯。

P.P.S.不要忘记,这可能是一个暂时的现象。http://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.8 警告我们,“强烈建议不要在将泛型引入Java编程语言后编写的代码中使用原始类型。Java编程语言的未来版本可能会禁止使用原始类型。


推荐