Dlaczego String.chars () jest strumieniem ints w Javie 8?

198

W Javie 8 istnieje nowa metoda, String.chars()która zwraca strumień ints ( IntStream) reprezentujący kody znaków. Sądzę, że wiele osób spodziewałoby się tutaj strumienia chars. Jaka była motywacja do zaprojektowania API w ten sposób?

Adam Dyga
źródło
4
@RohitJain Nie miałem na myśli żadnego konkretnego strumienia. Jeśli CharStreamnie istnieje, jaki byłby problem z jego dodaniem?
Adam Dyga
5
@AdamDyga: Projektanci wyraźnie postanowili uniknąć eksplozji klas i metod, ograniczając prymitywne strumienie do 3 typów, ponieważ inne typy (char, short, float) mogą być reprezentowane przez ich większy odpowiednik (int, double) bez znaczących kara za wyniki.
JB Nizet
3
@JBNizet Rozumiem. Ale nadal wydaje się to brudnym rozwiązaniem ze względu na uratowanie kilku nowych klas.
Adam Dyga
9
@JB Nizet: Dla mnie to wygląda na to, że już mają eksplozję interfejsów podanych cały strumień przeciążenia, jak również wszystkie interfejsy funkcyjne ...
Holger
5
Tak, wybuch już istnieje, nawet przy trzech prymitywnych specjalizacjach strumieniowych. Co by było, gdyby wszystkie osiem prymitywów miało specjalizacje strumieniowe? Kataklizm? :-)
Stuart Marks

Odpowiedzi:

218

Jak już wspomnieli inni, podstawą projektu było zapobieżenie eksplozji metod i klas.

Mimo to osobiście uważam, że była to bardzo zła decyzja, i dlatego, biorąc pod uwagę, że nie chcą podejmować CharStream, co jest rozsądne, różnych metod zamiast chars(), pomyślałbym:

  • Stream<Character> chars(), co daje strumień pudełkowych postaci, które będą miały niewielką utratę wydajności.
  • IntStream unboxedChars(), który miałby zostać użyty do kodu wydajności.

Jednak zamiast skupiać się na tym, dlaczego odbywa się to w ten sposób obecnie, myślę, że ta odpowiedź powinna skupić się na wskazaniu sposobu, aby to zrobić za pomocą interfejsu API, który otrzymaliśmy w Javie 8.

W Javie 7 zrobiłbym to tak:

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

Sądzę, że rozsądną metodą wykonania tego w Javie 8 jest:

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

Tutaj otrzymuję IntStreami mapuję go na obiekt za pomocą lambda i -> (char)i, to automatycznie umieści go w a Stream<Character>, a następnie możemy zrobić, co chcemy, i nadal używać referencji metod jako plus.

Pamiętaj jednak, że musisz to zrobić mapToObj, jeśli zapomnisz i użyjesz map, nic nie będzie narzekać, ale nadal skończy się na IntStreami możesz nie zastanawiać się, dlaczego wypisuje wartości całkowite zamiast ciągów reprezentujących znaki.

Inne brzydkie alternatywy dla Java 8:

Pozostając w IntStreami chcąc je ostatecznie wydrukować, nie możesz już używać odwołań do metod do drukowania:

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

Co więcej, używanie referencji metod do własnej metody już nie działa! Rozważ następujące:

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

i wtedy

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

Daje to błąd kompilacji, ponieważ możliwa jest stratna konwersja.

Wniosek:

Interfejs API został zaprojektowany w ten sposób, ponieważ nie chcę dodawać CharStream, osobiście uważam, że metoda powinna zwrócić a Stream<Character>, a obejściem jest obecnie użycie mapToObj(i -> (char)i), IntStreamaby móc poprawnie z nimi pracować.

skiwi
źródło
7
Mój wniosek: ta część API jest zepsuta przez projekt. Ale dzięki za obszerną odpowiedź
Adam Dyga
27
+1, ale moją propozycją jest użycie codePoints()zamiast chars(), a wiele funkcji bibliotecznych już akceptuje intpunkt kodowy oprócz char, np. Wszystkich metod, java.lang.Characterjak StringBuilder.appendCodePoint, itp. Wsparcie to istnieje od tego czasu jdk1.5.
Holger
6
Dobra uwaga na temat punktów kodowych. Korzystanie z nich obsłuży dodatkowe znaki, które są reprezentowane jako pary zastępcze w a Stringlub char[]. Założę się, że większość charnieudolnych par kodu zastępczego.
Stuart Marks
2
@skiwi, zdefiniuj, void print(int ch) { System.out.println((char)ch); }a następnie możesz użyć odwołań do metod.
Stuart Marks
2
Zobacz moją odpowiedź, dlaczego Stream<Character>został odrzucony.
Stuart Marks
90

Odpowiedź od skiwi pokryte wielu głównych punktów już. Wypełnię nieco więcej tła.

Projekt dowolnego API to szereg kompromisów. W Javie jednym z trudnych problemów jest podejmowanie decyzji projektowych, które zostały podjęte dawno temu.

Prymitywy są w Javie od 1.0. Sprawiają, że Java jest „nieczystym” językiem obiektowym, ponieważ prymitywy nie są obiektami. Dodanie prymitywów było, moim zdaniem, pragmatyczną decyzją o poprawie wydajności kosztem obiektowej czystości.

Jest to kompromis, z którym nadal żyjemy dzisiaj, prawie 20 lat później. Funkcja autoboxowania dodana w Javie 5 w większości eliminowała potrzebę zaśmiecania kodu źródłowego wywołaniami metod boxowania i rozpakowywania, ale narzut nadal istnieje. W wielu przypadkach nie jest to zauważalne. Jeśli jednak wykonasz boksowanie lub rozpakowanie w wewnętrznej pętli, zobaczysz, że może to nałożyć znaczne obciążenie procesora i odśmiecania.

Podczas projektowania interfejsu API Streams było jasne, że musimy wspierać operacje podstawowe. Narzut związany z boksem / rozpakowaniem zabiłby jakąkolwiek korzyść z wydajności wynikającą z równoległości. Nie chcieliśmy jednak obsługiwać wszystkich prymitywów, ponieważ spowodowałoby to mnóstwo bałaganu w interfejsie API. (Czy naprawdę widzisz zastosowanie ShortStream?) „Wszystko” lub „brak” to wygodne miejsca na projekt, ale żadne z nich nie było do zaakceptowania. Musieliśmy więc znaleźć rozsądną wartość „niektórych”. Skończyło się z prymitywnych specjalizacje int, longoraz double. (Osobiście bym to pominął, intale to tylko ja.)

Dla CharSequence.chars()rozważaliśmy powrót Stream<Character>(wczesny prototyp może wdrożyły ten), ale została ona odrzucona z powodu boksu napowietrznych. Biorąc pod uwagę, że Łańcuch ma charwartości jako prymitywy, błędem byłoby bezwarunkowe narzucanie boksu, gdy osoba wywołująca prawdopodobnie po prostu trochę przetworzyłaby tę wartość i rozpakowała ją z powrotem do łańcucha.

Rozważaliśmy również CharStreamprymitywną specjalizację, ale jej użycie wydaje się być dość wąskie w porównaniu z ilością, jaką dodałoby do API. Dodanie go nie wydawało się opłacalne.

Kara nakładana na osoby dzwoniące polega na tym, że muszą wiedzieć, że IntStreamzawiera charwartości reprezentowane jako intsi że rzutowanie musi odbywać się w odpowiednim miejscu. Jest to podwójnie mylące, ponieważ istnieją przeciążone wywołania API, takie jak PrintStream.print(char)i PrintStream.print(int)które różnią się znacznie swoim zachowaniem. Prawdopodobnie powstaje dodatkowy błąd, ponieważ codePoints()wywołanie również zwraca an, IntStreamale wartości, które zawiera, są zupełnie inne.

Sprowadza się to zatem do pragmatycznego wyboru spośród kilku alternatyw:

  1. Nie moglibyśmy zapewnić prymitywnych specjalizacji, co skutkowałoby prostym, eleganckim, spójnym API, ale które narzuca wysoką wydajność i ogólne obciążenie GC;

  2. moglibyśmy zapewnić pełny zestaw prymitywnych specjalizacji, kosztem zaśmiecania interfejsu API i nakładania obciążeń konserwacyjnych na programistów JDK; lub

  3. moglibyśmy podać podzbiór prymitywnych specjalizacji, dając API o średniej wielkości i wysokiej wydajności, które nakładają stosunkowo niewielkie obciążenie na osoby dzwoniące w dość wąskim zakresie przypadków użycia (przetwarzanie znaków).

Wybraliśmy ostatni.

Znaki Stuarta
źródło
1
Niezła odpowiedź! Jednak nie odpowiada, dlaczego nie mogą istnieć dwie różne metody chars(), jedna zwracająca Stream<Character>(z niewielką utratą wydajności) i druga istota IntStream, czy to również zostało wzięte pod uwagę? Jest całkiem prawdopodobne, że ludzie i tak skończą na mapowaniu, Stream<Character>jeśli uważają, że warto przekonać się nad karą za wydajność.
skiwi
3
Tutaj pojawia się minimalizm. Jeśli istnieje już chars()metoda, która zwraca wartości char w IntStream, to nie dodaje wiele do wywołania API, które otrzymuje te same wartości, ale w formie pudełkowej. Dzwoniący może bez problemu wpisać wartości. Na pewno wygodniej byłoby nie robić tego w (prawdopodobnie rzadkim) przypadku, ale kosztem dodawania bałaganu do API.
Stuart Marks
5
Dzięki duplikatowi pytania zauważyłem to. Zgadzam się, że chars()powrót IntStreamnie jest dużym problemem, zwłaszcza biorąc pod uwagę fakt, że ta metoda rzadko była używana. Jednak dobrze byłoby mieć wbudowany sposób na powrót IntStreamdo String. Można to zrobić .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), ale to naprawdę długo.
Tagir Valeev
7
@TagirValeev Tak, to trochę kłopotliwe. Strumieniem punktów kodowych (an IntStream) nie jest tak źle: collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Wydaje mi się, że nie jest tak naprawdę krótszy, ale użycie punktów kodowych pozwala uniknąć (char)rzutów i pozwala na użycie odwołań do metod. Ponadto prawidłowo obsługuje parametry zastępcze.
Stuart Marks
2
@IlyaBystrov Niestety prymitywne strumienie, takie jak IntStreamnie mają collect()metody, która wymaga Collector. Mają tylko collect()metodę trzech argumentów , jak wspomniano w poprzednich komentarzach.
Stuart Marks