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

Каковы преимущества стирания стилей Java?

Сегодня я прочитал твит, в котором говорилось:

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

Таким образом, мой вопрос:

Есть ли преимущества от стирания стилей Java? Какой технический или программный стиль приносит пользу (возможно), помимо предпочтений JVM, для обратной совместимости и производительности во время выполнения?

4b9b3361

Ответ 1

Тип Erasure Is Good

Будем придерживаться фактов

Многие ответы до сих пор чрезмерно связаны с пользователем Twitter. Полезно сосредоточиться на сообщениях, а не на посланниках. Существует довольно последовательное сообщение даже с приведенными выше выдержками:

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

Я получаю огромные преимущества (например, параметричность) и стоимость ноль (предполагаемая стоимость - это предел воображения).

new T является сломанной программой. Он изоморфен требованию "все утверждения верны". Я не большой в этом.

Цель: разумные программы

Эти твиты отражают точку зрения, которая не заинтересована в том, можем ли мы заставить машину что-то сделать, но еще больше, можем ли мы рассуждать о том, что машина будет делать то, что мы действительно хотим. Хорошее рассуждение является доказательством. Доказательства могут быть указаны в формальной нотации или что-то менее формальное. Независимо от языка спецификации, они должны быть четкими и строгими. Неформальные спецификации не являются правильными, но часто ошибочны в практическом программировании. В итоге мы получаем исправления, такие как автоматизированные и исследовательские тесты, чтобы компенсировать проблемы, которые мы имеем с неофициальными рассуждениями. Это не означает, что тестирование по сути является плохим, но цитируемый пользователь Twitter предлагает, что есть намного лучший способ.

Таким образом, наша цель состоит в том, чтобы иметь правильные программы, которые мы можем рассуждать четко и строго, таким образом, который соответствует тому, как машина будет фактически выполнять программу. Это, однако, не единственная цель. Мы также хотим, чтобы наша логика имела определенную степень выразительности. Например, только так мы можем выразить с пропозициональной логикой. Хорошо иметь универсальную (∀) и экзистенциальную (∃) количественную оценку от чего-то вроде логики первого порядка.

Использование систем типов для рассуждений

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

Это соответствие несколько глубокое. Мы можем принимать логические выражения и переводить их через соответствие типам. Тогда, если у нас есть программа с той же сигнатурой типа, которая компилируется, мы доказали, что логическое выражение универсально истинно (тавтология). Это связано с тем, что соответствие является двусторонним. Преобразование между типом/программой и миром теоремы/доказательства является механическим и во многих случаях может быть автоматизировано.

Карри-Ховард прекрасно играет в то, что мы хотели бы сделать со спецификациями для программы.

Являются ли типы систем полезными в Java?

Даже при понимании Карри-Говарда некоторым людям легко отбросить значение системы типов, когда она

  • чрезвычайно сложно работать с
  • соответствует (через Карри-Говард) логике с ограниченной выразительностью
  • нарушается (что доходит до характеристики систем как "слабых" или "сильных" ).

Что касается первого момента, возможно, IDE упрощают работу с системой типа Java (что очень субъективно).

Что касается второй точки, Java, похоже, почти соответствует логике первого порядка. Дженерики используют системный эквивалент универсальной количественной оценки. К сожалению, подстановочные знаки дают нам лишь небольшую часть экзистенциальной количественной оценки. Но универсальная количественная оценка - довольно хорошее начало. Приятно иметь возможность сказать, что функции для List<A> работают повсеместно для всех возможных списков, поскольку A полностью неограничен. Это приводит к тому, что говорит пользователь Twitter в отношении "параметричности".

Часто цитируемая статья о параметризме - это Филипп Вадлер Теоремы бесплатно!. Что интересно в этой статье, так это то, что только из одной только подписи типа мы можем доказать некоторые очень интересные инварианты. Если бы мы писали автоматизированные тесты для этих инвариантов, мы бы очень тратили наше время. Например, для List<A>, только от сигнатуры типа для flatten

<A> List<A> flatten(List<List<A>> nestedLists);

мы можем предположить, что

flatten(nestedList.map(l -> l.map(any_function)))
    ≡ flatten(nestList).map(any_function)

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

Не стирание может привести к нарушениям

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

  • побочные эффекты с внешней системой
  • отражение

Неиспользуемые дженерики во многом связаны с отражением. Без стирания есть информация о времени выполнения, которая выполняется с реализацией, которую мы можем использовать для разработки наших алгоритмов. Это означает, что статически, когда мы рассуждаем о программах, у нас нет полной картины. Отражение серьезно угрожает правильности любых доказательств, которые мы ставим статически. Не случайно совпадение также приводит к множеству сложных ошибок.

Итак, каковы способы "нечистых" дженериков "полезны"? Давайте рассмотрим использование, упомянутое в твитте:

<T> T broken { return new T(); }

Что произойдет, если T не имеет конструктора no-arg? На некоторых языках то, что вы получаете, равно null. Или, возможно, вы пропустите нулевое значение и сразу перейдете к созданию исключения (какие нулевые значения, похоже, все равно приводят). Поскольку наш язык является полным для Тьюринга, невозможно рассуждать о том, какие вызовы broken будут включать "безопасные" типы с конструкторами no-arg, а какие - нет. Мы потеряли уверенность в том, что наша программа работает повсеместно.

Стирание означает, что мы рассуждали (так что давайте стереть)

Поэтому, если мы хотим рассуждать о наших программах, нам настоятельно рекомендуется не использовать языковые функции, которые сильно угрожают нашим рассуждениям. Как только мы это сделаем, почему бы просто не отбрасывать типы во время выполнения? Они не нужны. Мы можем получить некоторую эффективность и простоту с удовлетворением, что никакие трансляции не сбой или что методы могут отсутствовать при вызове.

Стирание поощряет рассуждения.

Ответ 2

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

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

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

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

Erasure важно для поддержания свойства "параметричности" типа данных. Скажем, у меня есть тип "Список", параметризованный по типу T компонента, т.е. List <T> . Этот тип является предложением, что этот тип List работает одинаково для любого типа T. Тот факт, что T является абстрактным, неограниченным типом, означает, что мы ничего не знаем об этом типе, поэтому ему нечего делать для особых случаев T.

например. У меня есть List xs = asList ( "3" ). Я добавляю элемент: xs.add( "q" ). Я получаю [ "3", "q" ]. Поскольку это параметрическое, я могу предположить, что List xs = asList (7); xs.add(8) заканчивается [7,8] Я знаю по типу, что он не делает ничего для String и одно для Int.

Кроме того, я знаю, что функция List.add не может изобретать значения T из тонкого воздуха. Я знаю, что если мой asList ( "3" ) добавит к нему "7", единственные возможные ответы будут построены из значений "3" и "7". В списке нет возможности добавления "2" или "z", потому что функция не сможет ее построить. Ни одно из этих других значений не было бы разумным для добавления, и параметричность предотвращает создание этих неправильных программ.

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

Ответ 3

(Хотя я уже написал ответ здесь, пересматривая этот вопрос два года спустя, я понимаю, что есть другой, совершенно другой способ ответить на него, поэтому я оставляю прежний ответ неповрежденным и добавляю его.)суб >


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

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

Стирание типа Java не достигает этого: он искажает компилятор, как в этом примере:

void doStuff(List<Integer> collection) { 
}

void doStuff(List<String> collection) // ERROR: a method cannot have 
                   // overloads which only differ in type parameters

(Вышеуказанные два объявления сворачиваются в одну и ту же подпись метода после стирания.)

С другой стороны, среда выполнения может все еще проверять тип объекта и причину этого, но поскольку его понимание истинного типа искалечено стиранием, нарушения статического типа тривиальны для достижения и их трудно предотвратить.

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

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

(избыточность extends Object должна была быть добавлена ​​для сохранения обратной совместимости стираемой подписи.)

Теперь, имея в виду это, вернемся к цитате:

Это забавно, когда пользователи Java жалуются на стирание стилей, что является единственной вещью, которую получила Java.

Что именно получило Java? Это само слово, независимо от смысла? Для контраста взгляните на скромный тип int: проверка выполнения во время выполнения никогда не выполняется или даже возможна, и выполнение всегда идеально безопасно для типов. Вот как выглядит стирание стилей, когда все делается правильно: вы даже не знаете его там.

Ответ 4

Следующий пост того же пользователя в одном разговоре:

new T является сломанной программой. Он изоморфен требованию "все утверждения верны". Я не большой в этом.

(Это было сделано в ответ на выражение другого пользователя, а именно, что "кажется, в„новой Т“некоторых ситуациях будет лучше", идея в том, что new T() невозможно из-за типа стирания. (Это спорно Даже если T были доступны во время выполнения, это может быть абстрактный класс или интерфейс, или он может быть Void, или ему может не хватить конструктор no-arg, или его конструктор no-arg может быть закрытым (например, потому что он должен быть одноэлементным классом), или его конструктор no-arg может указывать проверенное исключение, которое генерируемый метод не обнаруживает или не указывает — но это было предпосылкой. Независимо от того, что без стирания вы могли бы по крайней мере напишите T.class.newInstance(), который обрабатывает эти проблемы.))

Это представление, что типы изоморфны предложениям, предполагает, что у пользователя есть фон в теории формального типа. (S) он, скорее всего, не любит "динамические типы" или "типы времени выполнения" и предпочел бы Java без downcasts и instanceof и отражения и так далее. (Подумайте о языке, таком как Standard ML, который имеет очень богатую (статическую) систему типов и динамическая семантика которой не зависит от какой-либо информации о типе.)

Стоит иметь в виду, кстати, что пользователь троллирует: в то время как он, по-видимому, искренне предпочитает (статически) типизированные языки, он не искренне пытается убедить других в этом взгляде. Скорее, основная цель оригинального твита заключалась в том, чтобы высмеять тех, кто не согласен, и после того, как некоторые из этих несогласие вошли в систему, пользователь опубликовал последующие твиты, такие как "причина, по которой у java стирание стилей является то, что Wadler и другие знают, что они делают, в отличие от пользователей java". К сожалению, это затрудняет выяснение того, что он на самом деле думает; но, к счастью, это также, вероятно, означает, что это не очень важно. Люди с фактической глубиной своих взглядов обычно не прибегают к троллям, которые совершенно не содержат содержания.

Ответ 5

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

Требование "стирание - это единственное, что получила Java", подразумевает утверждение о том, что "все языки, основанные на динамической диспетчеризации для аргумента функции времени исполнения, в корне ошибочны". Несмотря на то, что он, безусловно, является законным требованием, и который можно даже считать действительной критикой всех языков ООП, включая Java, он не может считаться ключевым моментом для оценки и критики функций в контексте Java, где полиморфизм во время выполнения является аксиоматическим.

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



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

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

Ответ 6

Хорошо, что нет необходимости менять JVM, когда были введены дженерики. Java реализует генерики только на уровне компилятора.

Ответ 7

Стирание типа причины - это хорошо, что вещи, которые он делает невозможными, вредны. Предотвращение проверки аргументов типа во время выполнения облегчает понимание и рассуждение о программах.

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

public List<Integer> XXX(final List<Integer> l);

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

public <T> List<T> XXX(final List<T> l);

Сколько реализаций этой функции существует? Поскольку реализация не может знать тип элементов, теперь можно исключить огромное количество реализаций: элементы нельзя комбинировать или добавлять в список или отфильтровывать, и др. Мы ограничены такими вещами, как: идентификация (без изменений в списке), удаление элементов или изменение списка. Эта функция проще рассуждать только на основе ее подписи.

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

Если у Java не было стирания типа (т.е. аргументы типа были подтверждены во время выполнения), то это просто разрешило бы более серьезное ослабление махинаций такого рода. В приведенном выше примере реализация может только нарушать ожидания, установленные сигнатурой типа, если список имеет хотя бы один элемент; но если T было подтверждено, оно могло бы сделать это, даже если список был пустым. Типы Reified просто увеличили (уже очень много) возможности для препятствования нашему пониманию кода.

Тип стирания делает язык менее "мощным". Но некоторые формы "власти" на самом деле вредны.

Ответ 8

Это не прямой ответ (OP спросил "каковы преимущества", я отвечаю "каковы недостатки")

По сравнению с системой типов С# стирание типов Java - настоящая боль для двух людей

Вы не можете реализовать интерфейс дважды

В С# вы можете безопасно реализовать IEnumerable<T1> и IEnumerable<T2>, особенно если два типа не имеют общего предка (т.е. их предок - Object).

Практический пример: в Spring Framework вы не можете реализовать ApplicationListener<? extends ApplicationEvent> несколько раз. Если вам нужно другое поведение на основе T, вам нужно протестировать instanceof

Вы не можете сделать новый T()

(для этого вам нужна ссылка на Class)

Как прокомментировали другие, выполнение эквивалента new T() может быть выполнено только с помощью отражения, только путем вызова экземпляра Class<T>, чтобы убедиться в параметрах, требуемых конструктором. С# позволяет вам делать new T() только, если вы ограничиваете T конструктором без параметров. Если T не соблюдает это ограничение, возникает ошибка компиляции.

В Java вам часто приходится писать методы, которые выглядят следующим образом:

public <T> T create(....params, Class<T> classOfT)
{

    ... whatever you do
    ... you will end up
    T = classOfT.newInstance();


    ... or more advanced reflection
    Constructor<T> parameterizedConstructorThatYouKnowAbout = classOfT.getConstructor(...,...);
}

Недостатки в приведенном выше коде:

  • Class.newInstance работает только с конструктором без параметров. Если ничего не доступно, ReflectiveOperationException генерируется во время выполнения
  • Отраженный конструктор не выделяет проблемы во время компиляции, как указано выше. Если вы проведете рефакторинг, то поменяйте аргументы, вы узнаете только во время выполнения

Если бы я был автором С#, я бы представил возможность указать одно или несколько ограничений конструктора, которые легко проверить во время компиляции (поэтому мне может потребоваться, например, конструктор с параметрами string,string). Но последний спекуляция

Ответ 9

Дополнительная точка, по-видимому, не рассматривалась ни в одном из других ответов: если вам действительно нужны дженерики с типизацией во время выполнения, вы можете реализовать ее самостоятельно следующим образом:

public class GenericClass<T>
{
     private Class<T> targetClass;
     public GenericClass(Class<T> targetClass)
     {
          this.targetClass = targetClass;
     }

Этот класс затем может выполнить все, что было бы возможно по умолчанию, если бы Java не использовала стирание: он может выделять новый T (при условии, что T имеет конструктор, который соответствует шаблону, который он ожидает использовать), или массивы T s, он может динамически тестироваться во время выполнения, если конкретный объект является T и изменяет поведение в зависимости от этого и т.д.

Например:

     public T newT () { 
         try {
             return targetClass.newInstance(); 
         } catch(/* I forget which exceptions can be thrown here */) { ... }
     }

     private T value;
     /** @throws ClassCastException if object is not a T */
     public void setValueFromObject (Object object) {
         value = targetClass.cast(object);
     }
}

Ответ 10

избегает С++ - как разбухание кода, потому что тот же код используется для нескольких типов; однако для стирания типа требуется виртуальная диспетчеризация, тогда как подход С++-code-bloat может выполнять не виртуально отправленные дженерики

Ответ 11

Большинство ответов больше касается философии программирования, чем реальных технических деталей.

И хотя этому вопросу уже более 5 лет, он все еще остается открытым: почему стирание типов желательно с технической точки зрения? В конце ответ довольно прост (на более высоком уровне): https://en.wikipedia.org/wiki/Type_erasure

C++ шаблоны не существуют во время выполнения. Компилятор выдает полностью оптимизированную версию для каждого вызова, что означает, что выполнение не зависит от информации о типе. Но как JIT работает с разными версиями одной и той же функции? Разве не было бы лучше иметь только одну функцию? Не хотелось бы, чтобы JIT оптимизировал все его версии. Ну, а как насчет безопасности типов? Угадай, что должно выйти из окна.

Но подождите секунду: как .NET это делает? Отражение! Таким образом, они должны оптимизировать только одну функцию, а также получить информацию о типе времени выполнения. И именно поэтому .NET дженерики раньше были медленнее (хотя они стали намного лучше). Я не утверждаю, что это не удобно! Но это дорого и не должно использоваться, когда это не является абсолютно необходимым (это не считается дорогим в динамически типизированных языках, потому что компилятор/интерпретатор все равно полагается на отражение).

Таким образом, общее программирование с удалением типа близко к нулю (некоторые проверки/приведение во время выполнения все еще необходимы): https://docs.oracle.com/javase/tutorial/java/generics/erasure.html