W Javie 8 istnieje nowa metoda, String.chars()
która zwraca strumień int
s ( IntStream
) reprezentujący kody znaków. Sądzę, że wiele osób spodziewałoby się tutaj strumienia char
s. Jaka była motywacja do zaprojektowania API w ten sposób?
W Javie 8 istnieje nowa metoda, String.chars()
która zwraca strumień int
s ( IntStream
) reprezentujący kody znaków. Sądzę, że wiele osób spodziewałoby się tutaj strumienia char
s. Jaka była motywacja do zaprojektowania API w ten sposób?
Odpowiedzi:
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ę IntStream
i 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 IntStream
i 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 IntStream
i 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)
, IntStream
aby móc poprawnie z nimi pracować.
codePoints()
zamiast chars()
, a wiele funkcji bibliotecznych już akceptuje int
punkt kodowy oprócz char
, np. Wszystkich metod, java.lang.Character
jak StringBuilder.appendCodePoint
, itp. Wsparcie to istnieje od tego czasu jdk1.5
.
String
lub char[]
. Założę się, że większość char
nieudolnych par kodu zastępczego.
void print(int ch) { System.out.println((char)ch); }
a następnie możesz użyć odwołań do metod.
Stream<Character>
został odrzucony.
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
, long
oraz double
. (Osobiście bym to pominął, int
ale 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 char
wartoś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ż CharStream
prymitywną 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 IntStream
zawiera char
wartości reprezentowane jako ints
i ż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, IntStream
ale wartości, które zawiera, są zupełnie inne.
Sprowadza się to zatem do pragmatycznego wyboru spośród kilku alternatyw:
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;
moglibyśmy zapewnić pełny zestaw prymitywnych specjalizacji, kosztem zaśmiecania interfejsu API i nakładania obciążeń konserwacyjnych na programistów JDK; lub
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.
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ść.
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.
chars()
powrót IntStream
nie 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 IntStream
do String
. Można to zrobić .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString()
, ale to naprawdę długo.
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.
IntStream
nie mają collect()
metody, która wymaga Collector
. Mają tylko collect()
metodę trzech argumentów , jak wspomniano w poprzednich komentarzach.
CharStream
nie istnieje, jaki byłby problem z jego dodaniem?