1. Luồng dữ liệu

Hiếm khi một chương trình tồn tại như một hòn đảo riêng. Các chương trình thường bằng cách nào đó tương tác với "thế giới bên ngoài". Điều này có thể xảy ra thông qua việc đọc dữ liệu từ bàn phím, gửi tin nhắn, tải xuống các trang từ Internet hoặc ngược lại, tải tệp lên máy chủ từ xa.

Chúng ta có thể đề cập đến tất cả các hành vi này trong một từ: trao đổi dữ liệu giữa chương trình và thế giới bên ngoài. Đợi đã, đó không chỉ là một từ.

Tất nhiên, bản thân việc trao đổi dữ liệu có thể được chia thành hai phần: nhận dữ liệu và gửi dữ liệu. Ví dụ: bạn đọc dữ liệu từ bàn phím bằng một Scannerđối tượng — đây là nhận dữ liệu. Và bạn hiển thị dữ liệu trên màn hình bằng System.out.println()lệnh — đây là gửi dữ liệu.

Trong lập trình, thuật ngữ "luồng" được sử dụng để mô tả việc trao đổi dữ liệu. Thuật ngữ đó đến từ đâu?

Trong cuộc sống thực, bạn có thể có một dòng nước hoặc một dòng ý thức. Trong lập trình, chúng ta có các luồng dữ liệu .

Luồng là một công cụ đa năng. Chúng cho phép chương trình nhận dữ liệu từ bất kỳ đâu (luồng đầu vào) và gửi dữ liệu đến bất kỳ đâu (luồng đầu ra). Như vậy, có hai loại:

  • Một luồng đầu vào là để nhận dữ liệu
  • Một luồng đầu ra là để gửi dữ liệu

Để làm cho các luồng 'hữu hình', những người tạo ra Java đã viết hai lớp: InputStreamOutputStream.

Lớp InputStreamcó một read()phương thức cho phép bạn đọc dữ liệu từ nó. Và OutputStreamlớp có một write()phương thức cho phép bạn ghi dữ liệu vào nó. Họ cũng có các phương pháp khác, nhưng nhiều hơn về điều đó sau.

luồng byte

Chúng ta đang nói về loại dữ liệu nào? Nó có định dạng gì? Nói cách khác, các lớp này hỗ trợ những kiểu dữ liệu nào?

Đây là các lớp chung, vì vậy chúng hỗ trợ kiểu dữ liệu phổ biến nhất — tệp byte. An OutputStreamcó thể ghi byte (và mảng byte) và InputStreamđối tượng có thể đọc byte (hoặc mảng byte). Vậy là xong — chúng không hỗ trợ bất kỳ loại dữ liệu nào khác.

Do đó, các luồng này còn được gọi là luồng byte .

Một đặc điểm của luồng là dữ liệu của chúng chỉ có thể được đọc (hoặc ghi) tuần tự. Bạn không thể đọc dữ liệu từ giữa luồng mà không đọc tất cả dữ liệu trước nó.

Đây là cách đọc dữ liệu từ bàn phím hoạt động thông qua Scannerlớp: bạn đọc dữ liệu từ bàn phím một cách tuần tự, từng dòng một. Chúng ta đọc một dòng, rồi dòng tiếp theo, rồi dòng tiếp theo, v.v. Một cách phù hợp, phương thức đọc dòng được gọi là nextLine().

Ghi dữ liệu vào một OutputStreamcũng xảy ra tuần tự. Một ví dụ điển hình về điều này là đầu ra của bảng điều khiển. Bạn xuất một dòng, tiếp theo là dòng khác và dòng khác. Đây là đầu ra tuần tự. Bạn không thể xuất dòng đầu tiên, sau đó là dòng thứ mười và sau đó là dòng thứ hai. Tất cả dữ liệu chỉ được ghi vào luồng đầu ra một cách tuần tự.

Luồng ký tự

Gần đây, bạn đã biết rằng chuỗi là loại dữ liệu phổ biến thứ hai và thực tế là như vậy. Rất nhiều thông tin được truyền xung quanh dưới dạng ký tự và toàn bộ chuỗi. Một máy tính vượt trội trong việc gửi và nhận mọi thứ dưới dạng byte, nhưng con người không hoàn hảo như vậy.

Giải thích cho thực tế này, các lập trình viên Java đã viết thêm hai lớp: ReaderWriter. Lớp Readertương tự như lớp InputStream, nhưng read()phương thức của nó không đọc byte, mà đọc các ký tự ( char). Lớp Writertương ứng với OutputStreamlớp. Và cũng giống như Readerlớp, nó hoạt động với các ký tự ( char), không phải byte.

Nếu chúng ta so sánh bốn lớp này, chúng ta sẽ có được bức tranh sau:

Byte (byte) Ký tự (char)
Đọc dữ liệu
InputStream
Reader
Ghi dữ liệu
OutputStream
Writer

ứng dụng thực tế

Bản thân các lớp InputStream, OutputStream, ReaderWriterkhông được sử dụng trực tiếp bởi bất kỳ ai, vì chúng không được liên kết với bất kỳ đối tượng cụ thể nào mà từ đó dữ liệu có thể được đọc (hoặc dữ liệu có thể được ghi vào). Nhưng bốn lớp này có rất nhiều lớp hậu duệ có thể làm được nhiều việc.


2. InputStreamlớp học

Lớp này InputStreamthú vị vì nó là lớp cha của hàng trăm lớp con. Nó không có bất kỳ dữ liệu nào của riêng nó, nhưng nó có các phương thức mà tất cả các lớp dẫn xuất của nó kế thừa.

Nói chung, hiếm khi các đối tượng luồng lưu trữ dữ liệu bên trong. Luồng là một công cụ để đọc/ghi dữ liệu, nhưng không lưu trữ. Điều đó nói rằng, có những trường hợp ngoại lệ.

Các phương thức của InputStreamlớp và tất cả các lớp con của nó:

phương pháp Sự miêu tả
int read()
Đọc một byte từ luồng
int read(byte[] buffer)
Đọc một mảng byte từ luồng
byte[] readAllBytes()
Đọc tất cả các byte từ luồng
long skip(long n)
Bỏ qua ncác byte trong luồng (đọc và loại bỏ chúng)
int available()
Kiểm tra xem còn lại bao nhiêu byte trong luồng
void close()
Đóng luồng

Chúng ta hãy xem qua các phương pháp này một cách ngắn gọn:

read()phương pháp

Phương read()thức đọc một byte từ luồng và trả về nó. Bạn có thể bị nhầm lẫn bởi intkiểu trả về. Loại này được chọn vì intlà loại số nguyên tiêu chuẩn. Ba byte đầu tiên của intsẽ bằng không.

read(byte[] buffer)phương pháp

Đây là biến thể thứ hai của read()phương pháp. Nó cho phép bạn đọc một mảng byte từ InputStreamtất cả cùng một lúc. Mảng sẽ lưu trữ các byte phải được truyền dưới dạng đối số. Phương thức này trả về một số — số byte thực sự được đọc.

Giả sử bạn có bộ đệm 10 kilobyte và bạn đang đọc dữ liệu từ tệp bằng cách sử dụng lớp FileInputStream. Nếu tệp chỉ chứa 2 kilobyte, tất cả dữ liệu sẽ được tải vào mảng bộ đệm và phương thức sẽ trả về số 2048 (2 kilobyte).

readAllBytes()phương pháp

Một phương pháp rất tốt. Nó chỉ đọc tất cả dữ liệu từ cho InputStreamđến khi hết và trả về dưới dạng một mảng byte đơn. Điều này rất thuận tiện để đọc các tệp nhỏ. Các tệp lớn có thể không vừa với bộ nhớ và phương thức này sẽ đưa ra một ngoại lệ.

skip(long n)phương pháp

Phương pháp này cho phép bạn bỏ qua n byte đầu tiên từ InputStreamđối tượng. Bởi vì dữ liệu được đọc tuần tự nghiêm ngặt, phương pháp này chỉ đọc n byte đầu tiên từ luồng và loại bỏ chúng.

Trả về số byte đã thực sự bị bỏ qua (trong trường hợp luồng kết thúc trước khi nbyte bị bỏ qua).

int available()phương pháp

Phương thức trả về số byte vẫn còn trong luồng

void close()phương pháp

Phương close()thức đóng luồng dữ liệu và giải phóng các tài nguyên bên ngoài được liên kết với nó. Khi một luồng bị đóng, không thể đọc thêm dữ liệu từ luồng đó.

Hãy viết một chương trình ví dụ sao chép một tệp rất lớn. Chúng ta không thể sử dụng readAllBytes()phương pháp đọc toàn bộ tệp vào bộ nhớ. Ví dụ:

Mã số Ghi chú
String src = "c:\\projects\\log.txt";
String dest = "c:\\projects\\copy.txt";

try(FileInputStream input = new FileInputStream(src);
FileOutputStream output = new FileOutputStream(dest))
{
   byte[] buffer = new byte[65536]; // 64Kb
   while (input.available() > 0)
   {
      int real = input.read(buffer);
      output.write(buffer, 0, real);
   }
}



InputStreamđể đọc từ tệp
OutputStreamđể ghi vào

tệp Bộ đệm mà chúng ta sẽ đọc dữ liệu
miễn là có dữ liệu trong luồng

Đọc dữ liệu vào bộ đệm
Ghi dữ liệu từ bộ đệm vào luồng thứ hai

Trong ví dụ này, chúng tôi đã sử dụng hai lớp: FileInputStreamlà lớp con của InputStreamviệc đọc dữ liệu từ một tệp và FileOutputStreamlà lớp con của OutputStreamviệc ghi dữ liệu vào một tệp. Chúng ta sẽ nói về lớp thứ hai một lát sau.

Một điểm thú vị khác ở đây là realbiến. Khi khối dữ liệu cuối cùng được đọc từ một tệp, nó có thể dễ dàng có ít hơn 64KB dữ liệu. Theo đó, chúng ta không cần xuất toàn bộ bộ đệm mà chỉ một phần của bộ đệm - realcác byte đầu tiên. Đây chính xác là những gì xảy ra trong write()phương pháp.



3. Readerlớp học

Lớp Readerlà một tương tự hoàn toàn của InputStreamlớp. Điểm khác biệt duy nhất là nó hoạt động với các ký tự ( char), không phải với byte. Cũng giống như InputStreamlớp, Readerlớp không được sử dụng riêng ở bất cứ đâu: nó là lớp cha của hàng trăm lớp con và định nghĩa các phương thức chung cho tất cả chúng.

Các phương thức của Readerlớp (và tất cả các lớp con của nó):

phương pháp Sự miêu tả
int read()
Đọc một chartừ luồng
int read(char[] buffer)
Đọc một charmảng từ luồng
long skip(long n)
Bỏ qua n charstrong luồng (đọc và loại bỏ chúng)
boolean ready()
Kiểm tra xem có còn thứ gì trong luồng không
void close()
Đóng luồng

Các phương thức rất giống với các phương thức của InputStreamlớp, mặc dù có một số khác biệt nhỏ.

int read()phương pháp

Phương thức này đọc một chartừ luồng và trả về nó. Loại charmở rộng thành int, nhưng hai byte đầu tiên của kết quả luôn bằng 0.

int read(char[] buffer)phương pháp

Đây là biến thể thứ hai của read()phương pháp. Nó cho phép bạn đọc một mảng ký tự từ Readertất cả cùng một lúc. Mảng sẽ lưu trữ các ký tự phải được truyền dưới dạng đối số. Phương thức này trả về một số — số lượng ký tự thực sự được đọc.

skip(long n)phương pháp

Phương thức này cho phép bạn bỏ qua n ký tự đầu tiên của Readerđối tượng. Nó hoạt động giống hệt như phương thức tương tự của lớp InputStream. Trả về số ký tự đã thực sự bị bỏ qua.

boolean ready()phương pháp

Trả về truenếu có byte chưa đọc trong luồng.

void close()phương pháp

Phương close()thức đóng luồng dữ liệu và giải phóng các tài nguyên bên ngoài được liên kết với nó. Khi một luồng bị đóng, không thể đọc thêm dữ liệu từ luồng đó.

Để so sánh, hãy viết chương trình sao chép tệp văn bản:

Mã số Ghi chú
String src = "c:\\projects\\log.txt";
String dest = "c:\\projects\\copy.txt";

try(FileReader reader = new FileReader(src);
FileWriter writer = new FileWriter(dest))
{
   char[] buffer = new char[65536]; // 128Kb
   while (reader.ready())
   {
      int real = reader.read(buffer);
      writer.write(buffer, 0, real);
   }
}



Readerđể đọc từ tệp
Writerđể ghi vào tệp Bộ

đệm mà chúng ta sẽ đọc dữ liệu
miễn là có dữ liệu trong luồng

Đọc dữ liệu vào bộ đệm
Ghi dữ liệu từ bộ đệm vào luồng thứ hai