Tổng Quan Về Input/Output Trong Java
Bạn đã bao giờ tự hỏi làm thế nào một chương trình có thể đọc dữ liệu từ tệp, hiển thị thông tin lên màn hình hoặc gửi dữ liệu qua mạng chưa? Tất cả những hoạt động đó đều dựa vào cơ chế I/O (Input/Output – Nhập/Xuất). Trong Java, hệ thống I/O đóng vai trò quan trọng, cung cấp các API mạnh mẽ để giúp lập trình viên dễ dàng thao tác với dữ liệu từ nhiều nguồn khác nhau như tệp tin, bộ nhớ, mạng và dòng lệnh.
Java cung cấp hai mô hình chính để làm việc với I/O:
↳ Luồng I/O (I/O Streams): Cung cấp cách tiếp cận trừu tượng để đọc và ghi dữ liệu, có thể làm việc với cả dữ liệu nhị phân (byte streams) và văn bản (character streams).
↳ Tập tin I/O (File I/O hoặc NIO.2): Được giới thiệu từ Java 1.4 và nâng cấp trong Java 7, giúp cải thiện hiệu suất với khả năng xử lý I/O không đồng bộ và hỗ trợ tốt hơn cho thao tác tệp tin.
Phần bài học này cung cấp cho các bạn cái nhìn tổng quan về các lớp trong nền tảng Java dùng cho các hoạt động nhập/xuất cơ bản. Bài học sẽ bắt đầu với khái niệm luồng I/O (I/O Streams), một công cụ quan trọng giúp đơn giản hóa quá trình xử lý dữ liệu đầu vào và đầu ra. Tiếp theo, chúng ta sẽ khám phá Lưu trữ theo chuỗi (Serialization), một tính năng cho phép lưu trữ và khôi phục các đối tượng Java hoàn toàn, bằng cách ghi đối tượng ra luồng và đọc lại chúng khi cần.
Trong chương tiếp theo, chúng ta sẽ khám phá các bài học liên quan đến tập tin I/O (File I/O hoặc NIO.2), một cải tiến quan trọng trong Java giúp nâng cao hiệu suất và tối ưu hóa xử lý I/O. Bạn sẽ tìm hiểu về cách làm việc với đường dẫn, thao tác tệp và thư mục, quản lý siêu dữ liệu, theo dõi thay đổi trong thư mục, cũng như các phương pháp I/O hiện đại giúp tăng cường khả năng xử lý dữ liệu nhanh chóng và hiệu quả.
Hầu hết các lớp liên quan đến luồng I/O(I/O Streams) được tìm thấy trong gói java.io, trong khi các lớp liên quan đến tập tin I/O (File I/O hoặc NIO.2) và hệ thống file nằm trong gói java.nio.file.
Luồng I/O (I/O Streams) là gì?
Trong Java, luồng I/O (Input/Output Streams) đóng vai trò quan trọng trong việc đọc và ghi dữ liệu từ nhiều nguồn khác nhau, bao gồm tệp, mạng, bộ nhớ và dòng lệnh. Java cung cấp một hệ thống luồng linh hoạt, cho phép xử lý cả dữ liệu nhị phân và văn bản một cách hiệu quả, giúp lập trình viên thao tác với dữ liệu dễ dàng hơn.
Luồng I/O cung cấp một mô hình trừu tượng để xử lý dữ liệu đầu vào và đầu ra, bất kể nguồn hoặc đích là gì. Một luồng có thể đại diện cho tệp trên ổ đĩa, thiết bị ngoại vi, một chương trình khác hoặc thậm chí là vùng nhớ. Nhờ cơ chế này, Java giúp đơn giản hóa việc truyền dữ liệu giữa các thành phần trong hệ thống, đảm bảo hiệu suất cao và tính linh hoạt trong lập trình.
Các luồng I/O trong Java được chia thành nhiều loại khác nhau, mỗi loại phục vụ một mục đích riêng. Dưới đây là các loại luồng phổ biến:
↳ Luồng Byte (Byte Streams): Xử lý I/O của dữ liệu nhị phân thô. Các luồng byte đọc và ghi dữ liệu dưới dạng byte, chẳng hạn như FileInputStream và FileOutputStream.
↳ Luồng Ký tự (Character Streams):Xử lý I/O của dữ liệu ký tự, tự động chuyển đổi qua lại giữa bộ ký tự địa phương (local character set). Các luồng ký tự đọc và ghi dữ liệu dưới dạng ký tự, chẳng hạn như FileReader và FileWriter.
↳ Luồng Đệm (Buffered Streams): Tối ưu hóa việc nhập và xuất bằng cách giảm số lần gọi đến API gốc (native API). Các luồng đệm (buffered streams) giúp tăng hiệu suất đọc/ghi bằng cách sử dụng bộ nhớ đệm, như BufferedInputStream và BufferedOutputStream.
↳ Quét và Định dạng (Scanning and Formatting): Cho phép chương trình đọc và ghi văn bản đã định dạng. Chẳng hạn, Scanner được sử dụng để đọc dữ liệu đầu vào có định dạng, và Formatter hoặc PrintWriter để ghi dữ liệu ra với định dạng cụ thể.
↳ I/O từ Dòng Lệnh (I/O from the Command Line): Mô tả các luồng tiêu chuẩn (Standard Streams) như System.out, System.in, và đối tượng Console để đọc và ghi dữ liệu từ dòng lệnh.
↳ Luồng Dữ liệu (Data Streams): Xử lý I/O nhị phân của các kiểu dữ liệu nguyên thủy (primitive data types) và giá trị String. Các luồng dữ liệu như DataInputStream và DataOutputStream cho phép đọc và ghi các kiểu dữ liệu cơ bản.
↳ Luồng Đối tượng (Object Streams): Xử lý I/O nhị phân của các đối tượng. ObjectInputStream và ObjectOutputStream được sử dụng để ghi và đọc các đối tượng đã được tuần tự hóa (serialized).
Mỗi loại luồng trong Java được thiết kế để giải quyết các loại nhiệm vụ I/O khác nhau, từ xử lý dữ liệu thô đến dữ liệu có cấu trúc phức tạp như các đối tượng.
Các nguồn và đích dữ liệu có thể bao gồm nhiều loại lưu trữ, tạo ra hoặc tiêu thụ dữ liệu. Điều này không chỉ giới hạn ở các file trên đĩa; nguồn hoặc đích cũng có thể là một chương trình khác, thiết bị ngoại vi, socket mạng, hoặc mảng bộ nhớ.
Luồng đầu vào (Input stream)
Khi sử dụng luồng đầu vào (input stream), chương trình đọc dữ liệu từ một nguồn, từng phần tử một. Điều này có nghĩa là dữ liệu được lấy ra từ nguồn theo cách tuần tự, từng phần tử hoặc byte một. Quá trình này thường bao gồm:
↳ Mở luồng: Tạo một đối tượng luồng đầu vào và liên kết nó với nguồn dữ liệu.
↳ Đọc dữ liệu: Sử dụng các phương thức của luồng để lấy dữ liệu từ nguồn.
↳ Xử lý dữ liệu: Dữ liệu được đọc vào có thể được xử lý hoặc sử dụng ngay lập tức trong chương trình.
↳ Đóng luồng: Sau khi hoàn tất việc đọc dữ liệu, đóng luồng để giải phóng tài nguyên.

Luồng đầu ra (Output stream)
Khi sử dụng luồng đầu ra (output stream), chương trình ghi dữ liệu vào một đích, từng phần tử một. Điều này có nghĩa là dữ liệu được gửi từ chương trình tới đích theo cách tuần tự, byte hoặc ký tự một. Quá trình này thường bao gồm:
↳ Mở luồng: Tạo một đối tượng luồng đầu ra và liên kết nó với đích dữ liệu.
↳ Ghi dữ liệu: Sử dụng các phương thức của luồng để ghi dữ liệu vào đích.
↳ Xử lý dữ liệu: Dữ liệu có thể được ghi từng phần hoặc theo định dạng yêu cầu.
↳ Đóng luồng: Sau khi hoàn tất việc ghi dữ liệu, đóng luồng để đảm bảo dữ liệu được lưu và tài nguyên được giải phóng.

Trong Java, InputStream và OutputStream là hai lớp trừu tượng cơ bản đóng vai trò là nền tảng cho hầu hết các hoạt động đọc và ghi dữ liệu. Chúng cung cấp một giao diện chung để tương tác với nhiều loại nguồn và đích dữ liệu khác nhau. Trong chương tiếp theo, chúng ta sẽ tìm hiểu chi tiết hơn về InputStream và OutputStream, cách chúng hoạt động và cách sử dụng chúng để đọc, ghi dữ liệu từ các nguồn khác nhau như tệp, mạng hay bộ nhớ.