Почему новые методы java.util.Arrays в Java 8 не перегружены для всех примитивных типов?

Я просматриваю изменения API для Java 8, и я заметил, что новые методы в java.util.Arrays не перегружены для всех примитивов. Методы, которые я заметил, следующие:

В настоящее время эти новые методы обрабатывают только примитивы int, long и double.

int, long и double, вероятно, являются наиболее широко используемыми примитивами, поэтому имеет смысл, что если бы им пришлось ограничить API, чтобы они выбрали эти три, но почему они должны были ограничить API

Ответ 1

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

Почему загрязнение интерфейса в Java 8

Например, на языке С# существует набор предопределенных типов функций, принимающих любое количество аргументов с необязательным типом возвращаемого значения (Func и Действие, каждый из которых соответствует 16 параметрам разных типов T1, T2, T3,..., T16), но в JDK 8 мы представляют собой набор различных функциональных интерфейсов с разными именами и разными именами методов, а абстрактные методы представляют собой подмножество хорошо известных функций arities (то есть, нулевой, унарный, двоичный, тройной и т.д.). И тогда у нас есть взрыв случаев, связанных с примитивными типами, и есть даже другие сценарии, вызывающие взрыв более функциональных интерфейсов.

Проблема стирания типа

Итак, в некотором смысле, оба языка страдают от некоторой формы загрязнения интерфейса (или делегируют загрязнение в С#). Единственное различие заключается в том, что в С# все они имеют одинаковое имя. В Java, к сожалению, из-за типа erasure, нет разницы между 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 мы также отсутствие типов значений, как на языке С#. Это означает, что общие типы, используемые в наших общих классах, могут быть только ссылочными типами, а не примитивными типами.

Другими словами, мы не можем этого сделать:

List<int> numbers = asList(1,2,3,4,5);

Но мы действительно можем это сделать:

List<Integer> numbers = asList(1,2,3,4,5);

Второй пример, однако, несет в себе расходы на бокс и распаковку обернутых объектов назад и вперед от/до примитивных типов. Это может стать очень дорогостоящим в операциях, связанных с коллекциями примитивных значений. Таким образом, группа экспертов решила создать этот взрыв интерфейсов для решения различных сценариев. Чтобы сделать вещи "хуже", они решили иметь дело только с тремя основными типами: int, long и double.

Цитируя слова Брайана Гетца в списке рассылки лямбда:

[...] В более общем плане: философия за примитивные потоки (например, IntStream) чреваты неприятными компромиссами. С одной стороны, это много уродливого дублирования кода, интерфейса загрязнение и т.д. С другой стороны, любая арифметика на бокс-операциях отстой, и отсутствие истории для сокращения по сравнению с ints было бы ужасно. Поэтому мы находимся в сложном уголке, и мы стараемся не ухудшать ситуацию.

Трюк №1 за то, что он не усугубляет ситуацию: мы не делаем все восемь примитивных типов. Мы делаем int, long и double; все остальные могут имитироваться. Возможно, мы могли бы избавиться от int тоже, но мы не считаем, что большинство разработчиков Java готовы к этому. Да будут вызовы для символа, и ответ "вставьте его в int". (Каждая специализация проецируется на ~ 100K на след JRE.)

Trick # 2: мы используем примитивные потоки, чтобы выставлять вещи, которые лучше всего сделать в примитивном домене (сортировка, сокращение), но не пытаться дублировать все, что вы можете сделать в поле в штучной упаковке. Например, там нет IntStream.into(), как указывает Алексей. (Если бы были, следующий вопрос (ы): "Где IntCollection? IntArrayList? IntConcurrentSkipListMap?) Намерение состоит в том, что многие потоки могут начинаться как ссылочные потоки и заканчиваются как примитивные потоки, но не наоборот. Это нормально, и это уменьшает количество необходимых преобразований (например, нет перегрузка карты для int → T, нет специализации функции для int → T и т.д.) [...]

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

Проблема с проверенными исключениями

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

Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //oops! compiler error

Это невозможно сделать, потому что операция write выдает проверенное исключение (т.е. IOException), но подпись метода Consumer не объявляет, что это вообще исключает какое-либо исключение. Таким образом, единственным решением этой проблемы было бы создание еще большего количества интерфейсов, некоторые из которых объявляли бы исключения, а некоторые - нет (или придумали еще один механизм на уровне языка для прозрачности исключений Опять же, чтобы сделать вещи "хуже", экспертная группа в этом случае решила ничего не делать.

По словам Брайана Гетца в списке рассылки лямбда:

[...] Да, вам придется предоставить свои собственные исключительные SAM. Но потом лямбда-конверсия будет работать с ними хорошо.

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

Решения на базе библиотеки приводят к взрыву 2x в типах 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, вероятно, был бы другим. Таким образом, мы все должны понимать, что это были сложные проблемы с большим количеством компромиссов, и ЭГ должен был провести линию где-то и принять решения.