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

Я прочитала Твитнуть сегодня, который сказал:

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

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

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

10 ответов


Стирание Типа Хорошо

давайте придерживаться фактов

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

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

Я вам огромные преимущества (например, parametricity) и нулевой стоимости (предполагаемой стоимости-это предел фантазии).

new T-это сломанная программа. Это изоморфно утверждению: "все предложения истинны.- Я в этом не силен.

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

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

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

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

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

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

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

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

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

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

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

часто цитируемой статье о parametricity составляет теоремы бесплатно!. Что интересно в этой статье, так это то, что только из подписи типа мы можем доказать некоторые очень интересные инварианты. Если бы мы написали автоматические тесты для этих инварианты мы бы зря тратили время. Например, для 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. Или, возможно, вы пропустите значение null и перейдете прямо к созданию исключения (к которому, похоже, приводят значения null). Поскольку наш язык является полным Тьюрингом, невозможно рассуждать о том, какие вызовы broken будет включите "безопасные" типы с конструкторами no-arg, а какие-нет. Мы потеряли уверенность, что наша программа работает повсеместно.

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

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

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


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

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

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

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

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

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

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

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


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


весьма спорно, заслуживает ли процесс, выполняемый на 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 type: проверка типа среды выполнения никогда не выполняется или даже возможна, и выполнение всегда абсолютно безопасно для типа. это что стирания типа выглядит так: вы не даже знаю, что он там.


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

new T-это сломанная программа. Это изоморфно утверждению: "все предложения истинны.- Я в этом не силен.

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

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

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


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

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

в целом, в то время как можно достоверно заявить: "стирание типа-это путь в языковом дизайне", позиции, поддерживающие стирание типа в Java, неуместны просто потому, что слишком, слишком поздно для это



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

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


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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

как прокомментировали другие, doing new T() может быть сделано только через отражение, убедившись в параметрах, требуемых конструктором. C# позволяет делать new T() только если вы принуждаете T в конструктор без параметров. Если T не уважает это ограничение, a ошибка компиляции поднимается.

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


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

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

этот класс затем может делать все, что было бы достижимо по умолчанию, если бы Java не использовал erasure: он может выделить new Ts (предполагая T имеет конструктор, который соответствует шаблону, который он ожидает использовать), или массивы Ts, оно может динамически испытать на времени выполнения если конкретный объект 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);
     }
}

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