我们能区分钻石操作员和原始构造函数的结果吗?

时间:2015-03-09 02:59:33

标签: java generics language-lawyer

我有一些代码可以写

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

虽然同事会写出来

GenericClass<Foo> foos = new GenericClass();

认为在这种情况下钻石操作员不会添加任何内容。

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

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

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

5 个答案:

答案 0 :(得分:11)

对于两个ArrayList的实例化,一个在结尾处有钻石操作符而另一个没有... {/ p>

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>

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

但是,如果使用第二种方法,编译器将生成未经检查的警告。因此,第二种方法确实没有价值;您正在做的就是使用编译器生成错误的未经检查的警告,这会增加项目的噪音。


我已经设法演示了这样做的场景,其中主动有害。正式名称是heap pollution。您的代码库中This is not something that you want to occur,只要您看到此类调用,就应将其删除。

考虑这个类,它扩展了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);
        }
    }
}

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

然而,如果我们正在玩raw types ......这样做可能会产生一些不幸的副作用。

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

System.out.println(oops);

这会打印[[], 2, 3]而不是抛出任何异常。如果我们想对此列表中的所有Integer执行操作,那么我们会遇到ClassCastException,这要归功于令人愉快的ArrayList.class调用。

当然,如果添加钻石操作员,所有这一切都可以避免,这将保证我们不会有这样的情况。

现在,因为我们已经在混合中引入了原始类型,所以Java无法根据JLS 4.12.2执行类型检查:

  

例如,代码:

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

产生编译时未经检查的警告,因为它不是   可以在编译时(在...范围内)确定   编译时类型检查规则)或在运行时,是否   变量l确实引用了List<String>

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

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

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

Don't use raw types,mmkay?

答案 1 :(得分:3)

在这一点上,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保持值为真。

实例平等测试(==)表示xy共享完全相同的Class对象。

现在使用钻石操作员并且没有。

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

同样,ctrued也是如此。

因此,正如我所理解的那样,如果您在运行时或在使用钻石之间的字节码中是否存在某些差异,那么答案很简单。没有区别。

在这种情况下使用钻石操作员是否更好是一种风格和观点。

P.S。不要射击信使。在这种情况下,我总是使用钻石操作符。但那只是因为我喜欢编译器为我做的事情一般w / r / t泛型,我不想陷入任何坏习惯。

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

答案 2 :(得分:1)

如果您的通用参数有限,则可能会遇到默认构造函数问题。例如,这里是跟踪总和的数字列表的草率和不完整的实现:

public class NumberList<T extends Number> extends AbstractList<T> {
    List<T> list = new ArrayList<>();
    double sum = 0;

    @Override
    public void add(int index, T element) {
        list.add(index, element);
        sum += element.doubleValue();
    }

    @Override
    public T remove(int index) {
        T removed = list.remove(index);
        sum -= removed.doubleValue();
        return removed;
    }

    @Override
    public T get(int index) {
        return list.get(index);
    }

    @Override
    public int size() {
        return list.size();
    }

    public double getSum() {
        return sum;
    }
}

省略默认构造函数的泛型参数可能会在运行时导致ClassCastException

List<String> list = new NumberList(); // compiles with warning and runs normally
list.add("test"); // throws CCE

但是添加菱形运算符会产生编译时错误:

List<String> list = new NumberList<>(); // error: incompatible types
list.add("test");

答案 3 :(得分:1)

在您的具体示例中:是的,它们是相同的。

一般来说:注意,它们可能不是!

原因是当使用原始类型时,可能会调用不同的重载构造函数/方法; 不仅您可以获得更好的类型安全性并避免运行时ClassCastException

重载的构造函数

public class Main {

    public static void main(String[] args) {
        Integer anInteger = Integer.valueOf(1);
        GenericClass<Integer> foosRaw = new GenericClass(anInteger);
        GenericClass<Integer> foosDiamond = new GenericClass<>(anInteger);
    }

    private static class GenericClass<T> {

        public GenericClass(Number number) {
            System.out.println("Number");
        }

        public GenericClass(T t) {
            System.out.println("Parameter");
        }
    }
}

带菱形的版本调用不同的构造函数;上述程序的输出是:

Number
Parameter

重载方法

public class Main {

    public static void main(String[] args) {
        method(new GenericClass());
        method(new GenericClass<>());
    }

    private static void method(GenericClass<Integer> genericClass) {
        System.out.println("First method");
    }

    private static void method(Object object) {
        System.out.println("Second method");
    }

    private static class GenericClass<T> { }
}

带钻石的版本调用不同的方法;输出:

First method
Second method

答案 4 :(得分:0)

这不是一个完整的答案 - 但确实提供了一些细节。

虽然你无法区分像

这样的电话
GenericClass<T> x1 = new GenericClass<>();
GenericClass<T> x2 = new GenericClass<T>();
GenericClass<T> x3 = new GenericClass();

有一些工具可以让你区分

GenericClass<T> x4 = new GenericClass<T>() { };
GenericClass<T> x5 = new GenericClass() { };

注意:虽然看起来我们缺少new GenericClass<>() { },但它当前不是有效的Java。

关键是有关通用参数的类型信息是为匿名类存储的。特别是我们可以通过

获取通用参数
Type superclass = x.getClass().getGenericSuperclass();
Type tType = (superclass instanceof ParameterizedType) ?
             ((ParameterizedType) superclass).getActualTypeArguments()[0] : 
             null;
  • 对于x1x2x3 tType将是TypeVariableImpl的实例(在所有三种情况下都是相同的实例,因为getClass()为所有三种情况返回相同的对象,所以并不奇怪。

  • x4 tType将为T.class

  • 对于x5 getGenericSuperclass(),不会返回ParameterizedType的实例,而是返回Class(事实GenericClass.class

    < / LI>

然后我们可以用它来确定我们的对象是通过(x1,x2还是x3)还是x4或x5构建的。