Mengapa String.chars () aliran ints di Java 8?


198

Di Java 8, ada metode baru String.chars()yang mengembalikan aliran ints ( IntStream) yang mewakili kode karakter. Saya kira banyak orang akan mengharapkan aliran chardi sini sebagai gantinya. Apa motivasi untuk merancang API dengan cara ini?


4
@RohitJain Saya tidak bermaksud aliran tertentu. Jika CharStreamtidak ada, apa masalahnya untuk menambahkannya?
Adam Dyga

5
@AdamDyga: Para desainer secara eksplisit memilih untuk menghindari ledakan kelas dan metode dengan membatasi aliran primitif menjadi 3 jenis, karena jenis lain (char, short, float) dapat diwakili oleh padanannya yang lebih besar (int, dobel) tanpa signifikan penalti kinerja.
JB Nizet

3
@JBNizet saya mengerti. Tapi itu tetap terasa seperti solusi kotor hanya demi menyelamatkan beberapa kelas baru.
Adam Dyga

9
@JB Nizet: Bagi saya sepertinya kita sudah memiliki ledakan antarmuka karena semua aliran berlebih serta semua antarmuka fungsi ...
Holger

5
Ya, sudah ada ledakan, bahkan dengan hanya tiga spesialisasi aliran primitif. Apa jadinya jika kedelapan primitif itu memiliki spesialisasi aliran? Sebuah bencana? :-)
Stuart Marks

Jawaban:


218

Seperti yang telah disebutkan orang lain, keputusan desain di balik ini adalah untuk mencegah ledakan metode dan kelas.

Namun, secara pribadi saya pikir ini adalah keputusan yang sangat buruk, dan harus ada, mengingat mereka tidak ingin membuat CharStream, yang masuk akal, metode yang berbeda daripada chars(), saya akan memikirkan:

  • Stream<Character> chars(), yang memberikan aliran karakter kotak, yang akan memiliki beberapa penalti performa ringan.
  • IntStream unboxedChars(), yang akan digunakan untuk kode kinerja.

Namun , alih-alih berfokus pada mengapa hal itu dilakukan dengan cara saat ini, saya pikir jawaban ini harus fokus pada menunjukkan cara untuk melakukannya dengan API yang kami dapatkan dengan Java 8.

Di Java 7 saya akan melakukannya seperti ini:

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

Dan saya pikir metode yang masuk akal untuk melakukannya di Java 8 adalah sebagai berikut:

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

Di sini saya mendapatkan IntStreamdan memetakannya ke objek melalui lambda i -> (char)i, ini akan secara otomatis memasukkannya ke dalam Stream<Character>, dan kemudian kita bisa melakukan apa yang kita inginkan, dan masih menggunakan referensi metode sebagai nilai tambah.

Sadarilah bahwa Anda harus melakukannya mapToObj, jika Anda lupa dan menggunakan map, maka tidak ada yang mengeluh, tetapi Anda masih akan berakhir dengan IntStream, dan Anda mungkin akan bertanya-tanya mengapa ia mencetak nilai integer alih-alih string yang mewakili karakter.

Alternatif jelek lainnya untuk Java 8:

Dengan tetap dalam IntStreamdan ingin mencetaknya pada akhirnya, Anda tidak dapat menggunakan referensi metode lagi untuk mencetak:

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

Selain itu, menggunakan referensi metode ke metode Anda sendiri tidak berfungsi lagi! Pertimbangkan yang berikut ini:

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

lalu

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

Ini akan memberikan kesalahan kompilasi, karena mungkin ada konversi lossy.

Kesimpulan:

API dirancang dengan cara ini karena tidak ingin menambahkan CharStream, saya pribadi berpikir bahwa metode tersebut harus mengembalikan Stream<Character>, dan solusi saat ini adalah menggunakan mapToObj(i -> (char)i)padaIntStream agar dapat bekerja dengan baik dengan mereka.


7
Kesimpulan saya: bagian API ini rusak karena desain. Tapi terima kasih atas jawaban yang luas
Adam Dyga

27
+1, tetapi proposal saya akan digunakan codePoints()sebagai ganti chars()dan Anda akan menemukan banyak fungsi pustaka yang telah menerima inttitik kode untuk tambahan char, misalnya semua metode java.lang.Characterdan juga StringBuilder.appendCodePoint, dll. Dukungan ini ada sejak saat itu jdk1.5.
Holger

6
Poin bagus tentang poin kode. Menggunakannya akan menangani karakter tambahan, yang direpresentasikan sebagai pasangan pengganti dalam a Stringatau char[]. Saya berani bertaruh bahwa kebanyakan charkode pemrosesan salah pasang pasangan pengganti.
Stuart Marks

2
@skiwi, tentukan void print(int ch) { System.out.println((char)ch); }dan kemudian Anda dapat menggunakan referensi metode.
Stuart Marks

2
Lihat jawaban saya mengapa Stream<Character>ditolak.
Stuart Marks

90

The jawaban dari skiwi mencakup banyak poin utama sudah. Saya akan mengisi sedikit lebih banyak latar belakang.

Desain API apa pun adalah serangkaian pengorbanan. Di Jawa, salah satu masalah yang sulit adalah berurusan dengan keputusan desain yang dibuat sejak lama.

Primitif telah ada di Jawa sejak 1.0. Mereka menjadikan Java sebagai bahasa berorientasi objek yang "tidak murni", karena primitif bukan objek. Penambahan primitif, saya percaya, merupakan keputusan pragmatis untuk meningkatkan kinerja dengan mengorbankan kemurnian berorientasi objek.

Ini adalah tradeoff yang masih kita jalani hingga hari ini, hampir 20 tahun kemudian. Fitur autoboxing ditambahkan di Java 5 sebagian besar menghilangkan kebutuhan untuk mengacaukan kode sumber dengan panggilan metode tinju dan unboxing, tetapi overhead masih ada. Dalam banyak kasus itu tidak terlihat. Namun, jika Anda melakukan tinju atau unboxing dalam loop batin, Anda akan melihat bahwa itu dapat memaksakan CPU dan pengumpulan sampah yang signifikan.

Saat merancang Streams API, jelas bahwa kami harus mendukung primitif. Overhead tinju / unboxing akan membunuh manfaat kinerja dari paralelisme. Kami tidak ingin mendukung semua primitif, karena itu akan menambah kekacauan besar pada API. (Dapatkah Anda benar-benar melihat kegunaan untuk ShortStream?) "Semua" atau "tidak ada" adalah tempat yang nyaman untuk desain, namun tidak ada yang dapat diterima. Jadi kami harus menemukan nilai "beberapa" yang masuk akal. Kami berakhir dengan spesialisasi primitif untuk int, long, dan double. (Secara pribadi saya akan ditinggalkan inttetapi itu hanya saya.)

Karena CharSequence.chars()kami mempertimbangkan untuk kembali Stream<Character>(prototipe awal mungkin telah menerapkan ini) tetapi ditolak karena tinju overhead. Mempertimbangkan bahwa sebuah String memiliki charnilai sebagai primitif, itu akan menjadi kesalahan untuk memaksakan tinju tanpa syarat ketika penelepon mungkin hanya akan melakukan sedikit pemrosesan pada nilai dan membuka kotak itu kembali menjadi sebuah string.

Kami juga menganggap CharStreamspesialisasi primitif, tetapi penggunaannya tampaknya cukup sempit dibandingkan dengan jumlah massal yang akan ditambahkan ke API. Tampaknya tidak ada gunanya menambahkannya.

Hukuman yang dikenakan pada penelepon adalah bahwa mereka harus tahu bahwa nilai IntStream- charnilai yang diwakili sebagai intsdan bahwa casting harus dilakukan di tempat yang tepat. Ini membingungkan ganda karena ada panggilan API kelebihan beban seperti PrintStream.print(char)dan PrintStream.print(int)yang sangat berbeda dalam perilaku mereka. Titik kebingungan tambahan mungkin muncul karena codePoints()panggilan juga mengembalikan sebuahIntStream tetapi yang dikandungnya sangat berbeda.

Jadi, ini bermula untuk memilih secara pragmatis di antara beberapa alternatif:

  1. Kami tidak dapat memberikan spesialisasi primitif, menghasilkan API yang sederhana, elegan, konsisten, tetapi yang memaksakan kinerja tinggi dan overhead GC;

  2. kami dapat menyediakan satu set lengkap spesialisasi primitif, dengan biaya mengacaukan API dan membebankan beban pemeliharaan pada pengembang JDK; atau

  3. kami dapat menyediakan subkumpulan spesialisasi primitif, memberikan API berkinerja sedang, berukuran sedang, dan tinggi yang membebankan beban yang relatif kecil pada penelepon dalam rentang kasus penggunaan yang cukup sempit (pemrosesan char).

Kami memilih yang terakhir.


1
Jawaban bagus! Namun itu tidak menjawab mengapa tidak ada dua metode yang berbeda untuk chars(), satu yang mengembalikan Stream<Character>(dengan penalti kinerja kecil) dan yang lainnya IntStream, apakah ini juga dipertimbangkan? Sangat mungkin bahwa orang akhirnya akan memetakannya menjadi suatu cara Stream<Character>jika mereka pikir kenyamanan layak atas penalti kinerja.
skiwi

3
Minimalisme hadir di sini. Jika sudah ada chars()metode yang mengembalikan nilai char dalam IntStream, itu tidak menambah banyak untuk memiliki panggilan API lain yang mendapatkan nilai yang sama tetapi dalam bentuk kotak. Penelepon dapat mengemas nilai-nilai tanpa banyak kesulitan. Tentu akan lebih mudah untuk tidak harus melakukan ini dalam hal ini (mungkin jarang), tetapi dengan biaya menambahkan kekacauan ke API.
Stuart Marks

5
Berkat pertanyaan rangkap saya perhatikan yang satu ini. Saya setuju bahwa chars()pengembalian IntStreambukanlah masalah besar terutama mengingat fakta bahwa metode ini jarang digunakan sama sekali. Namun akan lebih baik untuk memiliki cara bawaan untuk mengkonversi kembali IntStreamke String. Itu bisa dilakukan dengan .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), tapi itu sangat panjang.
Tagir Valeev

7
@ TarirValeev Ya itu agak rumit. Dengan aliran poin kode (sebuah IntStream) itu tidak terlalu buruk: collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Saya kira itu tidak benar-benar lebih pendek, tetapi menggunakan poin kode menghindari (char)gips dan memungkinkan penggunaan referensi metode. Plus itu menangani pengganti dengan benar.
Stuart Marks

2
@IlyaBystrov Sayangnya aliran primitif seperti IntStreamtidak memiliki collect()metode yang membutuhkan Collector. Mereka hanya memiliki metode tiga-arg collect()seperti yang disebutkan dalam komentar sebelumnya.
Stuart Marks
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.