Подтвердить что ты не робот

Можно ли отличить результаты оператора алмаза от исходного конструктора?

У меня есть код, который я бы написал

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

Пока коллега напишет его

GenericClass<Foo> foos = new GenericClass();

утверждая, что в этом случае алмазный оператор ничего не добавляет.

Я знаю, что конструкторы, которые фактически используют аргументы, относящиеся к родовому типу, могут вызвать ошибку времени компиляции с <> вместо ошибки времени выполнения в сыром случае. И что ошибка времени компиляции намного лучше. (Как указано в этом вопросе)

Я также прекрасно понимаю, что компилятор (и IDE) может генерировать предупреждения для назначения типов raw для дженериков.

Вопрос заключается в том, что для аргументов нет аргументов или нет аргументов, связанных с родовым типом. В этом случае существует ли способ, по которому построенный объект GenericClass<Foo> foos может различаться в зависимости от того, какой конструктор использовался, или же стирание типа Javas гарантирует, что они идентичны?

4b9b3361

Ответ 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.