Java基础篇-IO流的概述与使用(超全Demo)

前言

之前我有发布过一些关于Socket的文章。其中就提到过关于Socket对象的输入流和输出流进行数据的读写操作。比如客户端向服务端发送数据时,将数据写入Socket的输入流,客户端也可以从Socket的输入流中读取数据。

那么这种输入输出流的概念,这篇文章再详细的介绍一下。

正文

1.1 概述

在我们平时项目开发过程中,使用流操作的地方,大部分都是在读取/写入文件的时候。

最具代表的两个抽象类:InputStreamoutputStream.它们分别代表字节输入和输出的抽象类。

在Java的IO流设计中,主要采用了装饰器模式的设计思想。

例如:Java在两个代表的抽象类基础上还提供了一系列的输入输出类:

FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream。

它们扩展了InputStream和OutputStream。不仅如此,Java还提供了FilterInputStream和FilterOutputStream。它们是装饰器的基类.用于提供通用的装饰器功能,例如缓冲、压缩、加密等功能,让我们对流的操作更加灵活。其它的装饰器都继承自这两个基类,通过继承和组合的方式实现了多个功能的扩展

1.2 FileInputStream

FileInputStream是Java中用于从文件读取数据的类,它继承自InputStream类,因此它提供了从文件读取字节的所有标准输入操作。

下面提供一个简单的Demo:

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()方法时都会从文件中读取一定数量的字节,然后将其存储到缓冲区中。读取完成后,缓冲区就会存储已被读取的字节。如果在读取完一个文件后,没有及时的调用close(),那么缓冲区可能会存在一些未刷新的数据。这次数据将会被你下一次循环读取到,从而导致数据重复。

那么我们该如何做到最优处理呢?解决它们很简单,在每次for循环中close掉InputStream的流就可以了。但是这又会导致一个新的问题,如果这是一个大文件,我们频繁的去打开/关闭流,它又会产生性能问题,因为我们每次打开和释放,Java都需要去计算这个文件所需要占用的资源。这会相当耗费性能。

优化思路:

1.将 InputStream 变量的声明和赋值合并到 try-with-resources 块中,避免手动关闭流的繁琐操作。

2.使用 try-with-resources 块来确保流的关闭。

3.在读取文件内容的同时进行编码转换。

4.将读取文件的代码封装到一个方法中,使其可以被多处调用。

5.使用 Path 和 Files 类来代替 File 类和 FileInputStream 类,可以让代码更简洁和易读。

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 FileOutputStream

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());
        }
    }
}

在这个Demo中我们首先指定了要写入的文件名和内容。然后,我们创建一个FileOutputStream对象并传递文件名作为参数。接下来,我们将内容转换为字节数组,并使用write()方法将其写入文件中。最后,我们关闭输出流并打印一条成功消息。

注意:在使用FileOutputStream时,我们需要在try-catch块中处理可能发生的IOException。这是因为在写入文件时可能会发生错误,例如无法打开文件磁盘已满。因此,我们需要确保在发生错误时能够捕获并处理异常。

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()方法从缓冲输入流中读取一个字节的数据,并将其打印到控制台上。如果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()方法,我们将data字符串转换为字节数组并将其写入缓冲输出流中。当缓冲区满时,数据将被刷新到底层输出流中。

通过使用BufferedOutputStream,可以有效地减少对底层资源的访问次数,从而提高写入数据的效率

1.6 FilterInputStream

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类创建了一个自定义的过滤器输入流,该流会在读取数据时对其进行加密。通过异或操作使用一个加密密钥将每个字节与其进行异或运算。加密后的数据被写入一个新的文件中

扩展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,具体取决于业务的需求.

猜你喜欢

转载自blog.csdn.net/qq_33351639/article/details/129422097