我有一些代码可以写
GenericClass<Foo> foos = new GenericClass<>();
虽然同事会写出来
GenericClass<Foo> foos = new GenericClass();
认为在这种情况下钻石操作员不会添加任何内容。
我知道实际使用与泛型类型相关的参数的构造函数可能会导致<>
的编译时错误,而不是原始情况下的运行时错误。并且编译时错误要好得多。 (如this question中所述)
我也非常清楚编译器(和IDE)可以生成将原始类型分配给泛型的警告。
问题在于没有参数或没有与泛型类型相关的参数的情况。在这种情况下,构造对象GenericClass<Foo> foos
是否可以根据使用的构造函数而有所不同,或者Javas类型擦除是否保证它们是相同的?
答案 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保持值为真。”
实例平等测试(==
)表示x
和y
共享完全相同的Class对象。
现在使用钻石操作员并且没有。
Vector<Integer> z = new Vector<>();
Vector<Integer> w = new Vector();
boolean c = z.getClass() == w.getClass();
boolean d = y.getClass() == z.getClass();
同样,c
为true
,d
也是如此。
因此,正如我所理解的那样,如果您在运行时或在使用钻石之间的字节码中是否存在某些差异,那么答案很简单。没有区别。
在这种情况下使用钻石操作员是否更好是一种风格和观点。
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;
对于x1
,x2
和x3
tType
将是TypeVariableImpl
的实例(在所有三种情况下都是相同的实例,因为getClass()
为所有三种情况返回相同的对象,所以并不奇怪。
x4
tType
将为T.class
对于x5
getGenericSuperclass()
,不会返回ParameterizedType
的实例,而是返回Class
(事实GenericClass.class
)
然后我们可以用它来确定我们的对象是通过(x1,x2还是x3)还是x4或x5构建的。