序文
以前にソケットに関する記事をいくつか公開しました。その中で、Socketオブジェクトの入力ストリームと出力ストリームを使用してデータを読み書きすることが言及されています。たとえば、クライアントがサーバーにデータを送信すると、データは Socket の入力ストリームに書き込まれ、クライアントは Socket の入力ストリームからデータを読み取ることもできます。
それでは、この入出力ストリームの概念について、この記事で詳しく紹介します。
文章
1.1 概要
私たちの通常のプロジェクト開発プロセスでは、ストリーム操作が使用される場所のほとんどはファイルの読み取り/書き込み時です。
最も代表的な 2 つの抽象クラス: InputStreamとoutputStream . これらは、それぞれバイト入力と出力の抽象クラスを表します。
JavaのIOフロー設計では主にデコレータモードの設計思想が採用されています。
例: Java は、2 つの代表的な抽象クラスに基づく一連の入力クラスと出力クラスも提供します。
FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream。
これらは InputStream と OutputStream を拡張します。それだけでなく、Java には FilterInputStream と FilterOutputStream も用意されています。これらはdecorators の基本クラスであり、ストリームをより柔軟に操作できるように、バッファリング、圧縮、暗号化などの一般的なデコレータ機能を提供するために使用されます。他のデコレーターはこれら 2 つの基本クラスから継承され、継承と結合によって複数の機能拡張が実現されます。
1.2 ファイルインプットストリーム
FileInputStream は、ファイルからデータを読み取るための Java のクラスであり、InputStream クラスから継承されるため、ファイルからバイトを読み取るためのすべての標準入力操作を提供します。
簡単なデモを以下に示します。
import java.io.*;
public class FileInputStreamExample {
public static void main(String[] args) {
try {
FileInputStream inputStream = new FileInputStream("file.txt");
int data;
while ((data = inputStream.read()) != -1) {
System.out.write(data);
}
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.2.1 拡張機能
最初にコードを見てみましょう。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReadDemo {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
InputStream is = null;
String filename = "C:\\Users\\Yi_Sen_Zhang\\" + i + ".txt";
try {
is = new FileInputStream(new File(filename));
byte[] buffer = new byte[is.available()];
int s = is.read(buffer);
StringBuilder streamMsg = new StringBuilder();
if (s != -1) {
streamMsg.append(new String(buffer, 0, s, StandardCharsets.UTF_8));
}
//最终读取到文件中的数据并转为streamMsg
//执行你的业务操作
//businessCode.....
} catch (IOException e) {
System.err.println("Error reading file " + filename + ": " + e.getMessage());
}
}
}
}
上記のコードによると、for ループで別のファイルを読み取り、IO ストリームを生成するためにループすると、次の問題が発生する可能性があることがわかります。
1.リソース リーク。ファイル ストリームが for ループの内容で開かれているが、for ループが完了した後にストリームが閉じられていない場合、リソース リークが発生する可能性があります。
2.読み取りの繰り返し (強調!)。 InputStream を使用してファイルが読み取られるたびに、 read() メソッドが呼び出されたときにファイルから特定のバイト数が読み取られ、buffer に格納されます。読み取りが完了すると、バッファには読み取られたバイトが格納されます。ファイルを読み取った後、close() が時間内に呼び出されない場合、バッファ内に更新されていないデータが存在する可能性があります。今回は、次のループでデータが読み取られるため、データが重複します。
では、どうすれば最適化できるのでしょうか? それらを解決するのは非常に簡単です。各 for ループで InputStream ストリームを閉じるだけです。しかし、これは新しい問題につながります.これが大きなファイルである場合、ストリームを頻繁に開いたり閉じたりすると、パフォーマンスの問題が発生します.これは、開いたり解放したりするたびに、Javaがファイルに必要なものを計算する必要があるためです.リソースが使用されます. これは、かなりのパフォーマンス集中型になる可能性があります。
最適化のアイデア:
1. InputStream 変数の宣言と割り当てを try-with-resources ブロックにマージして、ストリームを手動で閉じるという面倒な操作を回避します。
2. try-with-resources ブロックを使用して、ストリームが閉じていることを確認します。
3. ファイルの内容を読み込みながらエンコード変換を行います。
4. ファイルを読み取るコードをメソッドにカプセル化して、複数の場所で呼び出すことができるようにします。
5. File クラスと FileInputStream クラスの代わりに Path クラスと Files クラスを使用すると、コードがより簡潔になり、読みやすくなります。
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileReadDemo {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
String filename = "C:\\Users\\Yi_Sen_Zhang\\" + i + ".txt";
try (InputStream is = Files.newInputStream(Paths.get(filename))) {
String fileContents = readFileContents(is);
// 执行你的业务操作
// businessCode.....
} catch (IOException e) {
System.err.println("Error reading file " + filename + ": " + e.getMessage());
}
}
}
private static String readFileContents(InputStream is) throws IOException {
byte[] buffer = is.readAllBytes();
return new String(buffer, StandardCharsets.UTF_8);
}
1.3 ファイル出力ストリーム
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo {
public static void main(String[] args) {
String fileName = "example.txt";
String fileContent = "Hello, World!";
try {
FileOutputStream outputStream = new FileOutputStream(fileName);
byte[] bytes = fileContent.getBytes();
outputStream.write(bytes);
outputStream.close();
System.out.println("File '" + fileName + "' has been written successfully!");
} catch (IOException e) {
System.err.println("Error writing file '" + fileName + "': " + e.getMessage());
}
}
}
このデモでは、最初にファイル名と書き込む内容を指定します。次に、FileOutputStream オブジェクトを作成し、ファイル名をパラメーターとして渡します。次に、コンテンツをバイト配列に変換し、write() メソッドを使用してファイルに書き込みます。最後に、出力ストリームを閉じて、成功メッセージを出力します。
注: FileOutputStream を使用する場合、可能性のある IOException を try-catch ブロックで処理する必要があります。これは、ファイルを開けない、ディスクがいっぱいであるなど、ファイルの書き込み中にエラーが発生する可能性があるためです。したがって、エラーが発生したときに例外がキャッチされ、処理されるようにする必要があります。
1.4 BufferedInputStream
入力ストリームから大量のデータを読み取る必要がある場合、BufferedInputStream を使用するとデータの読み取り効率が向上します。BufferedInputStream は InputStream のサブクラスであり、キャッシングによって基盤となるリソースへのアクセス数を減らすことができます。
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class BufferedInputStreamDemo {
public static void main(String[] args) {
String fileName = "example.txt";
try (FileInputStream fileInputStream = new FileInputStream(fileName);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream)) {
int data;
while ((data = bufferedInputStream.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
最初に FileInputStream オブジェクトを作成して、指定されたファイルを開きます。次に、BufferedInputStream オブジェクトを作成し、それを FileInputStream のパラメータとして渡し、ファイルの読み取り時にバッファリングされたストリームを使用します。
while ループでは、read() メソッドを使用して、バッファリングされた入力ストリームから 1 バイトのデータを読み取り、それをコンソールに出力します。read() メソッドが -1 を返す場合、すべてのデータが読み取られ、ループが終了したことを意味します。
1.5 BufferedOutputStream
その本質は BufferedInputStream と同じです. バッファにデータを書き込んでから, バッファ内のデータを下層の出力ストリームに一度に書き込むことができます. また, 書き込み操作によって下層のリソースへのアクセス数を減らすことも目的としています. . .
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedOutputStreamDemo {
public static void main(String[] args) {
String fileName = "example.txt";
String data = "Hello, world!";
try (FileOutputStream fileOutputStream = new FileOutputStream(fileName);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream)) {
bufferedOutputStream.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
最初に FileOutputStream オブジェクトを作成して、指定されたファイルを開きます。次に、BufferedOutputStream オブジェクトを作成し、それを FileOutputStream のパラメータとして渡し、ファイルへの書き込み時にバッファリングされたストリームを使用します。
BufferedOutputStream の write() メソッドを使用して、データ文字列をバイト配列に変換し、それをバッファリングされた出力ストリームに書き込みます。バッファーがいっぱいになると、データは基になる出力ストリームにフラッシュされます。
BufferedOutputStream を使用することで、基になるリソースへのアクセス数を効果的に削減できるため、データの書き込み効率が向上します。
1.6 フィルタ入力ストリーム
FilterInputStream は InputStream のサブクラスであり、入力ストリームから読み取られたデータをフィルタリングまたは処理できます。一般的な用途には、暗号化された形式の復号化、解凍、または変換などがあります。
拡張機能 1: FilterInputStream を使用した暗号化
import java.io.*;
public class FileEncryptor {
// 加密密钥
private static final byte ENCRYPTION_KEY = 0x05;
public static void main(String[] args) throws IOException {
// 源文件路径
String sourceFilePath = "path/to/source/file";
// 加密后的文件路径
String encryptedFilePath = "path/to/encrypted/file";
// 创建文件输入流
FileInputStream inputStream = new FileInputStream(sourceFilePath);
// 创建加密过滤器输入流
FilterInputStream encryptedInputStream = new FilterInputStream(inputStream) {
@Override
public int read(byte[] b, int off, int len) throws IOException {
// 读取原始数据
int bytesRead = super.read(b, off, len);
// 加密数据
for (int i = off; i < off + bytesRead; i++) {
b[i] = (byte) (b[i] ^ ENCRYPTION_KEY);
}
return bytesRead;
}
};
// 创建加密后的文件输出流
FileOutputStream outputStream = new FileOutputStream(encryptedFilePath);
// 复制加密过的数据到加密文件中
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = encryptedInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭所有流
inputStream.close();
encryptedInputStream.close();
outputStream.close();
System.out.println("文件加密完成");
}
}
解釈:この例では、FilterInputStream
クラスを使用して、読み取り時にデータを暗号化するカスタム フィルター入力ストリームを作成します。各バイトは、 XOR 演算によって暗号化キーと XOR 演算されます。暗号化されたデータは新しいファイルに書き込まれます
拡張 2: FilterInputStream を使用した復号化
import java.io.*;
public class FileDecryptor {
// 加密密钥
private static final byte ENCRYPTION_KEY = 0x05;
public static void main(String[] args) throws IOException {
// 加密文件路径
String encryptedFilePath = "path/to/encrypted/file";
// 解密后的文件路径
String decryptedFilePath = "path/to/decrypted/file";
// 读取加密文件
FileInputStream encryptedFileInputStream = new FileInputStream(encryptedFilePath);
// 创建解密过滤器输入流
FilterInputStream decryptedInputStream = new FilterInputStream(encryptedFileInputStream) {
@Override
public int read(byte[] b, int off, int len) throws IOException {
// 读取加密后的数据
int bytesRead = super.read(b, off, len);
// 解密数据
for (int i = off; i < off + bytesRead; i++) {
b[i] = (byte) (b[i] ^ ENCRYPTION_KEY);
}
return bytesRead;
}
};
// 创建解密后的文件输出流
FileOutputStream decryptedOutputStream = new FileOutputStream(decryptedFilePath);
// 复制解密过的数据到解密文件中
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = decryptedInputStream.read(buffer)) != -1) {
decryptedOutputStream.write(buffer, 0, bytesRead);
}
// 关闭解密流和文件输出流
decryptedInputStream.close();
encryptedFileInputStream.close();
decryptedOutputStream.close();
System.out.println("文件解密完成");
}
}
解釈:この例では、読み取り時に暗号化されたファイルを復号化する復号化フィルター入力ストリームを作成します。同じ暗号化キーを使用してデータを復号化し、データを正しく復号化できるようにします。復号化されたデータは新しいファイルに書き込まれます。実際のプロジェクト制作プロセスでは、暗号化キーはより複雑で安全である必要があり、暗号化に使用されるキーと同じでなければならないことに注意してください。
拡張 3: FilterInputStream を使用した圧縮
1.圧縮する入力ファイルのパスと名前、および圧縮された出力ファイルのパスと名前を指定します。
2.入力ファイルを読み取る FileInputStream オブジェクトと、圧縮ファイルを書き込む FileOutputStream オブジェクトを作成します。
3. GZIPOutputStream オブジェクトを作成してデータを gzip 形式に圧縮し、それを FilterInputStream オブジェクトにラップします。
4.最後に、while ループを使用して入力ファイルからデータを読み取り、圧縮されたデータを出力ファイルに書き込みます。ファイル全体の読み取りと圧縮ファイルへの書き込みが完了すると、すべてのストリームが閉じられます。
注: BufferedInputStream は、上記のコードで FilterInputStream のラッパーとして使用されています。これは、読み取りパフォーマンスを向上させるためのものです。必要に応じて、他のタイプの InputStream をパッケージ化に使用できます。さらに、ビジネスのニーズに応じて、ZipOutputStream や DeflaterOutputStream など、他のタイプの圧縮アルゴリズムを使用することもできます。