Чому String.chars () є потоком вкладишів у Java 8?


198

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


4
@RohitJain Я не мав на увазі якогось конкретного потоку. Якщо CharStreamйого немає, у чому проблема була б додати його?
Адам Діга

5
@AdamDyga: Дизайнери чітко вирішили уникнути вибуху класів і методів, обмеживши примітивні потоки на 3 типи, оскільки інші типи (char, short, float) можуть бути представлені їх більшим еквівалентом (int, double) без будь-якого значного виконання штрафу.
JB Nizet

3
@JBNizet Я розумію. Але це все ще відчуває себе брудним рішенням заради економії пари нових класів.
Адам Діга

9
@JB Nizet: Для мене це виглядає так, що ми вже маємо вибух інтерфейсів, враховуючи всю перевантаження потоків, а також усі функціональні інтерфейси
Holger

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


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 - це ряд компромісів. На Яві одним із складних питань є вирішення дизайнерських рішень, прийнятих давно.

Примітиви були на Яві з 1.0. Вони роблять Java "нечистою" об'єктно-орієнтованою мовою, оскільки примітиви не є об'єктами. Додавання примітивів було, я вважаю, прагматичним рішенням покращити продуктивність за рахунок об'єктно-орієнтованої чистоти.

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

При розробці API Streams було зрозуміло, що ми повинні підтримувати примітиви. Накладні бокс / розблокування знищить будь-яку користь від паралелізму. Однак ми не хотіли підтримувати всіх примітивів, оскільки це додало б величезної кількості неприємностей 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 з помірними розмірами, високоефективними API, що накладає відносно невелике навантаження на абонентів у досить вузькому діапазоні випадків використання (обробка символів).

Ми вибрали останню.


1
Гарна відповідь! Однак це не дає відповіді, чому не може бути двох різних методів, для chars()одного, який повертає Stream<Character>(з невеликим покаранням продуктивності) та іншого IntStream, чи це також було враховано? Цілком ймовірно, що люди в кінцевому підсумку відобразять його на карту, Stream<Character>якщо вони вважають, що зручність заради цього покарання.
skiwi

3
Тут приходить мінімалізм. Якщо вже існує chars()метод, який повертає значення знаків char у 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()методу, який приймає a Collector. Вони мають лише collect()метод трьох аргументів, як згадувалося в попередніх коментарях.
Стюарт Маркс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.