Можно ли отличить результаты оператора алмаза от исходного конструктора?
У меня есть код, который я бы написал
GenericClass<Foo> foos = new GenericClass<>();
Пока коллега напишет его
GenericClass<Foo> foos = new GenericClass();
утверждая, что в этом случае алмазный оператор ничего не добавляет.
Я знаю, что конструкторы, которые фактически используют аргументы, относящиеся к родовому типу, могут вызвать ошибку времени компиляции с <>
вместо ошибки времени выполнения в сыром случае. И что ошибка времени компиляции намного лучше. (Как указано в этом вопросе)
Я также прекрасно понимаю, что компилятор (и IDE) может генерировать предупреждения для назначения типов raw для дженериков.
Вопрос заключается в том, что для аргументов нет аргументов или нет аргументов, связанных с родовым типом. В этом случае существует ли способ, по которому построенный объект GenericClass<Foo> foos
может различаться в зависимости от того, какой конструктор использовался, или же стирание типа Javas гарантирует, что они идентичны?
Ответы
Ответ 1
Для экземпляров двух ArrayList
s, один с алмазным оператором в конце и один без...
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);
Отправляет [[], 2, 3]
вместо того, чтобы выбрасывать любое исключение. Если бы мы хотели сделать операцию во всех Integer
в этом списке, мы бы столкнулись с ClassCastException
, благодаря этому восхитительному вызову ArrayList.class
.
Конечно, все это можно было бы избежать, если бы был добавлен оператор бриллиантов, что гарантировало бы, что у нас не будет такого сценария в наших руках.
Теперь, поскольку мы ввели в микс тип raw, Java не может выполнить проверку типов для JLS 4.12.2:
Например, код:
List l = new ArrayList<Number>();
List<String> ls = l; // Unchecked warning
приводит к неконтролируемому предупреждению о компиляции, потому что это не можно установить либо во время компиляции (в рамках правила проверки типа компиляции) или во время выполнения, независимо от того, переменная l
действительно относится к a List<String>
.
Ситуация выше очень похожа; если мы посмотрим на первый пример, который мы использовали, все, что мы делаем, это не добавление дополнительной переменной в этот вопрос. Загрязнение кучи все равно.
List rawFooList = new ArrayList();
List<Integer> fooList = rawFooList;
Итак, хотя байт-код идентичен (вероятно, из-за стирания), факт остается фактом, что из объявления, подобного этому, может возникнуть другое или аберрантное поведение.
Не использовать необработанные типы, mmkay?
Ответ 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 удерживает значение true".
Тест на равенство равенства (==
) говорит, что оба x
и y
совместно используют точно тот же объект класса.
Теперь сделайте это с помощью оператора алмаза и без него.
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. Не стреляйте в посланника. В этом случае я всегда буду использовать алмазный оператор. Но это только потому, что мне нравится то, что компилятор делает для меня вообще в /r/t generics, и я не хочу впадать в какие-либо вредные привычки.
P.P.S. Не забывайте, что это может быть временным явлением. http://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.8 предупреждает нас, что" использование исходных типов в коде, написанном после введения дженериков в язык программирования Java, сильно обескуражено. возможно, что будущие версии языка программирования Java будут запрещать использование необработанных типов.
Ответ 3
У вас может возникнуть проблема с конструктором по умолчанию, если ваши общие аргументы ограничены. Например, здесь неаккуратная и неполная реализация списка чисел, который отслеживает общую сумму:
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");
Ответ 4
В вашем конкретном примере: Да, они идентичны.
Как правило: Остерегайтесь, они могут быть не такими!
Причина в том, что при использовании типа raw может быть вызван другой перегруженный конструктор/метод; это не только, что вы получаете лучшую безопасность типа и избегаете времени выполнения 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
Ответ 5
Это не полный ответ, но он дает несколько подробностей.
Пока вы не можете различать такие вызовы, как
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
(infact GenericClass.class
)
Затем мы могли бы использовать это, чтобы определить, был ли наш объект выполнен по (x1, x2 или x3) или x4 или x5.