Tại sao String.chars () là một luồng ints trong Java 8?


194

Trong Java 8, có một phương thức mới String.chars()trả về luồng ints ( IntStream) đại diện cho mã ký tự. Tôi đoán nhiều người sẽ mong đợi một dòng chars ở đây thay thế. Động lực để thiết kế API theo cách này là gì?


4
@RohitJain Tôi không có nghĩa là bất kỳ luồng cụ thể. Nếu CharStreamkhông tồn tại, vấn đề cần thêm là gì?
Adam Dyga

5
@AdamDyga: Các nhà thiết kế rõ ràng đã chọn để tránh sự bùng nổ của các lớp và phương thức bằng cách giới hạn các luồng nguyên thủy ở 3 loại, vì các loại khác (char, short, float) có thể được biểu thị bằng tương đương lớn hơn (int, double) mà không đáng kể thực hiện phạt.
JB Nizet

3
@JBNizet Tôi hiểu rồi. Nhưng nó vẫn cảm thấy như một giải pháp bẩn thỉu chỉ vì mục đích cứu một vài lớp mới.
Adam Dyga

9
@JB Nizet: Đối với tôi có vẻ như chúng ta đã một sự bùng nổ của các giao diện cho tất cả các dòng quá tải cũng như tất cả các giao diện chức năng ...
Holger

5
Vâng, đã có một vụ nổ, thậm chí chỉ với ba chuyên ngành dòng nguyên thủy. Sẽ thế nào nếu cả tám nguyên thủy đều có chuyên môn về luồng? Một thảm họa? :-)
Stuart Marks

Câu trả lời:


214

Như những người khác đã đề cập, quyết định thiết kế đằng sau điều này là để ngăn chặn sự bùng nổ của các phương thức và các lớp.

Tuy nhiên, cá nhân tôi nghĩ rằng đây là một quyết định rất tồi tệ và do đó, nếu họ không muốn đưa ra CharStream, đó là phương pháp hợp lý, khác nhau thay vì chars(), tôi sẽ nghĩ đến:

  • Stream<Character> chars(), cung cấp một luồng các ký tự hộp, sẽ có một số hình phạt hiệu suất nhẹ.
  • IntStream unboxedChars(), mà sẽ được sử dụng cho mã hiệu suất.

Tuy nhiên , thay vì tập trung vào lý do tại sao nó được thực hiện theo cách này hiện tại, tôi nghĩ rằng câu trả lời này nên tập trung vào việc hiển thị cách thực hiện với API mà chúng ta đã nhận được với Java 8.

Trong Java 7 tôi đã làm nó như thế này:

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

Và tôi nghĩ một phương pháp hợp lý để làm điều đó trong Java 8 là như sau:

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

Ở đây tôi có được một IntStreamvà ánh xạ nó tới một đối tượng thông qua lambda i -> (char)i, điều này sẽ tự động đóng hộp nó vào một Stream<Character>, và sau đó chúng ta có thể làm những gì chúng ta muốn, và vẫn sử dụng các tham chiếu phương thức làm điểm cộng.

Mặc dù vậy, hãy lưu ý rằng bạn phải làm mapToObj, nếu bạn quên và sử dụng map, thì sẽ không có gì phàn nàn, nhưng bạn vẫn sẽ kết thúc bằng một IntStream, và bạn có thể không biết tại sao nó lại in các giá trị nguyên thay vì các chuỗi đại diện cho các ký tự.

Các lựa chọn thay thế xấu xí khác cho Java 8:

Bằng cách duy trì IntStreamvà muốn in chúng cuối cùng, bạn không thể sử dụng các tham chiếu phương thức nữa để in:

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

Hơn nữa, sử dụng tham chiếu phương thức cho phương thức của riêng bạn không hoạt động nữa! Hãy xem xét những điều sau đây:

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

và sau đó

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

Điều này sẽ đưa ra một lỗi biên dịch, vì có thể có một chuyển đổi mất mát.

Phần kết luận:

API được thiết kế theo cách này vì không muốn thêm CharStream, cá nhân tôi nghĩ rằng phương thức này sẽ trả về a Stream<Character>và cách giải quyết hiện tại là sử dụng mapToObj(i -> (char)i)trên IntStreamđể có thể hoạt động chính xác với chúng.


7
Kết luận của tôi: phần này của API bị phá vỡ bởi thiết kế. Nhưng cảm ơn vì câu trả lời sâu rộng
Adam Dyga

26
+1, nhưng đề xuất của tôi là sử dụng codePoints()thay vì chars()và bạn sẽ tìm thấy rất nhiều hàm thư viện đã chấp nhận một intđiểm mã bổ sung char, ví dụ như tất cả các phương thức java.lang.Charactercũng như StringBuilder.appendCodePoint, v.v. Hỗ trợ này tồn tại kể từ đó jdk1.5.
Holger

6
Điểm tốt về điểm mã. Sử dụng chúng sẽ xử lý các ký tự bổ sung, được biểu diễn dưới dạng các cặp thay thế trong một Stringhoặc char[]. Tôi cá rằng hầu hết charcác mã xử lý xử lý sai các cặp thay thế.
Stuart Marks

2
@skiwi, xác định void print(int ch) { System.out.println((char)ch); }và sau đó bạn có thể sử dụng tài liệu tham khảo phương pháp.
Stuart Marks

2
Xem câu trả lời của tôi tại sao Stream<Character>bị từ chối.
Stuart Marks

90

Câu trả lời từ skiwi bao gồm nhiều điểm chính. Tôi sẽ điền vào một chút nền tảng.

Thiết kế của bất kỳ API nào là một loạt các sự đánh đổi. Trong Java, một trong những vấn đề khó khăn là xử lý các quyết định thiết kế đã được đưa ra từ lâu.

Người nguyên thủy đã ở Java từ 1.0. Chúng làm cho Java trở thành một ngôn ngữ hướng đối tượng "không trong sạch", vì các nguyên thủy không phải là các đối tượng. Việc bổ sung các nguyên thủy là, tôi tin rằng, một quyết định thực dụng để cải thiện hiệu suất với chi phí cho độ tinh khiết hướng đối tượng.

Đây là một sự đánh đổi mà chúng ta vẫn đang sống với ngày hôm nay, gần 20 năm sau. Tính năng autoboxing được thêm vào trong Java 5 hầu như đã loại bỏ nhu cầu làm lộn xộn mã nguồn với các lệnh gọi phương thức đấm bốc và unboxing, nhưng chi phí vẫn còn đó. Trong nhiều trường hợp, nó không đáng chú ý. Tuy nhiên, nếu bạn thực hiện quyền anh hoặc bỏ hộp trong vòng lặp bên trong, bạn sẽ thấy rằng nó có thể áp đặt chi phí đáng kể cho CPU và bộ sưu tập rác.

Khi thiết kế API Streams, rõ ràng là chúng tôi phải hỗ trợ các nguyên thủy. Quyền anh / unboxing trên đầu sẽ giết chết bất kỳ lợi ích hiệu suất từ ​​song song. Tuy nhiên, chúng tôi không muốn hỗ trợ tất cả các nguyên thủy, vì điều đó sẽ thêm một lượng lớn sự lộn xộn vào API. (Bạn thực sự có thể thấy việc sử dụng cho một ShortStream?) "Tất cả" hoặc "không" là những nơi thoải mái cho một thiết kế, nhưng không được chấp nhận. Vì vậy, chúng tôi đã phải tìm một giá trị hợp lý của "một số". Chúng tôi đã kết thúc với chuyên ngành nguyên thủy cho int, longdouble. (Cá nhân tôi sẽ bỏ đi intnhưng đó chỉ là tôi.)

CharSequence.chars()chúng tôi đã cân nhắc việc quay trở lại Stream<Character>(một nguyên mẫu ban đầu có thể đã thực hiện điều này) nhưng nó đã bị từ chối vì quyền anh. Xem xét rằng một Chuỗi có charcác giá trị là nguyên thủy, có vẻ như là một sai lầm khi áp đặt quyền anh vô điều kiện khi người gọi có thể chỉ cần xử lý một chút về giá trị và bỏ hộp lại ngay thành một chuỗi.

Chúng tôi cũng đã xem xét một CharStreamchuyên môn nguyên thủy, nhưng việc sử dụng nó có vẻ khá hẹp so với số lượng lớn mà nó sẽ thêm vào API. Nó dường như không đáng để thêm nó.

Hình phạt này áp dụng cho người gọi là họ phải biết rằng các giá trị IntStreamchứa được charbiểu thị intsvà việc truyền phải được thực hiện tại địa điểm thích hợp. Điều này đôi khi gây nhầm lẫn bởi vì có các lệnh gọi API quá tải như thế PrintStream.print(char)PrintStream.print(int)khác nhau rõ rệt trong hành vi của họ. Một điểm nhầm lẫn bổ sung có thể phát sinh vì codePoints()cuộc gọi cũng trả về một IntStreamnhưng các giá trị mà nó chứa khá khác nhau.

Vì vậy, điều này có nghĩa là lựa chọn thực tế trong số một số lựa chọn thay thế:

  1. Chúng tôi không thể cung cấp các chuyên môn nguyên thủy, dẫn đến một API đơn giản, thanh lịch, nhất quán, nhưng áp dụng hiệu suất cao và chi phí hoạt động cao;

  2. chúng tôi có thể cung cấp một tập hợp đầy đủ các chuyên môn nguyên thủy, với chi phí làm lộn xộn API và áp đặt gánh nặng bảo trì cho các nhà phát triển JDK; hoặc là

  3. chúng tôi có thể cung cấp một tập hợp các chuyên môn nguyên thủy, cung cấp API hiệu suất cao, có kích thước vừa phải, tạo ra gánh nặng tương đối nhỏ cho người gọi trong phạm vi sử dụng khá hẹp (xử lý char).

Chúng tôi đã chọn cái cuối cùng.


1
Câu trả lời tốt đẹp! Tuy nhiên, nó không trả lời tại sao không thể có hai phương pháp khác nhau chars(), một phương thức trả về một Stream<Character>(với hình phạt hiệu suất nhỏ) và phương thức khác IntStream, liệu điều này cũng được xem xét? Nhiều khả năng mọi người sẽ kết thúc việc ánh xạ nó thành một cách Stream<Character>nào đó nếu họ nghĩ rằng sự đồng thuận là xứng đáng với hình phạt hiệu suất.
skiwi

3
Chủ nghĩa tối giản đến đây. Nếu đã có chars()phương thức trả về các giá trị char trong một IntStream, thì nó không thêm nhiều để có một lệnh gọi API khác có cùng giá trị nhưng ở dạng đóng hộp. Người gọi có thể đóng hộp các giá trị mà không gặp nhiều rắc rối. Chắc chắn sẽ thuận tiện hơn khi không phải làm điều này trong trường hợp này (có lẽ là hiếm), nhưng với chi phí thêm lộn xộn vào API.
Stuart Marks

5
Nhờ câu hỏi trùng lặp tôi nhận thấy điều này. Tôi đồng ý rằng việc chars()trở lại IntStreamkhông phải là một vấn đề lớn, đặc biệt là thực tế là phương pháp này hiếm khi được sử dụng. Tuy nhiên nó sẽ là tốt để có một cách tích hợp để chuyển đổi trở lại IntStreamđến String. Nó có thể được thực hiện với .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), nhưng nó thực sự dài.
Tagir Valeev

7
@TagirValeev Vâng, nó hơi cồng kềnh. Với một luồng các điểm mã (một IntStream), điều đó không quá tệ : collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Tôi đoán nó không thực sự ngắn hơn, nhưng sử dụng các điểm mã sẽ tránh các (char)phôi và cho phép sử dụng các tham chiếu phương thức. Thêm vào đó nó xử lý thay thế đúng cách.
Stuart Marks

2
@IlyaBystrov Thật không may, các luồng nguyên thủy như IntStreamkhông có collect()phương thức nào Collector. Họ chỉ có một collect()phương pháp ba đối số như đã đề cập trong các bình luận trước đó.
Stuart Marks
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.