Trong Java 8, có một phương thức mới String.chars()
trả về luồng int
s ( IntStream
) đại diện cho mã ký tự. Tôi đoán nhiều người sẽ mong đợi một dòng char
s ở đây thay thế. Động lực để thiết kế API theo cách này là gì?
Trong Java 8, có một phương thức mới String.chars()
trả về luồng int
s ( IntStream
) đại diện cho mã ký tự. Tôi đoán nhiều người sẽ mong đợi một dòng char
s ở đây thay thế. Động lực để thiết kế API theo cách này là gì?
Câu trả lời:
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 IntStream
và á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ì IntStream
và 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.
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.Character
cũng như StringBuilder.appendCodePoint
, v.v. Hỗ trợ này tồn tại kể từ đó jdk1.5
.
String
hoặc char[]
. Tôi cá rằng hầu hết char
các mã xử lý xử lý sai các cặp thay thế.
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.
Stream<Character>
bị từ chối.
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
, long
và double
. (Cá nhân tôi sẽ bỏ đi int
nhưng đó chỉ là tôi.)
Vì 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ó char
cá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 CharStream
chuyê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ị IntStream
chứa được char
biểu thị ints
và 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)
và 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 IntStream
như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ế:
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;
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à
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.
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.
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.
chars()
trở lại IntStream
khô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.
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.
IntStream
khô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 đó.
CharStream
không tồn tại, vấn đề cần thêm là gì?