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

Почему String.chars() поток ints в Java 8?

В Java 8 существует новый метод String.chars(), который возвращает поток int (IntStream), который представляет коды символов. Думаю, многие люди ожидали бы поток char здесь. Какова была мотивация проектирования API таким образом?

4b9b3361

Ответ 1

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

Тем не менее, лично я считаю, что это было очень плохое решение, и должно быть, если они не хотят делать CharStream, что разумно, разные методы вместо chars(), я бы подумал:

  • Stream<Character> chars(), который дает поток символов в ящиках, который будет иметь небольшое снижение производительности.
  • IntStream unboxedChars(), который должен использоваться для кода производительности.

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

В Java 7 я бы сделал это следующим образом:

for (int i = 0; i < hello.length(); i++) {
    System.out.println(hello.charAt(i));
}

И я думаю, что разумным способом сделать это в Java 8 является следующее:

hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);

Здесь я получаю IntStream и сопоставляю его объекту через lambda i -> (char)i, это автоматически помещает его в Stream<Character>, а затем мы можем делать то, что хотим, и по-прежнему использовать ссылки на методы как плюс.

Помните, хотя вы должны сделать mapToObj, если вы забудете и используете map, тогда ничего не будет жаловаться, но вы все равно получите IntStream, и вы можете не следует задаваться вопросом, почему он печатает целые значения вместо строк, представляющих символы.

Другие уродливые альтернативы для Java 8:

Оставаясь в IntStream и желая напечатать их в конечном счете, вы больше не можете использовать ссылки на методы для печати:

hello.chars()
        .forEach(i -> System.out.println((char)i));

Кроме того, использование ссылок метода на ваш собственный метод больше не работает! Рассмотрим следующее:

private void print(char c) {
    System.out.println(c);
}

а затем

hello.chars()
        .forEach(this::print);

Это даст ошибку компиляции, так как возможно преобразование с потерями.

Вывод:

API был спроектирован таким образом из-за того, что вы не хотите добавлять CharStream, я лично считаю, что метод должен возвращать Stream<Character>, и обходным пути в настоящее время является использование mapToObj(i -> (char)i) на IntStream, чтобы иметь возможность правильно работать с ними.

Ответ 2

Ответ от skiwi охватывал многие из основных моментов. Я запишу немного больше фона.

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

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

Это компромисс, который мы до сих пор живем сегодня, почти 20 лет спустя. Функция автобоксинга, добавленная в Java 5, в основном устраняла необходимость загромождать исходный код при вызове метода бокса и разблокировки, но накладные расходы все еще существуют. Во многих случаях это не заметно. Однако, если вы должны выполнять бокс или unboxing во внутреннем цикле, вы увидите, что он может налагать значительные накладные расходы процессора и мусора.

При разработке API Streams было ясно, что мы должны поддерживать примитивы. Накладные расходы в боксе/распаковке убьют любое преимущество в производительности от parallelism. Мы не хотели поддерживать все примитивов, хотя, поскольку это добавило бы огромное количество помех API. (Можете ли вы действительно увидеть использование для ShortStream?) "Все" или "Нет" являются удобными местами для дизайна, но ни один из них не является приемлемым. Поэтому нам пришлось найти разумную ценность "некоторых". Мы закончили с примитивными специализациями для int, long и double. (Лично я бы оставил int, но это только я.)

В CharSequence.chars() мы рассмотрели вопрос о возврате Stream<Character> (ранний прототип, возможно, реализовал это), но он был отклонен из-за боковых накладных расходов. Учитывая, что String имеет значения char в качестве примитивов, было бы ошибкой безоговорочно налагать бокс, когда вызывающий абонент, вероятно, просто выполнил бы бит обработки по значению и распакует его обратно в строку.

Мы также рассмотрели примитивную специализацию CharStream, но ее использование показалось бы довольно узким по сравнению с количеством объема, которое оно добавило бы в API. Не похоже было добавить его.

Штраф, налагаемый на вызывающих абонентов, заключается в том, что они должны знать, что IntStream содержит char значения, представленные как ints, и что кастинг должен выполняться в нужном месте. Это вдвойне запутанно, потому что есть перегруженные API-вызовы, такие как PrintStream.print(char) и PrintStream.print(int), которые заметно отличаются по своему поведению. Возможно, возникает еще одна путаница, потому что вызов codePoints() также возвращает IntStream, но значения, которые он содержит, совершенно разные.

Итак, это сводится к прагматичному выбору нескольких альтернатив:

  • Мы не можем предоставить примитивные специализации, в результате получим простой, элегантный, совместимый API, но который налагает высокую производительность и накладные расходы GC;

  • мы могли бы предоставить полный набор примитивных специализаций за счет загромождения API и наложения бремени обслуживания на разработчиков JDK; или

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

Мы выбрали последний.