Thao tác các tệp dữ liệu bằng Java

Java cung cấp một giao lập trình ứng dụng (API) chuẩn hoá, đơn giản để thực hiện các thao tác đọc/ghi tới như file, database và socket – API I/O. Bài này sẽ hướng dẫn bạn cách tiếp cận có hiệu quả trong việc đọc một lượng dữ liệu lớn trong trường hợp cần phải cân nhắc kỹ các các yếu tố như thời gian và cấp phát bộ nhớ để cải thiện hiệu nǎng tổng thể của hệ thống.

Cách tốt nhất để bắt đầu là chúng ta hãy xem xét một thí dụ cụ thể. Giả sử bạn cần đọc dữ liệu lớn từ một tệp nhị phân kích thước lớn và lưu vào một mảng để tiếp tục xử lý. Các thao tác I/O của Java đều dựa trên các stream (dòng), tức một chuỗi tuần tự các byte dữ liệu. Đầu tiên, bạn phải chọn loại stream. Chúng ta làm việc với các tệp nhị phân, vì vậy cần phải sử dụng lớp FileInputStream. Bạn có thể sử dụng lớp FileReader khi làm việc với các stream dữ liệu ký tự. Chúng ta có thể mở một kết nối (connection) tới tệp dữ liệu theo cách sau:

InputStream in = new FileInputStream (fileName);

Tại thời điểm này, bạn đã có thể đọc dữ liệu từ tệp fileName. Tuy nhiên, với ưu tiên về hiệu nǎng, chúng ta hãy xem xét một lớp khác từ gói (package) java.io – BufferedInputStream. Đây là lớp bọc (wrapper) của các stream nhập, nó cho phép lưu vào bộ đệm (buffering) các dữ liệu đầu vào của các stream để cải thiện tiến trình đọc. Bạn có thể kết nối với tệp như dưới đây:

InputStream is = new BufferedInputStream (new FileInputStream (fileName));

Khi đã thiết lập được kết nối tới tệp, bạn cũng có thể bắt đầu đọc nội dung của nó. Lớp InputStream có hai phương thức chính cho việc đọc dữ liệu: int read()int read (byte[] b, int off, int len). Phương thức thứ nhất chỉ đọc một byte dữ liệu một lần. Trái lại, phương thức thứ hai đọc nhiều byte một lúc (xác định theo tham số len) từ stream vào một mảng. Phương thức thứ hai có hiệu quả cao hơn và chúng ta có thể sử dụng nó trong Listing1.

Listing1

/**
* Đọc tệp và lưu dữ liệu vào mảng.
* Tham số file: tệp dữ liệu cần đọc
*
public byte[] read2array(String file) throws Exception {
InputStream in = null;
byte[] out = new byte[0];
try{
in = new BufferedInputStream(new FileInputStream(file));

// the length of a buffer can vary

int bufLen = 20000*1024;
byte[] buf = new byte[bufLen];
byte[] tmp = null;
int len = 0;
while((len = in.read(buf,0,bufLen)) != -1){

// mở rộng mảng

tmp = new byte[out.length + len];

// copy dữ liệu

System.arraycopy(out,0,tmp,0,out.length);
System.arraycopy(buf,0,tmp,out.length,len);
out = tmp;
tmp = null;
}
}finally{

//đóng stream
if (in != null) try{ in.close();}catch (Exception e){}
}
return out;
}

Có một số khía cạnh rất đáng quan tâm trong thí dụ này. Trước tiên, vì tệp có kích thước lớn, chúng ta cần một bộ nhớ đệm cũng khá lớn (20Mb) khi gọi phương thức đọc. Bộ nhớ đệm càng lớn, đọc dữ liệu sẽ càng nhanh. Trên thực tế, có những trường hợp ta không thể biết trước số byte cần đọc từ một stream nhập nên cần phải cấp phát bộ một dung lượng cố định ban đầu cho bộ nhớ đệm. Điều này có thể giải quyết được bằng cách sử dụng các phương thức thích hợp.

Tuy nhiên, phương thức này không phải lúc nào cũng cho kết quả mong muốn và có thể sinh ra các biệt lệ (exception). Biệt lệ có thể nảy sinh khi đọc các dữ liệu kiểu long hoặc BLOB qua stream. Thứ hai, vì được khởi tạo ở bên ngoài vòng while, các mảng out, buf và tmp có thể được sử dụng lại và hạn chế đối tượng cần phải giải phóng ở thường trình thu nhặt rác. Thứ ba, khi bộ đệm đầy, nó được copy vào một mảng có khả nǎng mở rộng kích thước bằng cách gọi phương thức System.arraycopy. Mặc dù thuật toán này khá hiệu quả nhưng tất cả các vòng lặp read đều tạo ra các mảng tạm thời và cần thực hiện 2 thao tác copy mảng trong một lần lặp.

Bạn có thể giảm thao tác copy dữ liệu và bộ nhớ cho mảng bằng cách thay đổi vòng lặp while như Thí dụ 2 dưới đây.

Listing2

/**
* Đọc tệp và lưu dữ liệu vào một danh sách. Phương pháp nhanh
* Tham số file: tệp dữ liệu cần đọc
*
*/
public byte[] read2list(String file) throws Exception {
InputStream in = null;
byte[] buf = null; // output buffer
int bufLen = 20000*1024;
try{
in = new BufferedInputStream(new FileInputStream(file));
buf = new byte[bufLen];
byte[] tmp = null;
int len = 0;
List data = new ArrayList(24); // lưu “các mảnh” dữ liệu
while((len = in.read(buf,0,bufLen)) != -1){
tmp = new byte[len];
System.arraycopy(buf,0,tmp,0,len); // vẫn cần copy
data.add(tmp);
}

/*

Đoạn này có thể tuỳ chọn. Phương pháp này có thể trả về một List dữ liệuđể xử lý sau này

*/
len = 0;
if (data.size() == 1) return (byte[]) data.get(0);
for (int i=0;i

Ơ’ đây, thay vì lưu dữ liệu ngay vào một mảng lớn và mở rộng mảng này vào những lúc dữ liệu được thu về, chúng ta duy trì một danh sách với các phần tử là các “mảnh” dữ liệu. Khi đã đến cuối stream, dữ liệu được lấy ra từ danh sách và điền vào một mảng. Điều này cho phép bạn chỉ cần sử dụng tới một mảng và thực hiện một thao tác copy dữ liệu. Nếu không cần kết quả trả về dưới dạng mảng, bạn có thể trả về danh sách để tiết kiệm thời gian và tài nguyên. Đọc dữ liệu sử dụng thuật toán này nhanh hơn đáng để so với phương pháp thứ nhất. Sự khác biệt trong tốc độ phụ thuộc vào kích thước mảng đệm sử dụng trong phương thức đọc.