Являются ли дженерики Java действительно неуклюжими? Зачем? - программирование
Подтвердить что ты не робот

Являются ли дженерики Java действительно неуклюжими? Зачем?

Понесите меня на некоторое время. Я знаю, что это звучит субъективно и аргументировано какое-то время, но я клянусь, что в конце есть вопросительный знак, и на вопрос можно ответить на объективный вопрос...

Исходя из фона .NET и С#, я в последние годы был испорчен синтаксическим сахаром, который генерирует вместе с методами расширения во многих решениях .NET для общих проблем. Одной из ключевых особенностей, которые делают С# generics настолько чрезвычайно мощным, является тот факт, что, если в информации достаточно информации, компилятор может вывести аргументы типа, поэтому мне почти никогда не приходится их выписывать. Вам не нужно писать много строк кода, прежде чем вы поймете, сколько нажатий клавиш вы сэкономите на этом. Например, я могу написать

var someStrings = new List<string>();
// fill the list with a couple of strings...
var asArray = someStrings.ToArray();

и С# будет просто знать, что я имею в виду, что первый var должен быть List<string>, второй - string[] и что .ToArray() действительно .ToArray<string>().

Затем я перехожу на Java.

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

 ArrayList<String> someStrings = new ArrayList<String>();
 // fill the list with a couple of strings...
 String[] asArray = someStrings.toArray(new String[0]); // <-- HERE!

Зачем мне создавать новый string[], без каких-либо элементов в нем, который не будет использоваться ни для чего, для компилятора Java знать, что это string[] и не любой другой тип массива, который я хочу?

Я понимаю, что так выглядит перегрузка, и что toArray() в настоящее время возвращает Object[]. Но почему было принято это решение, когда эта часть Java была изобретена? Почему этот дизайн лучше, чем, скажем, пропустить .ToArray(), который полностью перегружает Object[] и имеет только toArray(), который возвращает T[]? Является ли это ограничением в компиляторе или в воображении дизайнеров этой части фреймворка или что-то еще?

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

4b9b3361

Ответ 1

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

Полностью законно объявлять метод List<T> иметь подпись T[] toArray(). Однако не существует способа правильно реализовать такой метод. (Почему бы вам не попробовать это упражнение?)

Имейте в виду, что:

  • Массивы знают во время выполнения тип компонента, с которым они были созданы. Вставки в массив проверяются во время выполнения. При проверке во время выполнения проверяются отбрасывания от более общих типов массивов до более конкретных типов массивов. Чтобы создать массив, вы должны знать тип компонента во время выполнения (используя new Foo[x] или используя Array.newInstance()).
  • Объекты общих (параметризованных) типов не знают параметры типа, с которыми они были созданы. Параметры типа стираются до их стирания (нижняя граница), и только те проверяются во время выполнения.

Поэтому вы не можете создать массив типа типа параметра типа, т.е. new T[...].

Фактически, если у списков был метод T[] toArray(), тогда возможно создание общего массива (new T[n]), которое невозможно в настоящее время:

List<T> temp = new ArrayList<T>();
for (int i = 0; i < n; i++)
    temp.add(null);
T[] result = temp.toArray();
// equivalent to: T[] result = new T[n];

Generics - это просто синтаксический сахар с компиляцией. Дженерики могут быть добавлены или удалены с изменением нескольких деклараций и добавлением отбросов и т.д., Не затрагивая фактическую логику реализации кода. Давайте сравним API API 1.4 и API 1.5:

1.4 API:

Object[] toArray();
Object[] toArray(Object[] a);

Здесь у нас есть только объект List. Первый метод имеет объявленный тип возврата Object[], и он создает объект класса времени выполнения Object[]. (Помните, что для переменных типа и времени выполнения (динамические) типы компиляции (статические) - это разные вещи.)

Во втором методе предположим, что мы создаем объект String[] (т.е. new String[0]) и передаем это ему. Массивы имеют отношение подтипирования, основанное на подтипировании их типов компонентов, поэтому String[] является подклассом Object[], поэтому это находит. Здесь самое главное отметить, что он возвращает объект класса времени выполнения String[], хотя его объявленный тип возврата Object[]. (Опять же, String[] является подтипом Object[], поэтому это не является чем-то необычным.)

Однако, если вы попытаетесь применить результат первого метода к типу String[], вы получите исключение класса, потому что, как отмечалось ранее, его фактический тип времени выполнения Object[]. Если вы передадите результат второго метода (при условии, что вы прошли через String[]) на String[], он будет успешным.

Поэтому, даже если вы не заметите его (оба метода, похоже, возвращают Object[]), уже существует существенное принципиальное отличие в фактическом возвращенном объекте в предгенерации между этими двумя методами.

1.5 API:

Object[] toArray();
T[] toArray(T[] a);

То же самое происходит здесь. Generics добавляет некоторые интересные вещи, такие как проверка типа аргумента второго метода во время компиляции. Но основные принципы все те же: первый метод создает объект, реальный тип выполнения которого Object[]; а второй метод создает объект, реальный тип выполнения которого совпадает с массивом, в котором вы проходили.

На самом деле, если вы попытаетесь передать массив, класс которого на самом деле является подтипом T[], скажем U[], хотя у нас есть List<T>, угадайте, что он будет делать? Он попытается поместить все элементы в массив U[] (что может быть успешным (если все элементы имеют тип U)), или сбой (если нет)) возвращает объект, фактический тип которого U[].

Итак, вернемся к моей точке раньше. Почему вы не можете сделать метод T[] toArray()? Поскольку вы не знаете тип массива, который хотите создать (используя new или Array.newInstance()).

T[] toArray() {
    // what would you put here?
}

Почему вы не можете просто создать new Object[n], а затем перевести его на T[]? Он не будет сбой сразу (поскольку T стирается внутри этого метода), но когда вы пытаетесь вернуть его снаружи; и предполагая, что внешний код запрашивает конкретный тип массива, например. String[] strings = myStringList.toArray();, это вызовет исключение, потому что там неявный отброс от дженериков.

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

Ответ 2

Часть toArray(String[]) существует потому, что два метода toArray существовали до того, как дженерики были введены в Java 1.5. Тогда не было способа вывести аргументы типа, потому что они просто не существовали.

Java довольно большой по сравнению с обратной совместимостью, поэтому почему эта конкретная часть API неуклюжаема.

Вся вещь стирания стилей также необходима для сохранения обратной совместимости. Код, скомпилированный для 1.4, может с радостью взаимодействовать с более новым кодом, который содержит дженерики.

Да, это неуклюже, но, по крайней мере, это не сломало огромную базу Java-кода, которая существовала при введении дженериков.

EDIT: Итак, для справки, API 1.4:

Object[] toArray();
Object[] toArray(Object[] a);

и 1.5 API:

Object[] toArray();
T[] toArray(T[] a);

Я не уверен, почему было хорошо изменить подпись версии 1-arg, но не версию 0-arg. Кажется, это было бы логичным изменением, но, может быть, там была какая-то сложность, которую мне не хватает. Или, может быть, они просто забыли.

EDIT2: На мой взгляд, в таких случаях, как этот Java должен использовать аргументы inferred type, если таковые имеются, и явный тип, в котором доступный тип недоступен. Но я подозреваю, что было бы сложно включить в язык.

Ответ 3

Другие ответили на вопрос "почему" (я предпочитаю ответ Камерона Скиннера), я просто добавлю, что вам не нужно создавать экземпляр нового массива каждый раз, и он не должен быть пустым. Если массив достаточно велик для хранения коллекции, он будет использоваться как возвращаемое значение. Таким образом:

String[] asArray = someStrings.toArray(new String[someStrings.size()])

будет выделять только один массив правильного размера и заполнять его элементами из коллекции.

Кроме того, некоторые библиотеки библиотек коллекций Java включают в себя статически определенные пустые массивы, которые можно безопасно использовать для этой цели. См. Например, Apache Commons ArrayUtils.

Edit:

В приведенном выше коде экземпляр массива эффективно мусор, когда коллекция пуста. Поскольку массивы не могут быть изменены в Java, пустые массивы могут быть одноточечными. Таким образом, использование постоянного пустого массива из библиотеки, вероятно, немного более эффективно.

Ответ 4

Это из-за стирания типа. См. Проблемы с стиранием типов в статье Википедии о генерических машинах Java: информация об общем типе доступна только во время компиляции, она полностью лишена компилятором и отсутствует во время выполнения.

Итак, toArray нужен другой способ выяснить, какой тип массива будет возвращен.

Пример, приведенный в этой статье в Википедии, достаточно иллюстративен:

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass())             // evaluates to true <<==
  System.out.println("Equal");

Ответ 5

Согласно Нилу Гафтеру, у SUN просто не хватало ресурсов, подобных MS.

SUN вытолкнул Java 5 с стиранием типа, потому что создание общей информации типа, доступной для выполнения, означает гораздо большую работу. Они больше не могли задерживаться.

Примечание: стирание типа не требуется для обратной совместимости - эта часть обрабатывается необработанными типами, что является другой концепцией из повторяющихся типов. У Java все еще есть возможность удалить стирание стилей, т.е. Сделать все типы повторно доступными; однако он не считается достаточно срочным, чтобы быть в любой предсказуемой повестке дня.

Ответ 6

Java-дженерики - это действительно то, чего не существует. Это только синтаксический (сахара или, скорее, гамбургер?), Который обрабатывается только компилятором. Это на самом деле только короткое (?) Сокращение от класса, поэтому, если вы посмотрите на байт-код, вы можете быть сначала немного удивлены... Тип стирания, как отмечено в сообщении выше.

Этот ярлык для классного каста показался хорошей идеей при работе над коллекциями. В одно время вы могли бы хотя бы объявить, какой тип элемента вы храните, и механизм, независимый от программиста (компилятор), проверяет это. Однако, используя рефлексию, вы все равно можете поместить в такую ​​коллекцию, что хотите:)

Но когда вы делаете общую стратегию, которая работает на generic bean, и она помещается в общий сервис, генерируемый (?) из обеих bean и стратегии, принимая общий приемник для общего bean и т.д., вы общий ад. Я когда-то закончил с четырьмя (!) Типовыми типами, указанными в декларации, и когда я понял, что мне нужно больше, я решил не генерировать весь код, потому что у меня возникли проблемы с соблюдением общих типов.

А что касается оператора алмаза... Вы можете пропустить алмаз и получить точно такой же эффект, компилятор выполнит те же проверки и сгенерирует тот же код. Я сомневаюсь, что в следующих версиях java это изменится, из-за этой необходимости обратной совместимости... Итак, еще одна вещь, которая почти ничего не дает, где Java имеет гораздо больше проблем для решения, например. экстремальное неудобство при работе с типами даты и времени....

Ответ 7

Я не могу говорить с проектными решениями команды JDK, но многие неуклюжие характеры дженериков Java исходят из того, что дженерики были частью Java 5 (1.5 - независимо). В JDK существует множество методов, которые страдают от попытки сохранить обратную совместимость с API-интерфейсами, которые были датированы 1.5.

Что касается громоздкого синтаксиса List<String> strings = new ArrayList<String>(), я ставлю его, чтобы сохранить педантичный характер языка. Декларация, а не вывод, - это порядок продвижения Java.

strings.toArray( new String[0] );? Тип не может быть выведен, потому что сам массив считается общим классом (Array<String>, если он был открыт).

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