Qua những bài trước thì các bạn đã nắm được kha khá kiến thức về Stream API. Theo mình vậy là đủ để sử dụng thực tế rồi. Từ nay series sẽ bắt đầu với những bài nâng cao và chuyên sâu hơn nhé (trừ bài này ra 😂)
1. Những thứ linh tinh với stream
1.1. BaseStream<T, 😢>
interface
Hai bài về stream operation trước mình chỉ giới thiệu tới các operation cơ bản và thường dùng nhất. Hầu hết chúng đều thuộc interface Stream<T>
(nó thực sự là interface đấy, trước đó mình nhầm tí).
Và interface Stream<T>
lại kế thừa từ BaseStream<T, S extends BaseStream<T, S>>
, và nó bao gồm:
- Các operation đã nói như
sequential()
,parallel()
,unordered()
- Các method như
isParallel()
,iterator()
,spliterator()
có thể dễ dàng suy ra ý nghĩa được - Hai method
close()
vàonClose()
Vậy thôi, phần này chủ yếu là giới thiệu BaseStream<T, 😢>
và tổng hợp các method của nó. Có 2 method mới là close()
dùng để đóng stream và onClose()
để set handler khi đóng stream thôi.
1.2. Chuyển đổi object stream và primitive stream
Như ta đã biết có 2 loại stream là object stream và primitive stream, và giữa chúng có vài sự khác biệt. Vấn đề đặt ra là có cách nào để chuyển đổi qua lại giữa chúng không?
Rất may là có, Stream API cung cấp thêm vài intermediate operation cho việc này:
boxed()
của primitive stream để biến thành object streammapToInt()
,flatMapToInt()
(và cả choLong
,Double
) của object stream để chuyển thành primitive stream tương ứng
List<Double> nums = List.of(3.14, 1.41, 2.71);
Stream<Double> s = nums.stream();
// Từ object stream thành primitive stream
DoubleStream s1 = s.mapToDouble(Double::valueOf);
// Từ primitive stream thành object stream
Stream<Double> s2 = s1.boxed();
2. Vài lưu ý khi làm việc với stream
2.1. Stream không thể tái sử dụng
Stream trong Java khác với C#, đó là trong Java thì stream không thể tái sử dụng được. Điều đó dẫn tới các hệ quả sau.
Mỗi đối tượng stream chỉ được xử lý một lần duy nhất bởi một operation.
IllegalStateException sẽ được ném ra nếu phát hiện thấy stream được sử dụng lại.
Phần này có thể hơi khó hiểu, hãy xem qua đoạn code ví dụ sau nhé.
// Stream s1 đã gắn với map() operation, sẽ được xử lý bởi map()
// Nếu s1 gắn thêm filter() operation chẳng phải s1 có thể reuse sao
// Có thể hiểu như không thể gọi 2 operation trên cùng stream object được
Stream<Integer> s1 = Stream.of(2, 3, 5, 7);
s1.map(e -> 2 * e);
s1.filter(e -> e >10); // Ném IllegalStateException
// Trong trường hợp này sẽ chạy được bình thường
// Do s1 gắn với map() mà map() trả về stream mới
// Và filter() gắn vô stream mới này nên sẽ không bị lỗi
Stream<Integer> s2 = Stream.of(2, 3, 5, 7);
s2.map(e -> 2 * e).filter(e -> e > 10);
// Cách trên có thể dùng object stream mới gán kết quả vào
// Hoặc gán vào chính stream trước luôn như sau
Stream<Integer> s3 = Stream.of(2, 3, 5, 7);
s3 = s3.map(e -> 2 * e);
s3 = s3.filter(e -> e > 10);
Link tham khảo thêm nhé docs.oracle.com/javase/8/docs/api/java/util..
2.2. Đóng stream
Thường thì hầu hết stream dùng xong không cần đóng. Do nguồn dữ liệu thường là resource trên bộ nhớ như collection, array,... nên GC sẽ tự động dọn dẹp.
Tuy nhiên, với stream có nguồn dữ liệu từ IO resources (như File, Socket,...), ví dụ get stream từ Files.lines()
method thì cần thực hiện đóng stream thủ công.
Rất may Stream<T>
có implement AutoCloseable
interface, điều đó có nghĩa chúng ta có thể dùng cú pháp try with resource
để tự động đóng stream khi dùng xong như sau.
try (Stream<String> lines = Files.lines("data.txt")) {
// Xử lý stream
}
// Ra đây thì stream đã được đóng
Link tham khảo baeldung.com/java-stream-close (phần này mình cũng mới biết thôi)
Okay bài này có vẻ hơi dài rồi. Từ nay series Stream API sẽ bắt đầu với những bài khó và nâng cao hơn nhé, các bạn có thể đọc để hiểu thêm. Thanks ❤️