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

Создание общего массива в Java с помощью непроверенного типа

Если у меня есть общий класс Foo<Bar>, мне не разрешено создавать массив следующим образом:

Bar[] bars = new Bar[];

(Это приведет к ошибке "Невозможно создать общий массив Bar" ).

Но, как было предложено dimo414 в ответе на этот вопрос (Java: как создать Generic Array), я могу сделать следующее:

Bar[] bars = (Bar[]) new Object[];

(Это будет "только" генерировать предупреждение: "Тип безопасности: снят флажок с объекта [] на бар []" ).

В комментариях к ответам dimo414 некоторые люди утверждают, что использование этой конструкции может вызвать проблемы в определенных ситуациях, а другие говорят, что это нормально, поскольку единственная ссылка на массив - bars, которая уже имеет нужный тип.

Я немного запутался, в каких случаях это нормально, и в каких случаях это может вызвать у меня проблемы. Комментарии newacct и Aaron McDaid, например, кажутся прямо противоречащими друг другу. К сожалению, поток комментариев в исходном вопросе просто заканчивается без ответа "Почему это" уже не правильно "?", Поэтому я решил задать для него новый вопрос:

Если bars -array только когда-либо содержит записи типа Bar, могут ли быть какие-либо проблемы во время выполнения при использовании массива или его записей? Или это единственная опасность, что во время выполнения я мог технически применить массив к чему-то другому (например, String[]), что позволило бы мне заполнить его значениями, отличными от Bar?

Я знаю, что вместо Array.newInstance(...) можно использовать Array.newInstance(...), но я специально интересуюсь конструкцией литья типов выше, поскольку, например, в GWT newInstance(...) -option недоступен.

4b9b3361

Ответ 1

Поскольку я упоминался в вопросе, я буду звонить.

В принципе, это не вызовет никаких проблем, если вы не выставите эту переменную массива вне класса. (вроде как, что происходит в Вегасе, остается в Вегасе.)

Фактический тип времени выполнения массива Object[]. Поэтому включение его в переменную типа Bar[] является фактически "ложью", поскольку Object[] не является подтипом Bar[] (если Object не является Bar). Однако эта ложь в порядке, если она остается внутри класса, поскольку Bar стирается до Object внутри класса. (Нижняя граница Bar равна Object в этом вопросе. В случае, когда нижняя граница Bar является чем-то другим, замените все вхождения Object в этом обсуждении тем, что эта оценка.) Однако, если эта ложь каким-то образом подвергается внешней (самый простой пример возвращает переменную bars непосредственно как type Bar[]), тогда это вызовет проблемы.

Чтобы понять, что происходит на самом деле, поучительно смотреть на код с и без дженериков. Любая программа generics может быть переписана в эквивалентную программу без генериков, просто путем удаления дженериков и вставки бросков в нужном месте. Это преобразование называется стиранием типа.

Рассмотрим простую реализацию Foo<Bar> с методами получения и установки определенных элементов в массиве, а также метод для получения всего массива:

class Foo<Bar> {
    Bar[] bars = (Bar[])new Object[5];
    public Bar get(int i) {
        return bars[i];
    }
    public void set(int i, Bar x) {
        bars[i] = x;
    }
    public Bar[] getArray() {
        return bars;
    }
}

// in some method somewhere:
Foo<String> foo = new Foo<String>();
foo.set(2, "hello");
String other = foo.get(3);
String[] allStrings = foo.getArray();

После стирания типа это становится:

class Foo {
    Object[] bars = new Object[5];
    public Object get(int i) {
        return bars[i];
    }
    public void set(int i, Object x) {
        bars[i] = x;
    }
    public Object[] getArray() {
        return bars;
    }
}

// in some method somewhere:
Foo foo = new Foo();
foo.set(2, "hello");
String other = (String)foo.get(3);
String[] allStrings = (String[])foo.getArray();

Итак, в классе больше нет. Однако в вызывающем коде есть отбрасывания - при получении одного элемента и получении всего массива. Бросок, чтобы получить один элемент, не должен терпеть неудачу, потому что единственное, что мы можем поместить в массив, - это Bar, поэтому единственное, что мы можем получить, также Bar. Тем не менее, при передаче всего массива это приведет к сбою, так как массив имеет фактический тип времени выполнения Object[].

Написано не в общем, то, что происходит, и проблема становится намного более очевидной. Особенно беспокоит то, что отказ от бросания не происходит в классе, где мы написали актерский состав в generics - это происходит в чужом коде, который использует наш класс. И этот код другого человека абсолютно безопасен и невиновен. Это также не происходит в то время, когда мы сделали свой бросок в коде generics - это происходит позже, когда кто-то вызывает getArray() без предупреждения.

Если бы у нас не было этого метода getArray(), этот класс был бы безопасным. С помощью этого метода это небезопасно. Какая характеристика делает ее небезопасной? Он возвращает bars как тип Bar[], который зависит от "ложности", которую мы сделали ранее. Поскольку ложь не соответствует действительности, она вызывает проблемы. Если бы метод вместо этого возвращал массив как тип Object[], тогда он был бы безопасным, так как он не зависит от "лжи".

Люди скажут вам, что вы не делаете такого актера, как это, потому что оно вызывает исключения исключений в неожиданных местах, как показано выше, а не в исходном месте, где был отмечен неконтролируемый актерский состав. Компилятор не предупредит вас, что getArray() небезопасен (потому что с его точки зрения, учитывая типы, которые вы ему сказали, это безопасно). Таким образом, он зависит от того, чтобы программист был усердным в отношении этой ловушки и не использовал его небезопасным способом.

Однако я бы сказал, что на практике это не очень важно. Любой хорошо продуманный API никогда не будет выставлять внутренние переменные экземпляра снаружи. (Даже если есть метод для возврата содержимого в виде массива, он не будет возвращать внутреннюю переменную напрямую, он будет копировать его, чтобы предотвратить внешний код от изменения массива напрямую.) Таким образом, никакой метод не будет реализован, как getArray() во всяком случае.

Ответ 2

В отличие от списков типы массивов Java переопределяются, что означает, что тип выполнения Object[] отличается от String[]. Поэтому, когда вы пишете

Bar[] bars = (Bar[]) new Object[];

вы создали массив типа времени выполнения Object[] и "отбросили" его до Bar[]. Я говорю "cast" в кавычках, потому что это не реальная операция с проверкой: это всего лишь директива времени компиляции, которая позволяет вам назначить Object[] в переменной типа Bar[]. Естественно, это открывает двери для всех типов ошибок типа времени выполнения. Будет ли это на самом деле создавать ошибки, полностью зависит от вашего мастерства программирования и внимательности. Поэтому, если вы это чувствуете, тогда все в порядке; если вы этого не сделаете или этот код является частью более крупного проекта со многими разработчиками, то это опасно.

Ответ 3

Хорошо, я немного играл с этой конструкцией, и это может быть РЕАЛЬНЫЙ беспорядок.

Я думаю, что ответ на мой вопрос: все работает нормально, пока вы всегда обрабатываете массив как общий. Но как только вы попытаетесь обработать его не-общим образом, есть проблемы. Позвольте мне привести несколько примеров:

  • Внутри Foo<Bar> я могу создать массив, как показано, и работать с ним просто отлично. Это потому, что (если я правильно понимаю) компилятор "стирает" тип Bar и просто превращает его в Object всюду. Таким образом, внутри Foo<Bar> вы просто обрабатываете Object[], что хорошо.
  • Но если у вас есть такая функция внутри Foo<Bar>, которая предоставляет доступ к массиву:

    public Bar[] getBars(Bar bar) {
        Bar[] result = (Bar[]) new Object[1];
        result[0] = bar;
        return result;
    }
    

    вы можете столкнуться с серьезными проблемами, если используете их где-то еще. Вот несколько примеров сумасшествия (большинство из них на самом деле имеет смысл, но с первого взгляда кажется сумасшедшим):

    • String[] bars = new Foo<String>().getBars("Hello World");

      вызовет java.lang.ClassCastException: [Ljava.lang.Object; не может быть применено к [Ljava.lang.String;

    • for (String bar: new Foo<String>().getBars("Hello World"))

      также приведет к тому же java.lang.ClassCastException

    • но

      for (Object bar: new Foo<String>().getBars("Hello World"))
          System.out.println((String) bar);
      

      работает...

    • Вот тот, который для меня не имеет смысла:

      String bar = new Foo<String>().getBars("Hello World")[0];
      

      вызовет исключение java.lang.ClassCastException, даже если я не назначаю его в String [] где угодно.

    • Даже

      Object bar = new Foo<String>().getBars("Hello World")[0];
      

      приведет к тому же java.lang.ClassCastException!

    • Только

      Object[] temp = new Foo<String>().getBars("Hello World");
      String bar = (String) temp[0];
      

      работает...

    и none из них бросают любые ошибки времени компиляции, кстати.

  • Теперь, если у вас есть еще один родовой класс:

    class Baz<Bar> {
        Bar getFirstBar(Bar bar) {
            Bar[] bars = new Foo<Bar>().getBars(bar);
            return bars[0];
        }
    }
    

    Следующее работает отлично:

    String bar = new Baz<String>().getFirstBar("Hello World");
    

Большая часть этого имеет смысл, как только вы осознаете, что после стирания типа функция getBars(...) фактически возвращает Object[], независимо от Bar. Вот почему вы не можете (во время выполнения) присваивать возвращаемое значение String[] без генерации исключения, даже если Bar был установлен как String. Почему это мешает вам индексировать массив без предварительного отбрасывания его обратно в Object[]. Причина, по которой все работает в классе Baz<Bar>, заключается в том, что Bar[] также будет преобразован в Object[], независимо от Bar. Таким образом, это было бы эквивалентно тому, чтобы передать массив в Object[], затем проиндексировать его, а затем вернуть возвращенный элемент обратно в String.

В целом, увидев это, я определенно убежден, что использование этого метода для создания массивов - очень плохая идея, если вы никогда не вернете массив в любом месте вне вашего общего класса. Для моих целей я буду использовать Collection<...> вместо массива.

Ответ 4

Все работает нормально, пока вы не захотите использовать что-то в этом массиве, как это было бы типа Bar (или любого типа, который вы инициализируете общий класс), а не как Object, который он действительно есть. Например, имея метод:

<T> void init(T t) {
    T[] ts = (T[]) new Object[2];
    ts[0] = t;
    System.out.println(ts[0]);
}

похоже, работает нормально для всех типов. Если вы измените его на:

<T> T[] init(T t) {
    T[] ts = (T[]) new Object[2];
    ts[0] = t;
    System.out.println(ts[0]);
    return ts;
}

и назовите его

init("asdf");

он все еще работает нормально; но если вы хотите действительно использовать реальный массив T [] (который должен быть String [] в приведенном выше примере):

String[] strings = init("asfd");

тогда у вас есть проблема, так как Object[] и String[] - это два разных класса, а у вас есть Object[], поэтому вызывается ClassCastException.

Проблема возникает быстрее, если вы попробуете ограниченный общий тип:

<T extends Runnable> void init(T t) {
    T[] ts = (T[]) new Object[2];
    ts[0] = t;
    System.out.println(ts[0]);
    ts[0].run();
} 

Как хорошая практика, старайтесь избегать использования дженериков и массивов, так как они не очень хорошо сочетаются вместе.

Ответ 5

Листинг:

  Bar[] bars = (Bar[]) new Object[];

- операция, которая будет выполняться во время выполнения. Если тип выполнения Bar[] - это нечто иное, чем Object[], тогда это приведет к созданию ClassCastException.

Следовательно, если вы поместите границы на Bar, как в <Bar extends Something>, это не удастся. Это связано с тем, что тип выполнения Bar будет Something. Если Bar не имеет верхних границ, то тип будет стираться до Object, и компилятор сгенерирует все соответствующие приведения для размещения объектов в массиве или для чтения из него.

Если вы попытаетесь назначить bars чему-то с типом среды выполнения, который не является Object[] (например, String[] z = bars), операция перестанет работать. Компилятор предупреждает вас об этом вопросе с предупреждением "непроверенный бросок". Таким образом, следующее произойдет, даже если оно скомпилировано с предупреждением:

class Foo<Bar> {
    Bar[] get() {
       return (Bar[])new Object[1];
    }
}
void test() {
    Foo<String> foo = new Foo<>();
    String[] z = foo.get();
}