Почему String.chars () является потоком целых в Java 8?

198

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

Адам Дыга
источник
4
@RohitJain Я не имел в виду какой-то конкретный поток. Если CharStreamне существует, что было бы проблемой, чтобы добавить его?
Адам Дыга
5
@AdamDyga: Дизайнеры явно решили избежать взрыва классов и методов, ограничив примитивные потоки тремя типами, поскольку другие типы (char, short, float) могут быть представлены их большим эквивалентом (int, double) без каких-либо значительных снижение производительности.
JB Nizet
3
@JBNizet Я понял. Но это все еще похоже на грязное решение ради сохранения пары новых классов.
Адам Дыга
9
@JB Низет: Мне кажется, что у нас уже есть взрыв интерфейсов, учитывая всю перегрузку потока, а также все функциональные интерфейсы
Хольгер,
5
Да, взрыв уже есть, даже с тремя примитивными специализациями потоков. Что было бы, если бы все восемь примитивов имели специализации потока? Катаклизм? :-)
Стюарт Маркс

Ответы:

218

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

Тем не менее, лично я думаю, что это было очень плохое решение, и поэтому, учитывая, что они не хотят принимать 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и сопоставляю его с объектом через лямбду i -> (char)i, это автоматически помещает его в a 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, я лично считаю, что метод должен возвращать a Stream<Character>, и в настоящее время обходной путь заключается в том, чтобы использовать mapToObj(i -> (char)i)его IntStreamдля правильной работы с ними.

skiwi
источник
7
Мой вывод: эта часть API нарушена дизайном. Но спасибо за исчерпывающий ответ
Адам Дыга
27
+1, но я предлагаю использовать codePoints()вместо, chars()и вы найдете множество библиотечных функций, уже принимающих intдля кода точку дополнительно char, например, все методы, java.lang.Characterа также StringBuilder.appendCodePoint, и т. Д. Эта поддержка существует с тех пор jdk1.5.
Хольгер
6
Хороший вопрос о кодах. Их использование будет обрабатывать дополнительные символы, которые представлены как суррогатные пары в Stringили char[]. Могу поспорить, что большая часть charкода обрабатывает суррогатные пары.
Стюарт Маркс
2
@skiwi, определите, void print(int ch) { System.out.println((char)ch); }а затем вы можете использовать ссылки на методы.
Стюарт Маркс
2
Смотрите мой ответ, почему Stream<Character>был отклонен.
Стюарт Маркс
90

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

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

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

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

При разработке Streams API было ясно, что мы должны поддерживать примитивы. Затраты на упаковку / распаковку убили бы любую выгоду производительности от параллелизма. Мы не хотели поддерживать все примитивы, поскольку это добавило бы огромное количество беспорядка в 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но содержащиеся в нем значения совершенно разные.

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

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

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

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

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

Стюарт Маркс
источник
1
Хороший ответ! Однако это не отвечает, почему не может быть двух разных методов chars(): один, который возвращает a Stream<Character>(с небольшим снижением производительности), а другой IntStream- это тоже было рассмотрено? Вполне вероятно, что люди все Stream<Character>равно будут в конечном итоге сопоставлять его, если они считают, что удобство стоит того, чтобы снизить производительность.
SkiWi
3
Минимализм приходит сюда. Если уже есть chars()метод, который возвращает значения типа char в an IntStream, он не добавляет большого значения, чтобы иметь другой вызов API, который получает те же значения, но в штучной форме. Вызывающий может поместить значения без особых проблем. Конечно, было бы удобнее не делать это в этом (вероятно, редком) случае, но за счет добавления беспорядка в API.
Стюарт Маркс
5
Благодаря повторяющемуся вопросу я заметил этот. Я согласен, что chars()возвращение IntStreamне является большой проблемой, особенно учитывая тот факт, что этот метод используется редко вообще. Однако было бы хорошо иметь встроенный способ преобразования обратно IntStreamв String. Это может быть сделано .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), но это действительно долго.
Тагир Валеев
7
@TagirValeev Да, это несколько громоздко. С потоком кодовых точек (IntStream) это не так уж плохо: collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Я думаю, что это не совсем короче, но использование точек кода позволяет избежать (char)приведения и позволяет использовать ссылки на методы. Плюс это обрабатывает суррогаты должным образом.
Стюарт Маркс
2
@IlyaBystrov К сожалению, примитивные потоки, такие как IntStream, не имеют collect()метода, который принимает Collector. У них есть только collect()метод с тремя аргументами, как упомянуто в предыдущих комментариях.
Стюарт Маркс