Почему новые методы java.util.Arrays в Java 8 не перегружены для всех примитивных типов?
Я рассматриваю изменения API для Java 8 и заметил, что новые методы в java.util.Arrays
не перегружены для всех примитивов. Методы, которые я заметил:
В настоящее время эти новые методы обрабатывают только int
, long
, а также double
примитивы.
int
, long
, а также double
являются, вероятно, наиболее широко используемыми примитивами, поэтому имеет смысл, что если бы им пришлось ограничить API, они бы выбрали эти три, но почему они должны были ограничивать API?
1 ответ
Чтобы ответить на вопросы в целом, а не только этот конкретный сценарий, я думаю, что мы все хотим знать....
Почему загрязнение интерфейса в Java 8
Например, в языке, таком как C#, есть набор предопределенных типов функций, принимающих любое количество аргументов с необязательным типом возврата ( Func и Action, каждый из которых имеет до 16 параметров разных типов). T1
, T2
, T3
,..., T16
), но в JDK 8 мы имеем набор различных функциональных интерфейсов с разными именами и именами методов, и чьи абстрактные методы представляют собой подмножество хорошо известных функций (т. е. нулевые, унарные, двоичные, троичные и т. д.), И затем мы имеем множество случаев, связанных с примитивными типами, и есть даже другие сценарии, вызывающие взрыв более функциональных интерфейсов.
Проблема стирания типа
Таким образом, в некотором роде оба языка страдают от некоторой формы загрязнения интерфейса (или делегирования загрязнения в C#). Разница лишь в том, что в C# все они имеют одинаковые имена. В Java, к сожалению, из-за стирания типов, нет никакой разницы между Function<T1,T2>
а также Function<T1,T2,T3>
или же Function<T1,T2,T3,...Tn>
Очевидно, что мы не могли просто назвать их одинаково, и нам пришлось придумывать креативные имена для всех возможных типов комбинаций функций.
Не думаю, что экспертная группа не боролась с этой проблемой. По словам Брайана Гетца в лямбда-рассылке:
[...] В качестве одного примера, давайте возьмем типы функций. У лямбда-соломника, предлагаемого на devoxx, были типы функций. Я настоял, чтобы мы удалили их, и это сделало меня непопулярным. Но я возражал против типов функций не потому, что мне не нравятся типы функций - я люблю типы функций - но что типы функций плохо боролись с существующим аспектом системы типов Java, стиранием. Стираемые типы функций являются худшими из обоих миров. Таким образом, мы удалили это из дизайна.
Но я не хочу сказать, что "у Java никогда не будет типов функций" (хотя я признаю, что у Java никогда не может быть типов функций.) Я считаю, что для того, чтобы перейти к типам функций, мы должны сначала иметь дело с стиранием. Это может или не может быть возможно. Но в мире усовершенствованных структурных типов функциональные типы начинают приобретать гораздо больший смысл [...]
Преимущество этого подхода состоит в том, что мы можем определять наши собственные типы интерфейсов с методами, принимающими столько аргументов, сколько мы хотели бы, и мы могли бы использовать их для создания лямбда-выражений и ссылок на методы по своему усмотрению. Другими словами, мы можем загрязнять мир еще более новыми функциональными интерфейсами. Также мы можем создавать лямбда-выражения даже для интерфейсов в более ранних версиях JDK или для более ранних версий наших собственных API, которые определяли типы SAM, подобные этим. И теперь у нас есть возможность использовать Runnable
а также Callable
в качестве функциональных интерфейсов.
Тем не менее, эти интерфейсы становятся более трудными для запоминания, поскольку все они имеют разные имена и методы.
Тем не менее, я один из тех, кто интересуется, почему они не решили проблему, как в Scala, определяя такие интерфейсы, как Function0
, Function1
, Function2
,..., FunctionN
, Возможно, единственный аргумент, который я могу выдвинуть, заключается в том, что они хотели максимизировать возможности определения лямбда-выражений для интерфейсов в более ранних версиях API, как упоминалось ранее.
Проблема с отсутствием типов значений
Таким образом, очевидно, стирание типов является одной из движущих сил здесь. Но если вы один из тех, кто интересуется, почему нам также нужны все эти дополнительные функциональные интерфейсы с похожими именами и сигнатурами методов, и единственное отличие которых заключается в использовании примитивного типа, то позвольте мне напомнить, что в Java нам также не хватает типов значений, таких как те на языке как C#. Это означает, что универсальные типы, используемые в наших универсальных классах, могут быть только ссылочными типами, а не примитивными типами.
Другими словами, мы не можем сделать это:
List<int> numbers = asList(1,2,3,4,5);
Но мы действительно можем сделать это:
List<Integer> numbers = asList(1,2,3,4,5);
Второй пример, однако, влечет за собой затраты на упаковку и распаковку упакованных объектов назад и вперед от / к примитивным типам. Это может стать очень дорогостоящим в операциях, связанных с коллекциями примитивных значений. Таким образом, группа экспертов решила создать этот взрыв интерфейсов для работы с различными сценариями. Чтобы сделать вещи "хуже", они решили иметь дело только с тремя основными типами: int, long и double.
Цитируя слова Брайана Гетца в лямбда-рассылке:
[...] В более общем плане: философия, лежащая в основе специализированных примитивных потоков (например, IntStream), чревата неприятными компромиссами. С одной стороны, это много уродливого дублирования кода, загрязнения интерфейса и т. Д. С другой стороны, любая арифметика в операциях в штучной упаковке - отстой, и отсутствие истории о сокращении целых чисел было бы ужасно. Таким образом, мы находимся в сложном положении и стараемся не усугублять ситуацию.
Хитрость № 1 в том, чтобы не усугубить ситуацию: мы не делаем все восемь примитивных типов. Мы делаем int, long и double; все остальные могут быть смоделированы этим. Возможно, мы могли бы избавиться и от int, но мы не думаем, что большинство Java-разработчиков к этому готовы. Да, будут звонки для персонажа, и ответ "вставь в int". (Каждая специализация проецируется на ~100 тыс. К занимаемой площади JRE.)
Трюк № 2 заключается в следующем: мы используем примитивные потоки для демонстрации того, что лучше всего сделать в примитивном домене (сортировка, сокращение), но не пытаемся дублировать все, что вы можете делать в коробочном домене. Например, как указывает Алексей, нет IntStream.into(). (Если бы это было, следующий вопрос (-ы) был бы следующим: "Где находится IntCollection? IntArrayList? IntConcurrentSkipListMap?) Намерение состоит в том, что многие потоки могут начинаться как ссылочные потоки и заканчиваться как примитивные потоки, но не наоборот. Это нормально, и это уменьшает количество необходимых преобразований (например, нет перегрузки карты для int -> T, нет специализации Function для int -> T и т. д.) [...]
Мы видим, что это было трудное решение для экспертной группы. Думаю, мало кто согласится с тем, что это круто, и большинство из нас, скорее всего, согласятся, что это необходимо.
Проблема с проверенными исключениями
Была третья движущая сила, которая могла бы сделать вещи еще хуже, и это факт, что Java поддерживает два типа исключений: проверенный и непроверенный. Компилятор требует, чтобы мы обрабатывали или явно объявляли проверенные исключения, но ничего не требует для непроверенных. Таким образом, это создает интересную проблему, потому что сигнатуры методов большинства функциональных интерфейсов не объявляют никаких исключений. Так, например, это невозможно:
Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //oops! compiler error
Это невозможно сделать, потому что write
операция выдает проверенное исключение (т.е. IOException
) но подпись Consumer
Метод не объявляет, что выдает любое исключение. Таким образом, единственным решением этой проблемы было бы создание еще большего количества интерфейсов, некоторые из которых объявляли бы исключения, а некоторые нет (или предлагали еще один механизм на уровне языка для обеспечения прозрачности исключений. Опять же, чтобы сделать ситуацию "хуже" эксперта Группа решила ничего не делать в этом случае.
По словам Брайана Гетца в лямбда-рассылке:
[...] Да, вам нужно будет предоставить свои собственные исключительные SAM. Но тогда лямбда-преобразование будет нормально работать с ними.
ЭГ обсудила дополнительную языковую и библиотечную поддержку для этой проблемы, и в конце концов посчитала, что это плохой компромисс между затратами и выгодами.
Решения, основанные на библиотеках, вызывают двукратный взрыв в типах SAM (исключительный или нет), которые плохо взаимодействуют с существующими комбинаторными взрывами для примитивной специализации.
Доступные языковые решения были проигрышными из-за сложности / ценности. Хотя есть некоторые альтернативные решения, которые мы собираемся продолжить исследовать - хотя явно не для 8 и, вероятно, не для 9 тоже.
А пока у вас есть инструменты, чтобы делать то, что вы хотите. Я понимаю, что вы предпочитаете, чтобы мы предоставили эту последнюю милю для вас (и, во-вторых, ваш запрос на самом деле является тонко завуалированным запросом "почему бы вам просто не отказаться от проверенных исключений"), но я думаю, что текущее состояние позволяет Вы сделали свою работу. [...]
Поэтому мы, разработчики, должны разработать еще больше взрывов интерфейса, чтобы справиться с ними в каждом конкретном случае:
interface IOConsumer<T> {
void accept(T t) throws IOException;
}
static<T> Consumer<T> exceptionWrappingBlock(IOConsumer<T> b) {
return e -> {
try { b.accept(e); }
catch (Exception ex) { throw new RuntimeException(ex); }
};
}
Для того, чтобы сделать:
Writer out = new StringWriter();
Consumer<String> printer = exceptionWrappingBlock(s -> out.write(s));
Вероятно, в будущем (возможно, JDK 9), когда мы получим поддержку типов значений в Java и Reification, мы сможем избавиться (или, по крайней мере, больше не нужно больше использовать) от некоторых из этих многочисленных интерфейсов.
Таким образом, мы видим, что группа экспертов боролась с несколькими проблемами проектирования. Необходимость, требование или ограничение для обеспечения обратной совместимости усложняли ситуацию, поэтому у нас есть другие важные условия, такие как отсутствие типов значений, стирание типов и проверенные исключения. Если бы у Java было первое и не хватало двух других, дизайн JDK 8, вероятно, был бы другим. Итак, мы все должны понимать, что это были сложные проблемы с большим количеством компромиссов, и EG пришлось где-то подвести черту и принять решение.